plym ← Index

Creating templates for plym

plym supports custom templates which are easy to build, and are fully customizable. Read this guide to start creating templates for your plym blog. If you are an agent, use content negotiation to receive the markdown format of this page.

1. Overview

A plym template is a directory of Jinja2 and CSS files used to produce HTML for the blog index and individual posts.

Templates in plym are strictly body fragments. You only design what goes inside the <body> tag. Plym automatically generates the document skeleton (<!DOCTYPE>, <html>, <head>, <body>), SEO/OpenGraph meta tags, canonical links, CSS bundling and inlining, webfont fetching (self-hosted), optional Prism syntax-highlighting assets, and operator-configured head/body snippet injection.

2. File Layout

Template directories live in plym/templates/ and are referenced by directory name via the template key in config.yaml. Use lowercase kebab-case names by convention.

plym/templates/<template-name>/
├── index.html
├── post.html
├── template.yaml
└── css/
    └── base.css
File / Directory Status Purpose
index.html Required Body fragment for the blog index. Rendering fails with a template-not-found error if missing.
post.html Required Body fragment for a single post.
css/ Optional (expected in practice) All *.css files inside are concatenated in alphabetical order, minified, and inlined. If absent, the template ships no styles.
template.yaml Optional Design defaults shipped with the template (fonts, colors, Prism theme).

3. Context Variables

Global Context

Available in all templates: site and debug (Boolean, true when plym runs in debug mode).

Variable Type Nullable Description
site.name String No The name of the site.
site.website String No The marketing domain URL.
site.blog_home String No Public URL where plym is mounted.
site.blog_prefix String No Normalized mount prefix: leading slash, no trailing slash (e.g. /blog), or empty string when mounted at the domain root.
site.language String No ISO language code.
site.template String No Name of the active template.
site.pagination.page_size Integer No Number of posts per index page.
site.colors.<color> String No Values for primary, secondary, accent, background.
site.fonts.heading / site.fonts.body String No Configured font families.
site.prism.enabled Boolean No Whether syntax highlighting assets are shipped (operator-controlled, off by default).
site.prism.theme String No Prism theme name.
site.favicon String Yes URL to the favicon.
site.logo String Yes URL to the logo image.
site.public_blog_url() Method No Returns blog_home without trailing slash, prefixed with https:// when it lacks a scheme.

Index Context (posts)

Available only in index.html: a list of published-post dicts for the current page. Pagination is driven by the ?page= query parameter; no page number, total count, or next/previous variables are passed to the template.

Variable Type Nullable Description
post.id Integer No Database id.
post.slug String No URL-friendly identifier.
post.title String No Title of the post.
post.status String No Always published on the public index.
post.excerpt String Yes Short summary.
post.cover String Yes URL to the cover image.
post.canonical_url String Yes Operator-supplied canonical override, if any.
post.author.id Integer No Author id.
post.author.display_name String No Name of the writer.
post.author.avatar_url String Yes URL to the author's avatar.
post.published_at Datetime Yes Publish date. Format with strftime.
post.created_at / post.updated_at Datetime No Record timestamps.
post.reading_time Integer No Estimated reading time in minutes.
post.tags List No List of dicts with id, name, and slug.

Post Context (post)

Available only in post.html. This is not a superset of the index context — it is built separately and omits id, status, and created_at.

Variable Type Nullable Description
post.slug String No URL-friendly identifier.
post.title String No Title of the post (post.name is an alias).
post.content String No Rendered HTML of the post body. Must be emitted with the safe filter.
post.excerpt String Yes Short summary.
post.cover String Yes URL to the cover image.
post.canonical String No Canonical URL: the operator override if set, otherwise public_blog_url()/slug.
post.canonical_url String Yes Raw operator-supplied canonical override.
post.author.display_name String No Name of the writer (no id in this context).
post.author.avatar_url String Yes URL to the author's avatar.
post.reading_time Integer No Estimated reading time in minutes.
post.published_at Datetime Yes Publish date. Format with strftime.
post.updated_at Datetime Yes Last update date.
post.tags List No List of dicts with id, name, and slug.
post.toc List No Recursive list of dicts (level, id, name, children) covering H2–H4.
post.faqs List Yes List of dicts (with question and answer)

4. Design Defaults (template.yaml)

Templates can declare baseline design tokens. These are deep-merged under the operator's config.yaml, so operator values win key-by-key. Only the fields below are permitted — any other key (including prism.enabled or prism.languages, which are operator-only) fails validation at startup.

fonts:
  heading: Inter
  body: Merriweather

colors:
  primary: "#1a1a1a"
  secondary: "#6b6b6b"
  accent: "#E9793A"
  background: "#ffffff"

prism:
  theme: tomorrow

Note: syntax highlighting itself is opt-in by the operator (prism.enabled, default false). A template can only choose the default theme.

5. CSS and Styling

Do not hardcode hex values or font families in your CSS. Use the CSS variables automatically generated and injected by plym. The final bundle is assembled in this order — color variables, font variables, self-hosted webfont @font-face rules, Prism theme CSS (only when Prism is enabled), then your template's *.css files alphabetically — minified, and inlined as a single <style> block in <head>. When Prism is enabled, its core + language JS is inlined in a <script> before </body>.

Plym sets plym-index or plym-post as the class on the <body> element itself, so you can scope styles per page type.

CSS Variable Maps To Example Usage
var(--color-primary) config.colors.primary color: var(--color-primary);
var(--color-secondary) config.colors.secondary color: var(--color-secondary);
var(--color-accent) config.colors.accent color: var(--color-accent);
var(--color-background) config.colors.background background: var(--color-background);
var(--font-heading) config.fonts.heading font-family: var(--font-heading), sans-serif;
var(--font-body) config.fonts.body font-family: var(--font-body), serif;

The font variables already contain the quoted family name, so append only generic fallbacks.

Available Font Weights

Plym restricts downloaded Google Fonts for performance, and subsets them to basic Latin (ASCII letters, digits, punctuation). Characters outside that range fall back to system fonts.

Font Slot Available Weights Italics Allowed
Heading 600, 900 No
Body 400 No

Note: Any other weight, or italic styles, will be synthesized by the browser (faux bold / faux italic).

6. Markdown Target Elements

Your CSS must account for the HTML output generated by plym's markdown renderer.

Markdown Element HTML Output Example
Headings (H2–H4) <h2 id="slug"><a class="toclink" href="#slug">Text</a></h2>
Fenced Code <pre><code class="language-python">...</code></pre>
Inline Code <code>...</code>
Images <img src="url" alt="alt" loading="lazy" decoding="async">
Gallery / carousel (custom ```gallery fence, one image per line) <div class="plym-gallery"><img src="url" alt="alt" loading="lazy" decoding="async">...</div>
Tables Standard <table> markup.
Task Lists <li class="task-list-item"><input type="checkbox" disabled checked>...</li>
Strikethrough (~~text~~) <del>strike</del>
Subscript (~text~) <sub>text</sub>
Footnotes <sup id="fnref:1"><a href="#fn:1">...</a></sup>

7. Minimum Viable Template

Below is a functional starting point for a template.

index.html

<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.slug }}">{{ 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 %}
      </small>
    </article>
  {% else %}
    <p>Nothing here yet.</p>
  {% endfor %}
</main>

post.html

<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>

css/base.css

body {
    max-width: 720px;
    margin: 0 auto;
    padding: 1rem;
    color: var(--color-primary);
    background: var(--color-background);
}
a { color: var(--color-accent); }
img { max-width: 100%; height: auto; }
pre { overflow-x: auto; padding: 1rem; }

8. Performance and Accessibility Budgets

Templates submitted to the main plym repository must meet the following budgets. These are review policy — they are not enforced by the build.

Performance Targets

Metric Threshold
Lighthouse Performance ≥ 95
LCP (Simulated 3G) < 1.5 s
Minified CSS Total < 30 KB
Inlined <style> block < 50 KB
Third-party Requests 0 (self-hosted webfonts under {blog_prefix}/webfonts/ are the only extra same-origin fetches)

Accessibility Rules

Requirement Detail
Interactivity All interactive elements must be keyboard reachable with visible focus rings.
ARIA Labels Required on icon-only buttons or links.
Contrast WCAG AA contrast (≥ 4.5:1) for body text against background.
Motion Must respect prefers-reduced-motion: reduce.

9. Anti-Patterns

  • Writing <html>, <head>, or <meta> tags in template files — plym does not detect this; it silently produces a malformed nested document.
  • Requesting external assets (CDNs, JS frameworks, font files) via <link> or <script> — violates the zero-third-party budget.
  • Rendering post.content without the safe filter — autoescaping is on, so the post body will display as escaped HTML source text.
  • Assuming nullable variables (e.g. post.cover, post.excerpt, site.favicon, site.logo, post.published_at) are always present without {% if %} guards.
  • Declaring operator-only keys (prism.enabled, prism.languages, site identity, injection snippets) in template.yaml — this fails config validation and prevents startup.

Frequently asked

Can I customise the design & layout of my plym template?
Yes, you can customise every bit of CSS. plym engine expects you to handle context objects it passes. The UI and layout are independent.
Adarsh

Written by

Adarsh