This project uses aphid, a static site generator. Themes are directories containing Tera
templates (Jinja2-style) and optional static files. The goal is to design a complete theme.
# Theme directory layout
```
mytheme/
theme.toml
templates/
base.html
home.html
blog_post.html
blog_index.html
wiki_page.html
wiki_index.html
page.html
tag.html
tags_index.html
pagination.html
404.html
static/
css/
js/
```
`theme.toml` is required and must contain at least:
```toml
name = "mytheme"
version = "0.1.0"
```
`description` is optional.
# Template engine
Templates use Tera — a Jinja2-style engine. Key syntax:
- `{{ variable }}` — output a value
- `{{ variable | safe }}` — output HTML without escaping (required for rendered content)
- `{% block name %}...{% endblock %}` — define/override blocks
- `{% extends "base.html" %}` — inherit from a parent template
- `{% for item in list %}...{% endfor %}` — loops
- `{% if condition %}...{% elif %}...{% else %}...{% endif %}` — conditionals
- `{# comment #}` — comments
The standard pattern is a `base.html` layout that all other templates extend.
# Global variables (available in every template)
| `site_title` | string | Site title from `aphid.toml` |
| `site_description` | string? | Site description from `aphid.toml` |
| `social_image_url` | string? | Default OpenGraph image URL |
| `version` | string | The aphid binary version |
| `nav_pages` | list | Standalone pages sorted by `order`; each has `title` and `url` |
| `socials` | list | Social links from `aphid.toml`; each has `platform` and `url` |
| `favicon_tags` | string | Pre-rendered `<link rel="icon">` tags |
| `feed_atom_url` | string | URL for the Atom feed |
| `feed_rss_url` | string | URL for the RSS feed |
# base.html
The root layout. All other templates extend this. Must define blocks that child templates
override. Typically contains `<html>`, `<head>`, navigation, header, footer.
Example skeleton:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block page_title %}{{ site_title }}{% endblock %}</title>
<link rel="stylesheet" href="/static/css/theme.css">
{{ favicon_tags | safe }}
</head>
<body>
<nav>
<a href="/">{{ site_title }}</a>
{% for page in nav_pages %}
<a href="{{ page.url }}">{{ page.title }}</a>
{% endfor %}
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% for social in socials %}
<a href="{{ social.url }}">{{ social.platform }}</a>
{% endfor %}
<span>Built with aphid {{ version }}</span>
</footer>
</body>
</html>
```
# home.html
Renders the site root (`/index.html`).
| `posts` | list | All blog posts (see post entry shape below) |
| `home` | object? | Present when `content/home.md` exists; has `content` (rendered HTML — use `\| safe`) |
| `contains_mermaid` | bool | True if any rendered block uses Mermaid |
| `popular_tags` | list | All tags across blog and wiki, sorted by descending count then ascending name. Each entry has `name`, `slug`, `count`. Counts match `/tags/`. Slice with `\| slice(end=N)` for top-N. |
# blog_post.html
Renders a single blog post.
| `title` | string | Post title |
| `url` | string | Clean URL, e.g. `/blog/my-post/` |
| `canonical_url` | string | Absolute canonical URL |
| `content` | string | Rendered HTML body — always use `\| safe` |
| `toc` | list | Heading entries; each has `level` (int), `text` (string), `id` (string) |
| `author` | object | `{ name, link?, image? }` |
| `image` | string? | Hero image path or URL |
| `og_image` | string? | Absolute social-share image URL |
| `description` | string? | Short summary |
| `created` | string | Publication date `YYYY-MM-DD` |
| `updated` | string? | Last-edited date |
| `reading_time_minutes` | int | Estimated read time |
| `tags` | list | Each has `name` and `slug` |
| `newer_post` | object? | `{ title, url }` for navigation |
| `older_post` | object? | `{ title, url }` for navigation |
| `contains_mermaid` | bool | |
# blog_index.html
Renders the blog listing at `/blog/`.
| `posts` | list | Posts on this page (see post entry shape below) |
| `pagination` | object? | Pagination state |
# Post entry shape (used in home.html, blog_index.html, tag.html)
| `title` | string | Post title |
| `url` | string | Clean URL |
| `created` | string? | Publication date |
| `image` | string? | Hero image path or URL |
| `description` | string? | Short summary from frontmatter |
| `reading_time_minutes` | int | Reading-time estimate (rounded up, minimum 1) — same value as on the post template |
| `tags` | list | Each has `name` and `slug` |
# wiki_page.html
Renders a single wiki page. Has many of the same variables as `blog_post.html`, plus:
| `category` | string | Category name. Falls back to `wiki_default_category` (default `"Other"`) when frontmatter omits it, so always non-empty |
| `backlinks` | list | Pages linking here; each has `title` and `url` |
| `wiki_categories` | list | All wiki pages grouped by category — for a sidebar. Each entry has `name` (string), `description` (string, optional), `icon` (string, optional — full `/static/…` path to an SVG), and `pages` (list of `{title, url}`). Named categories listed in `wiki_categories` config appear first, then alphabetical; the default catch-all group sorts last. |
`author`, `image` are always absent on wiki pages. `created`, `updated`, `tags` are present
only if set in frontmatter.
# wiki_index.html
Renders the wiki listing at `/wiki/`.
| `categories` | list | Same shape as `wiki_categories` on `wiki_page.html` |
| `wiki_intro` | object? | Present when `content/wiki.md` exists. Has `content` (string — rendered HTML, pass through `\| safe`). |
| `contains_mermaid` | bool | `true` when `wiki.md` contains mermaid blocks. |
# page.html
Renders standalone pages (About, Contact, etc.). Same variables as `blog_post.html`, but
`author`, `image`, `created`, `updated`, and `tags` are always absent.
# tag.html
Renders a single tag page.
| `tag` | string | Tag display name |
| `tag_slug` | string | URL-safe slug |
| `blog_posts` | list | All blog posts with this tag (post entry shape). Guard with a length check — may be empty |
| `wiki_pages` | list | All wiki pages with this tag. Same shape. Also may be empty |
Tag pages are not paginated.
# tags_index.html
Renders the tag listing at `/tags/`.
| `tags` | list | All tags; each has `name`, `slug`, and `count` |
# pagination.html
Included by listing templates (`blog_index.html`). Receives the surrounding template's
`pagination` object:
| `current` | int | 1-indexed current page number |
| `total` | int | Total number of pages |
| `prev_url` | string? | URL of the previous page, `null` on page 1 |
| `next_url` | string? | URL of the next page, `null` on the last page |
| `pages` | list | Every page — each entry has `n` (int) and `url` (string) for numeric nav |
`pagination` is `null` when the entire listing fits on one page; templates should guard
the include with `{% if pagination %}`. Tag pages are not paginated — `tag.html` never
receives a `pagination` variable.
# 404.html
Error page.
| `not_found` | object? | Present when `content/404.md` exists; has `content` (use `\| safe`) |
| `contains_mermaid` | bool | |
# Static files and CSS
Place stylesheets, scripts, and other assets in `mytheme/static/`. They are copied to the
output's `static/` directory. Reference them with absolute paths:
```html
<link rel="stylesheet" href="/static/css/theme.css">
```
If the user's `static_dir` has a file with the same name, the user's version wins.
# Syntax highlighting CSS
Code blocks use CSS classes prefixed `hl-`. The theme stylesheet must define colors for
these classes. Key token classes:
- `hl-keyword` — language keywords (`fn`, `if`, `return`)
- `hl-string` — string literals
- `hl-comment` — comments
- `hl-type` — type names
- `hl-function` — function/method names
- `hl-number` — numeric literals
- `hl-operator` — operators
- `hl-punctuation` — brackets, commas, semicolons
- `hl-variable` — variable names
- `hl-attribute` — attributes/decorators
- `hl-tag` — HTML/XML tags
- `hl-entity` — entities and special names
Wrap code blocks in a container with `overflow-x: auto` for horizontal scrolling. Use a
monospace font and a background color that contrasts with the page.
# Mermaid diagrams
When `contains_mermaid` is true, the template should load `/static/js/mermaid.min.js`
(bundled by aphid) and initialise it. The renderer wraps Mermaid blocks in
`<pre class="mermaid">`, which Mermaid picks up on initialise. Gate the script tag on
`contains_mermaid` so pages without diagrams don't pay the download cost:
```html
{% if contains_mermaid %}
<script src="/static/js/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true });</script>
{% endif %}
```
# Social meta tags
`base.html` is expected to emit OpenGraph and Twitter card meta tags in the `<head>` of
every page. Two Tera blocks are reserved for per-template overrides:
| `og_type` | `"website"` | `blog_post.html` → `"article"` |
| `article_meta` | empty | `blog_post.html` → `article:published_time`, `article:modified_time`, `article:author`, `article:tag` |
Tag content comes from these context fields, with fallbacks:
- `og:title` / `twitter:title` — page `title`, falling back to `site_title`
- `og:description` / `twitter:description` / `<meta name="description">` — page `description`, falling back to `site_description`
- `og:url` — page `canonical_url` (only emitted when the page exposes one)
- `og:image` / `twitter:image` — blog post `og_image`, falling back to `social_image_url`
- `twitter:card` — `summary_large_image` when an image is set, `summary` otherwise
- `og:site_name` — `site_title`
Pages without an image still produce valid tags — they just drop the `og:image` /
`twitter:image` lines and downgrade the card type to `summary`.
# Design guidelines
- The page title is `<h1>`; body headings start at `<h2>` (the markdown pipeline shifts
heading levels up by one).
- `content` is rendered HTML — use `{{ content | safe }}`.
- `toc` entries can build a table of contents sidebar or in-page nav.
- `backlinks` are most useful on wiki pages — show them in a footer or sidebar section.
- `wiki_categories` on `wiki_page.html` enables a sidebar showing all wiki pages grouped by
category, with the current page highlighted (compare `page.url == url`). Each category
may also have `description` and `icon` for richer rendering.
- `wiki_intro` on `wiki_index.html` renders an optional intro section above the category cards.
- Test the theme against pages with: no image, no tags, no TOC, very long content, many
backlinks, and the 404 page.
- Ensure the layout is responsive — test at mobile, tablet, and desktop widths.