NAME

    Mojolicious::Plugin::Branding - Global and per-request branding of
    templates and public files

VERSION

    This document describes Mojolicious::Plugin::Branding version 0.003.

SYNOPSIS

       # Mojolicious::Lite
    
       # individual branding: the application decides the brand of each
       # request and puts it in the stash
       my %brand_for = (
          'acme.example.com'     => 'acme',
          'umbrella.example.com' => 'umbrella',
       );
       hook before_dispatch => sub {
          my $c = shift;
          $c->brand($brand_for{$c->req->url->to_abs->host // ''});
       };
    
       plugin Branding => {
    
          # global branding: these directories override the application's
          # own templates/ and public/, first listed wins
          global => [ '/etc/myapp/look-and-feel' ],
    
          # individual branding: the directories of each brand
          individual => {
             brands => {
                acme     => 'brands/acme',
                umbrella => [ 'brands/umbrella', '/srv/shared/umbrella' ],
             },
          },
       };

DESCRIPTION

    This plugin lets you change the templates and public files of a
    Mojolicious application without touching its source code. It supports
    two styles of branding, which can be used separately or together.

    Global branding

      A project ships its own "brand-neutral" templates and public files,
      and whoever deploys it can customize the look and feel by pointing
      the plugin to one or more directories with the same layout:

         /etc/myapp/look-and-feel/templates/layouts/default.html.ep
         /etc/myapp/look-and-feel/public/css/site.css
         /etc/myapp/look-and-feel/public/img/logo.png

      Files found there take precedence over the application's own;
      anything else falls back to the application. See "global".

    Individual branding

      In a multi-tenant setup the brand is decided for each request, e.g.
      from the Host header, and each brand keeps its own files in one or
      more directories:

         brands/acme/templates/index.html.ep
         brands/acme/public/css/site.css
         brands/umbrella/public/img/logo.png

      Again, only the files that a brand overrides need to be there,
      everything else falls back to the (possibly globally branded)
      application files. Deciding the brand of a request is up to the
      application, see "individual" and "How the brand is applied".

    Templates and public files are looked up at the same URLs and with the
    same names as usual, so stylesheet '/css/site.css' or render(template
    => 'index') pick the branded version automatically. This includes
    layouts and partial templates.

CONFIGURATION

    The plugin accepts a hash reference with the following keys, both
    optional.

 global

       global => '/path/to/dir',
       global => [ 'dir1', $mojo_file, ... ],
       global => {
          paths  => '/etc/myapp/look-and-feel',
          public => '/srv/myapp/static',
       },

    A specification as for a brand (see "brands"), except that
    "ignore_app_paths" is not allowed: e.g. one directory or an array of
    directories, each with templates and public sub-directories (if
    present). The resulting directories are put in front of the
    application's renderer and static paths; the order among them, and the
    handling of undef items and missing directories, are the same as for a
    brand. This allows using the same configuration across environments,
    e.g. global => [ $ENV{MYAPP_LOOK} ].

    When the plugin is registered more than once with global, the
    directories of each registration are put in front of those of the
    earlier ones (last in, first out): a later global wins, while the
    earlier ones are still used for the files it does not have.

 individual

    A hash reference with the following keys, both optional:

    brands

         brands => {
            acme     => 'brands/acme',
            umbrella => [ 'brands/umbrella', '/srv/shared/umbrella' ],
            special  => {
               paths     => 'brands/special',
               templates => { paths => '/srv/special/views', ignore_app_paths => 1 },
               public    => '/srv/special/static',
            },
         },

      A hash reference from brand names to their specification. The name is
      what the application puts in the stash (see "stash_key") and does not
      need to match any directory name. A specification is either:

	* one directory or an array of directories, short for { paths =>
	... };

	* a hash reference with the following keys, all optional:

	paths

	  One directory or an array of directories, each holding the files
	  of the brand in its templates and public sub-directories (both
	  optional):

             <dir>/templates/...
             <dir>/public/...

	templates

	public

	  Further directories of templates or public files, used directly
	  (i.e. they are not expected to have templates or public
	  sub-directories). Either one directory, an array of directories,
	  or a hash reference with keys:

	  paths

	    one directory or an array of directories;

	  ignore_app_paths

	    if true, the application's own directories (including the
	    "global" ones) are not used for this brand, e.g. a template or
	    public file that the brand does not have is not found, instead
	    of falling back to the application's one. Files embedded in
	    __DATA__ sections and the files bundled with Mojolicious are
	    still found. If the brand ends up with no directories of that
	    kind, e.g. because they are missing on this installation, a
	    warning is logged at startup: the brand would have no templates
	    or public files at all.

      For each brand, templates and public files are searched in this
      order: the directories in "templates" or "public", then the matching
      sub-directories of the directories in "paths", then the application's
      directories (unless "ignore_app_paths"). In each list, the first
      directory wins.

      Directories are given as strings or Mojo::File objects; relative
      paths are resolved from the application home. undef items and
      directories that do not exist are silently skipped, and so are brands
      whose specification is undef: this allows e.g. acme =>
      $ENV{ACME_LOOK}. A brand whose directories are all skipped is still a
      valid brand, it just has no files of its own.

      The plugin croaks on empty strings, on references other than
      Mojo::File objects where a directory is expected, and on unknown keys
      in a specification, so that a typo like ignore_app_path does not go
      unnoticed.

    stash_key

         stash_key => 'tenant',

      The stash key where the application puts the brand of the request,
      branding.brand by default. See "How the brand is applied".

    The plugin can be registered more than once. Brands from all
    registrations are collected; when a brand is configured again, the
    later specification replaces the earlier one entirely (unlike "global",
    see there). "stash_key" is set by the first registration (to
    branding.brand if not given, even when that registration has only
    "global"); later registrations can omit it or repeat the same value,
    any other value makes the plugin croak.

How the brand is applied

    The plugin does not decide the brand of a request: the application puts
    it in the stash key "stash_key" (branding.brand by default), e.g. with
    the "brand" helper from a before_dispatch hook. The brand can come from
    anywhere: the Host header (as in the "SYNOPSIS"), a header set by a
    reverse proxy, a cookie, a database...

    Each brand gets its own Mojolicious::Renderer and Mojolicious::Static,
    created the first time they are needed (a brand with no directories of
    its own, and not ignoring the application's ones, just uses the
    application's). Their directories are those of the brand followed by
    the application's ones (see "brands"), everything else is taken from
    the application's renderer and static file server at that moment: the
    handlers and the helpers are shared, roles already applied to the
    application's renderer or static file server apply to the brand ones
    too. When a request has a brand, the application's renderer and static
    file server just hand the work over to the brand ones, which then
    follow the usual Mojolicious rules.

    Public files are served by Mojolicious itself, at the usual point of
    the request: after all before_dispatch hooks, whatever the order in
    which they and the plugin were registered, and only if none of them has
    already rendered a response or called render_later. The static prefix,
    after_static hooks and reply->static all work as for the application's
    files. Hence, the brand must be in the stash by the end of the
    before_dispatch hooks for public files to be branded.

    Templates are rendered according to the brand in the stash at the time
    of rendering. This covers layouts, partial templates (include) and the
    pages for exceptions and missing resources too, e.g. a brand can
    override just layouts/default.html.ep and keep the application's pages.
    The brand can also be set later than for public files, e.g. by a route
    with $c->brand('acme'); this changes the brand of the templates, but
    not of the public files, which are served before routing. Brand
    templates can use any handler (e.g. .ep or .epl), independently of the
    application's template with the same name.

    The brand and variant are independent: the application can keep using
    variants (e.g. phone) and brands can provide their own variant
    templates (e.g. index.html+phone.ep). As usual in Mojolicious, a
    variant template found in any directory wins over a plain template, so
    a variant template of the application is preferred over a plain
    template of the brand: a brand overriding a page should also override
    its variants.

    Only brands listed in "brands" are ever used: any other value in the
    stash, including one coming straight from the request, just means "no
    branding". There is no need to validate the brand against path
    traversal, although the application should still make sure that it is
    allowed for the request.

INTERFACE

 brand

       my $brand = $c->brand;
       $c->brand('acme');

    Helper to get or set the brand of the request, i.e. the stash value at
    "stash_key". Setting undef, or a brand that is not in "brands", means
    no branding.

 register

       $plugin->register($app, \%config);

    Registers the plugin in the application, see "CONFIGURATION". Called by
    "plugin" in Mojolicious, you should not need to call it directly.

BUGS AND LIMITATIONS

    Minimum perl version 5.24, minimum Mojolicious version 9.34.

      * url_for_asset uses the application's static files and ignores the
      brand.

      * Mojolicious computes ETags of static files from their modification
      time only. When the brand does not come from the host (e.g. from a
      cookie), a browser switching brand may get a 304 Not Modified for a
      file of the previous brand. In that case, put the brand in the URL.

      * Directories, both global and of brands, are checked at startup:
      adding a new directory, or a templates or public sub-directory to an
      existing one, requires a restart.

      * The renderer and static file server of a brand copy the
      application's directories and settings (e.g. the static prefix) when
      they are created, i.e. at the first request for that brand in each
      process. Later changes to the application's ones are not seen by the
      brand.

      * As Mojolicious does for the application at startup, the renderer of
      a brand indexes its templates when it is created: new templates with
      a handler other than the default one, and new variant templates, need
      a restart. Changes to existing files and new public files are picked
      up as usual. Note that morbo only watches lib and templates by
      default: use e.g. morbo -w brands during development.

      * Only rendering and serving files are branded: calling methods of
      the application's renderer or static file server directly (e.g.
      $app->renderer->template_path(...)) ignores the brand, and so would a
      template handler that uses $app->renderer instead of the renderer it
      is given.

      * As in plain Mojolicious, a leading / in a template name is ignored,
      but only for templates with the default handler: e.g. render(template
      => '/page') does not find page.html.epl.

    Report bugs through Codeberg (patches welcome) at
    https://codeberg.org/polettix/Mojolicious-Plugin-Branding.

USE OF AI

    This module was developed with the help of an AI coding assistant
    (Claude, by Anthropic). It was used to review the code and find bugs,
    to write most of the test suite and to draft this documentation. All
    changes were reviewed by the author, who designed the module and wrote
    or approved every fix.

AUTHOR

    Flavio Poletti <flavio@polettix.it>

COPYRIGHT AND LICENSE

    Copyright 2026 by Flavio Poletti <flavio@polettix.it>

    Licensed under the Apache License, Version 2.0 (the "License"); you may
    not use this file except in compliance with the License. You may obtain
    a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    implied. See the License for the specific language governing
    permissions and limitations under the License.

