@geekity/cms 0.3.0 → 0.4.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 +2091 -214
- package/admin/components/fields.njk +165 -0
- package/admin/layouts/base.njk +1 -1
- package/admin/layouts/settings-page.njk +26 -0
- package/admin/layouts/shell.njk +17 -3
- package/admin/pages/account/forgot.njk +47 -0
- package/admin/pages/account/login.njk +26 -0
- package/admin/pages/account/reset.njk +38 -0
- package/admin/pages/account/setup.njk +28 -0
- package/admin/pages/appearance/themes.njk +69 -0
- package/admin/pages/comments/all.njk +133 -0
- package/admin/{layouts/dashboard.njk → pages/dashboard/home.njk} +8 -15
- package/admin/{layouts/document-conflict.njk → pages/documents/conflict.njk} +1 -0
- package/admin/pages/documents/editor.njk +196 -0
- package/admin/{layouts/document-list.njk → pages/documents/list.njk} +19 -2
- package/admin/pages/documents/taxonomy.njk +95 -0
- package/admin/pages/federation/followers.njk +238 -0
- package/admin/pages/media/library.njk +160 -0
- package/admin/pages/messages/all.njk +110 -0
- package/admin/{layouts → pages}/placeholder.njk +5 -1
- package/admin/pages/settings/discussion.njk +99 -0
- package/admin/pages/settings/email.njk +203 -0
- package/admin/pages/settings/federation.njk +75 -0
- package/admin/pages/settings/general.njk +29 -0
- package/admin/pages/settings/permalinks.njk +50 -0
- package/admin/pages/settings/reading.njk +72 -0
- package/admin/pages/users/edit.njk +239 -0
- package/admin/pages/users/list.njk +48 -0
- package/admin/pages/users/new.njk +41 -0
- package/admin/static/admin.css +498 -3
- package/admin/static/copy.js +52 -0
- package/admin/static/editor.js +16 -16
- package/admin/static/slug.js +61 -0
- package/dist/admin/accounts.d.ts +413 -0
- package/dist/admin/accounts.d.ts.map +1 -0
- package/dist/admin/accounts.js +680 -0
- package/dist/admin/accounts.js.map +1 -0
- package/dist/admin/appearance.d.ts +80 -0
- package/dist/admin/appearance.d.ts.map +1 -0
- package/dist/admin/appearance.js +136 -0
- package/dist/admin/appearance.js.map +1 -0
- package/dist/admin/comments.d.ts +90 -0
- package/dist/admin/comments.d.ts.map +1 -0
- package/dist/admin/comments.js +244 -0
- package/dist/admin/comments.js.map +1 -0
- package/dist/admin/credentials.d.ts +26 -2
- package/dist/admin/credentials.d.ts.map +1 -1
- package/dist/admin/credentials.js +35 -2
- package/dist/admin/credentials.js.map +1 -1
- package/dist/admin/documents.d.ts +127 -10
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +360 -41
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +151 -50
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +225 -93
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/formatting.d.ts +20 -0
- package/dist/admin/formatting.d.ts.map +1 -0
- package/dist/admin/formatting.js +72 -0
- package/dist/admin/formatting.js.map +1 -0
- package/dist/admin/headers.d.ts +65 -0
- package/dist/admin/headers.d.ts.map +1 -0
- package/dist/admin/headers.js +104 -0
- package/dist/admin/headers.js.map +1 -0
- package/dist/admin/index.d.ts +38 -12
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +25 -7
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/media.d.ts +168 -0
- package/dist/admin/media.d.ts.map +1 -0
- package/dist/admin/media.js +295 -0
- package/dist/admin/media.js.map +1 -0
- package/dist/admin/menu.d.ts +79 -0
- package/dist/admin/menu.d.ts.map +1 -0
- package/dist/admin/menu.js +126 -0
- package/dist/admin/menu.js.map +1 -0
- package/dist/admin/messages.d.ts +80 -0
- package/dist/admin/messages.d.ts.map +1 -0
- package/dist/admin/messages.js +152 -0
- package/dist/admin/messages.js.map +1 -0
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +31 -7
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/recovery.d.ts +92 -0
- package/dist/admin/recovery.d.ts.map +1 -0
- package/dist/admin/recovery.js +229 -0
- package/dist/admin/recovery.js.map +1 -0
- package/dist/admin/routes.d.ts +0 -20
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +155 -105
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts +37 -0
- package/dist/admin/settings-discussion.d.ts.map +1 -0
- package/dist/admin/settings-discussion.js +117 -0
- package/dist/admin/settings-discussion.js.map +1 -0
- package/dist/admin/settings-email.d.ts +54 -0
- package/dist/admin/settings-email.d.ts.map +1 -0
- package/dist/admin/settings-email.js +189 -0
- package/dist/admin/settings-email.js.map +1 -0
- package/dist/admin/settings-federation.d.ts +39 -0
- package/dist/admin/settings-federation.d.ts.map +1 -0
- package/dist/admin/settings-federation.js +103 -0
- package/dist/admin/settings-federation.js.map +1 -0
- package/dist/admin/settings-general.d.ts +12 -0
- package/dist/admin/settings-general.d.ts.map +1 -0
- package/dist/admin/settings-general.js +48 -0
- package/dist/admin/settings-general.js.map +1 -0
- package/dist/admin/settings-page.d.ts +102 -0
- package/dist/admin/settings-page.d.ts.map +1 -0
- package/dist/admin/settings-page.js +126 -0
- package/dist/admin/settings-page.js.map +1 -0
- package/dist/admin/settings-pages.d.ts +19 -0
- package/dist/admin/settings-pages.d.ts.map +1 -0
- package/dist/admin/settings-pages.js +33 -0
- package/dist/admin/settings-pages.js.map +1 -0
- package/dist/admin/settings-permalinks.d.ts +16 -0
- package/dist/admin/settings-permalinks.d.ts.map +1 -0
- package/dist/admin/settings-permalinks.js +32 -0
- package/dist/admin/settings-permalinks.js.map +1 -0
- package/dist/admin/settings-reading.d.ts +33 -0
- package/dist/admin/settings-reading.d.ts.map +1 -0
- package/dist/admin/settings-reading.js +48 -0
- package/dist/admin/settings-reading.js.map +1 -0
- package/dist/admin/settings.d.ts +325 -96
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +650 -308
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +606 -152
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +863 -268
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/taxonomy.d.ts +114 -0
- package/dist/admin/taxonomy.d.ts.map +1 -0
- package/dist/admin/taxonomy.js +289 -0
- package/dist/admin/taxonomy.js.map +1 -0
- package/dist/admin/templates.d.ts +44 -14
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +44 -14
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/throttle.d.ts +84 -0
- package/dist/admin/throttle.d.ts.map +1 -0
- package/dist/admin/throttle.js +143 -0
- package/dist/admin/throttle.js.map +1 -0
- package/dist/admin/uploads.d.ts +28 -2
- package/dist/admin/uploads.d.ts.map +1 -1
- package/dist/admin/uploads.js +43 -4
- package/dist/admin/uploads.js.map +1 -1
- package/dist/admin/users.d.ts +100 -13
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +412 -39
- package/dist/admin/users.js.map +1 -1
- package/dist/cache.d.ts +141 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +207 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +14 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +424 -21
- package/dist/cli.js.map +1 -1
- package/dist/comments/akismet.d.ts +130 -0
- package/dist/comments/akismet.d.ts.map +1 -0
- package/dist/comments/akismet.js +274 -0
- package/dist/comments/akismet.js.map +1 -0
- package/dist/comments/conversation.d.ts +29 -0
- package/dist/comments/conversation.d.ts.map +1 -0
- package/dist/comments/conversation.js +60 -0
- package/dist/comments/conversation.js.map +1 -0
- package/dist/comments/form.d.ts +75 -0
- package/dist/comments/form.d.ts.map +1 -0
- package/dist/comments/form.js +64 -0
- package/dist/comments/form.js.map +1 -0
- package/dist/comments/index.d.ts +15 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +9 -0
- package/dist/comments/index.js.map +1 -0
- package/dist/comments/markdown.d.ts +8 -0
- package/dist/comments/markdown.d.ts.map +1 -0
- package/dist/comments/markdown.js +85 -0
- package/dist/comments/markdown.js.map +1 -0
- package/dist/comments/moderate.d.ts +71 -0
- package/dist/comments/moderate.d.ts.map +1 -0
- package/dist/comments/moderate.js +88 -0
- package/dist/comments/moderate.js.map +1 -0
- package/dist/comments/policy.d.ts +57 -0
- package/dist/comments/policy.d.ts.map +1 -0
- package/dist/comments/policy.js +80 -0
- package/dist/comments/policy.js.map +1 -0
- package/dist/comments/records.d.ts +266 -0
- package/dist/comments/records.d.ts.map +1 -0
- package/dist/comments/records.js +487 -0
- package/dist/comments/records.js.map +1 -0
- package/dist/comments/routes.d.ts +58 -0
- package/dist/comments/routes.d.ts.map +1 -0
- package/dist/comments/routes.js +177 -0
- package/dist/comments/routes.js.map +1 -0
- package/dist/comments/submission.d.ts +263 -0
- package/dist/comments/submission.d.ts.map +1 -0
- package/dist/comments/submission.js +241 -0
- package/dist/comments/submission.js.map +1 -0
- package/dist/config.d.ts +190 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +118 -6
- package/dist/config.js.map +1 -1
- package/dist/contact/delivery.d.ts +60 -0
- package/dist/contact/delivery.d.ts.map +1 -0
- package/dist/contact/delivery.js +87 -0
- package/dist/contact/delivery.js.map +1 -0
- package/dist/contact/form.d.ts +130 -0
- package/dist/contact/form.d.ts.map +1 -0
- package/dist/contact/form.js +163 -0
- package/dist/contact/form.js.map +1 -0
- package/dist/contact/index.d.ts +17 -0
- package/dist/contact/index.d.ts.map +1 -0
- package/dist/contact/index.js +13 -0
- package/dist/contact/index.js.map +1 -0
- package/dist/contact/records.d.ts +124 -0
- package/dist/contact/records.d.ts.map +1 -0
- package/dist/contact/records.js +218 -0
- package/dist/contact/records.js.map +1 -0
- package/dist/contact/routes.d.ts +17 -0
- package/dist/contact/routes.d.ts.map +1 -0
- package/dist/contact/routes.js +127 -0
- package/dist/contact/routes.js.map +1 -0
- package/dist/contact/submission.d.ts +103 -0
- package/dist/contact/submission.d.ts.map +1 -0
- package/dist/contact/submission.js +166 -0
- package/dist/contact/submission.js.map +1 -0
- package/dist/content/document.d.ts +7 -2
- package/dist/content/document.d.ts.map +1 -1
- package/dist/content/document.js +1 -0
- package/dist/content/document.js.map +1 -1
- package/dist/content/index.d.ts +7 -2
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +24 -2
- package/dist/content/markdown.js.map +1 -1
- package/dist/content/parser.d.ts.map +1 -1
- package/dist/content/parser.js +11 -4
- package/dist/content/parser.js.map +1 -1
- package/dist/content/save.d.ts +5 -0
- package/dist/content/save.d.ts.map +1 -1
- package/dist/content/save.js +44 -1
- package/dist/content/save.js.map +1 -1
- package/dist/content/schedule.d.ts +125 -0
- package/dist/content/schedule.d.ts.map +1 -0
- package/dist/content/schedule.js +207 -0
- package/dist/content/schedule.js.map +1 -0
- package/dist/content/search.d.ts +64 -0
- package/dist/content/search.d.ts.map +1 -0
- package/dist/content/search.js +136 -0
- package/dist/content/search.js.map +1 -0
- package/dist/content/store.d.ts +202 -7
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +374 -67
- package/dist/content/store.js.map +1 -1
- package/dist/content/sync.d.ts +7 -1
- package/dist/content/sync.d.ts.map +1 -1
- package/dist/content/sync.js +18 -10
- package/dist/content/sync.js.map +1 -1
- package/dist/content/time.d.ts +56 -0
- package/dist/content/time.d.ts.map +1 -0
- package/dist/content/time.js +141 -0
- package/dist/content/time.js.map +1 -0
- package/dist/content/writer.d.ts.map +1 -1
- package/dist/content/writer.js +3 -0
- package/dist/content/writer.js.map +1 -1
- package/dist/env.d.ts +58 -4
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/actor.d.ts +95 -41
- package/dist/federation/actor.d.ts.map +1 -1
- package/dist/federation/actor.js +180 -53
- package/dist/federation/actor.js.map +1 -1
- package/dist/federation/article.d.ts +44 -17
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +98 -46
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/delivery.d.ts +63 -15
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +154 -80
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts +22 -17
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +98 -77
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/followers.d.ts +3 -2
- package/dist/federation/followers.d.ts.map +1 -1
- package/dist/federation/followers.js +6 -5
- package/dist/federation/followers.js.map +1 -1
- package/dist/federation/import-wordpress.d.ts +128 -0
- package/dist/federation/import-wordpress.d.ts.map +1 -0
- package/dist/federation/import-wordpress.js +426 -0
- package/dist/federation/import-wordpress.js.map +1 -0
- package/dist/federation/inbox.d.ts +36 -13
- package/dist/federation/inbox.d.ts.map +1 -1
- package/dist/federation/inbox.js +120 -49
- package/dist/federation/inbox.js.map +1 -1
- package/dist/federation/index.d.ts +22 -10
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +13 -8
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/keys.d.ts +98 -20
- package/dist/federation/keys.d.ts.map +1 -1
- package/dist/federation/keys.js +269 -39
- package/dist/federation/keys.js.map +1 -1
- package/dist/federation/mount.d.ts +42 -10
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +266 -26
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/paths.d.ts +23 -35
- package/dist/federation/paths.d.ts.map +1 -1
- package/dist/federation/paths.js +26 -39
- package/dist/federation/paths.js.map +1 -1
- package/dist/federation/records.d.ts +223 -0
- package/dist/federation/records.d.ts.map +1 -0
- package/dist/federation/records.js +506 -0
- package/dist/federation/records.js.map +1 -0
- package/dist/federation/relays.d.ts +114 -0
- package/dist/federation/relays.d.ts.map +1 -0
- package/dist/federation/relays.js +290 -0
- package/dist/federation/relays.js.map +1 -0
- package/dist/federation/replies.d.ts +80 -0
- package/dist/federation/replies.d.ts.map +1 -0
- package/dist/federation/replies.js +166 -0
- package/dist/federation/replies.js.map +1 -0
- package/dist/federation/wordpress.d.ts +131 -0
- package/dist/federation/wordpress.d.ts.map +1 -0
- package/dist/federation/wordpress.js +341 -0
- package/dist/federation/wordpress.js.map +1 -0
- package/dist/files/atomic.d.ts +103 -0
- package/dist/files/atomic.d.ts.map +1 -0
- package/dist/files/atomic.js +150 -0
- package/dist/files/atomic.js.map +1 -0
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +2 -0
- package/dist/files/index.js.map +1 -0
- package/dist/forms/protection.d.ts +85 -0
- package/dist/forms/protection.d.ts.map +1 -0
- package/dist/forms/protection.js +116 -0
- package/dist/forms/protection.js.map +1 -0
- package/dist/images/icons.d.ts +59 -0
- package/dist/images/icons.d.ts.map +1 -0
- package/dist/images/icons.js +131 -0
- package/dist/images/icons.js.map +1 -0
- package/dist/images/index.d.ts +7 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +4 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/markup.d.ts +47 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +175 -0
- package/dist/images/markup.js.map +1 -0
- package/dist/images/paths.d.ts +34 -0
- package/dist/images/paths.d.ts.map +1 -0
- package/dist/images/paths.js +41 -0
- package/dist/images/paths.js.map +1 -0
- package/dist/images/variants.d.ts +104 -0
- package/dist/images/variants.d.ts.map +1 -0
- package/dist/images/variants.js +320 -0
- package/dist/images/variants.js.map +1 -0
- package/dist/index.d.ts +123 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +327 -25
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +18 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +34 -3
- package/dist/init.js.map +1 -1
- package/dist/mail/brevo.d.ts +35 -0
- package/dist/mail/brevo.d.ts.map +1 -0
- package/dist/mail/brevo.js +85 -0
- package/dist/mail/brevo.js.map +1 -0
- package/dist/mail/credentials.d.ts +62 -0
- package/dist/mail/credentials.d.ts.map +1 -0
- package/dist/mail/credentials.js +86 -0
- package/dist/mail/credentials.js.map +1 -0
- package/dist/mail/index.d.ts +15 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +8 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/memory.d.ts +35 -0
- package/dist/mail/memory.d.ts.map +1 -0
- package/dist/mail/memory.js +29 -0
- package/dist/mail/memory.js.map +1 -0
- package/dist/mail/provider.d.ts +80 -0
- package/dist/mail/provider.d.ts.map +1 -0
- package/dist/mail/provider.js +23 -0
- package/dist/mail/provider.js.map +1 -0
- package/dist/mail/service.d.ts +156 -0
- package/dist/mail/service.d.ts.map +1 -0
- package/dist/mail/service.js +265 -0
- package/dist/mail/service.js.map +1 -0
- package/dist/mail/smtp.d.ts +39 -0
- package/dist/mail/smtp.d.ts.map +1 -0
- package/dist/mail/smtp.js +58 -0
- package/dist/mail/smtp.js.map +1 -0
- package/dist/mail/templates.d.ts +91 -0
- package/dist/mail/templates.d.ts.map +1 -0
- package/dist/mail/templates.js +69 -0
- package/dist/mail/templates.js.map +1 -0
- package/dist/notifications/comments.d.ts +66 -0
- package/dist/notifications/comments.d.ts.map +1 -0
- package/dist/notifications/comments.js +151 -0
- package/dist/notifications/comments.js.map +1 -0
- package/dist/notifications/digest.d.ts +135 -0
- package/dist/notifications/digest.d.ts.map +1 -0
- package/dist/notifications/digest.js +287 -0
- package/dist/notifications/digest.js.map +1 -0
- package/dist/notifications/index.d.ts +13 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +8 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/links.d.ts +60 -0
- package/dist/notifications/links.d.ts.map +1 -0
- package/dist/notifications/links.js +63 -0
- package/dist/notifications/links.js.map +1 -0
- package/dist/notifications/optouts.d.ts +48 -0
- package/dist/notifications/optouts.d.ts.map +1 -0
- package/dist/notifications/optouts.js +93 -0
- package/dist/notifications/optouts.js.map +1 -0
- package/dist/notifications/preferences.d.ts +158 -0
- package/dist/notifications/preferences.d.ts.map +1 -0
- package/dist/notifications/preferences.js +195 -0
- package/dist/notifications/preferences.js.map +1 -0
- package/dist/notifications/routes.d.ts +28 -0
- package/dist/notifications/routes.d.ts.map +1 -0
- package/dist/notifications/routes.js +184 -0
- package/dist/notifications/routes.js.map +1 -0
- package/dist/notifications/tokens.d.ts +60 -0
- package/dist/notifications/tokens.d.ts.map +1 -0
- package/dist/notifications/tokens.js +125 -0
- package/dist/notifications/tokens.js.map +1 -0
- package/dist/notify.d.ts +84 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +149 -0
- package/dist/notify.js.map +1 -0
- package/dist/web/archive.d.ts +49 -0
- package/dist/web/archive.d.ts.map +1 -0
- package/dist/web/archive.js +59 -0
- package/dist/web/archive.js.map +1 -0
- package/dist/web/assets.d.ts +6 -5
- package/dist/web/assets.d.ts.map +1 -1
- package/dist/web/assets.js +5 -7
- package/dist/web/assets.js.map +1 -1
- package/dist/web/authors.d.ts +143 -0
- package/dist/web/authors.d.ts.map +1 -0
- package/dist/web/authors.js +178 -0
- package/dist/web/authors.js.map +1 -0
- package/dist/web/comments.d.ts +60 -0
- package/dist/web/comments.d.ts.map +1 -0
- package/dist/web/comments.js +207 -0
- package/dist/web/comments.js.map +1 -0
- package/dist/web/context.d.ts +162 -28
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +123 -26
- package/dist/web/context.js.map +1 -1
- package/dist/web/conversation.d.ts +209 -0
- package/dist/web/conversation.d.ts.map +1 -0
- package/dist/web/conversation.js +557 -0
- package/dist/web/conversation.js.map +1 -0
- package/dist/web/documents.d.ts +36 -6
- package/dist/web/documents.d.ts.map +1 -1
- package/dist/web/documents.js +52 -10
- package/dist/web/documents.js.map +1 -1
- package/dist/web/feed-atom.d.ts +22 -0
- package/dist/web/feed-atom.d.ts.map +1 -0
- package/dist/web/feed-atom.js +83 -0
- package/dist/web/feed-atom.js.map +1 -0
- package/dist/web/feed-item.d.ts +145 -0
- package/dist/web/feed-item.d.ts.map +1 -0
- package/dist/web/feed-item.js +120 -0
- package/dist/web/feed-item.js.map +1 -0
- package/dist/web/feed-json.d.ts +79 -0
- package/dist/web/feed-json.d.ts.map +1 -0
- package/dist/web/feed-json.js +53 -0
- package/dist/web/feed-json.js.map +1 -0
- package/dist/web/feed-rss.d.ts +26 -0
- package/dist/web/feed-rss.d.ts.map +1 -0
- package/dist/web/feed-rss.js +203 -0
- package/dist/web/feed-rss.js.map +1 -0
- package/dist/web/feed-source.d.ts +244 -0
- package/dist/web/feed-source.d.ts.map +1 -0
- package/dist/web/feed-source.js +201 -0
- package/dist/web/feed-source.js.map +1 -0
- package/dist/web/feed-xml.d.ts +63 -0
- package/dist/web/feed-xml.d.ts.map +1 -0
- package/dist/web/feed-xml.js +116 -0
- package/dist/web/feed-xml.js.map +1 -0
- package/dist/web/feeds.d.ts +31 -96
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +94 -198
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/health.d.ts +31 -0
- package/dist/web/health.d.ts.map +1 -0
- package/dist/web/health.js +44 -0
- package/dist/web/health.js.map +1 -0
- package/dist/web/index.d.ts +30 -10
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +18 -7
- package/dist/web/index.js.map +1 -1
- package/dist/web/navigation.d.ts +73 -0
- package/dist/web/navigation.d.ts.map +1 -0
- package/dist/web/navigation.js +110 -0
- package/dist/web/navigation.js.map +1 -0
- package/dist/web/negotiate.d.ts +9 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +6 -1
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/recent.d.ts +43 -0
- package/dist/web/recent.d.ts.map +1 -0
- package/dist/web/recent.js +37 -0
- package/dist/web/recent.js.map +1 -0
- package/dist/web/render.d.ts +197 -9
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +334 -17
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts +25 -8
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +845 -93
- package/dist/web/routes.js.map +1 -1
- package/dist/web/sanitize.d.ts +19 -0
- package/dist/web/sanitize.d.ts.map +1 -0
- package/dist/web/sanitize.js +272 -0
- package/dist/web/sanitize.js.map +1 -0
- package/dist/web/search.d.ts +79 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +94 -0
- package/dist/web/search.js.map +1 -0
- package/dist/web/sitemap.d.ts +120 -0
- package/dist/web/sitemap.d.ts.map +1 -0
- package/dist/web/sitemap.js +199 -0
- package/dist/web/sitemap.js.map +1 -0
- package/dist/web/taxonomy.d.ts +152 -0
- package/dist/web/taxonomy.d.ts.map +1 -0
- package/dist/web/taxonomy.js +237 -0
- package/dist/web/taxonomy.js.map +1 -0
- package/dist/web/templates.d.ts +54 -16
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +109 -28
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +205 -0
- package/dist/web/themes.d.ts.map +1 -0
- package/dist/web/themes.js +277 -0
- package/dist/web/themes.js.map +1 -0
- package/dist/webmention/discovery.d.ts +51 -0
- package/dist/webmention/discovery.d.ts.map +1 -0
- package/dist/webmention/discovery.js +176 -0
- package/dist/webmention/discovery.js.map +1 -0
- package/dist/webmention/html.d.ts +71 -0
- package/dist/webmention/html.d.ts.map +1 -0
- package/dist/webmention/html.js +285 -0
- package/dist/webmention/html.js.map +1 -0
- package/dist/webmention/index.d.ts +12 -0
- package/dist/webmention/index.d.ts.map +1 -0
- package/dist/webmention/index.js +8 -0
- package/dist/webmention/index.js.map +1 -0
- package/dist/webmention/links.d.ts +18 -0
- package/dist/webmention/links.d.ts.map +1 -0
- package/dist/webmention/links.js +64 -0
- package/dist/webmention/links.js.map +1 -0
- package/dist/webmention/microformats.d.ts +93 -0
- package/dist/webmention/microformats.d.ts.map +1 -0
- package/dist/webmention/microformats.js +376 -0
- package/dist/webmention/microformats.js.map +1 -0
- package/dist/webmention/receive.d.ts +141 -0
- package/dist/webmention/receive.d.ts.map +1 -0
- package/dist/webmention/receive.js +264 -0
- package/dist/webmention/receive.js.map +1 -0
- package/dist/webmention/routes.d.ts +37 -0
- package/dist/webmention/routes.d.ts.map +1 -0
- package/dist/webmention/routes.js +75 -0
- package/dist/webmention/routes.js.map +1 -0
- package/dist/webmention/service.d.ts +94 -0
- package/dist/webmention/service.d.ts.map +1 -0
- package/dist/webmention/service.js +207 -0
- package/dist/webmention/service.js.map +1 -0
- package/package.json +11 -6
- package/templates/site/content/_data/site.json +1 -0
- package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
- package/templates/site/geekity.config.ts +11 -1
- package/templates/site/tsconfig.json +1 -1
- package/themes/default/README.md +1096 -50
- package/themes/default/layouts/404.njk +2 -3
- package/themes/default/layouts/author.njk +37 -0
- package/themes/default/layouts/base.njk +172 -31
- package/themes/default/layouts/category.njk +27 -0
- package/themes/default/layouts/front-page.njk +57 -0
- package/themes/default/layouts/home.njk +17 -1
- package/themes/default/layouts/page.njk +57 -5
- package/themes/default/layouts/post.njk +78 -19
- package/themes/default/layouts/search.njk +57 -0
- package/themes/default/layouts/tag.njk +3 -6
- package/themes/default/mail/comment-digest.html.njk +37 -0
- package/themes/default/mail/comment-digest.subject.njk +3 -0
- package/themes/default/mail/comment-digest.txt.njk +40 -0
- package/themes/default/mail/comment-pending.html.njk +27 -0
- package/themes/default/mail/comment-pending.subject.njk +2 -0
- package/themes/default/mail/comment-pending.txt.njk +24 -0
- package/themes/default/mail/comment-reply.html.njk +21 -0
- package/themes/default/mail/comment-reply.subject.njk +2 -0
- package/themes/default/mail/comment-reply.txt.njk +22 -0
- package/themes/default/mail/contact-message.html.njk +24 -0
- package/themes/default/mail/contact-message.subject.njk +5 -0
- package/themes/default/mail/contact-message.txt.njk +20 -0
- package/themes/default/mail/password-changed.html.njk +18 -0
- package/themes/default/mail/password-changed.subject.njk +2 -0
- package/themes/default/mail/password-changed.txt.njk +17 -0
- package/themes/default/mail/password-reset.html.njk +22 -0
- package/themes/default/mail/password-reset.subject.njk +2 -0
- package/themes/default/mail/password-reset.txt.njk +18 -0
- package/themes/default/mail/test.html.njk +14 -0
- package/themes/default/mail/test.subject.njk +2 -0
- package/themes/default/mail/test.txt.njk +12 -0
- package/themes/default/partials/archive.njk +27 -0
- package/themes/default/partials/bio.njk +77 -0
- package/themes/default/partials/comment-form.njk +109 -0
- package/themes/default/partials/contact-form.njk +88 -0
- package/themes/default/partials/conversation.njk +139 -0
- package/themes/default/partials/feeds.njk +26 -0
- package/themes/default/partials/jsonld.njk +120 -0
- package/themes/default/partials/pagination.njk +12 -6
- package/themes/default/partials/post-list.njk +51 -18
- package/themes/default/partials/search-form.njk +17 -0
- package/themes/default/partials/tags.njk +26 -5
- package/themes/default/static/highlight.js +4 -0
- package/themes/default/static/style.css +1119 -194
- package/themes/default/theme.json +5 -0
- package/admin/layouts/document-editor.njk +0 -167
- package/admin/layouts/federation.njk +0 -145
- package/admin/layouts/login.njk +0 -23
- package/admin/layouts/settings.njk +0 -116
- package/admin/layouts/setup.njk +0 -27
- package/admin/layouts/users.njk +0 -91
- /package/admin/{layouts/_flash.njk → components/flash.njk} +0 -0
package/themes/default/README.md
CHANGED
|
@@ -5,17 +5,37 @@ without copying anything.
|
|
|
5
5
|
|
|
6
6
|
```
|
|
7
7
|
themes/default/
|
|
8
|
+
theme.json the manifest: name, kind and description
|
|
8
9
|
layouts/
|
|
9
10
|
base.njk the page every other layout extends
|
|
10
11
|
home.njk the post archive, paginated
|
|
12
|
+
front-page.njk the page the Reading setting serves at /
|
|
11
13
|
post.njk one post
|
|
12
14
|
page.njk one page
|
|
13
15
|
tag.njk a tag archive, paginated
|
|
16
|
+
category.njk a category archive, paginated
|
|
17
|
+
author.njk one person's archive, paginated
|
|
18
|
+
search.njk the search form and what it found, paginated
|
|
14
19
|
404.njk nothing at this URL
|
|
15
20
|
partials/
|
|
16
|
-
post-list.njk
|
|
17
|
-
pagination.njk
|
|
18
|
-
tags.njk
|
|
21
|
+
post-list.njk the h-feed a listing is made of
|
|
22
|
+
pagination.njk previous/next pager
|
|
23
|
+
tags.njk macros for tag and category links
|
|
24
|
+
bio.njk who an entry is by, as an h-card, with the site menu
|
|
25
|
+
feeds.njk macros for the feed links in <head>
|
|
26
|
+
conversation.njk the replies, likes and boosts under a post
|
|
27
|
+
comment-form.njk the form under a post that is taking comments
|
|
28
|
+
contact-form.njk the form on a page whose front matter says contact: true
|
|
29
|
+
archive.njk every post by month, on a page that says archive: true
|
|
30
|
+
search-form.njk the search box, in the footer and on the search page
|
|
31
|
+
mail/
|
|
32
|
+
test.*.njk the Send test email message
|
|
33
|
+
password-reset.*.njk the forgot-password link
|
|
34
|
+
password-changed.*.njk the notice sent once a password has been set
|
|
35
|
+
comment-pending.*.njk the moderation notice
|
|
36
|
+
comment-digest.*.njk the hourly or daily digest of what is waiting
|
|
37
|
+
comment-reply.*.njk the notice a commenter gets about a reply
|
|
38
|
+
contact-message.*.njk a message from a page's contact form
|
|
19
39
|
static/
|
|
20
40
|
style.css served at /theme/style.css
|
|
21
41
|
```
|
|
@@ -23,18 +43,62 @@ themes/default/
|
|
|
23
43
|
Plain Nunjucks and plain CSS. There is no build step and no dependency: a site
|
|
24
44
|
that wants Tailwind or anything else brings its own.
|
|
25
45
|
|
|
46
|
+
## The manifest
|
|
47
|
+
|
|
48
|
+
`theme.json` is what makes a directory a theme:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"name": "Default",
|
|
53
|
+
"kind": "site",
|
|
54
|
+
"description": "One line about the theme."
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`name` is what a person sees, `kind` is `site` — the only kind there is, and
|
|
59
|
+
the field exists so another can be added later without the format changing —
|
|
60
|
+
and `description` is optional. The directory name is the theme's id. A
|
|
61
|
+
directory without a readable manifest, or one naming a kind this CMS does not
|
|
62
|
+
have, is not a theme.
|
|
63
|
+
|
|
26
64
|
## Overriding a template
|
|
27
65
|
|
|
28
|
-
|
|
29
|
-
|
|
66
|
+
A site keeps its themes under one directory, `themes/` by default and
|
|
67
|
+
`themesDir` in the config, with one folder per theme and a `theme.json` in
|
|
68
|
+
each. Which one it wears is one setting, `theme` in `content/_data/site.json`,
|
|
69
|
+
holding the folder's name:
|
|
30
70
|
|
|
31
71
|
```
|
|
32
|
-
|
|
72
|
+
themes/
|
|
73
|
+
midnight/
|
|
74
|
+
theme.json
|
|
75
|
+
layouts/post.njk
|
|
76
|
+
static/style.css
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "theme": "midnight" }
|
|
33
81
|
```
|
|
34
82
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
83
|
+
The setting is what **Appearance > Themes** in the admin writes: the screen
|
|
84
|
+
lists this theme and every folder under `themes/`, marks the one in use, and
|
|
85
|
+
Activate on another writes its name — or, on the packaged theme, takes the key
|
|
86
|
+
out again. A change takes effect on the next request, with no restart, and a
|
|
87
|
+
folder whose manifest will not read is listed under "Not themes" with the
|
|
88
|
+
reason rather than quietly left out.
|
|
89
|
+
|
|
90
|
+
Resolution order for any template is then that theme first, this directory
|
|
91
|
+
second, one file at a time. The theme above replaces the post layout and keeps
|
|
92
|
+
receiving updates to every other template, and `static/style.css` replaces the
|
|
93
|
+
stylesheet the same way — assets under `/theme/` resolve in the same order. A
|
|
94
|
+
site that has chosen no theme reads this one and nothing else, and a theme
|
|
95
|
+
sitting in `themes/` that the setting does not name is never on the path at
|
|
96
|
+
all. A `theme` naming a folder that is not there, or is not a theme, falls back
|
|
97
|
+
to this one with a warning in the log rather than a broken site.
|
|
98
|
+
|
|
99
|
+
The admin is not a theme and has no setting. Its templates live in their own
|
|
100
|
+
tree with a loader of their own, off this search path entirely, so no theme can
|
|
101
|
+
shadow the login form or the CSRF field inside it.
|
|
38
102
|
|
|
39
103
|
An override is a normal Nunjucks template, so it can extend or include the
|
|
40
104
|
packaged ones by name:
|
|
@@ -53,6 +117,570 @@ packaged ones by name:
|
|
|
53
117
|
`layouts/base.njk` defines the blocks `title`, `head`, `alternates`, `header`,
|
|
54
118
|
`content`, `footer` and `scripts`, so most sites never have to copy it.
|
|
55
119
|
|
|
120
|
+
Two layouts are named for the pages the Reading settings pick:
|
|
121
|
+
|
|
122
|
+
| Template | Rendered for | Shipped here | Falls back to |
|
|
123
|
+
| ------------------------ | ----------------------------- | ------------ | ------------------ |
|
|
124
|
+
| `layouts/front-page.njk` | the page served at `/` | yes | `layouts/page.njk` |
|
|
125
|
+
| `layouts/posts-page.njk` | the listing on the posts page | no | `layouts/home.njk` |
|
|
126
|
+
|
|
127
|
+
`layouts/front-page.njk` is a layout here rather than only an override point:
|
|
128
|
+
see [The front page](#the-front-page) for what it draws. Overriding it is how a
|
|
129
|
+
front page is laid out differently from every other page without overriding the
|
|
130
|
+
layout they all use, and the fallback above is reached only by a theme that
|
|
131
|
+
replaced it with nothing.
|
|
132
|
+
|
|
133
|
+
There is no posts page layout: a site that sets one gets the listing layout
|
|
134
|
+
until it writes one. On the posts page the page's own front matter and rendered
|
|
135
|
+
body are on the context beside the listing, so `{{ content | safe }}` prints its
|
|
136
|
+
words above the posts; `layouts/home.njk` already does.
|
|
137
|
+
|
|
138
|
+
## The page shell
|
|
139
|
+
|
|
140
|
+
`layouts/base.njk` is the andrewshell.org design's shell (decision-16): the
|
|
141
|
+
skip link, one `.global-wrapper` at the 42rem measure, `.global-header`,
|
|
142
|
+
`<main id="main">` and the footer, in that order.
|
|
143
|
+
|
|
144
|
+
**The header has one rule.** On the front page it is the site title as
|
|
145
|
+
`h1.main-heading`, linked home, with `site.tagline` in a paragraph under it; on
|
|
146
|
+
every other page it is `a.header-link-home`, the site title small and linked
|
|
147
|
+
home, and no tagline. The wrapper carries `data-is-root-path="true"` at `/` and
|
|
148
|
+
nothing anywhere else, which is how the stylesheet tells the two apart. There is
|
|
149
|
+
no navigation in the header — see [Navigation](#navigation).
|
|
150
|
+
|
|
151
|
+
**The footer** prints the copyright with the current year and `site.author`,
|
|
152
|
+
`Published with Geekity`, and then one `ul.hlist` holding an RSS link to
|
|
153
|
+
`/feed/` and one `rel="me"` link per entry of `siteAuthor.links`. A site whose
|
|
154
|
+
`author` setting names nobody with an account here gets the line and the RSS
|
|
155
|
+
link and no identity links, because `siteAuthor` is absent. The year is
|
|
156
|
+
`{{ "now" | date("year") }}` — `now` is the one word the `date` filter reads
|
|
157
|
+
rather than parses — so it is the year at the moment the page is rendered, in
|
|
158
|
+
the site's own timezone.
|
|
159
|
+
|
|
160
|
+
Webrings, badges, a licence notice and anything else particular to one site are
|
|
161
|
+
deliberately not in the package. They go in a site theme's `footer` block:
|
|
162
|
+
|
|
163
|
+
```njk
|
|
164
|
+
{% extends "layouts/base.njk" %}
|
|
165
|
+
|
|
166
|
+
{% block footer %}
|
|
167
|
+
{{ super() }}
|
|
168
|
+
<p class="webring">
|
|
169
|
+
<a href="https://example.ring/previous">←</a>
|
|
170
|
+
An <a href="https://example.ring">example webring</a>
|
|
171
|
+
<a href="https://example.ring/next">→</a>
|
|
172
|
+
</p>
|
|
173
|
+
{% endblock %}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### An entry
|
|
177
|
+
|
|
178
|
+
`layouts/post.njk` and `layouts/page.njk` draw the same thing, the source
|
|
179
|
+
design's entry, and it is a microformats2 `h-entry`:
|
|
180
|
+
|
|
181
|
+
```html
|
|
182
|
+
<article class="blog-post h-entry">
|
|
183
|
+
<header>
|
|
184
|
+
<h1 class="p-name">Hello</h1>
|
|
185
|
+
</header>
|
|
186
|
+
<section class="e-content">
|
|
187
|
+
<p>The rendered body.</p>
|
|
188
|
+
<p class="entry-meta">
|
|
189
|
+
<a class="u-category small" href="https://news.indieweb.org/en"
|
|
190
|
+
>#indienews</a
|
|
191
|
+
>
|
|
192
|
+
<a href="/2026/09/hello/" class="u-url"
|
|
193
|
+
><time class="small dt-published" datetime="2026-09-02T09:00:00.000Z"
|
|
194
|
+
>Published 2 September 2026</time
|
|
195
|
+
></a
|
|
196
|
+
>
|
|
197
|
+
<br /><time class="small dt-updated" datetime="2026-09-05T09:00:00.000Z"
|
|
198
|
+
>Updated 5 September 2026</time
|
|
199
|
+
>
|
|
200
|
+
</p>
|
|
201
|
+
</section>
|
|
202
|
+
<p class="post-categories">…categories…</p>
|
|
203
|
+
<p class="post-categories">…tags…</p>
|
|
204
|
+
<hr />
|
|
205
|
+
<footer>…the bio…</footer>
|
|
206
|
+
</article>
|
|
207
|
+
<nav class="blog-post-nav">…the posts either side…</nav>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**The Published line is inside the `e-content`**, which is the one thing about
|
|
211
|
+
this shape worth knowing. It is what the source theme does: the date and the
|
|
212
|
+
permalink are part of the words, so a reader — or a fediverse peer reading the
|
|
213
|
+
`e-content` — takes them with the post. The permalink wraps the `dt-published`
|
|
214
|
+
time as a `u-url`, and a `dt-updated` line follows it only when the update
|
|
215
|
+
happened on a different day in the site's own timezone; a typo fixed an hour
|
|
216
|
+
later is not news. A post tagged `indienews` opens the line with a
|
|
217
|
+
`u-category` link to <https://news.indieweb.org/en>, which is how IndieNews is
|
|
218
|
+
told the post is for it; a page never prints one, and its line is a
|
|
219
|
+
`p.page-meta` rather than a `p.entry-meta`.
|
|
220
|
+
|
|
221
|
+
What it is filed under is printed under the words and inside the article, so
|
|
222
|
+
that each link is a `p-category` of this entry: the categories first, then the
|
|
223
|
+
tags, each as one `p.post-categories` from `partials/tags.njk`.
|
|
224
|
+
|
|
225
|
+
After the entry a post prints `nav.blog-post-nav`, `rel="prev"` and
|
|
226
|
+
`rel="next"` links to `previous` and `next` with an arrow either side, and
|
|
227
|
+
nothing at all at the ends of the archive; then the conversation and the
|
|
228
|
+
comment form. A page prints the contact form when its front matter asked for
|
|
229
|
+
one. A page has no neighbours, no taxonomy and no IndieNews link, because none
|
|
230
|
+
of those are things a page has.
|
|
231
|
+
|
|
232
|
+
### The bio
|
|
233
|
+
|
|
234
|
+
`partials/bio.njk` is who the page is by: the `p-author h-card` the rule and
|
|
235
|
+
the footer at the end of an entry hold, and the heading of an author archive.
|
|
236
|
+
It replaces `partials/byline.njk`, which is gone — a byline was a name in a
|
|
237
|
+
meta line, and this is the whole credit.
|
|
238
|
+
|
|
239
|
+
```njk
|
|
240
|
+
{% set bioAuthor = author or siteAuthor %}
|
|
241
|
+
{% include "partials/bio.njk" %}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`bioAuthor` is the one thing to set, and nothing renders when it is absent.
|
|
245
|
+
It is one of the profile objects the context already carries: `author` on a
|
|
246
|
+
post, falling back to `siteAuthor`; `siteAuthor` on a page; the archive's
|
|
247
|
+
person on an author archive. `layouts/base.njk` reads it too — see
|
|
248
|
+
[Navigation](#navigation).
|
|
249
|
+
|
|
250
|
+
What the bio prints: a round `u-photo` at 50px when they have an avatar,
|
|
251
|
+
"Written by" and their name as a `p-name u-url` linked `rel="author me"` to
|
|
252
|
+
their archive, then `p-job-title` and `p-locality` when the profile says. A
|
|
253
|
+
name this site has no account for is printed unlinked, because the file still
|
|
254
|
+
said somebody wrote this. Then the site menu.
|
|
255
|
+
|
|
256
|
+
`bioLead` is what the line opens with, `Written by` unless a layout sets
|
|
257
|
+
another; `layouts/author.njk` sets `Posts by`, because the card there heads
|
|
258
|
+
somebody's writing rather than crediting one piece of it.
|
|
259
|
+
|
|
260
|
+
Set `bioProfile` as well and it also prints their `p-note` and their `rel="me"`
|
|
261
|
+
links; `layouts/author.njk` does, because that page is about the person rather
|
|
262
|
+
than about something they wrote. An entry leaves it unset: the page footer
|
|
263
|
+
already prints the site's identity links, and the `rel="author me"` link leads
|
|
264
|
+
to the archive where this person's own are.
|
|
265
|
+
|
|
266
|
+
**A site that used `partials/byline.njk`** — `{{ byline.line(author) }}` from
|
|
267
|
+
an overridden layout — either includes this partial instead or writes the line
|
|
268
|
+
itself. It was four lines of markup, and the object behind it has not changed:
|
|
269
|
+
|
|
270
|
+
```njk
|
|
271
|
+
{% set writer = author or { name: site.author } %}
|
|
272
|
+
<span class="p-author h-card">
|
|
273
|
+
{%- if writer.url %}<a class="u-url" href="{{ writer.url | url }}">{{ writer.name }}</a>
|
|
274
|
+
{%- else %}{{ writer.name }}{% endif %}
|
|
275
|
+
</span>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`apps/demo/themes/demo/layouts/post.njk` is that, worked: the demo wants a
|
|
279
|
+
byline under the title rather than a bio in the footer, so it writes one.
|
|
280
|
+
|
|
281
|
+
### A listing
|
|
282
|
+
|
|
283
|
+
`partials/post-list.njk` is the feed every listing is made of — the home page,
|
|
284
|
+
the posts page, a tag, category or author archive — and it is a microformats2
|
|
285
|
+
`h-feed`:
|
|
286
|
+
|
|
287
|
+
```html
|
|
288
|
+
<div class="feed h-feed">
|
|
289
|
+
<article class="feed-item h-entry">
|
|
290
|
+
<div class="feed-content">
|
|
291
|
+
<h2 class="feed-title p-name">
|
|
292
|
+
<a href="/2026/09/hello/" class="u-url">Hello</a>
|
|
293
|
+
</h2>
|
|
294
|
+
<div class="feed-excerpt p-summary">
|
|
295
|
+
<p>What the post is about, in about 280 characters...</p>
|
|
296
|
+
</div>
|
|
297
|
+
<p class="feed-more">
|
|
298
|
+
<a href="/hello/" aria-label="Continue reading: Hello">
|
|
299
|
+
Continue reading<span aria-hidden="true"> →</span>
|
|
300
|
+
</a>
|
|
301
|
+
</p>
|
|
302
|
+
<div class="feed-meta">
|
|
303
|
+
<p>
|
|
304
|
+
<time class="feed-date dt-published" datetime="…"
|
|
305
|
+
>2 September 2026</time
|
|
306
|
+
>
|
|
307
|
+
</p>
|
|
308
|
+
<p class="post-categories">
|
|
309
|
+
<a href="/category/notes/" class="p-category" rel="category">notes</a>
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</article>
|
|
314
|
+
<hr class="feed-separator" />
|
|
315
|
+
<article class="feed-item h-entry">…</article>
|
|
316
|
+
</div>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The excerpt is the entry's `summary` — the very line the feeds publish, so a
|
|
320
|
+
reader and a feed cannot be told two different things — through Nunjucks'
|
|
321
|
+
`truncate(280)`, which cuts at the last space before 280 characters and adds an
|
|
322
|
+
ellipsis. A `description` shorter than that is printed whole. It is plain text
|
|
323
|
+
rather than markup, deliberately: an excerpt with half a code block in it is
|
|
324
|
+
not an excerpt.
|
|
325
|
+
|
|
326
|
+
An entry's tags are not in a feed item. They belong under the entry, where
|
|
327
|
+
there is one post's worth of them rather than twenty.
|
|
328
|
+
|
|
329
|
+
**The heading level belongs to the layout**, not to the list. `feedHeading` is
|
|
330
|
+
the level `post-list.njk` prints its titles at and defaults to 2, which is what
|
|
331
|
+
a listing wants; a page whose feed sits under a heading of its own sets 3
|
|
332
|
+
before including it, so the outline never skips a level:
|
|
333
|
+
|
|
334
|
+
```njk
|
|
335
|
+
{% set feedHeading = 3 %}
|
|
336
|
+
{% include "partials/post-list.njk" %}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A listing with nothing on it says `No posts found.` in `p.empty`.
|
|
340
|
+
|
|
341
|
+
`partials/pagination.njk` prints two arrows in `nav.pagination` — `← Previous`
|
|
342
|
+
to the page before this one and `Next →` to the page after it, with `rel="prev"`
|
|
343
|
+
and `rel="next"` — and nothing at all on a listing of one page. The arrows
|
|
344
|
+
themselves are `aria-hidden`, because a screen reader that announced them would
|
|
345
|
+
read the decoration and then the word.
|
|
346
|
+
|
|
347
|
+
Each listing layout heads its own page: `layouts/home.njk` with the listing's
|
|
348
|
+
title, `layouts/tag.njk` and `layouts/category.njk` with the term, and
|
|
349
|
+
`layouts/author.njk` with the person as an `h-card`. The category archive puts
|
|
350
|
+
its heading in a `header.category-header` and prints a `div.category-description`
|
|
351
|
+
under it when the context carries a `categoryDescription`; the CMS has no store
|
|
352
|
+
of term descriptions, so nothing writes one today and the header is the heading
|
|
353
|
+
alone.
|
|
354
|
+
|
|
355
|
+
`layouts/404.njk` says `Content not found.` and links home and to the search.
|
|
356
|
+
|
|
357
|
+
### Search
|
|
358
|
+
|
|
359
|
+
`layouts/search.njk` is the page at `/search/` (TASK-22). It prints
|
|
360
|
+
`partials/search-form.njk`, and once there is a `query` a `p.search-summary`
|
|
361
|
+
saying how many documents matched, a `div.search-results` of
|
|
362
|
+
`article.search-result` entries, and the ordinary pager. It asks not to be
|
|
363
|
+
indexed with `<meta name="robots" content="noindex">` in its `head` block.
|
|
364
|
+
|
|
365
|
+
The context is a listing's with two differences. `query` is the words searched
|
|
366
|
+
for, trimmed, and an empty string on the page before a search; it is defined
|
|
367
|
+
only on this page, which is how `layouts/base.njk` knows to leave the footer's
|
|
368
|
+
search box off it. Each entry in `posts` carries a `snippet`: a few words of
|
|
369
|
+
HTML around the match, escaped, with every matched word in `<mark>`. Print it
|
|
370
|
+
with `safe`. The results are best match first rather than newest first, and
|
|
371
|
+
only what the public site would serve is ever among them.
|
|
372
|
+
|
|
373
|
+
`partials/search-form.njk` is a `GET` form to `/search/` with the words in `q`,
|
|
374
|
+
so it needs no JavaScript and a search is a URL. `layouts/base.njk` puts it in
|
|
375
|
+
the footer of every other page. The search page takes the path `/search/` ahead
|
|
376
|
+
of any document permalinked there.
|
|
377
|
+
|
|
378
|
+
### The front page
|
|
379
|
+
|
|
380
|
+
`layouts/front-page.njk` is what a site gets at `/` once the Reading settings
|
|
381
|
+
name a homepage (`homepage` in `content/_data/site.json`). It is that page read
|
|
382
|
+
somewhere else: the same document, the same context, with `page.url` saying `/`
|
|
383
|
+
rather than the permalink that redirects there.
|
|
384
|
+
|
|
385
|
+
It draws the page's own words and then what the site has been writing:
|
|
386
|
+
|
|
387
|
+
1. `div.page-body.e-content`, the rendered body. No title — `layouts/base.njk`
|
|
388
|
+
heads the root path with the site title, and a second `h1` under it would be
|
|
389
|
+
one heading too many — and no Published line, because a front page is read
|
|
390
|
+
as the site rather than as a page somebody wrote on a Tuesday.
|
|
391
|
+
2. `<h2>Recent Posts</h2>` over `partials/post-list.njk` with `feedHeading` set
|
|
392
|
+
to 3, so the entries sit under that heading rather than beside it.
|
|
393
|
+
3. `p.front-links`, a line of links to where the writing is. The posts page is
|
|
394
|
+
linked by its own title when the site names one, and the search always is
|
|
395
|
+
(TASK-22).
|
|
396
|
+
4. The bio, under a rule, exactly as an entry ends — so the site menu is there
|
|
397
|
+
too, because that is where this design keeps it.
|
|
398
|
+
|
|
399
|
+
Two context keys are the front page's alone. `recentPosts` is the entries to
|
|
400
|
+
list, in the same shape a listing's are: the posts of the current month when
|
|
401
|
+
there are at least five of them, and the five newest otherwise. `postsPage` is
|
|
402
|
+
`{ title, url }` for the page carrying the listing, and is **absent** when the
|
|
403
|
+
site names none — a link to a listing that has no URL is a link to nothing.
|
|
404
|
+
|
|
405
|
+
### An archive page
|
|
406
|
+
|
|
407
|
+
A page whose front matter says `archive: true` prints every post the site has
|
|
408
|
+
published under its own words, newest month first:
|
|
409
|
+
|
|
410
|
+
```html
|
|
411
|
+
<section class="archive">
|
|
412
|
+
<h2>September 2026</h2>
|
|
413
|
+
<ol class="list-none">
|
|
414
|
+
<li>
|
|
415
|
+
<a href="/2026/09/hello/"><span>Hello</span></a>
|
|
416
|
+
</li>
|
|
417
|
+
</ol>
|
|
418
|
+
<h2>August 2026</h2>
|
|
419
|
+
…
|
|
420
|
+
</section>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`partials/archive.njk` draws it and `layouts/page.njk` includes it. It is the
|
|
424
|
+
whole archive on one page and nothing paginates it: an archive page is a way of
|
|
425
|
+
finding one piece of writing rather than a listing to read through, which is
|
|
426
|
+
also why there are no excerpts on it. Drafts and posts whose date has not
|
|
427
|
+
arrived are absent, as they are everywhere else.
|
|
428
|
+
|
|
429
|
+
`archiveMonths` is what it loops over, and it is on the context **only** for a
|
|
430
|
+
page whose front matter says `archive: true`, so a layout asks:
|
|
431
|
+
|
|
432
|
+
```njk
|
|
433
|
+
{% if archiveMonths %}
|
|
434
|
+
{% include "partials/archive.njk" %}
|
|
435
|
+
{% endif %}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Each entry of it is `{ month, posts }`: `month` is the heading, `September
|
|
439
|
+
2026`, and `posts` is `{ title, url, date }` for each post of it, newest first.
|
|
440
|
+
Which month a post belongs to is the CMS's answer rather than the template's,
|
|
441
|
+
because a date in a file is a UTC instant and the site's `timezone` is the lens
|
|
442
|
+
it is read through — so the grouping and the date printed under an entry are
|
|
443
|
+
one decision rather than two.
|
|
444
|
+
|
|
445
|
+
The key is honoured wherever it is written, exactly as `contact: true` is: a
|
|
446
|
+
theme that wants the list on some other kind of page adds the same three lines
|
|
447
|
+
to that layout.
|
|
448
|
+
|
|
449
|
+
### The head
|
|
450
|
+
|
|
451
|
+
Beside the title, the canonical link and the feeds, every page carries a
|
|
452
|
+
description, Open Graph and Twitter card tags, the site's icons and one
|
|
453
|
+
JSON-LD graph. All of it is in the `head` block, so an override that only means
|
|
454
|
+
to add a tag calls `{{ super() }}` first.
|
|
455
|
+
|
|
456
|
+
**The description** is the page's own `description` from the front matter, else
|
|
457
|
+
the entry's `summary` — the line the feeds publish — else `site.tagline`. It is
|
|
458
|
+
printed once, as `<meta name="description">`, and the Open Graph and Twitter
|
|
459
|
+
descriptions say the same thing.
|
|
460
|
+
|
|
461
|
+
**The card.** `og:title` is the page's title, or the site's on the front page;
|
|
462
|
+
`og:site_name` is always the site's. `og:type` is `article` on a rendered post
|
|
463
|
+
or page and `website` everywhere else, a listing carrying a page's front matter
|
|
464
|
+
included. `og:url` is the canonical URL. `twitter:card` is `summary`, the small
|
|
465
|
+
square picture beside the words, because the picture is usually a face rather
|
|
466
|
+
than a wide photograph.
|
|
467
|
+
|
|
468
|
+
**The picture** is the `image` in the entry's front matter, else `site.avatar`.
|
|
469
|
+
A site with neither prints no `og:image` and no `twitter:image` rather than an
|
|
470
|
+
empty one.
|
|
471
|
+
|
|
472
|
+
**The icons** come from the site's avatar through the derived images
|
|
473
|
+
(decision-10): `icon` at 32 and 16 pixels and `apple-touch-icon` at 180, each a
|
|
474
|
+
square PNG cropped from the middle of the avatar and encoded the first time a
|
|
475
|
+
browser asks for it. They are on the context as `icons`, a list of
|
|
476
|
+
`{ rel, sizes, href }`, which is empty — and the links are not printed at all —
|
|
477
|
+
when the site has no avatar, when its avatar is a file no icon can be made of,
|
|
478
|
+
or when image optimization is off. There is no web manifest; a site that wants
|
|
479
|
+
one adds it in its own `head` block.
|
|
480
|
+
|
|
481
|
+
**The structured data** is `partials/jsonld.njk`, one `<script
|
|
482
|
+
type="application/ld+json">` holding one `@graph` per page, and it is the only
|
|
483
|
+
structured data the theme emits — there is no Microdata anywhere, by
|
|
484
|
+
decision-16, because the visible markup already carries microformats2 for the
|
|
485
|
+
IndieWeb. The graph holds:
|
|
486
|
+
|
|
487
|
+
- `WebSite`, always, with the site's title, tagline and URL, and a `publisher`
|
|
488
|
+
pointing at the Person, and a `potentialAction` that is a `SearchAction` on
|
|
489
|
+
`/search/?q={search_term_string}`.
|
|
490
|
+
- `Person`, from `siteAuthor`: their name, archive URL, avatar, bio, job title
|
|
491
|
+
and location, and a `sameAs` of their profile links and their actor id, which
|
|
492
|
+
is what asserts that the schema.org Person and the fediverse actor are one
|
|
493
|
+
identity. Identity comes from a user profile, so a site — or a byline —
|
|
494
|
+
naming nobody with an account here prints no Person, and the `author` and
|
|
495
|
+
`publisher` references go with it.
|
|
496
|
+
- `ProfilePage` on an author archive, whose `mainEntity` is that Person.
|
|
497
|
+
- `BlogPosting` on a post and `Article` on a page, with the headline, URL,
|
|
498
|
+
`mainEntityOfPage`, `datePublished`, `dateModified`, description, image,
|
|
499
|
+
`author` and `publisher`.
|
|
500
|
+
|
|
501
|
+
A site that wants a different graph — more types, an `Organization` publisher,
|
|
502
|
+
nothing at all — writes its own `partials/jsonld.njk` and that file replaces
|
|
503
|
+
this one, like any other partial.
|
|
504
|
+
|
|
505
|
+
### Colours
|
|
506
|
+
|
|
507
|
+
`static/style.css` is the source design: a serif body and sans headings at an
|
|
508
|
+
18px root on a 1.2 minor-third scale, warm paper with near-black text, a rust
|
|
509
|
+
primary and a blue secondary, one column, links that invert to the primary
|
|
510
|
+
colour on hover, and a rule in the primary colour. Everything is a custom
|
|
511
|
+
property on `:root`, so a site that only wants different colours overrides the
|
|
512
|
+
half-dozen `--color-*` tokens rather than the stylesheet.
|
|
513
|
+
|
|
514
|
+
The source is light only. The theme adds a second scheme under
|
|
515
|
+
`@media (prefers-color-scheme: dark)` that redefines the same colour tokens on
|
|
516
|
+
dark paper, with the rust and the blue lifted until they read on it, and
|
|
517
|
+
`color-scheme: light dark` so a browser paints its own form controls and
|
|
518
|
+
scrollbars to match. There is no toggle: the reader's system setting is the
|
|
519
|
+
setting.
|
|
520
|
+
|
|
521
|
+
Both schemes meet WCAG 2.2 AA — 4.5:1 for body text, 3:1 for large text, rules
|
|
522
|
+
and focus outlines — and that is a test rather than a claim.
|
|
523
|
+
`src/web/theme-colors.test.ts` reads the custom properties out of this
|
|
524
|
+
stylesheet and computes the ratios for every pair the design puts on screen, so
|
|
525
|
+
a colour changed here that breaks one fails the build.
|
|
526
|
+
|
|
527
|
+
| Token | What it colours |
|
|
528
|
+
| ------------------------- | -------------------------------------------------- |
|
|
529
|
+
| `--color-body` | The paper, and the text of an inverted link. |
|
|
530
|
+
| `--color-text` | The ink. |
|
|
531
|
+
| `--color-primary` | Links, the rule, the focus outline, table headers. |
|
|
532
|
+
| `--color-secondary` | Blockquote text and its border. |
|
|
533
|
+
| `--color-base` | A warm sunk surface: rules between entries. |
|
|
534
|
+
| `--color-base-2` | A cooler sunk surface. |
|
|
535
|
+
| `--color-base-3` | The raised surface: the focused skip link, inputs. |
|
|
536
|
+
| `--color-code-background` | Behind `code` and a fenced block. |
|
|
537
|
+
| `--color-code-text` | Code with no highlighting on it. |
|
|
538
|
+
| `--color-error` | A form field that will not do. |
|
|
539
|
+
|
|
540
|
+
### Code highlighting
|
|
541
|
+
|
|
542
|
+
The CMS renders a fenced block as `<pre tabindex="0"><code
|
|
543
|
+
class="language-x">` and highlights nothing: highlighting is a theme's, and it
|
|
544
|
+
happens on the client (decision-16). The `tabindex` is the core's doing and is
|
|
545
|
+
there on every `<pre>`, because a block wider than the measure scrolls sideways
|
|
546
|
+
and a scroll container nothing can focus cannot be read from a keyboard.
|
|
547
|
+
|
|
548
|
+
This theme highlights with **[highlight.js](https://highlightjs.org/)**,
|
|
549
|
+
self-hosted at `static/highlight.js` and pinned — nothing is fetched from a
|
|
550
|
+
CDN. The `scripts` block of `layouts/base.njk` loads it, deferred, **only on a
|
|
551
|
+
page whose rendered body holds a `language-` class**, so a page with no code on
|
|
552
|
+
it ships no JavaScript at all. Auto-detection is off: a block with no language,
|
|
553
|
+
or one naming a language the bundle does not carry, is left exactly as the
|
|
554
|
+
renderer wrote it — its class, its plain text, no highlighting.
|
|
555
|
+
|
|
556
|
+
The bundle carries the core and eighteen grammars — `bash`, `css`, `diff`,
|
|
557
|
+
`dockerfile`, `go`, `ini`, `javascript`, `json`, `markdown`, `nginx`, `php`,
|
|
558
|
+
`python`, `rust`, `shell`, `sql`, `typescript`, `xml` and `yaml`, with their
|
|
559
|
+
aliases, so `js`, `ts`, `html`, `yml` and `sh` all work. It is a build product
|
|
560
|
+
that is nevertheless committed, so a site gets a working highlighter out of the
|
|
561
|
+
package with no build step. Rebuilding it is one command:
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
pnpm --filter @geekity/cms build:highlight
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Run that after bumping the `highlight.js` devDependency or editing the language
|
|
568
|
+
list in `scripts/build-highlight.js`, and commit what it writes.
|
|
569
|
+
`src/web/highlight-bundle.test.ts` fails when the committed file was built from
|
|
570
|
+
a different version of highlight.js than the one installed.
|
|
571
|
+
|
|
572
|
+
**The palette is Tomorrow**, the one both of the sites this design comes from
|
|
573
|
+
used: Chris Kempson's Tomorrow on light paper and Tomorrow Night on dark, from
|
|
574
|
+
highlight.js's own base16 themes, which is also what decides which `hljs-`
|
|
575
|
+
scope takes which slot. Five of the light colours are not the stock ones — a
|
|
576
|
+
syntax theme is drawn for an editor, where a faint comment is a feature, and on
|
|
577
|
+
this paper the stock greys, orange, yellow, green and teal read between 1.7:1
|
|
578
|
+
and 3.5:1. Each was darkened along its own hue until it made 4.5:1 and no
|
|
579
|
+
further; on the dark scheme only the `base0F` brown needed lifting. Every one
|
|
580
|
+
of them is in the contrast test beside the rest of the palette.
|
|
581
|
+
|
|
582
|
+
| Token | base16 | What it colours |
|
|
583
|
+
| --------------------------------- | -------- | ------------------------------------------ |
|
|
584
|
+
| `--color-code-comment` | `base03` | Comments. |
|
|
585
|
+
| `--color-code-tag` | `base04` | Markup tags. |
|
|
586
|
+
| `--color-code-name` | `base08` | Variables, names, selectors, list bullets. |
|
|
587
|
+
| `--color-code-literal` | `base09` | Numbers, constants, attributes, links. |
|
|
588
|
+
| `--color-code-class` | `base0A` | Class names, bold. |
|
|
589
|
+
| `--color-code-string` | `base0B` | Strings and inline code. |
|
|
590
|
+
| `--color-code-support` | `base0C` | Built-ins, regexps, quotes. |
|
|
591
|
+
| `--color-code-function` | `base0D` | Function names, headings, attributes. |
|
|
592
|
+
| `--color-code-keyword` | `base0E` | Keywords, types, italic. |
|
|
593
|
+
| `--color-code-meta` | `base0F` | Preprocessor and embedded-language lines. |
|
|
594
|
+
| `--color-code-added-background` | — | An added line in a diff. |
|
|
595
|
+
| `--color-code-removed-background` | — | A removed line in a diff. |
|
|
596
|
+
|
|
597
|
+
Delimiters, operators and substitutions are deliberately `--color-code-text`,
|
|
598
|
+
the block's own ink: they are most of the characters on screen and a colour
|
|
599
|
+
there is noise. A `diff` block reads by the line rather than by the token, the
|
|
600
|
+
way the source site's `prism-diff.css` did: `.hljs-addition` and
|
|
601
|
+
`.hljs-deletion` take a tint out to the edges of the block and keep the ink, so
|
|
602
|
+
the `+` and the `-` in the text carry the meaning as well as the colour does.
|
|
603
|
+
|
|
604
|
+
**Swapping it.** Four sizes of change, smallest first:
|
|
605
|
+
|
|
606
|
+
- _Another palette, same highlighter._ Redefine the dozen `--color-code-*`
|
|
607
|
+
tokens. Note that `static/style.css` is an all-or-nothing override: a theme
|
|
608
|
+
that ships its own stylesheet owns these rules too, and a fenced block on it
|
|
609
|
+
is unhighlighted until it writes them.
|
|
610
|
+
- _Other languages._ Edit `LANGUAGES` in `scripts/build-highlight.js`, rebuild,
|
|
611
|
+
and commit — or put your own `static/highlight.js` in your theme, which the
|
|
612
|
+
`/theme/` route resolves before the packaged one.
|
|
613
|
+
- _Another highlighter._ Override the `scripts` block with your own tag. The
|
|
614
|
+
markup is `pre > code.language-x`, which is what every highlighter reads.
|
|
615
|
+
- _None._ Override the `scripts` block with nothing in it.
|
|
616
|
+
|
|
617
|
+
```njk
|
|
618
|
+
{% extends "layouts/base.njk" %}
|
|
619
|
+
|
|
620
|
+
{% block scripts %}{% endblock %}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
## Mail templates
|
|
624
|
+
|
|
625
|
+
The messages the CMS sends live under `mail/` and resolve the same way, so a
|
|
626
|
+
`mail/test.txt.njk` in the theme the site wears replaces the text of the test
|
|
627
|
+
message and leaves its subject and HTML twin coming from the package. Each message is up to three
|
|
628
|
+
files:
|
|
629
|
+
|
|
630
|
+
| File | What it is |
|
|
631
|
+
| ------------------------- | -------------------------------------------------------------- |
|
|
632
|
+
| `mail/<name>.subject.njk` | The subject. Rendered to a single line. Optional. |
|
|
633
|
+
| `mail/<name>.txt.njk` | The plain text body. **Required.** |
|
|
634
|
+
| `mail/<name>.html.njk` | The HTML twin. Optional; without it the message is plain text. |
|
|
635
|
+
|
|
636
|
+
A message that has no `.txt.njk` anywhere on the search path is an error rather
|
|
637
|
+
than an empty email, so a typo in a template name is reported instead of sent.
|
|
638
|
+
|
|
639
|
+
The package ships seven messages:
|
|
640
|
+
|
|
641
|
+
| Name | When it goes | What `data` carries |
|
|
642
|
+
| ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
643
|
+
| `test` | Send test email, on Settings. | Nothing. |
|
|
644
|
+
| `password-reset` | Somebody asked to reset a password. | `username`, `resetUrl`, `expiresAt`, `expiresInHours`. |
|
|
645
|
+
| `password-changed` | A reset link was used. | `username`, `signedOut` (how many sessions ended). |
|
|
646
|
+
| `comment-pending` | A comment or webmention is waiting for approval. | `comment`, `post` (`title`, `url`), `actions` (one `{ action, label, url }` each for approve, spam and delete), `queueUrl`. |
|
|
647
|
+
| `comment-digest` | An hourly or daily digest of what is waiting. | `items` (one `{ comment, post, actions }` each), `total` waiting, `more` beyond the ones listed, the `mode` that asked for it, `queueUrl`. |
|
|
648
|
+
| `comment-reply` | A reply to somebody's comment was approved. | `reply`, `comment` (the one it answers), `post`, `unsubscribeUrl`. |
|
|
649
|
+
| `contact-message` | Somebody filled in a page's contact form. | `message` (`id`, `subject`, `text`, `received`), `from` (`name`, `email`), `page` (`slug`, `permalink`, `title`, `url`), `messagesUrl`. |
|
|
650
|
+
|
|
651
|
+
The `comment` inside each of a digest's `items` is the same shape as
|
|
652
|
+
`comment-pending`'s, and its `actions` are the same three links, minted per
|
|
653
|
+
recipient because each is spent the first time it is used. A digest is built
|
|
654
|
+
from the queue as it stands when it is sent, so an item moderated in the
|
|
655
|
+
meantime is simply not in it, and no digest is sent to somebody with an empty
|
|
656
|
+
queue.
|
|
657
|
+
|
|
658
|
+
`comment` and `reply` above are the same shape: `author`, `website`, `source`
|
|
659
|
+
(`comment` or `webmention`), `kind`, `text`, `html`, `submitted`, and `url`.
|
|
660
|
+
The commenter's email address is deliberately **not** among them — the message
|
|
661
|
+
goes to a moderator, but it also goes through a mail provider's servers, and
|
|
662
|
+
nothing on that screen needs it.
|
|
663
|
+
|
|
664
|
+
The links in `actions` and `unsubscribeUrl` each carry a signed token and work
|
|
665
|
+
without a login. Print them; do not try to build one.
|
|
666
|
+
|
|
667
|
+
`contact-message` is sent with reply-to set to the sender, so answering it in a
|
|
668
|
+
mail reader answers the person who wrote it. `message.text` is plain text as it
|
|
669
|
+
was typed — print it, do not render it as Markdown or as HTML.
|
|
670
|
+
|
|
671
|
+
The subject and the plain text body render with autoescaping **off**, because
|
|
672
|
+
neither is HTML: `&` between two query parameters stays an `&`, and an
|
|
673
|
+
apostrophe in somebody's name stays an apostrophe. The HTML twin escapes as
|
|
674
|
+
every other template does.
|
|
675
|
+
|
|
676
|
+
The context is `site` (that is `content/_data/site.json`), `baseUrl`, and
|
|
677
|
+
whatever the feature that sent it passed as `data`. The `date`, `url` and
|
|
678
|
+
`absoluteUrl` filters are the same ones a page has, so a message can write a
|
|
679
|
+
link with `{{ "/admin/" | absoluteUrl }}`.
|
|
680
|
+
|
|
681
|
+
Writing `mail/welcome.txt.njk` into the theme is enough to add a message this
|
|
682
|
+
package never shipped; nothing has to be registered.
|
|
683
|
+
|
|
56
684
|
## Template context
|
|
57
685
|
|
|
58
686
|
The context mirrors what an Eleventy layout receives, so a layout ported from
|
|
@@ -60,34 +688,48 @@ an Eleventy build needs few edits. It is part of the package's semver contract.
|
|
|
60
688
|
|
|
61
689
|
Every template gets:
|
|
62
690
|
|
|
63
|
-
| Key
|
|
64
|
-
|
|
|
65
|
-
| `site`
|
|
691
|
+
| Key | What it holds |
|
|
692
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
693
|
+
| `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. |
|
|
694
|
+
| `menu` | The site menu for this page: a list of `{ label, url, current }`. See [Navigation](#navigation). |
|
|
695
|
+
| `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
|
|
696
|
+
| `icons` | The site's icons, as `{ rel, sizes, href }`. Empty until the site has an avatar to derive them from. See [The head](#the-head). |
|
|
66
697
|
|
|
67
698
|
A document — one post, one page, or one entry of a listing — adds:
|
|
68
699
|
|
|
69
|
-
| Key
|
|
70
|
-
|
|
|
71
|
-
| `title`
|
|
72
|
-
| `date`
|
|
73
|
-
| `updated`
|
|
74
|
-
| `tags`
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `page.
|
|
80
|
-
| `page.
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
700
|
+
| Key | What it holds |
|
|
701
|
+
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
702
|
+
| `title` | Display title. |
|
|
703
|
+
| `date` | Publish date, a JavaScript `Date` at the UTC instant the file holds. Absent when the document has none. |
|
|
704
|
+
| `updated` | Last modified date, a `Date`, when the front matter has one. |
|
|
705
|
+
| `tags` | The document's tags, in file order. |
|
|
706
|
+
| `categories` | The document's categories, in file order. |
|
|
707
|
+
| `content` | The Markdown body rendered to HTML. Print it with `\| safe`. |
|
|
708
|
+
| `summary` | Its `description` as plain text, else an excerpt of the body, else empty. The line the feeds publish. |
|
|
709
|
+
| `url` | The document's URL path, the same value as `page.url`. |
|
|
710
|
+
| `page.url` | The document's URL path. Always ends in `/`. |
|
|
711
|
+
| `page.date` | The same `Date` as `date`. |
|
|
712
|
+
| `page.fileSlug` | The permalink's last segment. |
|
|
713
|
+
| `page.inputPath` | The source file, relative to the content directory. |
|
|
714
|
+
| `type` | `post` or `page`. |
|
|
715
|
+
| `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
|
|
716
|
+
| `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
|
|
717
|
+
| `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
|
|
718
|
+
| `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
|
|
719
|
+
| `next` | The published post after it. Absent on the newest post, and on a page. |
|
|
720
|
+
| `recentPosts` | The newest posts, as entries, on the front page only: this month's when it holds five, else five. |
|
|
721
|
+
| `postsPage` | The page carrying the listing, as `{ title, url }`, on the front page only. Absent when the site names none. |
|
|
722
|
+
| `archiveMonths` | Every published post as `{ month, posts }`, newest month first. Only on a page that says `archive: true`. |
|
|
723
|
+
| `webmention` | Where a webmention about this page is sent. Only on a rendered document, and only while the site takes them. |
|
|
724
|
+
| `conversation` | The replies, likes and boosts under the post. Only when there are any. See [The conversation](#the-conversation). |
|
|
725
|
+
| everything else | Any front matter key the CMS does not model is on the context under its own name. |
|
|
726
|
+
|
|
727
|
+
A listing — the home page, a tag archive, a category archive or an author
|
|
728
|
+
archive — adds:
|
|
87
729
|
|
|
88
730
|
| Key | What it holds |
|
|
89
731
|
| ------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
90
|
-
| `title` | The site title on the home page, the
|
|
732
|
+
| `title` | The site title on the home page, the term on a taxonomy archive. |
|
|
91
733
|
| `posts` | The documents on this page of the listing, newest first, each in the shape above. |
|
|
92
734
|
| `pagination.items` | The same array, under the name Eleventy gives it. |
|
|
93
735
|
| `pagination.pageNumber` | Zero-based index of this page, as in Eleventy. |
|
|
@@ -95,39 +737,193 @@ A listing — the home page or a tag archive — adds:
|
|
|
95
737
|
| `pagination.href.first/last/next/previous` | Pager URLs. `next` and `previous` are `null` at the ends. |
|
|
96
738
|
| `pagination.pages` | Every page's URL, in order. |
|
|
97
739
|
| `tag` | The tag, on a tag archive only. |
|
|
740
|
+
| `category` | The category, on a category archive only. |
|
|
741
|
+
| `author` | The person, on an author archive only. Same shape as a post's `author`. |
|
|
98
742
|
|
|
99
743
|
How many posts a listing page holds comes from `postsPerPage` in
|
|
100
744
|
`content/_data/site.json`, and defaults to 10. How many entries a feed holds
|
|
101
|
-
comes from `feedSize` in the same file, and defaults to 20.
|
|
745
|
+
comes from `feedSize` in the same file, and defaults to 20. The `<html lang>`
|
|
746
|
+
comes from `language`, and defaults to `en`. The rssCloud and WebSub server the
|
|
747
|
+
feeds advertise comes from `notifyServer`, and is empty for none; the theme
|
|
748
|
+
writes nothing for it, because a cloud is advertised in the feed rather than on
|
|
749
|
+
the page.
|
|
750
|
+
|
|
751
|
+
## Bylines and author archives
|
|
752
|
+
|
|
753
|
+
`author` on a document is the person the front matter names, resolved against
|
|
754
|
+
the site's users. The fields come from their profile on **Users** in the admin,
|
|
755
|
+
which is the one place identity is written:
|
|
756
|
+
|
|
757
|
+
| Key | What it holds |
|
|
758
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
759
|
+
| `author.name` | What to print: their display name, else their username, else the raw name the file gives. |
|
|
760
|
+
| `author.url` | Their archive, `/author/{username}/`. **Absent** when the name is nobody this site has. |
|
|
761
|
+
| `author.username` | Their login. Absent for the same reason `url` is. |
|
|
762
|
+
| `author.bio` | What they wrote about themselves, when they wrote any. |
|
|
763
|
+
| `author.avatar` | Their picture, as a path or URL. Absent when they have none. |
|
|
764
|
+
| `author.jobTitle` | What they do, when their profile says. |
|
|
765
|
+
| `author.location` | Where they are, as they wrote it. |
|
|
766
|
+
| `author.links` | `{ label, href }` for each link on their profile, in the order they listed them. Absent when they listed none. |
|
|
767
|
+
|
|
768
|
+
`author` is absent altogether when the document names no author, so anything
|
|
769
|
+
printing it is `{% if author %}`. Guard the link with `{% if author.url %}`: a
|
|
770
|
+
file may name somebody who has no account here — a guest post, or a colleague
|
|
771
|
+
whose account has gone — and that name is still printed, it simply links
|
|
772
|
+
nowhere. [The bio](#the-bio) does both, and is what the theme prints instead of
|
|
773
|
+
the `partials/byline.njk` it used to have.
|
|
774
|
+
|
|
775
|
+
`layouts/author.njk` is that person's archive, at `/author/{username}/`, with
|
|
776
|
+
their pages at `/author/{username}/page/2/` and their three feeds under
|
|
777
|
+
`/author/{username}/feed/`. It is headed with their name and then the bio with
|
|
778
|
+
`bioProfile` set — the avatar, the note and the `rel="me"` links — and lists
|
|
779
|
+
their published posts newest first. The source design has no author archive, so
|
|
780
|
+
the heading is the CMS's own; the h-card under it is the one partial the theme
|
|
781
|
+
has. A user with no profile still has one; they are called by their username.
|
|
782
|
+
|
|
783
|
+
`siteAuthor` is the same object on a different question: not who wrote this
|
|
784
|
+
document, but who the page in front of the reader is by. It is what the bio,
|
|
785
|
+
the footer's `rel="me"` links and any structured data a theme emits should all
|
|
786
|
+
read, so that what a reader sees and what a machine reads cannot drift apart.
|
|
787
|
+
It resolves in this order:
|
|
788
|
+
|
|
789
|
+
- the document's own `author`, on a post or a page that names one;
|
|
790
|
+
- the person whose archive it is, on an author archive;
|
|
791
|
+
- the profile behind the site's `author` setting, everywhere else — the home
|
|
792
|
+
page, a taxonomy archive, a page that names nobody, the 404.
|
|
793
|
+
|
|
794
|
+
It is **absent** when none of those name anybody this site has. The site
|
|
795
|
+
setting is read more strictly than a byline is: a byline prints the name a file
|
|
796
|
+
gives whether or not somebody answers to it, but a site author with no profile
|
|
797
|
+
behind it has no picture, no bio and nowhere to link, so there is nothing to
|
|
798
|
+
print and the key is not there. Write `{% if siteAuthor %}` around the bio and
|
|
799
|
+
the identity links.
|
|
800
|
+
|
|
801
|
+
The URL is not only a page. Each user is an ActivityPub actor at that address,
|
|
802
|
+
so it is the page a follower lands on when they click through from the
|
|
803
|
+
fediverse. `author` is therefore a reserved first URL segment, like `tag` and
|
|
804
|
+
`feed`: no document can be permalinked under it and no taxonomy base can take
|
|
805
|
+
it.
|
|
806
|
+
|
|
807
|
+
## Navigation
|
|
808
|
+
|
|
809
|
+
`menu` is the site menu, already in order and already knowing which of its items
|
|
810
|
+
is the page being looked at. Every template gets it.
|
|
811
|
+
|
|
812
|
+
The design has no header navigation: the menu is the horizontal list inside
|
|
813
|
+
[the bio](#the-bio). So it is printed once, in one of two places. A page with a
|
|
814
|
+
bio — an entry, an author archive — carries it there. A page without one — a
|
|
815
|
+
listing, the 404, a layout of a site's own — gets it from the `footer` block of
|
|
816
|
+
`layouts/base.njk`. What decides is `bioAuthor`: a layout that renders the bio
|
|
817
|
+
sets it, and the footer then leaves the menu out. A layout that sets nothing
|
|
818
|
+
keeps the footer menu, which is why an overridden `layouts/post.njk` does not
|
|
819
|
+
lose the navigation by not having a bio.
|
|
820
|
+
|
|
821
|
+
The markup is the same either way, and a layout that overrides `header` or
|
|
822
|
+
`footer` writes it the same way:
|
|
823
|
+
|
|
824
|
+
```njk
|
|
825
|
+
{% if menu.length %}
|
|
826
|
+
<nav class="site-nav" aria-label="Site">
|
|
827
|
+
<ul class="hlist">
|
|
828
|
+
{% for item in menu %}
|
|
829
|
+
<li><a href="{{ item.url | url }}"{% if item.current %} class="is-current" aria-current="page"{% endif %}>{{ item.label }}</a></li>
|
|
830
|
+
{% endfor %}
|
|
831
|
+
</ul>
|
|
832
|
+
</nav>
|
|
833
|
+
{% endif %}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Each item is `{ label, url, current }`. `url` is a site-root path or an absolute
|
|
837
|
+
URL for somewhere else, so put it through the `url` filter as above and a site
|
|
838
|
+
served from a subdirectory still links correctly. `current` is true for the item
|
|
839
|
+
whose path is the one being rendered, comparing without the trailing slash; an
|
|
840
|
+
item pointing off the site is never current.
|
|
841
|
+
|
|
842
|
+
The list is the `navigation` setting first, in the order the settings screen
|
|
843
|
+
names it, and then every published page whose front matter says
|
|
844
|
+
`navigation: true`, ordered by `navigationOrder` and then by title. A page that
|
|
845
|
+
names no order sorts after every page that does.
|
|
846
|
+
|
|
847
|
+
It is called `menu` rather than `navigation` because `navigation` is the
|
|
848
|
+
front-matter key a page opts in with, and a document's own front matter goes on
|
|
849
|
+
top of the globals exactly as Eleventy's data cascade does. The setting is
|
|
850
|
+
mirrored to `navigation` in `content/_data/site.json`, so an Eleventy build of
|
|
851
|
+
the same content renders the same menu; `docs/eleventy.config.example.js` builds
|
|
852
|
+
it as `collections.menu`.
|
|
102
853
|
|
|
103
854
|
## Feeds
|
|
104
855
|
|
|
105
|
-
`
|
|
106
|
-
links for
|
|
107
|
-
|
|
108
|
-
|
|
856
|
+
`partials/feeds.njk` holds two macros. `feedLinks(root, title)` writes the three
|
|
857
|
+
`rel="alternate"` links for a listing's RSS, Atom and JSON feeds — RSS first,
|
|
858
|
+
because it is what most subscribers hold:
|
|
859
|
+
|
|
860
|
+
```njk
|
|
861
|
+
{% import "partials/feeds.njk" as feeds %}
|
|
862
|
+
{{ feeds.feedLinks("/", site.title) }}
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
`root` is any listing URL ending in `/`; the macro appends `feed/`, `feed/atom/`
|
|
866
|
+
and `feed/json/` and puts each through the `url` filter, so the links carry the
|
|
867
|
+
base path of a site served from a subdirectory. The arguments carry everything
|
|
868
|
+
the macro needs, so it can be imported without `with context`.
|
|
869
|
+
|
|
870
|
+
`commentsFeedLink(feed, title)` writes the one link a comments feed has. It
|
|
871
|
+
takes the feed's own URL rather than a root, because a comments feed is RSS 2.0
|
|
872
|
+
and nothing else:
|
|
873
|
+
|
|
874
|
+
```njk
|
|
875
|
+
{{ feeds.commentsFeedLink("/comments/feed/", site.title + " comments") }}
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
`layouts/tag.njk`, `layouts/category.njk` and `layouts/author.njk` each call
|
|
879
|
+
`feedLinks` over their own root, so an archive advertises its own three feeds
|
|
880
|
+
as well as the site's — on an author archive that root is `author.url`.
|
|
881
|
+
|
|
882
|
+
`layouts/base.njk` fills the `alternates` block with `feedLinks` over `/` and
|
|
883
|
+
then the site's comments feed, so every page that extends it advertises all
|
|
884
|
+
four. On a published post it adds that post's own comments feed, from
|
|
885
|
+
`commentsFeed` on the context, and a link pointing at the post's ActivityPub
|
|
886
|
+
object:
|
|
109
887
|
|
|
110
888
|
```html
|
|
111
889
|
<link
|
|
112
890
|
rel="alternate"
|
|
113
891
|
type="application/activity+json"
|
|
114
|
-
href="https://example.com/
|
|
892
|
+
href="https://example.com/2026/09/hello/"
|
|
115
893
|
/>
|
|
116
894
|
```
|
|
117
895
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
896
|
+
A post's object id is its permalink, so that link usually points at the page it
|
|
897
|
+
is on: it says the URL answers ActivityStreams as well as HTML. A post migrated
|
|
898
|
+
from elsewhere keeps the id its file names, and the link points there instead.
|
|
899
|
+
It comes from `activityStreams`, which — like `commentsFeed` — is only on the
|
|
900
|
+
context of a rendered published post, so a layout that overrides the block and
|
|
901
|
+
does not call `super()` has to emit both itself. They are on the context rather
|
|
902
|
+
than in `post.njk` so that a site which overrides that layout, as the demo
|
|
903
|
+
does, keeps them.
|
|
122
904
|
|
|
123
|
-
|
|
124
|
-
|
|
905
|
+
After the block, and outside it, the base layout writes the webmention
|
|
906
|
+
endpoint:
|
|
907
|
+
|
|
908
|
+
```html
|
|
909
|
+
<link rel="webmention" href="/_geekity/webmention" />
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
It comes from `webmention` on the context, which is on a rendered document only
|
|
913
|
+
when the site is taking webmentions, so a theme asks `{% if webmention %}` and a
|
|
914
|
+
site that has turned them off advertises nothing. The CMS also sends the same
|
|
915
|
+
endpoint as a `Link` header on every representation of a document, so a sender
|
|
916
|
+
that does not parse HTML still finds it.
|
|
917
|
+
|
|
918
|
+
`layouts/tag.njk` and `layouts/category.njk` override that block, call `super()`
|
|
919
|
+
and add the archive's own three feeds:
|
|
125
920
|
|
|
126
921
|
```njk
|
|
127
|
-
{%
|
|
922
|
+
{% import "partials/feeds.njk" as feeds %}
|
|
923
|
+
{% set tagRoot = "/" + (site.tagBase or "tag") + "/" + (tag | urlencode) + "/" %}
|
|
128
924
|
{% block alternates %}
|
|
129
925
|
{{ super() }}
|
|
130
|
-
|
|
926
|
+
{{ feeds.feedLinks(tagRoot, site.title + ": " + tag) }}
|
|
131
927
|
{% endblock %}
|
|
132
928
|
```
|
|
133
929
|
|
|
@@ -135,13 +931,263 @@ A layout that does not extend the base one has to emit the links itself. The
|
|
|
135
931
|
feeds are generated by the CMS, not by a template, so nothing here decides what
|
|
136
932
|
goes in them.
|
|
137
933
|
|
|
934
|
+
Category archives get their own three the same way.
|
|
935
|
+
|
|
936
|
+
## The conversation
|
|
937
|
+
|
|
938
|
+
`conversation` is what has been said about a post: the fediverse replies, likes
|
|
939
|
+
and boosts its inbox was sent, the comments people left on the page itself, and
|
|
940
|
+
the webmentions other pages sent it.
|
|
941
|
+
It is on the context of a rendered post **only when there is something in it**,
|
|
942
|
+
so a post nobody has answered renders no empty section and a layout can simply
|
|
943
|
+
ask:
|
|
944
|
+
|
|
945
|
+
```njk
|
|
946
|
+
{% if conversation %}
|
|
947
|
+
{% include "partials/conversation.njk" %}
|
|
948
|
+
{% endif %}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
That is what `layouts/post.njk` does. `partials/conversation.njk` is the whole
|
|
952
|
+
section — a `div.reactions-section` of the likes, the boosts and the mentions
|
|
953
|
+
as facepiles grouped by kind, and then the thread as
|
|
954
|
+
`div#comments.comments-area` — and a site replaces it with a
|
|
955
|
+
`partials/conversation.njk` of its own in the theme it wears, exactly as it
|
|
956
|
+
replaces any other template. It defines three macros, `face(item, icon, href)`,
|
|
957
|
+
`group(items, label, kind, icon, source)` and `comment(reply)`, and a layout
|
|
958
|
+
that wants to place the pieces itself can import them:
|
|
959
|
+
|
|
960
|
+
```njk
|
|
961
|
+
{% import "partials/conversation.njk" as thread with context %}
|
|
962
|
+
{{ thread.group(conversation.likes, "Likes", "p-like", "❤️") }}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### The shape
|
|
966
|
+
|
|
967
|
+
The conversation is deliberately **not** spelled in ActivityPub's vocabulary.
|
|
968
|
+
Native comments and webmentions land in the same thread, so every
|
|
969
|
+
entry says where it came from and what it is, and nothing else about it changes
|
|
970
|
+
with the source. A theme that never looks at `source` renders all of them
|
|
971
|
+
correctly; one that does can style them apart. The one thing a theme does have
|
|
972
|
+
to look at is `kind`: a `mention` is in `conversation.mentions` rather than in
|
|
973
|
+
the thread, because it is not an answer.
|
|
974
|
+
|
|
975
|
+
| Key | What it holds |
|
|
976
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
977
|
+
| `replies` | The replies to the post, oldest first, each carrying its own `replies`. See below. |
|
|
978
|
+
| `likes` | The likes, oldest first, in the same shape. |
|
|
979
|
+
| `boosts` | The boosts, oldest first, in the same shape. A webmention `repost` is one of them. |
|
|
980
|
+
| `mentions` | The pages that linked here without answering, oldest first, in the same shape. |
|
|
981
|
+
| `counts.replies` | How many replies, counted through the whole thread rather than the top of it. |
|
|
982
|
+
| `counts.likes`, `counts.boosts`, `counts.mentions` | How many of each. |
|
|
983
|
+
| `counts.total` | All four added up. Zero never reaches a template: there would be no `conversation` at all. |
|
|
984
|
+
|
|
985
|
+
Each entry — a reply, a like or a boost — is:
|
|
986
|
+
|
|
987
|
+
| Key | What it holds |
|
|
988
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
989
|
+
| `id` | What it is called: a reply's own note id, which is what an answer to it names. |
|
|
990
|
+
| `source` | `"activitypub"` for a fediverse reply, `"comment"` for one left on the page, `"webmention"` for another page linking here. |
|
|
991
|
+
| `kind` | `"reply"`, `"like"`, `"boost"`, `"repost"` or `"mention"`. The last two only ever come from a webmention. |
|
|
992
|
+
| `author.name` | The best name available: their display name, else their handle, else their id. For a webmention, the source's `h-card` name, else its host. |
|
|
993
|
+
| `author.handle` | `@user@host`, or `null`. Taken from the follower profile the site holds, else guessed from the actor URL. `null` for a native comment. |
|
|
994
|
+
| `author.url` | Their profile page, the website a commenter typed, or a webmention author's `u-url`. May be `null`, so guard the link. |
|
|
995
|
+
| `author.avatar` | Their avatar, or `null`. The site knows one for an actor that follows it and for a webmention whose `h-card` carried a `u-photo`. |
|
|
996
|
+
| `author.actorId` | Their id, which is what identifies them however they are named. `null` for a native comment. |
|
|
997
|
+
| `url` | Where it can be read: the remote note's `url` for a fediverse reply, the source page for a webmention, and `{permalink}#comment-{id}` — this page's own anchor — for a native comment. |
|
|
998
|
+
| `content` | What it says, **already sanitised**, so print it with `\| safe`. Empty for a like or a boost. |
|
|
999
|
+
| `published` | A `Date`: when it was published, or when it arrived if it did not say. Use the `date` filter. |
|
|
1000
|
+
| `inReplyTo` | What it answers — the post's ActivityPub id, or another reply's — and `null` for a reaction. |
|
|
1001
|
+
| `status` | `"published"`. On the record for the sources that moderate. |
|
|
1002
|
+
| `replies` | The replies to this one, oldest first, nested as deep as the site has seen. |
|
|
1003
|
+
|
|
1004
|
+
Three rules decide what is in the thread, and they are the CMS's rather than a
|
|
1005
|
+
theme's: a reply whose author deleted it is gone, and its own answers move up to
|
|
1006
|
+
whatever it was answering; a like is counted once per actor and disappears when
|
|
1007
|
+
that actor undoes it; and a note answering something this post has nothing to do
|
|
1008
|
+
with is left out.
|
|
1009
|
+
|
|
1010
|
+
`content` is safe to print whatever produced it, and safe for different reasons.
|
|
1011
|
+
A fediverse reply is HTML somebody else's server composed, rebuilt from an
|
|
1012
|
+
allowlist — `a`, `p`, `br`, lists, `blockquote`, `pre`, `code` and the inline
|
|
1013
|
+
emphasis tags, with every link carrying `rel="nofollow noopener noreferrer"`. A
|
|
1014
|
+
webmention's `e-content` goes through the same allowlist. A native comment is
|
|
1015
|
+
Markdown the commenter typed, rendered with raw HTML off, no images embedded,
|
|
1016
|
+
and every link carrying `rel="nofollow ugc"`. Nothing else survives any of the
|
|
1017
|
+
three routes, so a theme may print all of them directly.
|
|
1018
|
+
|
|
1019
|
+
The packaged partial draws each entry as the source design does: an
|
|
1020
|
+
`li.comment.h-entry` carrying `id="comment-{{ reply.id }}"` and a
|
|
1021
|
+
`comment-{{ reply.source }}` class, holding an `article.comment-body` whose
|
|
1022
|
+
`footer.comment-meta` is the author as a `.comment-author.vcard.p-author.h-card`
|
|
1023
|
+
and the permalink as a `.comment-metadata` link around a `time.dt-published`,
|
|
1024
|
+
then the words in `div.comment-content.e-content`. Answers nest in an
|
|
1025
|
+
`ol.children` inside what they answer. A Reply link in a `div.reply` goes on the
|
|
1026
|
+
ones written here — `source == "comment"` — when the post is still open; it
|
|
1027
|
+
carries the comment's id to the form as `?reply_to=`, so threading needs no
|
|
1028
|
+
JavaScript. A fediverse reply is answered on the server that holds it and a
|
|
1029
|
+
webmention on the page that sent it, so neither gets one.
|
|
1030
|
+
|
|
1031
|
+
Above the thread, `group()` draws one `div.reaction-group` per kind that has
|
|
1032
|
+
anything — `p-like`, `p-repost` and `p-mention`, the source theme's classes — as
|
|
1033
|
+
an `h2.reaction-title` of the label and the count beside a `div.facepile` of
|
|
1034
|
+
`a.u-url` faces. A face is a round `u-photo` avatar; a reaction whose author the
|
|
1035
|
+
site knows no picture of is the emoji badge of its kind and a `.reaction-name`
|
|
1036
|
+
instead. Likes and boosts link to the person, and a mention to the page it came
|
|
1037
|
+
from, because the page is the thing worth reading.
|
|
1038
|
+
|
|
1039
|
+
There is no key saying a post has stopped taking comments, and none is needed:
|
|
1040
|
+
**the absence of `commentForm` is what a closed post looks like**, since the CMS
|
|
1041
|
+
puts the form on the context only while the site switch, the closing window and
|
|
1042
|
+
the post's own front matter all agree it is open. The packaged partial prints
|
|
1043
|
+
`p.no-comments` under a thread when there is no form, which is WordPress's
|
|
1044
|
+
"Comments are closed." and the reason a reader cannot find one.
|
|
1045
|
+
|
|
1046
|
+
An Eleventy build of the same content gets the same thing from the same files:
|
|
1047
|
+
`docs/eleventy.config.example.js` adds a `conversation` filter over
|
|
1048
|
+
`federation.inbox`, and puts the post's ActivityPub id on the context as
|
|
1049
|
+
`activityStreams` and its slug as `geekitySlug`, so a layout reads
|
|
1050
|
+
|
|
1051
|
+
```njk
|
|
1052
|
+
{% set conversation = federation.inbox | conversation(activityStreams, geekitySlug) %}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
and then loops over exactly the keys above. The slug is what names the post's
|
|
1056
|
+
comment file under `content/_data/comments/`; the format is documented in
|
|
1057
|
+
backlog doc-6.
|
|
1058
|
+
|
|
1059
|
+
## The comment form
|
|
1060
|
+
|
|
1061
|
+
`commentForm` is on the context of a rendered post **only when that post is
|
|
1062
|
+
still taking comments**. A site switch, a closing window counted from the
|
|
1063
|
+
post's date, and `comments: true` or `comments: false` in the post's own front
|
|
1064
|
+
matter all agree before it gets here, so a layout asks:
|
|
1065
|
+
|
|
1066
|
+
```njk
|
|
1067
|
+
{% if commentForm %}
|
|
1068
|
+
{% include "partials/comment-form.njk" %}
|
|
1069
|
+
{% endif %}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
That is what `layouts/post.njk` does, after the conversation. A closed post
|
|
1073
|
+
still shows the thread — including every fediverse reply, which arrives whether
|
|
1074
|
+
a post is open or not — and simply has no form, which is what the conversation
|
|
1075
|
+
reads to print "Comments are closed." A site replaces
|
|
1076
|
+
`partials/comment-form.njk` the way it replaces any other template.
|
|
1077
|
+
|
|
1078
|
+
The packaged partial is a `section#respond.comment-respond` with an
|
|
1079
|
+
`h2.comment-reply-title`, one paragraph per field and the button in a
|
|
1080
|
+
`p.form-submit` — the source design's names, so the stylesheet styles every
|
|
1081
|
+
label, field and button of both forms through `.comment-respond` and a site
|
|
1082
|
+
that has CSS for the WordPress theme can bring it.
|
|
1083
|
+
|
|
1084
|
+
| Key | What it holds |
|
|
1085
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1086
|
+
| `action` | Where the form posts. One fixed path; the post travels as a field. |
|
|
1087
|
+
| `fields` | The name each field is submitted under: `post`, `name`, `email`, `url`, `body`, `inReplyTo`, `trap`, `loaded`, `notify`. Use these rather than typing the names. |
|
|
1088
|
+
| `post` | The post's slug, for the hidden field. |
|
|
1089
|
+
| `loaded` | When this form was rendered, in epoch milliseconds, for the hidden field. A submission that comes back too fast is refused. |
|
|
1090
|
+
| `values` | What is in the fields: empty on a fresh form, what was typed on a refused one. |
|
|
1091
|
+
| `problems` | One message per field a person has to put right — `name`, `email`, `url`, `body`. |
|
|
1092
|
+
| `error` | A message about the submission as a whole, when there is one. |
|
|
1093
|
+
| `notifiable` | Whether to offer `fields.notify`, the "email me when somebody replies" box. False on a site that sends no mail, where the box would promise a message nothing could send. |
|
|
1094
|
+
| `nameLength`, `bodyLength` | The `maxlength` for the two fields that have one. |
|
|
1095
|
+
|
|
1096
|
+
`fields.trap` is a honeypot: render it, hide it from sight and from assistive
|
|
1097
|
+
technology, and give it `tabindex="-1"` and `autocomplete="off"`. A submission
|
|
1098
|
+
that filled it is dropped. **Do not** remove it from a replacement partial —
|
|
1099
|
+
it is one of three things standing between the site and a spam queue.
|
|
1100
|
+
|
|
1101
|
+
Two more keys travel beside it, both from the URL rather than from the post:
|
|
1102
|
+
|
|
1103
|
+
| Key | What it holds |
|
|
1104
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
1105
|
+
| `commentNotice` | The thank-you after a submission, from the `?comment=` the redirect carried. |
|
|
1106
|
+
| `commentReplyTo`, `commentReplyingTo` | The comment a `?reply_to=` link named and the name on it, once the CMS has checked it is an approved comment on this very post. |
|
|
1107
|
+
|
|
1108
|
+
## The contact form
|
|
1109
|
+
|
|
1110
|
+
`contactForm` is on the context of a rendered document **only when its front
|
|
1111
|
+
matter says `contact: true`**, so a layout asks:
|
|
1112
|
+
|
|
1113
|
+
```njk
|
|
1114
|
+
{% if contactForm %}
|
|
1115
|
+
{% include "partials/contact-form.njk" %}
|
|
1116
|
+
{% endif %}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
That is what `layouts/page.njk` does, after the page's content. The key is
|
|
1120
|
+
honoured wherever it is written, so a theme that wants the form under a post as
|
|
1121
|
+
well only has to add the same two lines to `layouts/post.njk`; the editor
|
|
1122
|
+
offers the checkbox on pages.
|
|
1123
|
+
|
|
1124
|
+
`archive: true` is the other front matter key of this kind: it puts every
|
|
1125
|
+
published post on the page instead of a form. See
|
|
1126
|
+
[An archive page](#an-archive-page).
|
|
1127
|
+
|
|
1128
|
+
The packaged partial is a `section#contact.comment-respond.contact-form`: it is
|
|
1129
|
+
the same kind of form as the comment one, so it wears the same class and the two
|
|
1130
|
+
share one set of rules rather than keeping two that drift.
|
|
1131
|
+
|
|
1132
|
+
| Key | What it holds |
|
|
1133
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1134
|
+
| `action` | Where the form posts. One fixed path; the page travels as a field. |
|
|
1135
|
+
| `fields` | The name each field is submitted under: `page`, `name`, `email`, `subject`, `message`, `trap`, `loaded`. Use these rather than typing the names. |
|
|
1136
|
+
| `page` | The page's slug, for the hidden field. |
|
|
1137
|
+
| `loaded` | When this form was rendered, in epoch milliseconds, for the hidden field. A submission that comes back too fast is refused. |
|
|
1138
|
+
| `values` | What is in the fields: empty on a fresh form, what was typed on a refused one. |
|
|
1139
|
+
| `problems` | One message per field a person has to put right — `name`, `email`, `subject`, `message`. |
|
|
1140
|
+
| `error` | A message about the submission as a whole, when there is one. |
|
|
1141
|
+
| `nameLength`, `subjectLength`, `messageLength` | The `maxlength` for the three fields that have one. |
|
|
1142
|
+
|
|
1143
|
+
`fields.trap` is the same honeypot the comment form carries, and the same rules
|
|
1144
|
+
apply: render it, hide it, and **do not** remove it from a replacement partial.
|
|
1145
|
+
|
|
1146
|
+
One more key travels beside it, from the URL rather than from the page:
|
|
1147
|
+
|
|
1148
|
+
| Key | What it holds |
|
|
1149
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1150
|
+
| `contactNotice` | The thank-you after a message was sent, from the `?contact=` the redirect carried. The shipped partial shows it **instead of** the form. |
|
|
1151
|
+
|
|
1152
|
+
**Where the message goes is never on the context.** The contact address is read
|
|
1153
|
+
when a submission arrives, so it cannot end up in the HTML however this partial
|
|
1154
|
+
is rewritten. Do not try to print it, and do not put a `mailto:` beside the
|
|
1155
|
+
form.
|
|
1156
|
+
|
|
1157
|
+
## Taxonomy macros
|
|
1158
|
+
|
|
1159
|
+
`partials/tags.njk` holds one macro per taxonomy, and each renders nothing at
|
|
1160
|
+
all for an empty list. Both are one `p.post-categories` of `p-category` links,
|
|
1161
|
+
which is the shape the design prints in a feed item's meta line and under an
|
|
1162
|
+
entry — `categories` marks each link `rel="category"` and `list` marks each
|
|
1163
|
+
`rel="tag"`, so under an entry the two read as one line of what it is filed
|
|
1164
|
+
under, categories first:
|
|
1165
|
+
|
|
1166
|
+
```njk
|
|
1167
|
+
{% import "partials/tags.njk" as taxonomy with context %}
|
|
1168
|
+
{{ taxonomy.categories(categories) }}
|
|
1169
|
+
{{ taxonomy.list(tags) }}
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
`list` links to `/{{ site.tagBase }}/{tag}/` and `categories` to
|
|
1173
|
+
`/{{ site.categoryBase }}/{category}/`, which is where the CMS serves each
|
|
1174
|
+
archive. Both bases are settings — `tag` and `category` until the settings
|
|
1175
|
+
screen says otherwise — and they reach a template through `site`, which is why
|
|
1176
|
+
the import above carries `with context`: a macro imported without it cannot see
|
|
1177
|
+
`site` and would fall back to the defaults.
|
|
1178
|
+
|
|
138
1179
|
## Filters
|
|
139
1180
|
|
|
140
|
-
| Filter
|
|
141
|
-
|
|
|
142
|
-
| `date(format)` | Formats a `Date` or a date string. `readable` (the default) gives `2 September 2026`, `html` gives `2026-09-02` for a `<time datetime>`, `year` gives `2026`, `iso` gives the full ISO 8601 instant.
|
|
143
|
-
| `url`
|
|
144
|
-
| `absoluteUrl`
|
|
1181
|
+
| Filter | What it does |
|
|
1182
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1183
|
+
| `date(format, zone)` | Formats a `Date` or a date string. `readable` (the default) gives `2 September 2026`, `html` gives `2026-09-02` for a `<time datetime>`, `year` gives `2026`, `month` gives `September 2026`, `iso` gives the full ISO 8601 instant. 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. A value that is not a date renders as the empty string. |
|
|
1184
|
+
| `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. |
|
|
1185
|
+
| `absoluteUrl` | The same path as a fully qualified URL against the site's `baseUrl`. |
|
|
1186
|
+
|
|
1187
|
+
`date` reads one word rather than parsing it: `{{ "now" | date("year") }}` is
|
|
1188
|
+
the year at the moment the page is rendered, in the site's own timezone. It is
|
|
1189
|
+
what the footer's copyright line is written with, and it is the one date a page
|
|
1190
|
+
has that no file carries.
|
|
145
1191
|
|
|
146
1192
|
Nunjucks' own filters — `default`, `join`, `urlencode`, `safe` and the rest —
|
|
147
1193
|
are all available. Autoescaping is on, so rendered Markdown is the one thing
|