Theme Portability

A well-written Accent CMS theme produces identical output whether you run accent serve in development, accent serve --production in production, or accent build for static deployment. This guide covers the guidelines and template features that make themes portable across all three modes.

Theme Structure

Every Accent theme consists of:

  • theme.yaml – theme configuration (name, assets, styling)
  • templates/*.html.jinja – Jinja templates
  • assets/ – CSS, JS, images, fonts

The CMS loads these files at startup and uses them for all page rendering. The rendering pipeline, template functions, and context variables are identical in serve and build modes.

What Stays the Same

Everything a theme developer interacts with works identically across serve and build:

Featureaccent serveaccent build
Jinja template renderingyesyes
Template inheritance (extends, include)yesyes
All template functions (now, get_env, get_hash, etc.)yesyes
| fingerprint filteryes (no-op)yes (active)
theme.yaml configurationyesyes
theme.assets.css / theme.assets.jsyesyes
Page context (page, site, theme, pages, etc.)yesyes
Shortcodesyesyes
Taxonomy pagesyesyes
Paginationyesyes

What Differs Between Modes

A few behaviors are mode-specific, but they require no changes to your theme:

Hot Reload (Development Only)

With dev.hot_reload: true, templates and assets refresh automatically when you save a file. In production mode and static builds, hot reload is disabled. Your templates don’t need to handle this – it’s automatic.

Asset Fingerprinting

The | fingerprint filter is active only during accent build. In accent serve, it returns the original path unchanged. Templates using | fingerprint work correctly in both modes without conditional logic:

{# Works in both modes: fingerprinted path in build, original path in serve #}
<link rel="stylesheet" href="/{{ ('theme/assets/' ~ css) | fingerprint }}">

Environment Variables

get_env() reads from the process environment in both modes. In build mode, values are baked into the static HTML. In serve mode, values are read per-request but frozen in the page cache for the duration of the cache TTL.

File Hashing

get_hash(path='...') resolves files against the theme’s assets directory. The hash output is identical for the same file content in both serve and build modes.

Writing Portable Themes

Follow these guidelines to ensure your theme works correctly in both serve and build:

Avoid Hardcoded Root-Absolute Paths

Sub-path deployments – a GitHub Pages project site, or any site configured with site.base_path – serve every page, asset, and API path under a prefix like /repo. A template that hardcodes a root-absolute path (href="/tags", src="/theme/assets/css/style.css") bypasses that prefix and 404s once the site moves off the domain root.

Wrap every root-absolute path in the url() template function – also available as the | url filter – so it composes the configured base path automatically:

{# Site-title home link #}
<a href="{{ url('/') }}">{{ site.name }}</a>

{# Tag link #}
<a href="{{ url('/tags/' ~ tag) }}">{{ tag }}</a>

{# Theme asset #}
<link rel="stylesheet" href="{{ url('/theme/assets/css/style.css') }}">

With no site.base_path configured, url() returns the path unchanged, so themes behave identically on sites that don’t use a sub-path.

Context values you read from page, pages, or a collection – page.url, page.media[*].url, pagination URLs – already come pre-prefixed, so most theme code needs no change at all. url() is idempotent: calling it again on an already-prefixed path (url(page.url)) is safe and does not double the prefix. absolute_url() and cdn_url() are base-path-aware in the same way, and site.base_path itself is exposed to templates if you need to branch on whether a prefix is configured.

When a base path is configured, accent build warns – with the offending file and line – about templates that still use a hardcoded root-absolute href, src, or action. The build finishes with a conformance check over the rendered output that fails the build if any emitted file still contains an unprefixed root-absolute internal URL. Code samples inside <pre>/<code> blocks are ignored, since they are illustrative markup rather than live links.

Use Relative Asset Paths

Reference assets relative to the theme’s assets directory, wrapped in url() for sub-path portability:

{# Correct: relative to theme assets, portable across sub-path deployments #}
<link rel="stylesheet" href="{{ url('/theme/assets/css/style.css') }}">

{# With fingerprinting for cache busting #}
<link rel="stylesheet" href="{{ url('/' ~ ('theme/assets/css/style.css' | fingerprint)) }}">

Use Template Functions for Dynamic Values

Use template functions to derive values rather than hardcoding environment-specific information:

{# Cache busting via content hash #}
<link rel="stylesheet" href="{{ url('/theme/assets/css/style.css') }}?v={{ get_hash(path='theme/assets/css/style.css') }}">

{# Environment-specific configuration #}
{% set analytics_id = get_env(name="GA_ID") %}
{% if analytics_id %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ analytics_id }}"></script>
{% endif %}

Keep theme.yaml Complete

Declare all CSS and JS files in theme.yaml so the CMS knows which assets to load:

name: My Theme
assets:
  css:
    - css/main.css
    - css/components.css
  js:
    - js/app.js

Test with accent build

Run accent build to verify your theme produces correct static output. The build uses the same rendering pipeline as serve mode, so if the build output is correct, your theme works:

accent build --output ./public --clean
accent serve-static --dir ./public
# Verify the site at https://127.0.0.1:4403

Avoid Dev-Only Assumptions

Don’t assume hot reload, debug panels, or SSE reload scripts are available. Use the dev.debug context variable for development-only markup:

{% if dev.debug %}
<div class="debug-panel">
  <pre>{{ debug(page) }}</pre>
</div>
{% endif %}

Edition Availability

Theme portability across serve and build modes requires no license key.