NAME

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

VERSION

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

SYNOPSIS

       # Mojolicious::Lite
       plugin Branding => {
    
          # global branding: these directories override the application's
          # own templates/ and public/, first listed wins
          global => [ '/etc/myapp/look-and-feel' ],
    
          # per-request branding (multi-tenant)
          individual => {
             bases   => [ 'brands' ],
             mapping => {
                'acme.example.com'  => 'acme',
                'umbrella.example.com' => 'umbrella',
             },
          },
       };
    
       # in a template, call the helper with parentheses. The default name
       # of the helper is `brand`, but you can change it via plugin option
       # `helper_name`.
       % if (my $brand = brand()) {
         <p>Welcome to <%= $brand %></p>
       % }

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 a
      sub-directory of one or more bases:

         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. See "individual".

    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, ... ],

    One directory or an array of directories, as strings or Mojo::File
    objects. Relative paths are resolved from the application home.

    For each directory, its templates and public sub-directories (if
    present) are put in front of the application's renderer and static
    paths. When the same file exists in more than one directory, the first
    directory in the list wins. Directories that do not exist are silently
    skipped, so the same configuration can be used across environments.

 individual

    A hash reference with the following keys:

    bases

         bases => 'brands',
         bases => [ 'brands', '/srv/more-brands' ],

      One directory or an array of directories (strings or Mojo::File
      objects, relative to the application home) holding one sub-directory
      per brand, each with optional templates and public sub-directories:

         <base>/<brand>/templates/...
         <base>/<brand>/public/...

      Templates are taken from the first base that has them; public files
      are searched in all bases, in order. Bases that do not exist are
      skipped; if none exists, individual branding is turned off (the
      helper is installed anyway, see "helper_name").

    mapping

         mapping => { 'acme.example.com' => 'acme', ... },
         mapping => sub ($host) { ... return $brand_or_undef },

      How the default helper turns the request's host into a brand. The
      host is taken from the Host header, lowercased and checked to be a
      valid hostname before being passed on, so hash keys must be
      lowercase.

      A hash reference is used as a lookup table; a code reference receives
      the host and returns the brand, or undef for no brand. Without a
      mapping, the host itself is the brand (e.g.
      brands/acme.example.com/...).

      Cannot be used together with "helper".

    helper

         helper => sub ($c) { ... return $brand_or_undef },

      A code reference that replaces the default helper altogether, e.g. to
      take the brand from a session, a header set by a reverse proxy or a
      database. It is called with the controller and returns the brand, or
      undef for no brand.

      Cannot be used together with "mapping".

    helper_name

      The name under which the helper is installed, brand by default.

HELPERS

 brand (default, see "helper_name")

       my $brand = $c->brand;
       %= brand() // 'default'

    Returns the brand for the current request, or undef/an empty string
    when there is none. The name can be changed with "helper_name".

    The default helper returns the stash value variant if set (see "How the
    brand is applied"); otherwise it computes the brand from the Host
    header and "mapping", caching the result in the stash key
    branding.brand for the rest of the request. Consider this key reserved:
    to force a brand for a request, set variant instead.

    Call it with parentheses in templates: in brand // 'default', Perl
    reads // as the start of a regular expression.

BRAND NAMES

    A brand name must:

      * match /\A[0-9a-zA-Z_][-.0-9a-zA-Z_]*\z/: ASCII letters, digits, _,
      - and ., not starting with - or .;

      * resolve to a directory directly below one of the "bases". A brand
      directory may be a symbolic link, as long as it points to another
      directory in the same base.

    Brands that do not comply are ignored, i.e. the request is served with
    the non-branded files. This also applies to brands coming from a custom
    "helper" or "mapping", so that a brand derived from request data can
    never reach files outside the bases. To share brand directories living
    elsewhere, add their parent directory to "bases".

How the brand is applied

    For each request, a before_dispatch hook calls the helper and, when
    there is a valid brand:

      * serves the file from <base>/<brand>/public if there is one,
      otherwise lets Mojolicious serve the application's public files as
      usual. after_static hooks run for branded files too;

      * sets the stash value variant to the brand; the renderer then looks
      for <base>/<brand>/templates/<name> before the usual template.

    Because the brand travels in variant, standard Mojolicious variants
    work too: templates/index.html+acme.ep is used for brand acme if the
    brand directory has no index.html.ep. Note that a variant file in the
    application templates takes precedence over a plain template in the
    brand directory, so avoid having both.

    A route can also set variant itself; this changes the brand of the
    templates rendered by that route (the same name rules apply), but not
    of the public files, which are served before routing.

INTERFACE

 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 and reply->static use the application's static files
      and ignore 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.

      * The plugin's before_dispatch hook must run after any hook that
      rewrites the request path or base URL (e.g. for a reverse proxy), so
      register the plugin after those hooks.

      * The public directories of a brand are cached as soon as at least
      one is found; adding a public directory to another base for that
      brand requires a restart. Templates are looked up at each render.

    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.

