@geekity/cms 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -6
- package/admin/pages/account/login.njk +3 -0
- package/admin/pages/documents/editor.njk +7 -0
- package/admin/pages/indieauth/consent.njk +51 -0
- package/admin/pages/indieauth/refused.njk +11 -0
- package/admin/pages/settings/general.njk +9 -0
- package/admin/pages/settings/reading.njk +93 -0
- package/admin/pages/users/apps.njk +74 -0
- package/admin/pages/users/edit.njk +12 -0
- package/admin/static/admin.css +27 -0
- package/dist/admin/accounts.d.ts +3 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +5 -0
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/documents.d.ts +2 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +17 -1
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/headers.d.ts +5 -1
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +7 -3
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +2 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +48 -5
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-general.js +1 -1
- package/dist/admin/settings-general.js.map +1 -1
- package/dist/admin/settings-reading.d.ts.map +1 -1
- package/dist/admin/settings-reading.js +15 -1
- package/dist/admin/settings-reading.js.map +1 -1
- package/dist/admin/settings.d.ts +79 -2
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +168 -2
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/templates.d.ts +3 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +3 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/users.d.ts +8 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +9 -0
- package/dist/admin/users.js.map +1 -1
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/env.d.ts +14 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +18 -5
- package/dist/federation/article.js.map +1 -1
- package/dist/images/icons.d.ts +44 -25
- package/dist/images/icons.d.ts.map +1 -1
- package/dist/images/icons.js +205 -60
- package/dist/images/icons.js.map +1 -1
- package/dist/images/index.d.ts +2 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +1 -1
- package/dist/images/index.js.map +1 -1
- package/dist/images/markup.d.ts +14 -0
- package/dist/images/markup.d.ts.map +1 -1
- package/dist/images/markup.js +16 -11
- package/dist/images/markup.js.map +1 -1
- package/dist/images/variants.d.ts.map +1 -1
- package/dist/images/variants.js +3 -4
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/indexnow.d.ts +34 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +141 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/indieauth/bearer.d.ts +39 -0
- package/dist/indieauth/bearer.d.ts.map +1 -0
- package/dist/indieauth/bearer.js +63 -0
- package/dist/indieauth/bearer.js.map +1 -0
- package/dist/indieauth/client.d.ts +61 -0
- package/dist/indieauth/client.d.ts.map +1 -0
- package/dist/indieauth/client.js +139 -0
- package/dist/indieauth/client.js.map +1 -0
- package/dist/indieauth/connected-apps.d.ts +23 -0
- package/dist/indieauth/connected-apps.d.ts.map +1 -0
- package/dist/indieauth/connected-apps.js +73 -0
- package/dist/indieauth/connected-apps.js.map +1 -0
- package/dist/indieauth/consent.d.ts +28 -0
- package/dist/indieauth/consent.d.ts.map +1 -0
- package/dist/indieauth/consent.js +185 -0
- package/dist/indieauth/consent.js.map +1 -0
- package/dist/indieauth/discovery.d.ts +87 -0
- package/dist/indieauth/discovery.d.ts.map +1 -0
- package/dist/indieauth/discovery.js +127 -0
- package/dist/indieauth/discovery.js.map +1 -0
- package/dist/indieauth/grants.d.ts +57 -0
- package/dist/indieauth/grants.d.ts.map +1 -0
- package/dist/indieauth/grants.js +35 -0
- package/dist/indieauth/grants.js.map +1 -0
- package/dist/indieauth/identity.d.ts +36 -0
- package/dist/indieauth/identity.d.ts.map +1 -0
- package/dist/indieauth/identity.js +78 -0
- package/dist/indieauth/identity.js.map +1 -0
- package/dist/indieauth/redeem.d.ts +47 -0
- package/dist/indieauth/redeem.d.ts.map +1 -0
- package/dist/indieauth/redeem.js +69 -0
- package/dist/indieauth/redeem.js.map +1 -0
- package/dist/indieauth/request.d.ts +47 -0
- package/dist/indieauth/request.d.ts.map +1 -0
- package/dist/indieauth/request.js +104 -0
- package/dist/indieauth/request.js.map +1 -0
- package/dist/indieauth/token.d.ts +19 -0
- package/dist/indieauth/token.d.ts.map +1 -0
- package/dist/indieauth/token.js +110 -0
- package/dist/indieauth/token.js.map +1 -0
- package/dist/indieauth/tokens.d.ts +131 -0
- package/dist/indieauth/tokens.d.ts.map +1 -0
- package/dist/indieauth/tokens.js +205 -0
- package/dist/indieauth/tokens.js.map +1 -0
- package/dist/web/archive.d.ts +1 -1
- package/dist/web/archive.d.ts.map +1 -1
- package/dist/web/archive.js +3 -2
- package/dist/web/archive.js.map +1 -1
- package/dist/web/context.d.ts +25 -0
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +18 -0
- package/dist/web/context.js.map +1 -1
- package/dist/web/feed-atom.d.ts.map +1 -1
- package/dist/web/feed-atom.js +3 -1
- package/dist/web/feed-atom.js.map +1 -1
- package/dist/web/feed-item.d.ts +11 -2
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +9 -3
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-json.d.ts +4 -0
- package/dist/web/feed-json.d.ts.map +1 -1
- package/dist/web/feed-json.js +4 -1
- package/dist/web/feed-json.js.map +1 -1
- package/dist/web/feed-rss.d.ts.map +1 -1
- package/dist/web/feed-rss.js +16 -3
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +28 -0
- package/dist/web/feed-source.d.ts.map +1 -1
- package/dist/web/feed-source.js +31 -0
- package/dist/web/feed-source.js.map +1 -1
- package/dist/web/feed-xml.d.ts +2 -0
- package/dist/web/feed-xml.d.ts.map +1 -1
- package/dist/web/feed-xml.js +2 -0
- package/dist/web/feed-xml.js.map +1 -1
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +8 -1
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/index.d.ts +3 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +2 -1
- package/dist/web/index.js.map +1 -1
- package/dist/web/indexnow.d.ts +9 -0
- package/dist/web/indexnow.d.ts.map +1 -0
- package/dist/web/indexnow.js +27 -0
- package/dist/web/indexnow.js.map +1 -0
- package/dist/web/llms.d.ts +58 -0
- package/dist/web/llms.d.ts.map +1 -0
- package/dist/web/llms.js +91 -0
- package/dist/web/llms.js.map +1 -0
- package/dist/web/locale.d.ts +41 -0
- package/dist/web/locale.d.ts.map +1 -0
- package/dist/web/locale.js +56 -0
- package/dist/web/locale.js.map +1 -0
- package/dist/web/manifest.d.ts +50 -0
- package/dist/web/manifest.d.ts.map +1 -0
- package/dist/web/manifest.js +56 -0
- package/dist/web/manifest.js.map +1 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +6 -0
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/render.d.ts +6 -1
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +68 -22
- package/dist/web/render.js.map +1 -1
- package/dist/web/robots.d.ts +89 -0
- package/dist/web/robots.d.ts.map +1 -0
- package/dist/web/robots.js +259 -0
- package/dist/web/robots.js.map +1 -0
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +105 -6
- package/dist/web/routes.js.map +1 -1
- package/dist/web/share-image.d.ts +51 -0
- package/dist/web/share-image.d.ts.map +1 -0
- package/dist/web/share-image.js +43 -0
- package/dist/web/share-image.js.map +1 -0
- package/dist/web/sitemap.d.ts +0 -26
- package/dist/web/sitemap.d.ts.map +1 -1
- package/dist/web/sitemap.js +0 -44
- package/dist/web/sitemap.js.map +1 -1
- package/dist/web/templates.d.ts +16 -9
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +83 -55
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +33 -0
- package/dist/web/themes.d.ts.map +1 -1
- package/dist/web/themes.js +57 -0
- package/dist/web/themes.js.map +1 -1
- package/dist/webmention/discovery.d.ts +4 -2
- package/dist/webmention/discovery.d.ts.map +1 -1
- package/dist/webmention/discovery.js +14 -8
- package/dist/webmention/discovery.js.map +1 -1
- package/dist/webmention/fetch-public.d.ts +2 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -1
- package/dist/webmention/fetch-public.js +1 -1
- package/dist/webmention/fetch-public.js.map +1 -1
- package/package.json +7 -2
- package/themes/default/README.md +320 -139
- package/themes/default/layouts/404.njk +2 -0
- package/themes/default/layouts/500.njk +2 -0
- package/themes/default/layouts/503.njk +2 -0
- package/themes/default/layouts/author.njk +3 -1
- package/themes/default/layouts/base.njk +96 -17
- package/themes/default/layouts/category.njk +4 -2
- package/themes/default/layouts/front-page.njk +7 -2
- package/themes/default/layouts/home.njk +10 -0
- package/themes/default/layouts/page.njk +7 -4
- package/themes/default/layouts/post.njk +35 -25
- package/themes/default/layouts/search.njk +5 -9
- package/themes/default/layouts/tag.njk +7 -1
- package/themes/default/partials/archive.njk +3 -1
- package/themes/default/partials/bio.njk +9 -2
- package/themes/default/partials/breadcrumbs.njk +20 -0
- package/themes/default/partials/comment-form.njk +8 -0
- package/themes/default/partials/contact-form.njk +3 -0
- package/themes/default/partials/conversation.njk +14 -4
- package/themes/default/partials/jsonld.njk +19 -4
- package/themes/default/partials/kicker.njk +36 -0
- package/themes/default/partials/lang.njk +7 -0
- package/themes/default/partials/post-list.njk +43 -47
- package/themes/default/partials/reply-context.njk +14 -5
- package/themes/default/src/style.css +1109 -0
- package/themes/default/static/style.css +2124 -1160
- package/themes/default/theme.json +3 -1
package/themes/default/README.md
CHANGED
|
@@ -23,6 +23,8 @@ themes/default/
|
|
|
23
23
|
post-list.njk the h-feed a listing is made of
|
|
24
24
|
pagination.njk previous/next pager
|
|
25
25
|
tags.njk macros for tag and category links
|
|
26
|
+
kicker.njk the line above an entry naming its kind and date
|
|
27
|
+
reply-context.njk the citation of what a reply answers
|
|
26
28
|
bio.njk who an entry is by, as an h-card
|
|
27
29
|
menu.njk one named menu, as a nav of links
|
|
28
30
|
feeds.njk macros for the feed links in <head>
|
|
@@ -39,12 +41,39 @@ themes/default/
|
|
|
39
41
|
comment-digest.*.njk the hourly or daily digest of what is waiting
|
|
40
42
|
comment-reply.*.njk the notice a commenter gets about a reply
|
|
41
43
|
contact-message.*.njk a message from a page's contact form
|
|
44
|
+
src/
|
|
45
|
+
style.css the stylesheet's source, compiled with Tailwind
|
|
42
46
|
static/
|
|
43
|
-
style.css served at /theme/style.css
|
|
47
|
+
style.css the compiled stylesheet, served at /theme/style.css
|
|
48
|
+
highlight.js the code highlighter
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
Plain Nunjucks and
|
|
47
|
-
|
|
51
|
+
Plain Nunjucks templates and one stylesheet. The stylesheet is written in
|
|
52
|
+
Tailwind v4 (`src/style.css`) and compiled when the package is built, so
|
|
53
|
+
`static/style.css` is a build product: gitignored in this repository, written
|
|
54
|
+
by `pnpm build` (which the root `prepare` runs after an install and before a
|
|
55
|
+
publish), and shipped in the package like every other file here. The compile
|
|
56
|
+
happens once, on the machine that builds the package. A reader gets one plain
|
|
57
|
+
CSS file and no script, web font or CDN request because of it.
|
|
58
|
+
|
|
59
|
+
The design is Paper (doc-9, decision-22): one warm column, serif throughout,
|
|
60
|
+
sans for small labels, and a kicker above every entry naming its kind.
|
|
61
|
+
|
|
62
|
+
**Rebuilding the stylesheet.** After editing `src/style.css`:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pnpm --filter @geekity/cms build:theme
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Tailwind scans only this theme's `layouts/` and `partials/`, so a utility class
|
|
69
|
+
written in a template here is compiled in and nothing outside the theme is
|
|
70
|
+
read. The templates themselves use the semantic class names below and the look
|
|
71
|
+
lives in `src/style.css`, through `@apply`.
|
|
72
|
+
|
|
73
|
+
**A site theme owes Tailwind nothing.** Its own `static/style.css` replaces this
|
|
74
|
+
one wholesale, and any CSS that knows the class names in this README will do.
|
|
75
|
+
The demo theme's is plain CSS. A site that only wants different colours
|
|
76
|
+
overrides the `--color-*` custom properties instead; see [Colours](#colours).
|
|
48
77
|
|
|
49
78
|
## The manifest
|
|
50
79
|
|
|
@@ -54,7 +83,9 @@ that wants Tailwind or anything else brings its own.
|
|
|
54
83
|
{
|
|
55
84
|
"name": "Default",
|
|
56
85
|
"kind": "site",
|
|
57
|
-
"description": "One line about the theme."
|
|
86
|
+
"description": "One line about the theme.",
|
|
87
|
+
"colorScheme": "light dark",
|
|
88
|
+
"themeColor": { "light": "#faf7f2", "dark": "#171412" }
|
|
58
89
|
}
|
|
59
90
|
```
|
|
60
91
|
|
|
@@ -64,6 +95,14 @@ and `description` is optional. The directory name is the theme's id. A
|
|
|
64
95
|
directory without a readable manifest, or one naming a kind this CMS does not
|
|
65
96
|
have, is not a theme.
|
|
66
97
|
|
|
98
|
+
`colorScheme` and `themeColor` are the colours the head declares before the
|
|
99
|
+
stylesheet loads (see [The head](#the-head)). `colorScheme` is one of `normal`,
|
|
100
|
+
`light`, `dark`, `light dark`, `dark light` or `only light`, preferred scheme
|
|
101
|
+
first. `themeColor` is the colour of the browser around the page, one per
|
|
102
|
+
scheme, as a three- or six-digit hex colour with no alpha. Make it the page's
|
|
103
|
+
own background. A value outside those is ignored, and so is one a theme leaves
|
|
104
|
+
out: each falls back to this theme's, just as a template does.
|
|
105
|
+
|
|
67
106
|
## Overriding a template
|
|
68
107
|
|
|
69
108
|
A site keeps its themes under one directory, `themes/` by default and
|
|
@@ -122,8 +161,8 @@ packaged ones by name:
|
|
|
122
161
|
```
|
|
123
162
|
|
|
124
163
|
`layouts/base.njk` defines the blocks `title`, `speculationRules`,
|
|
125
|
-
`viewTransitions`, `head`, `alternates`, `header`, `
|
|
126
|
-
`scripts`, so most sites never have to copy it.
|
|
164
|
+
`viewTransitions`, `head`, `alternates`, `header`, `breadcrumbs`, `content`,
|
|
165
|
+
`footer` and `scripts`, so most sites never have to copy it.
|
|
127
166
|
|
|
128
167
|
Two layouts are named for the pages the Reading settings pick:
|
|
129
168
|
|
|
@@ -145,9 +184,10 @@ words above the posts; `layouts/home.njk` already does.
|
|
|
145
184
|
|
|
146
185
|
## The page shell
|
|
147
186
|
|
|
148
|
-
`layouts/base.njk` is the
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
`layouts/base.njk` is the page shell: the skip link, one `.global-wrapper` at the
|
|
188
|
+
42rem measure, `.global-header`, `<main id="main">` and the footer, in that
|
|
189
|
+
order. The shape is the andrewshell.org design's (decision-16) and the look is
|
|
190
|
+
Paper's (doc-9).
|
|
151
191
|
|
|
152
192
|
**One home for each kind of link.** The header carries the site menu, on every
|
|
153
193
|
page. [The bio](#the-bio) carries the person whose page or post it is. The
|
|
@@ -156,7 +196,7 @@ nowhere else.
|
|
|
156
196
|
|
|
157
197
|
**The header has one rule, and the menu under it.** On the front page it is the
|
|
158
198
|
site title as `h1.main-heading`, linked home, with `site.tagline` in a
|
|
159
|
-
|
|
199
|
+
`p.site-tagline` under it; on every other page it is `a.header-link-home`, the site
|
|
160
200
|
title small and linked home, and no tagline. The wrapper carries
|
|
161
201
|
`data-is-root-path="true"` at `/` and nothing anywhere else, which is how the
|
|
162
202
|
stylesheet tells the two apart. `menus.primary` is printed inside the
|
|
@@ -176,6 +216,18 @@ and the copyright line stands on its own. The year is
|
|
|
176
216
|
rather than parses — so it is the year at the moment the page is rendered, in
|
|
177
217
|
the site's own timezone.
|
|
178
218
|
|
|
219
|
+
**The breadcrumb** opens `<main>` on a page with a place in the site's
|
|
220
|
+
hierarchy (TASK-150): a post filed under a category, and a category, tag or
|
|
221
|
+
author archive. It is `nav.breadcrumbs` labelled `Breadcrumb`, holding an `ol`
|
|
222
|
+
whose items run Home, then the post's first category and the post, or the
|
|
223
|
+
archive's term or person, then `Page N` past page one. Every crumb but the last
|
|
224
|
+
is a link; the last is the page itself, a `span` with `aria-current="page"`.
|
|
225
|
+
The trail is `breadcrumbs`, a list of `{ name, url }` that `layouts/base.njk`
|
|
226
|
+
sets before anything is printed, and the JSON-LD prints the same list, so the
|
|
227
|
+
two cannot disagree. Every other page has an empty trail and prints nothing. A
|
|
228
|
+
site that wants no visible breadcrumb empties the `breadcrumbs` block, and the
|
|
229
|
+
`BreadcrumbList` stays in the graph.
|
|
230
|
+
|
|
179
231
|
Webrings, badges, a licence notice and anything else that is markup rather than
|
|
180
232
|
a link are deliberately not in the package. They go in a site theme's `footer`
|
|
181
233
|
block:
|
|
@@ -260,13 +312,19 @@ html.geekity-admin-bar {
|
|
|
260
312
|
|
|
261
313
|
### An entry
|
|
262
314
|
|
|
263
|
-
`layouts/post.njk` and `layouts/page.njk` draw the same thing, the
|
|
264
|
-
|
|
315
|
+
`layouts/post.njk` and `layouts/page.njk` draw the same thing, the entry, and it
|
|
316
|
+
is a microformats2 `h-entry`. An article, and a page:
|
|
265
317
|
|
|
266
318
|
```html
|
|
267
319
|
<article class="blog-post h-entry">
|
|
268
|
-
<header>
|
|
320
|
+
<header class="post-header">
|
|
321
|
+
<p class="kicker">
|
|
322
|
+
<span class="kicker-kind">Article</span>
|
|
323
|
+
<span class="kicker-dot" aria-hidden="true">·</span>
|
|
324
|
+
<a href="/category/notes/" class="p-category" rel="category">notes</a>
|
|
325
|
+
</p>
|
|
269
326
|
<h1 class="p-name">Hello</h1>
|
|
327
|
+
<p class="post-deck p-summary">The description the author wrote.</p>
|
|
270
328
|
</header>
|
|
271
329
|
<section class="e-content">
|
|
272
330
|
<p>The rendered body.</p>
|
|
@@ -279,38 +337,47 @@ design's entry, and it is a microformats2 `h-entry`:
|
|
|
279
337
|
>Published 2 September 2026</time
|
|
280
338
|
></a
|
|
281
339
|
>
|
|
282
|
-
<
|
|
340
|
+
<span aria-hidden="true">·</span>
|
|
341
|
+
<time class="small dt-updated" datetime="2026-09-05T09:00:00.000Z"
|
|
283
342
|
>Updated 5 September 2026</time
|
|
284
343
|
>
|
|
285
344
|
</p>
|
|
286
345
|
</section>
|
|
287
|
-
<p class="post-categories">…categories…</p>
|
|
288
346
|
<p class="post-categories">…tags…</p>
|
|
289
347
|
<hr />
|
|
290
348
|
<footer>…the bio…</footer>
|
|
291
349
|
</article>
|
|
292
|
-
<nav class="blog-post-nav"
|
|
350
|
+
<nav class="blog-post-nav" aria-label="Neighbouring posts">…</nav>
|
|
293
351
|
```
|
|
294
352
|
|
|
353
|
+
**The kicker names the kind.** `p.kicker`, from `partials/kicker.njk`, opens
|
|
354
|
+
every entry: the kind word in `span.kicker-kind` (Article, Note or Reply for a
|
|
355
|
+
post, from its `postType`; Page for a page), then the categories it is filed
|
|
356
|
+
under as `p-category` links. It is the only place the kind is said, small and
|
|
357
|
+
once, and an article is additionally the only kind with a visible headline.
|
|
358
|
+
The deck is the front matter's `description` as a `p.post-deck.p-summary`,
|
|
359
|
+
printed only when the author wrote one.
|
|
360
|
+
|
|
295
361
|
**The Published line is inside the `e-content`**, which is the one thing about
|
|
296
362
|
this shape worth knowing. It is what the source theme does: the date and the
|
|
297
|
-
permalink are part of the words, so a reader
|
|
298
|
-
`e-content
|
|
299
|
-
time as a `u-url`, and a `dt-updated`
|
|
300
|
-
happened on a different day in the site's own timezone; a typo fixed
|
|
301
|
-
later is not news. A post tagged `indienews` opens the line with a
|
|
363
|
+
permalink are part of the words, so a reader, or a fediverse peer reading the
|
|
364
|
+
`e-content`, takes them with the post. The permalink wraps the `dt-published`
|
|
365
|
+
time as a `u-url`, and a `dt-updated` follows it after a middle dot only when
|
|
366
|
+
the update happened on a different day in the site's own timezone; a typo fixed
|
|
367
|
+
an hour later is not news. A post tagged `indienews` opens the line with a
|
|
302
368
|
`u-category` link to <https://news.indieweb.org/en>, which is how IndieNews is
|
|
303
369
|
told the post is for it; a page never prints one, and its line is a
|
|
304
370
|
`p.page-meta` rather than a `p.entry-meta`.
|
|
305
371
|
|
|
306
372
|
**Every post's page has one `h1`.** A post with a name of its own is headed by
|
|
307
373
|
it, as above. A note, or a reply without a title, opens on its words instead,
|
|
308
|
-
so it has no `header`; in its place is an `h1` a screen reader and
|
|
309
|
-
navigate by and a sighted reader never sees, saying what the post is,
|
|
310
|
-
it and when:
|
|
374
|
+
set larger, so it has no `header`; in its place is an `h1` a screen reader and
|
|
375
|
+
a crawler navigate by and a sighted reader never sees, saying what the post is,
|
|
376
|
+
who wrote it and when, and then the kicker:
|
|
311
377
|
|
|
312
378
|
```html
|
|
313
379
|
<h1 class="screen-reader-text">Note by Ada Lovelace, 11 September 2026</h1>
|
|
380
|
+
<p class="kicker"><span class="kicker-kind">Note</span></p>
|
|
314
381
|
```
|
|
315
382
|
|
|
316
383
|
It says `Reply` for a reply and leaves out the name or the date the post does
|
|
@@ -319,16 +386,24 @@ for an article. The headings under it step down one level at a time: the
|
|
|
319
386
|
conversation and the comment form are `h2`, and a refused form's error summary
|
|
320
387
|
is an `h3` inside the form's section.
|
|
321
388
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
389
|
+
**A reply cites what it answers** with `partials/reply-context.njk`, a
|
|
390
|
+
`div.reply-context.cite.u-in-reply-to.h-cite`: a rule on the start side, a
|
|
391
|
+
`p.cite-line` saying "In reply to" and the target's name, author and date as
|
|
392
|
+
far as they are known, and its excerpt as a `blockquote.cite-quote.p-content`.
|
|
393
|
+
An untitled reply prints it under the kicker; a titled reply prints it above
|
|
394
|
+
its header. The same partial cites a reply in a feed.
|
|
395
|
+
|
|
396
|
+
The tags are printed under the words and inside the article, so that each link
|
|
397
|
+
is a `p-category` of this entry, as one `p.post-categories` from
|
|
398
|
+
`partials/tags.njk`. The stylesheet draws the `#` in front of each, so the
|
|
399
|
+
category a parser reads is the tag and not the hash.
|
|
400
|
+
|
|
401
|
+
After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
|
|
402
|
+
posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
|
|
403
|
+
`span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
|
|
404
|
+
the conversation and the comment form. A page prints the contact form when its
|
|
405
|
+
front matter asked for one. A page has no neighbours, no tags and no IndieNews
|
|
406
|
+
link, because none of those are things a page has.
|
|
332
407
|
|
|
333
408
|
### The bio
|
|
334
409
|
|
|
@@ -352,7 +427,16 @@ What the bio prints: a round `u-photo` at 50px when they have an avatar,
|
|
|
352
427
|
their archive, then `p-job-title` and `p-locality` when the profile says, then
|
|
353
428
|
their `p-note` and their `rel="me"` links as a `ul.hlist.bio-links` — each
|
|
354
429
|
link's `rel` is on it ready to print, `me` first and then whatever else the
|
|
355
|
-
Links box typed.
|
|
430
|
+
Links box typed. Set `bioRelMe` to `false` to keep the card and drop its
|
|
431
|
+
claims: the name is linked `rel="author"` alone and `me` comes off every
|
|
432
|
+
link.
|
|
433
|
+
|
|
434
|
+
**The homepage** speaks for the site's author only on a solo author blog,
|
|
435
|
+
which `soloAuthor` on the context says. Then both `layouts/home.njk` at `/` and
|
|
436
|
+
`layouts/front-page.njk` end with that person's card, with its `rel="me"`
|
|
437
|
+
claims, and their author archive's link home carries `rel="me"` back, so the
|
|
438
|
+
homepage and the archive name each other. On any other site a listing homepage
|
|
439
|
+
prints no card, and the front page prints its card with `bioRelMe` false. Each of
|
|
356
440
|
those is printed only when the profile says it, so a profile holding a name
|
|
357
441
|
alone prints a name alone. A name this site has no account for is printed
|
|
358
442
|
unlinked, because the file still said somebody wrote this.
|
|
@@ -387,49 +471,66 @@ byline under the title rather than a bio in the footer, so it writes one.
|
|
|
387
471
|
|
|
388
472
|
### A listing
|
|
389
473
|
|
|
390
|
-
`partials/post-list.njk` is the feed every listing is made of
|
|
391
|
-
the posts page, a tag, category or author archive
|
|
392
|
-
`h-feed
|
|
474
|
+
`partials/post-list.njk` is the feed every listing is made of (the home page,
|
|
475
|
+
the posts page, a tag, category or author archive, and the front page's recent
|
|
476
|
+
posts) and it is a microformats2 `h-feed` of `h-entry` items, each opening on
|
|
477
|
+
its kicker:
|
|
393
478
|
|
|
394
479
|
```html
|
|
395
480
|
<div class="feed h-feed">
|
|
396
481
|
<article class="feed-item h-entry">
|
|
397
|
-
<
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
</
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
<div class="feed-meta">
|
|
410
|
-
<p>
|
|
411
|
-
<time class="feed-date dt-published" datetime="…"
|
|
412
|
-
>2 September 2026</time
|
|
413
|
-
>
|
|
414
|
-
</p>
|
|
415
|
-
<p class="post-categories">
|
|
416
|
-
<a href="/category/notes/" class="p-category" rel="category">notes</a>
|
|
417
|
-
</p>
|
|
418
|
-
</div>
|
|
482
|
+
<p class="kicker">
|
|
483
|
+
<span class="kicker-kind">Article</span>
|
|
484
|
+
<span class="kicker-dot" aria-hidden="true">·</span>
|
|
485
|
+
<time class="feed-date dt-published" datetime="…">2 September 2026</time>
|
|
486
|
+
<span class="kicker-dot" aria-hidden="true">·</span>
|
|
487
|
+
<a href="/category/notes/" class="p-category" rel="category">notes</a>
|
|
488
|
+
</p>
|
|
489
|
+
<h2 class="feed-title p-name">
|
|
490
|
+
<a href="/2026/09/hello/" class="u-url">Hello</a>
|
|
491
|
+
</h2>
|
|
492
|
+
<div class="feed-excerpt p-summary">
|
|
493
|
+
<p>What the post is about, in about 280 characters...</p>
|
|
419
494
|
</div>
|
|
495
|
+
<p class="feed-more">
|
|
496
|
+
<a href="/2026/09/hello/" aria-label="Continue reading: Hello">
|
|
497
|
+
Continue reading<span aria-hidden="true"> →</span>
|
|
498
|
+
</a>
|
|
499
|
+
</p>
|
|
420
500
|
</article>
|
|
421
501
|
<hr class="feed-separator" />
|
|
422
|
-
<article class="feed-item h-entry"
|
|
502
|
+
<article class="feed-item h-entry">
|
|
503
|
+
<p class="kicker">
|
|
504
|
+
<span class="kicker-kind">Note</span>
|
|
505
|
+
<span class="kicker-dot" aria-hidden="true">·</span>
|
|
506
|
+
<a href="/2026/09/coffee/" class="u-url"
|
|
507
|
+
><time class="feed-date dt-published" datetime="…"
|
|
508
|
+
>3 September 2026</time
|
|
509
|
+
></a
|
|
510
|
+
>
|
|
511
|
+
</p>
|
|
512
|
+
<div class="feed-excerpt e-content"><p>The whole note.</p></div>
|
|
513
|
+
</article>
|
|
423
514
|
</div>
|
|
424
515
|
```
|
|
425
516
|
|
|
426
|
-
|
|
427
|
-
|
|
517
|
+
**Only an article has a headline.** A note, which has no title, is printed
|
|
518
|
+
whole as its `e-content`, and the date in its kicker is its `u-url`. A reply is
|
|
519
|
+
a note that first cites what it answers with `partials/reply-context.njk`, and
|
|
520
|
+
a reply with a title is an article that does. An article's categories are in
|
|
521
|
+
its kicker; a note's are on its own page.
|
|
522
|
+
|
|
523
|
+
The excerpt is the entry's `summary`, the very line the feeds publish, so a
|
|
524
|
+
reader and a feed cannot be told two different things, through Nunjucks'
|
|
428
525
|
`truncate(280)`, which cuts at the last space before 280 characters and adds an
|
|
429
526
|
ellipsis. A `description` shorter than that is printed whole. It is plain text
|
|
430
527
|
rather than markup, deliberately: an excerpt with half a code block in it is
|
|
431
528
|
not an excerpt.
|
|
432
529
|
|
|
530
|
+
Each entry of a listing carries `replyContext` beside `inReplyTo` when it is a
|
|
531
|
+
reply whose target has been fetched, the same object a reply's own page gets,
|
|
532
|
+
so the feed can cite it in full.
|
|
533
|
+
|
|
433
534
|
An entry's tags are not in a feed item. They belong under the entry, where
|
|
434
535
|
there is one post's worth of them rather than twenty.
|
|
435
536
|
|
|
@@ -445,21 +546,24 @@ before including it, so the outline never skips a level:
|
|
|
445
546
|
|
|
446
547
|
A listing with nothing on it says `No posts found.` in `p.empty`.
|
|
447
548
|
|
|
448
|
-
`partials/pagination.njk` prints two
|
|
549
|
+
`partials/pagination.njk` prints two links in `nav.pagination`, `← Previous`
|
|
449
550
|
to the page before this one and `Next →` to the page after it, with `rel="prev"`
|
|
450
|
-
and `rel="next"
|
|
551
|
+
and `rel="next"`, and nothing at all on a listing of one page. The arrows
|
|
451
552
|
themselves are `aria-hidden`, because a screen reader that announced them would
|
|
452
553
|
read the decoration and then the word.
|
|
453
554
|
|
|
454
555
|
Each listing layout heads its own page: `layouts/home.njk` with the listing's
|
|
455
|
-
title,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
556
|
+
title, and the three archives with a `header.archive-header` whose kicker says
|
|
557
|
+
what kind of archive it is (Tag, Category, Author) over an `h1.page-title`
|
|
558
|
+
naming whose: the term, or the person, followed by their `h-card`. The
|
|
559
|
+
category archive's header is also a `header.category-header` and prints a
|
|
560
|
+
`div.category-description` under the name when the context carries a
|
|
561
|
+
`categoryDescription`; the CMS has no store of term descriptions, so nothing
|
|
562
|
+
writes one today and the header is the heading alone.
|
|
563
|
+
|
|
564
|
+
`layouts/404.njk` says `Content not found.` under a `Not found` kicker and
|
|
565
|
+
links home and to the search. The 500 and 503 pages below open on a kicker the
|
|
566
|
+
same way.
|
|
463
567
|
|
|
464
568
|
`layouts/500.njk` is the page a request gets when the server fails while
|
|
465
569
|
answering it (TASK-129). It says `Something went wrong.` in an
|
|
@@ -583,25 +687,52 @@ the entry's `summary` — the line the feeds publish — else `site.tagline`. It
|
|
|
583
687
|
printed once, as `<meta name="description">`, and the Open Graph and Twitter
|
|
584
688
|
descriptions say the same thing.
|
|
585
689
|
|
|
690
|
+
**The colours** come from the theme's `theme.json` (see
|
|
691
|
+
[The manifest](#the-manifest)), on the context as `theme`.
|
|
692
|
+
`<meta name="color-scheme">` comes before the stylesheet, so a reader in dark
|
|
693
|
+
mode gets a dark canvas rather than a white flash while the stylesheet loads.
|
|
694
|
+
`<meta name="theme-color">` is printed twice, once with
|
|
695
|
+
`media="(prefers-color-scheme: light)"` and once for dark.
|
|
696
|
+
|
|
586
697
|
**The card.** `og:title` is the page's title, or the site's on the front page;
|
|
587
698
|
`og:site_name` is always the site's. `og:type` is `article` on a rendered post
|
|
588
699
|
or page and `website` everywhere else, a listing carrying a page's front matter
|
|
589
|
-
included. `og:url` is the canonical URL.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
700
|
+
included. `og:url` is the canonical URL. An article also carries
|
|
701
|
+
`article:published_time`, `article:modified_time` (the `updated` date, else the
|
|
702
|
+
publish date), `article:author` (the author's profile URL here, else their
|
|
703
|
+
name) and one `article:tag` per tag.
|
|
704
|
+
|
|
705
|
+
**The picture** is the `image` in the entry's front matter, else `site.avatar`,
|
|
706
|
+
on the context as `shareImage`. A site with neither prints no `og:image` and no
|
|
707
|
+
`twitter:image` rather than an empty one. The picture always carries
|
|
708
|
+
`og:image:alt` and `twitter:image:alt`. For the entry's own picture, the alt
|
|
709
|
+
text is its `imageAlt` front matter, else what the media library says about the
|
|
710
|
+
upload, else the entry's title. For the avatar, it is the media library's text,
|
|
711
|
+
else the site's author. `og:image:width` and `og:image:height` are printed when
|
|
712
|
+
the image's variants have been derived, because the size is read from their
|
|
713
|
+
sidecar rather than from the file. `twitter:card` is `summary_large_image` for a
|
|
714
|
+
picture at least 1200 pixels wide and wider than it is tall. Everything else,
|
|
715
|
+
the avatar included, is `summary`, the small picture beside the words.
|
|
716
|
+
|
|
717
|
+
**The icons** come from the site's `icon` setting in `site.json`, else
|
|
718
|
+
`site.avatar`, through the derived images (decision-10). `icon` exists for a
|
|
719
|
+
site whose avatar is a face and whose mark is a logo. The head links `icon` at
|
|
720
|
+
32 and 16 pixels and `apple-touch-icon` at 180, each a square PNG cropped from
|
|
721
|
+
the middle of the picture and encoded the first time a browser asks for it. An
|
|
722
|
+
SVG icon is linked first as itself, with `type="image/svg+xml"` and no
|
|
723
|
+
`sizes`, and the PNGs are rasterised from it. They are on the context as
|
|
724
|
+
`icons`, a list of `{ rel, sizes, type, href }`, which is empty, and the links
|
|
725
|
+
are not printed at all, when the site has neither setting, when it names a
|
|
726
|
+
file no icon can be made of, or when image optimization is off.
|
|
727
|
+
|
|
728
|
+
**The favicon and the manifest** are routes, not templates. `/favicon.ico`
|
|
729
|
+
holds 16, 32 and 48 pixel frames drawn from the same picture, because browsers
|
|
730
|
+
and crawlers ask the root for it whatever the head links; it answers 404 on a
|
|
731
|
+
site with no icon. `/manifest.webmanifest` gives the site's title as `name` and
|
|
732
|
+
`short_name`, `start_url` `/`, `display` `minimal-ui`, the theme's light
|
|
733
|
+
`themeColor` as `theme_color` and `background_color`, and icons at 192 and 512
|
|
734
|
+
pixels plus a maskable 512 whose picture sits inside the middle 80%, padded
|
|
735
|
+
with its own dominant colour. The head links it on every page.
|
|
605
736
|
|
|
606
737
|
**The structured data** is `partials/jsonld.njk`, one `<script
|
|
607
738
|
type="application/ld+json">` holding one `@graph` per page, and it is the only
|
|
@@ -611,7 +742,8 @@ IndieWeb. The graph holds:
|
|
|
611
742
|
|
|
612
743
|
- `WebSite`, always, with the site's title, tagline and URL, and a `publisher`
|
|
613
744
|
pointing at the Person, and a `potentialAction` that is a `SearchAction` on
|
|
614
|
-
`/search/?q={search_term_string}`.
|
|
745
|
+
`/search/?q={search_term_string}`. On the homepage of a solo author blog it
|
|
746
|
+
also has an `about` pointing at the site author's Person.
|
|
615
747
|
- `Person`, from `siteAuthor`: their name, archive URL, avatar, bio, job title
|
|
616
748
|
and location, and a `sameAs` of their profile links and their actor id, which
|
|
617
749
|
is what asserts that the schema.org Person and the fediverse actor are one
|
|
@@ -622,6 +754,9 @@ IndieWeb. The graph holds:
|
|
|
622
754
|
- `BlogPosting` on a post and `Article` on a page, with the headline, URL,
|
|
623
755
|
`mainEntityOfPage`, `datePublished`, `dateModified`, description, image,
|
|
624
756
|
`author` and `publisher`.
|
|
757
|
+
- `BreadcrumbList` wherever the page prints a breadcrumb (see
|
|
758
|
+
[The page shell](#the-page-shell)): one `ListItem` per crumb with its
|
|
759
|
+
`position`, `name` and absolute URL as `item`.
|
|
625
760
|
|
|
626
761
|
A site that wants a different graph — more types, an `Organization` publisher,
|
|
627
762
|
nothing at all — writes its own `partials/jsonld.njk` and that file replaces
|
|
@@ -682,38 +817,48 @@ to its own stylesheet.
|
|
|
682
817
|
|
|
683
818
|
### Colours
|
|
684
819
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
a
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
|
709
|
-
|
|
|
710
|
-
| `--color-
|
|
711
|
-
| `--color-
|
|
712
|
-
| `--color-
|
|
713
|
-
| `--color-
|
|
714
|
-
| `--color-
|
|
715
|
-
| `--color-
|
|
716
|
-
| `--color-
|
|
820
|
+
Every colour is a custom property on `:root`, and these tokens are the
|
|
821
|
+
override contract: a site that only wants different colours redefines them in
|
|
822
|
+
its own stylesheet rather than replacing this one. Tailwind's own palette is
|
|
823
|
+
cleared in `src/style.css`, and its utilities (`text-ink`, `bg-paper`,
|
|
824
|
+
`border-edge` and the rest) are mapped onto these properties with
|
|
825
|
+
`@theme inline`, so redefining a token recolours every rule that uses it.
|
|
826
|
+
|
|
827
|
+
A second scheme under `@media (prefers-color-scheme: dark)` redefines the same
|
|
828
|
+
tokens on dark paper, and `color-scheme: light dark` lets a browser paint its
|
|
829
|
+
own form controls and scrollbars to match. There is no toggle: the reader's
|
|
830
|
+
system setting is the setting.
|
|
831
|
+
|
|
832
|
+
Both schemes meet the Colour contrast rule of the
|
|
833
|
+
[specification.website checklist](https://specification.website/checklist.md):
|
|
834
|
+
every run of text, the small kickers, dates and meta lines included, at 7:1
|
|
835
|
+
(WCAG 1.4.6, AAA) on whatever it sits on, and every border a reader relies on
|
|
836
|
+
(a field, a card, a code block, the focus outline, the citation and blockquote
|
|
837
|
+
rules) at 3:1 (1.4.11). Nothing a reader needs is drawn at reduced opacity.
|
|
838
|
+
That is a test rather than a claim: `src/web/theme-colors.test.ts` reads the
|
|
839
|
+
tokens out of the compiled stylesheet and computes the ratio of every pair the
|
|
840
|
+
design puts on screen, so a colour changed in `src/style.css` that breaks one
|
|
841
|
+
fails the build.
|
|
842
|
+
|
|
843
|
+
| Token | Utility | What it colours |
|
|
844
|
+
| ------------------------- | ---------- | ---------------------------------------------------------------------- |
|
|
845
|
+
| `--color-body` | `paper` | The paper, and the words on a submit button. |
|
|
846
|
+
| `--color-text` | `ink` | Body text and headlines. |
|
|
847
|
+
| `--color-muted` | `muted` | Kickers, dates, meta lines, decks, quotes, the bio note, the footer. |
|
|
848
|
+
| `--color-primary` | `accent` | Links, a kicker's kind word, the citation rule, the submit button. |
|
|
849
|
+
| `--color-secondary` | `accent-2` | Tag links and the focus outline. |
|
|
850
|
+
| `--color-rule` | `rule` | Decorative hairlines only: between entries, above the footer. |
|
|
851
|
+
| `--color-edge` | `edge` | Borders a reader relies on: fields, the previous and next cards, code. |
|
|
852
|
+
| `--color-base` | `surface` | A sunk surface: fields, notices, a face with no avatar. |
|
|
853
|
+
| `--color-base-2` | — | A cooler sunk surface. |
|
|
854
|
+
| `--color-base-3` | — | The raised surface: the focused skip link. |
|
|
855
|
+
| `--color-code-background` | `code` | Behind `code` and a fenced block. |
|
|
856
|
+
| `--color-code-text` | — | Code with no highlighting on it. |
|
|
857
|
+
| `--color-error` | `error` | A field that will not do, and what the form says about it. |
|
|
858
|
+
|
|
859
|
+
`--color-rule` is the one token in no contrast pair, because it is decoration:
|
|
860
|
+
a site that recolours it may make it as faint as it likes. Anything drawn with
|
|
861
|
+
`--color-edge` has to stay at 3:1 against the paper and the surface.
|
|
717
862
|
|
|
718
863
|
### Code highlighting
|
|
719
864
|
|
|
@@ -750,11 +895,12 @@ a different version of highlight.js than the one installed.
|
|
|
750
895
|
**The palette is Tomorrow**, the one both of the sites this design comes from
|
|
751
896
|
used: Chris Kempson's Tomorrow on light paper and Tomorrow Night on dark, from
|
|
752
897
|
highlight.js's own base16 themes, which is also what decides which `hljs-`
|
|
753
|
-
scope takes which slot.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
further
|
|
898
|
+
scope takes which slot. None of the light colours is the stock one: a syntax
|
|
899
|
+
theme is drawn for an editor, where a faint comment is a feature, and here code
|
|
900
|
+
is text like any other and meets 7:1 on its block, the bar every run of text
|
|
901
|
+
on the page meets. Each light colour was darkened along its own hue until it
|
|
902
|
+
did and no further, and four of the dark ones (the comment grey, the red, the
|
|
903
|
+
blue and the `base0F` brown) were lightened toward white the same way. Every one
|
|
758
904
|
of them is in the contrast test beside the rest of the palette.
|
|
759
905
|
|
|
760
906
|
| Token | base16 | What it colours |
|
|
@@ -871,7 +1017,11 @@ Every template gets:
|
|
|
871
1017
|
| `site` | `content/_data/site.json`, if the site has one, over the defaults `title` and `url`. Any key in the file is readable, so `site.tagline`, `site.author` and anything else a site adds are all available. |
|
|
872
1018
|
| `menus` | Every menu the site stores, by name, marked for this page: `menus.primary`, `menus.footer`, and any other name. See [Navigation](#navigation). |
|
|
873
1019
|
| `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
|
|
874
|
-
| `
|
|
1020
|
+
| `soloAuthor` | The site author's profile, on a site whose Solo author blog setting is on. **Absent** when it is off or the author setting names nobody. See [The bio](#the-bio). |
|
|
1021
|
+
| `icons` | The site's icons, as `{ rel, sizes, type, href }`. Empty until the site has an icon or an avatar to derive them from. See [The head](#the-head). |
|
|
1022
|
+
| `theme` | The colours the theme declares, as `{ colorScheme, themeColor: { light, dark } }`, each absent when no theme declares it. See [The manifest](#the-manifest). |
|
|
1023
|
+
| `shareImage` | The picture a shared link shows, as `{ url, alt, size: { width, height }, card }`. `size` is absent when it is not known. **Absent** when there is no picture. See [The head](#the-head). |
|
|
1024
|
+
| `llmsTxt` | The path of `/llms.txt`, while the site serves it. **Absent** when the site has turned it off. The default theme links it from the front page only. |
|
|
875
1025
|
|
|
876
1026
|
A document — one post, one page, or one entry of a listing — adds:
|
|
877
1027
|
|
|
@@ -891,6 +1041,7 @@ A document — one post, one page, or one entry of a listing — adds:
|
|
|
891
1041
|
| `page.inputPath` | The source file, relative to the content directory. |
|
|
892
1042
|
| `type` | `post` or `page`. |
|
|
893
1043
|
| `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
|
|
1044
|
+
| `lang` | The `lang` front matter as a canonical tag, such as `fr-CA`. Absent when it names none or no tag. |
|
|
894
1045
|
| `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
|
|
895
1046
|
| `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
|
|
896
1047
|
| `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
|
|
@@ -921,7 +1072,10 @@ archive — adds:
|
|
|
921
1072
|
How many posts a listing page holds comes from `postsPerPage` in
|
|
922
1073
|
`content/_data/site.json`, and defaults to 10. How many entries a feed holds
|
|
923
1074
|
comes from `feedSize` in the same file, and defaults to 20. The `<html lang>`
|
|
924
|
-
comes from `language`, and defaults to `en`.
|
|
1075
|
+
comes from `language`, and defaults to `en`. A document whose `lang` names
|
|
1076
|
+
another language carries it on its own `<article>`, in an entry, a listing and
|
|
1077
|
+
search results alike, through `partials/lang.njk`, and the dates inside that
|
|
1078
|
+
article are written in it (see the `date` filter). The rssCloud and WebSub server the
|
|
925
1079
|
feeds advertise comes from `notifyServer`, and is empty for none; the theme
|
|
926
1080
|
writes nothing for it, because a cloud is advertised in the feed rather than on
|
|
927
1081
|
the page.
|
|
@@ -1169,6 +1323,15 @@ site that has turned them off advertises nothing. The CMS also sends the same
|
|
|
1169
1323
|
endpoint as a `Link` header on every representation of a document, so a sender
|
|
1170
1324
|
that does not parse HTML still finds it.
|
|
1171
1325
|
|
|
1326
|
+
On the front page it also writes the site's index for language models:
|
|
1327
|
+
|
|
1328
|
+
```html
|
|
1329
|
+
<link rel="describedby" type="text/markdown" href="/llms.txt" />
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
It comes from `llmsTxt` on the context, which is absent once the site turns the
|
|
1333
|
+
file off. The CMS sends the same link as a `Link` header on the front page.
|
|
1334
|
+
|
|
1172
1335
|
`layouts/tag.njk` and `layouts/category.njk` override that block, call `super()`
|
|
1173
1336
|
and add the archive's own three feeds:
|
|
1174
1337
|
|
|
@@ -1275,8 +1438,11 @@ The packaged partial draws each entry as the source design does: an
|
|
|
1275
1438
|
`li.comment.h-entry` carrying `id="comment-{{ reply.id }}"` and a
|
|
1276
1439
|
`comment-{{ reply.source }}` class, holding an `article.comment-body` whose
|
|
1277
1440
|
`footer.comment-meta` is the author as a `.comment-author.vcard.p-author.h-card`
|
|
1278
|
-
|
|
1279
|
-
|
|
1441
|
+
(their avatar, or the first letter of their name in a `span.comment-avatar-empty`
|
|
1442
|
+
when there is none) and the permalink as a `.comment-metadata` link around a
|
|
1443
|
+
`time.dt-published`, with a `span.comment-source` saying "via webmention" or
|
|
1444
|
+
"via the fediverse" when it came that way, then the words in
|
|
1445
|
+
`div.comment-content.e-content`. Answers nest in an
|
|
1280
1446
|
`ol.children` inside what they answer. A Reply link in a `div.reply` goes on the
|
|
1281
1447
|
ones written here — `source == "comment"` — when the post is still open; it
|
|
1282
1448
|
carries the comment's id to the form as `?reply_to=`, so threading needs no
|
|
@@ -1285,7 +1451,8 @@ webmention on the page that sent it, so neither gets one.
|
|
|
1285
1451
|
|
|
1286
1452
|
Above the thread, `group()` draws one `div.reaction-group` per kind that has
|
|
1287
1453
|
anything — `p-like`, `p-repost` and `p-mention`, the source theme's classes — as
|
|
1288
|
-
an `h2.reaction-title` of the label and the count
|
|
1454
|
+
an `h2.reaction-title` of the label and the count, in a `span.reaction-count`,
|
|
1455
|
+
beside a `div.facepile` of
|
|
1289
1456
|
`a.u-url` faces. A face is a round `u-photo` avatar; a reaction whose author the
|
|
1290
1457
|
site knows no picture of is the emoji badge of its kind and a `.reaction-name`
|
|
1291
1458
|
instead. Likes and boosts link to the person, and a mention to the page it came
|
|
@@ -1352,7 +1519,10 @@ The packaged partial is a `section#respond.comment-respond` with an
|
|
|
1352
1519
|
`h2.comment-reply-title`, one paragraph per field and the button in a
|
|
1353
1520
|
`p.form-submit` — the source design's names, so the stylesheet styles every
|
|
1354
1521
|
label, field and button of both forms through `.comment-respond` and a site
|
|
1355
|
-
that has CSS for the WordPress theme can bring it.
|
|
1522
|
+
that has CSS for the WordPress theme can bring it. The name and email fields sit
|
|
1523
|
+
in a `div.field-row`, side by side on a wide screen, in both forms. On a site
|
|
1524
|
+
that takes webmentions a `p.comment-help` under the heading says a reader can
|
|
1525
|
+
reply from their own site instead.
|
|
1356
1526
|
|
|
1357
1527
|
| Key | What it holds |
|
|
1358
1528
|
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1508,18 +1678,29 @@ the import above carries `with context`: a macro imported without it cannot see
|
|
|
1508
1678
|
|
|
1509
1679
|
## Filters
|
|
1510
1680
|
|
|
1511
|
-
| Filter
|
|
1512
|
-
|
|
|
1513
|
-
| `date(format, zone)` | Formats a `Date` or a date string. `readable` (the default) gives `
|
|
1514
|
-
| `
|
|
1515
|
-
| `
|
|
1516
|
-
| `
|
|
1681
|
+
| Filter | What it does |
|
|
1682
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1683
|
+
| `date(format, zone, locale)` | Formats a `Date` or a date string. `long` gives `2 September 2026` on an `en` site and `2 septembre 2026` on an `fr` one; `full`, `medium` and `short` are Intl's other date styles, and `readable` (the default) is `long`. `month` gives `September 2026`, `year` gives `2026`, `html` gives `2026-09-02` for a `<time datetime>`, and `iso` gives the full ISO 8601 instant. Every format but `html` and `iso` is written in the site's locale: the `locale` setting, or the `language` when that is empty. Bare `en` writes the day first, as `en-GB` does; `en-US` gives `September 2, 2026`. A date in a file is a UTC instant; every format but `iso` is rendered in the site's `timezone` setting, and `iso` stays the instant. Pass `zone` — an IANA name — to override the setting for one call. Pass `locale` — a BCP 47 tag — to write the words in another language for one call; it comes after `zone`, so pass `none` for the zone to keep the site's: `{{ date \| date("readable", none, lang) }}`. An empty locale, or one that is no tag, writes in the site's. A value that is not a date renders as the empty string. |
|
|
1684
|
+
| `plural(forms, locale)` | Picks the form for a count by its plural category in the site's locale, through `Intl.PluralRules`: `{{ n \| plural({ one: "# reply", other: "# replies" }) }}`. The keys are the CLDR categories (`zero`, `one`, `two`, `few`, `many`, `other`), a category the forms leave out falls back to `other`, and `#` is the count in the locale's digits. Pass `locale` when the strings are in a language of their own: the default theme's words are English, so it passes `"en"`. |
|
|
1685
|
+
| `url` | Prefixes a root-relative path with the base URL's path, so a site served from a subdirectory links correctly. Eleventy's filter of the same name. |
|
|
1686
|
+
| `absoluteUrl` | The same path as a fully qualified URL against the site's `baseUrl`. |
|
|
1687
|
+
| `asset` | The URL of a file in the theme's `static/` directory, with the base path in front and a hash of the file's bytes on the end: `{{ "style.css" \| asset }}` gives `/theme/style.css?v=3f2a9c01b7d4`. The file is found the way `/theme/` finds it, the chosen theme first. A hashed URL is served with `Cache-Control: public, max-age=31536000, immutable`, and editing the file changes the hash on the next render. A file no theme has gets its plain `/theme/` URL. |
|
|
1517
1688
|
|
|
1518
1689
|
`date` reads one word rather than parsing it: `{{ "now" | date("year") }}` is
|
|
1519
1690
|
the year at the moment the page is rendered, in the site's own timezone. It is
|
|
1520
1691
|
what the footer's copyright line is written with, and it is the one date a page
|
|
1521
1692
|
has that no file carries.
|
|
1522
1693
|
|
|
1694
|
+
The default theme passes the document's `lang` as that locale for every date
|
|
1695
|
+
inside its `<article>`: the Published and Updated lines, the hidden heading of
|
|
1696
|
+
a note, the kicker in a listing or a search result (`kicker.line(…,
|
|
1697
|
+
lang=post.lang)`), and the date of the post a reply answers. So a post that
|
|
1698
|
+
says `lang: fr` on an `en` site reads `Published 2 septembre 2026`, while the
|
|
1699
|
+
navigation, the conversation under it, the archive's month headings and the
|
|
1700
|
+
footer stay in the site's locale. A theme that wants the same passes `lang`
|
|
1701
|
+
on an entry and `post.lang` in a listing; one that passes nothing writes every
|
|
1702
|
+
date in the site's locale, as before.
|
|
1703
|
+
|
|
1523
1704
|
Nunjucks' own filters — `default`, `join`, `urlencode`, `safe` and the rest —
|
|
1524
1705
|
are all available. Autoescaping is on, so rendered Markdown is the one thing
|
|
1525
1706
|
that has to be passed through `safe`.
|