Group
Extension

Mojolicious-Plugin-Inertia/lib/Mojolicious/Plugin/Inertia.pm

package Mojolicious::Plugin::Inertia;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::JSON qw(encode_json);
use Scalar::Util qw(reftype);
use Carp qw(croak);

our $VERSION = "0.01";

sub register {
    my ($self, $app, $conf) = @_;
    $conf ||= {};

    croak "Inertia plugin requires a 'version' configuration option" unless $conf->{version};
    croak "Inertia plugin requires a 'layout' configuration option" unless $conf->{layout};

    # Asset versioning. e.g. md5sum of your assets manifest file.
    my $version = $conf->{version};

    # Layout template for non-Inertia requests.
    # It must contain a <%= data_page %> placeholder at the application root.
    my $layout = ref $conf->{layout} ? $conf->{layout}->slurp : $conf->{layout};

    # History encryption settings (optional).
    # Ref: https://inertiajs.com/history-encryption
    my $default_encrypt_history = defined $conf->{encrypt_history} ? $conf->{encrypt_history} : 0;
    my $default_clear_history   = defined $conf->{clear_history} ? $conf->{clear_history} : 0;

    $app->helper(inertia => sub {
        my ($c, $component, $props, $options) = @_;
        $props ||= {};
        $options ||= {};

        # Options
        my $encrypt_history = defined $options->{encrypt_history} ? $options->{encrypt_history} : $default_encrypt_history;
        my $clear_history   = defined $options->{clear_history} ? $options->{clear_history} : $default_clear_history;

        # If the client's asset version does not match the server's version,
        # then the client must do a full page reload.
        # So, we respond with a 409 status and an X-Inertia-Location header
        # Ref: https://inertiajs.com/the-protocol#asset-versioning
        my $inertia_version = $c->req->headers->header('X-Inertia-Version');
        if ($c->req->method eq 'GET' && $inertia_version && $inertia_version ne $version) {
            $c->res->headers->header('X-Inertia-Location' => $c->req->url->to_string);
            return $c->rendered(409);
        }

        # Partial reloads allows you to request a subset of the props (data) from the server on subsequent visits to the same page component.
        # Ref: https://inertiajs.com/the-protocol#partial-reloads
        my $partial_data      = $c->req->headers->header('X-Inertia-Partial-Data');
        my $partial_component = $c->req->headers->header('X-Inertia-Partial-Component');
        if ($partial_data && $partial_component) {
            my @only_keys = split /,/, $partial_data;
            $props = { map { $_ => $props->{$_} } @only_keys };
        }

        # Resolve props that are coderefs by calling them with the current controller context.
        # Code refs are useful for lazy loading data only when needed.
        my $resolved_props = {};
        for my $key (keys %$props) {
            my $prop = $props->{$key};
            $resolved_props->{$key} = (reftype($prop) || '') eq 'CODE' ? $prop->($c) : $prop;
        }

        # Construct the page object.
        # Ref: https://inertiajs.com/the-protocol#the-page-object
        my $page_object = {
            component      => $component,
            props          => $resolved_props,
            url            => $c->req->url->to_string,
            version        => $version,
            encryptHistory => $encrypt_history,
            clearHistory   => $clear_history,
        };

        # Check if the request is an Inertia request.
        # If so, return a JSON response.
        # Else, return an HTML response with embedded page object.
        # Ref: https://inertiajs.com/the-protocol#inertia-responses
        my $is_inertia = $c->req->headers->header('X-Inertia');

        if ($is_inertia) {
            $c->res->headers->header('X-Inertia' => 'true');
            $c->res->headers->header('Vary' => 'X-Inertia');
            return $c->render(json => $page_object);
        }
        else {
            $c->res->headers->header('Vary' => 'X-Inertia');
            return $c->render(
                inline    => $layout,
                format    => 'html',
                data_page => encode_json($page_object)
            );
        }
    });
}

1;
__END__

=encoding utf-8

=head1 NAME

Mojolicious::Plugin::Inertia - Inertia.js adapter for Mojolicious

=head1 SYNOPSIS

  # Mojolicious
  $app->plugin('Inertia' => {
    version => '1.0.0',  # Asset version for cache busting
    layout  => '<div id="app" data-page="<%= $data_page %>"></div>'
  });

  # Mojolicious::Lite
  plugin 'Inertia' => {
    version => md5_sum($app->home->child('public/assets/manifest.json')->slurp),
    layout  => app->home->child('dist', 'index.html')
  };

  # In your controller
  sub index {
    my $c = shift;

    # Render Inertia page with props
    $c->inertia('Home', {
      user => { name => 'John Doe' },
      posts => \@posts
    });
  }

  # With lazy evaluation
  sub dashboard {
    my $c = shift;

    $c->inertia('Dashboard', {
      # Regular prop
      user => $c->current_user,

      # Lazy prop - only evaluated when needed
      stats => sub {
        return $c->calculate_expensive_stats;
      }
    });
  }

=head1 DESCRIPTION

L<Mojolicious::Plugin::Inertia> is a L<Mojolicious> plugin that provides server-side
adapter for L<Inertia.js|https://inertiajs.com/>, allowing you to build single-page
applications without building an API.

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using
classic server-side routing and controllers. It works by intercepting requests and
converting the responses to either full page loads or JSON with just the page component
name and props.

=head2 Features

=over 4

=item * Automatic handling of Inertia and standard HTTP requests

=item * Asset versioning for automatic cache busting

=item * Partial reloads to optimize data transfer

=item * Lazy evaluation of props for performance

=item * History encryption support

=back

=head1 OPTIONS

L<Mojolicious::Plugin::Inertia> supports the following options.

=head2 version

  plugin 'Inertia' => {version => '1.0.0'};

B<Required>. Asset version string used for cache busting. When the version changes,
Inertia will force a full page reload to ensure users get the latest assets.

Common approaches:

  # Static version
  version => '1.0.0'

  # MD5 hash of manifest file
  version => md5_sum($app->home->child('manifest.json')->slurp)

=head2 layout

  plugin 'Inertia' => {layout => 'layouts/inertia.html.ep'};

B<Required>. HTML template or template name containing the root element for your
JavaScript application. Must include a C<< <%= $data_page %> >> placeholder where
the page data will be inserted.

Example template:

  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>My App</title>
    </head>
    <body>
      <div id="app" data-page='<%= $data_page %>'></div>
      <script src="/js/app.js"></script>
    </body>
  </html>

=head2 encrypt_history

  plugin 'Inertia' => {encrypt_history => 1};

Optional. Enable history encryption by default (defaults to 0). When enabled,
page data in the browser's history state will be encrypted.

=head2 clear_history

  plugin 'Inertia' => {clear_history => 1};

Optional. Clear history on navigate by default (defaults to 0). When enabled,
the browser's history state will be cleared on each navigation.

=head1 HELPERS

L<Mojolicious::Plugin::Inertia> implements the following helpers.

=head2 inertia

  $c->inertia($component, \%props, \%options);

Render an Inertia response. Returns either a JSON response for Inertia requests
or a full HTML page for standard requests.

=head3 Arguments

=over 4

=item * C<$component> - Name of the JavaScript component to render (e.g., 'Users/Index')

=item * C<\%props> - Hash reference of props to pass to the component

=item * C<\%options> - Optional hash reference of options

=back

=head3 Options

=over 4

=item * C<encrypt_history> - Override the default history encryption setting

=item * C<clear_history> - Override the default clear history setting

=back

=head3 Examples

  # Basic usage
  $c->inertia('Home', { message => 'Welcome!' });

  # With nested components
  $c->inertia('Users/Show', {
    user => $user,
    permissions => \@permissions
  });

  # With lazy props
  $c->inertia('Dashboard', {
    user => $c->current_user,

    # This will only be evaluated if needed (e.g., partial reload)
    stats => sub {
      my $c = shift;  # Controller is passed to the sub
      return $c->db->calculate_stats;
    }
  });

  # With options
  $c->inertia('SecurePage', $props, {
    encrypt_history => 1,
    clear_history => 1
  });

=head2 Request Headers

=over 4

=item * C<X-Inertia> - Indicates this is an Inertia request

=item * C<X-Inertia-Version> - Asset version from the client

=item * C<X-Inertia-Partial-Data> - Comma-separated list of props to include (partial reload)

=item * C<X-Inertia-Partial-Component> - Component name for partial reload validation

=back

=head2 Response Headers

=over 4

=item * C<X-Inertia> - Set to "true" for Inertia responses

=item * C<X-Inertia-Location> - URL for redirect on version mismatch (409 response)

=item * C<Vary> - Set to "X-Inertia" to ensure proper caching

=back

=head2 Response Codes

=over 4

=item * C<200> - Successful response with page data

=item * C<409> - Asset version mismatch, triggers full page reload

=back

=head1 SEE ALSO

=over 4

=item * L<Inertia.js Documentation|https://inertiajs.com>

=item * L<Mojolicious>

=item * L<https://github.com/kfly8/Mojolicious-Plugin-Inertia>

=back

=head1 LICENSE

Copyright (C) kobaken.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

kobaken E<lt>kentafly88@gmail.comE<gt>

=cut



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.