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.contentwithout thesafefilter — 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) intemplate.yaml— this fails config validation and prevents startup.
Frequently asked