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