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 templatesassets/– 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:
| Feature | accent serve | accent build |
|---|---|---|
| Jinja template rendering | yes | yes |
Template inheritance (extends, include) | yes | yes |
All template functions (now, get_env, get_hash, etc.) | yes | yes |
| fingerprint filter | yes (no-op) | yes (active) |
theme.yaml configuration | yes | yes |
theme.assets.css / theme.assets.js | yes | yes |
Page context (page, site, theme, pages, etc.) | yes | yes |
| Shortcodes | yes | yes |
| Taxonomy pages | yes | yes |
| Pagination | yes | yes |
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.