plym Docs
    plym.io

    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

    1. Make a directory under templates/, named in lowercase kebab-case:
    mkdir -p templates/flapico/css
    1. 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>
    1. 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>
    1. 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; }
    1. 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
    1. 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

    1. Downloads the template into templates/<name>, unless a local copy exists and --update is absent.
    2. Rewrites the template: line in config.yaml, appending it if the key is missing.
    3. Restarts the api.
    4. 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>.

    Frequently asked questions

    Can a template ship images?
    Not through the template directory, which is bind-mounted read-only and serves nothing. Upload them as media, or host them alongside your site and reference absolute URLs.
    Can a template add JavaScript?
    Yes, as a `<script>` in `index.html` or `post.html`. Fetching it from a CDN breaks the zero-third-party budget; inline it or serve it from your own origin.
    How do I preview a template before publishing?
    The fastest way is to use preview.sh script from plym-templates repository. However, we highly recommend you run it on a second plym environment before pushing to production.