clarkwinkelmann/flarum-mithril2html

Renders Mithril components to HTML for use in blade templates

1.3.0 2022-12-30 17:22 UTC

This package is auto-updated.

Last update: 2024-03-29 04:45:55 UTC


README

Uses Chrome Puppeteer via Spatie Browsershot to render Mithril components as static HTML.

Follow Browsershot instructions to setup Node and Headless Chrome.

This is intended for use with emails or other offline content generation.

It's probably not a good idea to use this outside of a queue because of the delays it introduces.

Usage

In your extension's extend.php, call the setup extender before registering any asset:

The extender can be called by multiple extensions without issues. It won't do anything once already registered.

return [
    new \ClarkWinkelmann\Mithril2Html\Extend\Setup(),
    
    // Your other extenders
];

Create a new page just like you would a normal Flarum page:

import Page from 'flarum/common/components/Page';

class HelloWorld extends Page {
    view() {
        return <p>Hello World</p>;
    }
}

app.initializers.add('demo', function () {
    app.routes.helloWorld = {
        path: '/hello-world',
        component: HelloWorld,
    };
});

To save up space in the forum bundle or to prevent conflicts, you can add your page only to the mithril2html frontend. You will need to update your webpack config to add an additional entry file, see this package's webpack.config.js for an example.

If you created a separate bundle (not forum), register it using Flarum's Frontend extender:

    (new Frontend('mithril2html'))
        ->js(__DIR__ . '/js/dist/mithril2html.js'),

If you already have a forum bundle with exports, Flarum will unfortunately override all forum exports with mithril2html exports (even if you have none). To work around this, a different extender is available just for javascript:

    (new \ClarkWinkelmann\Mithril2Html\Extend\FrontendNoConflict('mithril2html'))
        ->js(__DIR__ . '/js/dist/mithril2html.js'),

You can then use the Renderer class to render the component:

$component = new ClarkWinkelmann\Mithril2Html\AnonymousComponent('hello-world');
echo resolve(ClarkWinkelmann\Mithril2Html\Renderer::class)->render($component);
// <p>Hello World</p>

Alternatively, you can use the blade directive directly:

@mithril2html(new ClarkWinkelmann\Mithril2Html\AnonymousComponent('hello-world'))

You can configure additional options using a component class. The class must implement ClarkWinkelmann\Mithril2Html\ComponentInterface. AnonymousComponent is a simple class that allows customizing all parameters without creating additional classes.

The parameters customizable through a component class are:

  • route: The Mithril route name without leading slash.
  • preload: An API route to preload through the API Client. With leading slash.
  • actor: An actor to use for the request. Defaults to guest.
  • selector: A CSS selector targeting the HTML to return. That element's innerHTML will be returned. If the selector can't be found, an exception will be thrown.

Using a custom component class helps keep things clean when preloading is necessary:

class InvoiceComponent implements ComponentInterface {
    protected $invoice;

    public function __construct(Invoice $invoice)
    {
        $this->invoice = $invoice;
    }

    public function route(): string
    {
        return 'invoice';
    }

    public function preload(): ?string
    {
        return '/invoices/' . $this->invoice->id;
    }

    public function actor(): ?User
    {
        return $this->invoice->user;
    }

    public function selector(): ?string
    {
        return '#content';
    }
}
<p>Below is a summary of your invoice:</p>

@mithril2html(new InvoiceComponent($invoice))

PDF and Screenshot

While it was not the primary use case for this extension, the Renderer class also exposes a browsershot method that gives access to a pre-configured but unused Browsershot instance.

When using that method, the selector property is ignored and the entire page/viewport is used. Browsershot methods must be used to configure the output.

Example:

$component = new ClarkWinkelmann\Mithril2Html\AnonymousComponent('hello-world');
echo resolve(ClarkWinkelmann\Mithril2Html\Renderer::class)->browsershot($component)->landscape()->pdf();

The page CSS is not loaded when calling Renderer::render but is loaded by default when calling Renderer::browsershot. It can be disabled by passing false as the second argument like $renderer->browsershot($component, false).

Known issues

At the moment, passing an actor will authenticate the base request and preloaded apiDocument, but not any additional API request the component will make after page load.

If you render a page with user-generated content and an XSS is possible, the attacker might be able to read any API GET endpoint as administrator by first stealing the mithril2html internal token and then using that token to preload arbitrary GET endpoints.

Tests

The integration tests are a bit special because they require a working webserver that can be accessed by Chrome.

Run composer test:server before running the tests to start the PHP development server on port 8080. The server is configured with a router script that takes care of routing back to the integration tmp folder.