Maintainers, Blogs & Comments
This is the authoring reference for three docs features: maintainer profiles,
blog posts, and blog commenting. Everything here is driven by page front
matter — the key: value block at the top of a Markdown file between ---
fences. No code changes are needed to add a maintainer, publish a post, or turn
comments on.
A note on image paths that applies throughout: avatar, cover, and blog
image are resolved against the asset base, i.e. the plugin's public/
folder. So avatar: img/jane.jpg loads from <plugin>/public/img/jane.jpg. If
an image doesn't show, open that resolved URL directly — a 404 means the file
isn't where the path points, not a bug in the page.
Maintainers
The maintainers feature has two kinds of page: one index that lists everyone as cards, and one profile per person that shows a full social-style header (cover banner, circular avatar, contacts, socials, projects) above a Markdown bio.
How a page is recognised
- A page becomes the index when its front matter sets
maintainers_dir. - A page becomes a profile when it lives inside a maintainers folder
(
maintainers/orteam/), or when it setsmaintainer: truefrom any folder. Profiles do not needmaintainers_dir— they're detected by location.
The index page
Create one file (for example maintainers.md) with this front matter:
| Key | Required | Description |
|---|---|---|
title | yes | Heading for the list page, e.g. Core Maintainers. |
maintainers_dir | yes | The folder that holds profile files. Normally maintainers. |
description | no | Short intro shown by the theme. |
order | no | Sidebar/sort weight. Higher sorts earlier. |
---
title: Core Maintainers
description: The people who build and steward SepoDesk.
maintainers_dir: maintainers
order: 90
---
Meet the team. Click any profile for full details.
Every non-underscore file inside the maintainers/ folder then appears as a
card, sorted by order (highest first), then by name.
A maintainer profile
Create one Markdown file per person inside the maintainers/ folder. The
Markdown body becomes the bio shown under the header.
| Key | Required | Description |
|---|---|---|
title | yes | The person's name. |
role | no | Job/role line, e.g. Core Maintainer — Routing. |
avatar | no | Circular profile image, from public/. Falls back to a placeholder icon if omitted. |
cover | no | Wide banner image behind the avatar, from public/. Falls back to a gradient if omitted. |
email | no | Shown as a mailto: chip. |
location | no | Shown with a pin icon. |
description | no | One-line blurb shown on the list card (not the profile page). |
socials | no | Pipe list of social links (see below). |
projects | no | Pipe list of projects (see below). |
order | no | Card sort weight. Higher sorts earlier. |
maintainer | no | Set true only if the file lives outside the maintainers//team/ folders and you still want it treated as a profile. |
banner is accepted as an alias for cover; image/photo are accepted
aliases for avatar.
socials and projects syntax
Both use the same pipe-delimited list the rest of the theme uses. Entries are
separated by commas; fields within an entry by |.
- socials —
Label|URL. The label picks the icon automatically.
socials: [GitHub|https://github.com/jane, Website|https://jane.dev, LinkedIn|https://linkedin.com/in/jane]
- projects —
Name|URL|Note. The note is an optional short description.
projects: [SepoEngine Core|https://github.com/sepodesk/core|Routing & CMS, Sepo CLI|https://github.com/sepodesk/cli|Scaffolding]
Because entries are comma-separated, avoid commas inside a label, URL, or note.
Social icon mapping
The label you write is matched (case-insensitively) to a built-in icon. Anything unmatched gets a generic link icon.
| If the label contains… | Icon shown |
|---|---|
| github, gitlab, bitbucket, git, code | code |
| mail, email | |
twitter, x, mastodon, threads, discord, slack, telegram, chat | message |
| youtube, vimeo, twitch | film |
| instagram, dribbble, behance, photo | camera |
| linkedin, facebook | users |
| site, web, blog, portfolio, home | globe |
| (anything else) | link |
Full profile example
---
title: Jane Okoro
role: Core Maintainer — Routing & CMS
avatar: img/team/jane.jpg
cover: img/team/jane-cover.jpg
email: jane@sepodesk.dev
location: Nairobi, KE
description: Leads the router, page store, and docs mount.
socials: [GitHub|https://github.com/jane, Website|https://jane.dev]
projects: [SepoEngine Core|https://github.com/sepodesk/core|Router & page store, Docs Renderer|/docs|The docs mount]
order: 100
---
Jane has maintained the routing layer since the first release.
## Focus areas
- Multi-root URL resolution
- The file-backed page store
Making it reachable
Maintainer files are hidden from the left sidebar automatically (the folder is in
the sidebar's exclusion list alongside blogs). To surface the index in the top
navigation, add a line to _nav.md:
- [Maintainers](/docs/maintainers)
Blogs
Blogs also have an index (the post list) and individual posts.
The blog index page
One file (for example blog.md) turns on the aggregator:
| Key | Required | Description |
|---|---|---|
title | yes | Heading for the list page. |
aggregate_dir | yes | The folder that holds posts. Normally blogs. |
order | no | Sidebar/sort weight. |
The index renders every post in that folder as a card with list/grid toggle,
pagination (10 per page), a support widget, and a featured-sponsor panel built
from posts that carry an advert.
A blog post
Create one Markdown file per post inside the blogs/ folder.
| Key | Required | Description | ||
|---|---|---|---|---|
title | yes | Post title. | ||
date | no | Display date, shown in the list meta line. | ||
author | no | Shown in the list meta line. | ||
description | no | Card blurb (alias: subtitle). | ||
image | no | Thumbnail on the list and a short banner at the top of the post (alias: hero_image). From public/. | ||
advert | no | Featured-sponsor entry, `Label | URL | ImagePath` (see below). |
order | no | List sort weight. Higher sorts earlier. | ||
allowCommentOnPost | no | true turns on the comment thread and form. Defaults to off. | ||
allowCommentsListToGuestUpto | no | How many comments a signed-out visitor sees before a login prompt. Defaults to 0. |
The banner image is intentionally short (about 210px, less on mobile). The two comment keys are covered in full in the next section.
advert syntax
A single pipe-delimited value: Label|URL|ImagePath. The image path resolves
from public/. Posts with an advert are collected into the "Featured Sponsors"
panel on the index (first four on the page).
advert: Acme Hosting|https://acme.example|img/ads/acme.png
The support widget (_support.md)
The donate/sponsor card on the blog index is configured by a file named
_support.md inside the blogs/ folder (underscore-prefixed files are never
listed as posts). Its front matter:
| Key | Description |
|---|---|
title | Card heading. |
description | Short text under the heading. |
raised | Amount raised (number). |
goal | Fundraising goal (number). |
currency | Currency symbol, e.g. $. |
donate_url | Donate button link. |
sponsor_url | Sponsor button link. |
donate_button_text | Donate button label. |
sponsor_button_text | Sponsor button label. |
Full post example
---
title: Shipping the new router
date: 2026-08-01
author: Jane Okoro
description: How multi-root URL resolution finally landed.
image: img/blog/router.jpg
advert: Acme Hosting|https://acme.example|img/ads/acme.png
allowCommentOnPost: true
allowCommentsListToGuestUpto: 5
order: 100
---
The routing rewrite is done. Here's what changed…
Comments
Comments are attached to blog posts and controlled entirely by the two post front-matter keys above.
Turning them on
| Key | Effect |
|---|---|
allowCommentOnPost: true | Shows the comment thread. Signed-in visitors also get the comment form; the server refuses posts to any post where this is not true. |
allowCommentsListToGuestUpto | The number of top-level comments a signed-out visitor may read. Beyond that they see a "log in to read them" prompt. Signed-in visitors always see everything. Set to 0 to show guests none. |
Omit allowCommentOnPost (or set it false) and the post has no comments at all.
Who can do what
- Signed-out visitors can read up to
allowCommentsListToGuestUptocomments, then are prompted to log in. They cannot post. - Signed-in visitors can read everything, post comments, and reply to any comment.
- Editing and deleting a comment is allowed for its author or an admin. This is enforced on the server — the buttons alone grant nothing.
How a commenter appears
The form does not expose real names from the database. Each commenter chooses how they're shown:
- Nickname — type one in the nickname field.
- Anonymous — leave the nickname blank; the comment shows as "Anonymous".
- Email metadata — an opt-in checkbox to attach their account email to the comment's stored metadata. The email is kept as metadata and is not printed in the thread.
Internally each comment stores the session user_id as an ownership key (never
displayed) so edit/delete can be authorised.
Where comments live (data model)
Comments are stored as one JSON file per post, as a nested tree:
content/pages/blogs/comments/<post-basename>.comments.json
<post-basename> is the post's filename without its folder or .md extension —
so blogs/shipping-the-new-router.md maps to
shipping-the-new-router.comments.json.
Each comment node:
| Field | Description |
|---|---|
id | Unique comment id. |
nickname | Display name, or Anonymous. |
body | Comment text. |
timestamp | Unix time posted. |
user_id | Author's session id — ownership key, never shown. |
replies | Array of child comment nodes (same shape). |
edited_timestamp | Present after an edit. |
API reference
All comment actions use one endpoint:
POST /api/v2/pages/comments (requires AuthMiddleware — any signed-in user)
The action is selected by the action field. JSON body fields:
| Field | Used by | Description |
|---|---|---|
slug | all | The post's slug, e.g. blogs/my-post.md. |
action | all | create (default), edit, or delete. |
comment_body | create, edit | The comment text. |
nickname | create | Chosen display name (blank → Anonymous). |
parent_id | create | Set to reply under an existing comment. |
comment_id | edit, delete | Which comment to change. |
use_email | create | 1 to attach account email as metadata. |
Responses follow the site convention: { "status": "success", "data": … } or
{ "status": "error", "message": … } with an appropriate HTTP code (401 not
signed in, 403 not permitted / comments closed, 404 comment not found, 422
validation).
Security model
- Only signed-in users reach the endpoint (
AuthMiddleware), and the route sits outside the admin-only pages group, so ordinary members can comment. createis refused unless the target post's front matter hasallowCommentOnPost: true.editanddeleterequire the caller'suser_idto match the comment's, or the caller to be an admin.- The guest read limit is enforced server-side as well as in the view.
Prerequisites
For the form to appear and for tokens to be sent, the docs shell must expose two
values to the view: $isLoggedIn (true for any signed-in user) and a
$csrfToken for signed-in users, not just editors. Both are set in the shell's
bootstrap; if the form never shows for a logged-in member, that's the place to
check.