Custom templates
An easy, step-by-step guide to building a plym template. plym runs on Jinja2 templating engine, and hence you can 100% of your own UI for your blogs.
Updated Aug 04, 2026
On this page
A plym template is a directory of Jinja2 body fragments and CSS. You design what goes inside <body>; plym writes the document skeleton, the SEO and OpenGraph tags, the canonical link, the JSON-LD, the inlined CSS, the self-hosted webfonts, and the optional Prism assets. Writing <html>, <head>, or <meta> yourself produces a nested, malformed document — plym does not warn about it.
Create a template
- Make a directory under
templates/, named in lowercase kebab-case:
mkdir -p templates/flapico/css
- Write
templates/flapico/index.html, the body of the blog index:
<header><a href="{{ site.blog_prefix or '/' }}"><h1>{{ site.name }}</h1></a></header>
<main>
{% for post in posts %}
<article>
<h2><a href="{{ site.blog_prefix }}/{{ post.path }}">{{ post.title }}</a></h2>
{% if post.excerpt %}<p>{{ post.excerpt }}</p>{% endif %}
<small>
{{ post.author.display_name }}
{% if post.published_at %} · {{ post.published_at.strftime('%b %d, %Y') }}{% endif %}
· {{ post.reading_time }} min
</small>
</article>
{% else %}
<p>Nothing here yet.</p>
{% endfor %}
</main>
- Write
templates/flapico/post.html, the body of a single post:
<header><a href="{{ site.blog_prefix or '/' }}">{{ site.name }}</a></header>
<article>
<h1>{{ post.title }}</h1>
{% if post.cover %}<img src="{{ post.cover }}" alt="">{% endif %}
{{ post.content | safe }}
</article>
- Write
templates/flapico/css/base.css, using plym's CSS variables instead of hardcoded values:
body {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: var(--color-primary);
background: var(--color-background);
font-family: var(--font-body), serif;
}
h1, h2 { font-family: var(--font-heading), sans-serif; }
a { color: var(--color-accent); }
img { max-width: 100%; height: auto; }
pre { overflow-x: auto; padding: 1rem; }
- Declare your design defaults in
templates/flapico/template.yaml:
fonts:
heading: Inter
body: Merriweather
colors:
primary: "#1a1a1a"
secondary: "#6b6b6b"
accent: "#E9793A"
background: "#FAF9F6"
prism:
theme: tomorrow
- Activate it:
plym template install flapico
Installing template 'flapico' into plym-flapico
Using local template (pass --update to fetch the latest from plym-io/plym-templates)
✓ Restarting api
✓ Re-rendering published posts
→ Template 'flapico' is live.
Required files
| File | Status | Purpose |
|---|---|---|
index.html |
required | Body fragment for the blog index. A render fails with a template-not-found error without it |
post.html |
required | Body fragment for a single post |
css/*.css |
optional | Concatenated alphabetically, minified, inlined. No files means no styles |
template.yaml |
optional | Design defaults: fonts, colors, prism.theme |
Files starting with _ are ordinary Jinja2 partials — {% include "_topbar.html" %} works, and every .html file in the directory counts toward the render stamp that decides when pages need re-rendering.
Render post.content with the safe filter. Autoescaping is on, so without it the post body displays as escaped HTML source.
Link to a post with post.path, not post.slug. path carries the category segment, so a categorised post lives at interview-loops/scorecards while its slug is scorecards; linking by slug returns 404 for every categorised post.
{{ site }}, {{ post }}, {{ posts }}, and {{ debug }} are the variables you get. The full context reference — every field, its type, and whether it can be null — is in Creating templates for plym, along with the performance and accessibility budgets a template needs to meet for the template repository.
Install from local
plym template install <name> uses templates/<name> when that directory exists and downloads nothing. This is the path for a template you are writing: edit the files, run the command, see the change.
Once template: already points at your template, re-render with plym rebuild instead — it picks up edited HTML and CSS the same way and skips the install step.
Install from remote
With no local copy, plym downloads the template from plym-io/plym-templates:
plym template install journal
Installing template 'journal' into plym-flapico
✓ Downloading journal from plym-io/plym-templates@main
✓ Restarting api
✓ Re-rendering published posts
→ Template 'journal' is live.
The repository ships aurelle, fleet, huzzl, journal, and memoir. A second argument pins a branch, tag, or commit; the default is main:
plym template install journal v1.2.0
Point the command at your own fork with two environment variables:
PLYM_TEMPLATE_REPO_OWNER=flapico \
PLYM_TEMPLATE_REPO_NAME=blog-templates \
plym template install flapico-dark
A name that doesn't exist in the repository at that ref fails with Template '<name>' not found and changes nothing.
The --update flag
--update forces a fresh download even when a local copy exists:
plym template install journal --update
The download deletes templates/<name> and replaces it. Local edits inside that directory are gone, with no backup and no prompt. Copy your changes out first, or work under a name that isn't in the remote repository.
What install does
- Downloads the template into
templates/<name>, unless a local copy exists and--updateis absent. - Rewrites the
template:line inconfig.yaml, appending it if the key is missing. - Restarts the api.
- Re-renders every published post, and clears any file a draft left behind.
Steps 3 and 4 are the same work as plym rebuild. The old template directory is left on disk, so switching back is one more plym template install <old-name>.