@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/dist/admin/settings.js
CHANGED
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_COMMENTS_CLOSE_AFTER_DAYS } from '../comments/policy.js';
|
|
4
|
+
import { readFileIfPresentSync, updateFileAtomically, writeFileAtomicallySync, } from '../files/atomic.js';
|
|
5
|
+
import { MAIL_PROVIDERS } from '../mail/provider.js';
|
|
4
6
|
import { SITE_DATA_FILE } from '../web/context.js';
|
|
5
|
-
import {
|
|
7
|
+
import { DEFAULT_NOTIFY_SERVER } from '../web/feeds.js';
|
|
8
|
+
import { DEFAULT_MENU_NAME, menuItemsFromText, menusOf } from '../web/navigation.js';
|
|
9
|
+
import { DEFAULT_TAXONOMY_BASES, taxonomyBaseProblems, taxonomyRedirectsOf, } from '../web/taxonomy.js';
|
|
10
|
+
import { readTheme, themeNameProblem } from '../web/themes.js';
|
|
6
11
|
import { ADMIN_PREFIX } from './session.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Where the settings live: the General page, and the root every other settings
|
|
14
|
+
* page hangs off ({@link settingsPagePath}).
|
|
15
|
+
*/
|
|
10
16
|
export const SETTINGS_PATH = `${ADMIN_PREFIX}/settings`;
|
|
11
|
-
/** Where the avatar's upload form and its Remove button post. */
|
|
12
|
-
export const AVATAR_PATH = `${SETTINGS_PATH}/avatar`;
|
|
13
|
-
/** The fields those two forms submit. */
|
|
14
|
-
export const AVATAR_FIELDS = { file: 'avatar', action: 'action' };
|
|
15
|
-
/** The {@link AVATAR_FIELDS.action} that takes the avatar down again. */
|
|
16
|
-
export const AVATAR_REMOVE = 'remove';
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* What a language tag may look like: BCP 47's shape rather than its registry —
|
|
19
|
+
* a two or three letter primary subtag followed by dash-separated subtags of
|
|
20
|
+
* letters and digits.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Checking the shape and not the registry is deliberate. The value ends up in
|
|
23
|
+
* `<html lang>`, an RSS `<language>` and an Atom `xml:lang`, where a
|
|
24
|
+
* well-formed tag a reader has never heard of is harmless and a malformed one
|
|
25
|
+
* is not; refusing a valid tag because this CMS shipped before it was
|
|
26
|
+
* registered would be worse than accepting one nobody uses.
|
|
22
27
|
*/
|
|
23
|
-
export const
|
|
24
|
-
'Person',
|
|
25
|
-
'Organization',
|
|
26
|
-
'Service',
|
|
27
|
-
'Group',
|
|
28
|
-
'Application',
|
|
29
|
-
];
|
|
28
|
+
export const LANGUAGE_TAG_PATTERN = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,4}$/;
|
|
30
29
|
/**
|
|
31
|
-
* What an
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* What an email address has to look like before the settings screen will keep
|
|
31
|
+
* it: something, an `@`, and a dotted host.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately loose. The grammar in RFC 5321 allows addresses nobody has ever
|
|
34
|
+
* typed, and the only test that settles whether an address works is sending to
|
|
35
|
+
* it — which is what the Send test email button is for. This catches the
|
|
36
|
+
* typo where a whole field was pasted into the wrong box.
|
|
34
37
|
*/
|
|
35
|
-
export const
|
|
38
|
+
export const EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
|
|
36
39
|
/**
|
|
37
40
|
* What a site is worth before anybody has said otherwise.
|
|
38
41
|
*
|
|
@@ -44,11 +47,28 @@ export const DEFAULT_SITE_SETTINGS = {
|
|
|
44
47
|
tagline: '',
|
|
45
48
|
baseUrl: '',
|
|
46
49
|
timezone: 'UTC',
|
|
50
|
+
language: 'en',
|
|
47
51
|
postsPerPage: 10,
|
|
52
|
+
homepage: '',
|
|
53
|
+
postsPage: '',
|
|
48
54
|
author: '',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
theme: '',
|
|
56
|
+
tagBase: DEFAULT_TAXONOMY_BASES.tag,
|
|
57
|
+
categoryBase: DEFAULT_TAXONOMY_BASES.category,
|
|
58
|
+
comments: true,
|
|
59
|
+
commentsCloseAfterDays: DEFAULT_COMMENTS_CLOSE_AFTER_DAYS,
|
|
60
|
+
webmentionsSend: true,
|
|
61
|
+
webmentionsReceive: true,
|
|
62
|
+
notifyServer: DEFAULT_NOTIFY_SERVER,
|
|
63
|
+
mailProvider: 'none',
|
|
64
|
+
mailFromName: '',
|
|
65
|
+
mailFromAddress: '',
|
|
66
|
+
mailReplyTo: '',
|
|
67
|
+
contactEmail: '',
|
|
68
|
+
relays: [],
|
|
69
|
+
wordpressActivityPub: false,
|
|
70
|
+
menus: {},
|
|
71
|
+
taxonomyRedirects: [],
|
|
52
72
|
};
|
|
53
73
|
/** The form field each setting is submitted under. */
|
|
54
74
|
export const SETTINGS_FIELDS = {
|
|
@@ -56,74 +76,151 @@ export const SETTINGS_FIELDS = {
|
|
|
56
76
|
tagline: 'tagline',
|
|
57
77
|
baseUrl: 'base_url',
|
|
58
78
|
timezone: 'timezone',
|
|
79
|
+
language: 'language',
|
|
59
80
|
postsPerPage: 'posts_per_page',
|
|
81
|
+
homepage: 'homepage',
|
|
82
|
+
postsPage: 'posts_page',
|
|
60
83
|
author: 'author',
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
theme: 'theme',
|
|
85
|
+
tagBase: 'tag_base',
|
|
86
|
+
categoryBase: 'category_base',
|
|
87
|
+
comments: 'comments',
|
|
88
|
+
commentsCloseAfterDays: 'comments_close_after_days',
|
|
89
|
+
webmentionsSend: 'webmentions_send',
|
|
90
|
+
webmentionsReceive: 'webmentions_receive',
|
|
91
|
+
notifyServer: 'notify_server',
|
|
92
|
+
mailProvider: 'mail_provider',
|
|
93
|
+
mailFromName: 'mail_from_name',
|
|
94
|
+
mailFromAddress: 'mail_from_address',
|
|
95
|
+
mailReplyTo: 'mail_reply_to',
|
|
96
|
+
contactEmail: 'contact_email',
|
|
97
|
+
relays: 'relays',
|
|
98
|
+
wordpressActivityPub: 'wordpress_activitypub',
|
|
63
99
|
};
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
? stored['actorType']
|
|
80
|
-
: DEFAULT_SITE_SETTINGS.actorType,
|
|
81
|
-
avatar: stored['avatar'] ?? DEFAULT_SITE_SETTINGS.avatar,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/** Write a whole settings object back to the store. */
|
|
85
|
-
export function writeSiteSettings(store, settings) {
|
|
86
|
-
store.setSettings({
|
|
87
|
-
title: settings.title,
|
|
88
|
-
tagline: settings.tagline,
|
|
89
|
-
baseUrl: settings.baseUrl,
|
|
90
|
-
timezone: settings.timezone,
|
|
91
|
-
postsPerPage: String(settings.postsPerPage),
|
|
92
|
-
author: settings.author,
|
|
93
|
-
actorHandle: settings.actorHandle,
|
|
94
|
-
actorType: settings.actorType,
|
|
95
|
-
avatar: settings.avatar,
|
|
96
|
-
});
|
|
100
|
+
/**
|
|
101
|
+
* A site's settings, as its `content/_data/site.json` says them right now.
|
|
102
|
+
*
|
|
103
|
+
* Read on every request that needs one rather than cached, which is what a
|
|
104
|
+
* source of truth being a file is worth: a hand edit shows on the public site,
|
|
105
|
+
* on the settings screen and in the actor document without a restart and
|
|
106
|
+
* without anything being told. The file is a few hundred bytes, so the read
|
|
107
|
+
* costs less than the parse of the template that is about to use it.
|
|
108
|
+
*
|
|
109
|
+
* A file that is missing, or one that will not parse, is the defaults rather
|
|
110
|
+
* than an error: a typo in `site.json` should leave a site up and answerable,
|
|
111
|
+
* with the settings screen there to put it right.
|
|
112
|
+
*/
|
|
113
|
+
export function readSiteSettings(contentDir) {
|
|
114
|
+
return settingsFromSiteJson(readSiteJsonSync(siteDataPath(contentDir)));
|
|
97
115
|
}
|
|
98
116
|
/**
|
|
99
|
-
*
|
|
117
|
+
* `content/_data/site.json` as the settings it names, defaults filled in.
|
|
100
118
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* rather than
|
|
119
|
+
* Read tolerantly, key by key, because the file is public, in git and editable
|
|
120
|
+
* by hand: a key of the wrong type, or one a site has never written, falls
|
|
121
|
+
* back to the default rather than taking the site down. The empty string is a
|
|
122
|
+
* value of its own for `tagline`, `author` and `notifyServer`, where empty is
|
|
123
|
+
* a decision — no tagline, no author, notifications off — and not for
|
|
124
|
+
* `title`, `timezone`, `language` or the two archive bases, where it is a hole
|
|
125
|
+
* only a default can fill.
|
|
104
126
|
*/
|
|
105
|
-
export function
|
|
127
|
+
export function settingsFromSiteJson(file) {
|
|
128
|
+
const postsPerPage = Number(file['postsPerPage']);
|
|
129
|
+
const closeAfterDays = Number(file['commentsCloseAfterDays']);
|
|
106
130
|
return {
|
|
107
|
-
...
|
|
108
|
-
...(
|
|
109
|
-
...(
|
|
110
|
-
...(
|
|
111
|
-
...(
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
...DEFAULT_SITE_SETTINGS,
|
|
132
|
+
...(typeof file['title'] === 'string' && file['title'] !== '' ? { title: file['title'] } : {}),
|
|
133
|
+
...(typeof file['tagline'] === 'string' ? { tagline: file['tagline'] } : {}),
|
|
134
|
+
...(typeof file['author'] === 'string' ? { author: file['author'] } : {}),
|
|
135
|
+
...(typeof file['timezone'] === 'string' && file['timezone'] !== ''
|
|
136
|
+
? { timezone: file['timezone'] }
|
|
137
|
+
: {}),
|
|
138
|
+
...(typeof file['language'] === 'string' && file['language'] !== ''
|
|
139
|
+
? { language: file['language'] }
|
|
140
|
+
: {}),
|
|
141
|
+
// Absent is the ordinary state of this one: a site on the packaged theme
|
|
142
|
+
// has never written the key, so anything but a string is that site.
|
|
143
|
+
...(typeof file['theme'] === 'string' ? { theme: file['theme'] } : {}),
|
|
144
|
+
// Absent is the ordinary state of these two: a site showing its latest
|
|
145
|
+
// posts writes neither key, so anything but a string is read as none.
|
|
146
|
+
...(typeof file['homepage'] === 'string' ? { homepage: file['homepage'] } : {}),
|
|
147
|
+
...(typeof file['postsPage'] === 'string' ? { postsPage: file['postsPage'] } : {}),
|
|
148
|
+
...(typeof file['tagBase'] === 'string' && file['tagBase'] !== ''
|
|
149
|
+
? { tagBase: file['tagBase'] }
|
|
150
|
+
: {}),
|
|
151
|
+
...(typeof file['categoryBase'] === 'string' && file['categoryBase'] !== ''
|
|
152
|
+
? { categoryBase: file['categoryBase'] }
|
|
153
|
+
: {}),
|
|
154
|
+
...(typeof file['comments'] === 'boolean' ? { comments: file['comments'] } : {}),
|
|
155
|
+
...(Number.isInteger(closeAfterDays) && closeAfterDays >= 0
|
|
156
|
+
? { commentsCloseAfterDays: closeAfterDays }
|
|
157
|
+
: {}),
|
|
158
|
+
...(typeof file['webmentionsSend'] === 'boolean'
|
|
159
|
+
? { webmentionsSend: file['webmentionsSend'] }
|
|
160
|
+
: {}),
|
|
161
|
+
...(typeof file['webmentionsReceive'] === 'boolean'
|
|
162
|
+
? { webmentionsReceive: file['webmentionsReceive'] }
|
|
163
|
+
: {}),
|
|
164
|
+
...(typeof file['notifyServer'] === 'string' ? { notifyServer: file['notifyServer'] } : {}),
|
|
165
|
+
// Only a provider this version ships, for the reason the actor type is
|
|
166
|
+
// read that way: a file naming one it has never heard of is a site that
|
|
167
|
+
// sends no mail rather than a boot that fails.
|
|
168
|
+
...(typeof file['mailProvider'] === 'string' &&
|
|
169
|
+
MAIL_PROVIDERS.includes(file['mailProvider'])
|
|
170
|
+
? { mailProvider: file['mailProvider'] }
|
|
171
|
+
: {}),
|
|
172
|
+
...(typeof file['mailFromName'] === 'string' ? { mailFromName: file['mailFromName'] } : {}),
|
|
173
|
+
...(typeof file['mailFromAddress'] === 'string'
|
|
174
|
+
? { mailFromAddress: file['mailFromAddress'] }
|
|
175
|
+
: {}),
|
|
176
|
+
...(typeof file['mailReplyTo'] === 'string' ? { mailReplyTo: file['mailReplyTo'] } : {}),
|
|
177
|
+
...(typeof file['contactEmail'] === 'string' ? { contactEmail: file['contactEmail'] } : {}),
|
|
178
|
+
// Through the same normaliser a submitted form goes through, so the file
|
|
179
|
+
// and the screen cannot mean different things by the same line.
|
|
180
|
+
...(Array.isArray(file['relays'])
|
|
181
|
+
? {
|
|
182
|
+
relays: relayList(file['relays'].filter((entry) => typeof entry === 'string').join('\n')),
|
|
183
|
+
}
|
|
184
|
+
: {}),
|
|
185
|
+
// Absent is the ordinary state of this one, like `homepage`: a site that
|
|
186
|
+
// has never carried WordPress's paths does not write the key at all.
|
|
187
|
+
...(typeof file['wordpressActivityPub'] === 'boolean'
|
|
188
|
+
? { wordpressActivityPub: file['wordpressActivityPub'] }
|
|
189
|
+
: {}),
|
|
190
|
+
// Every menu the site holds, whether or not the theme in use renders it.
|
|
191
|
+
// The `navigation` key the one menu used to live under is not read at all:
|
|
192
|
+
// menus were renamed rather than migrated (TASK-107).
|
|
193
|
+
...(file['menus'] === undefined ? {} : { menus: menusOf(file['menus']) }),
|
|
194
|
+
// The renames a site has published redirects for: facts about its URLs
|
|
195
|
+
// rather than preferences, which is why a content directory restored on
|
|
196
|
+
// its own keeps answering the archive URLs it used to.
|
|
197
|
+
...(Array.isArray(file['taxonomyRedirects'])
|
|
198
|
+
? { taxonomyRedirects: taxonomyRedirectsOf(file['taxonomyRedirects']) }
|
|
199
|
+
: {}),
|
|
200
|
+
...(Number.isInteger(postsPerPage) && postsPerPage > 0 ? { postsPerPage } : {}),
|
|
201
|
+
...(typeof file['url'] === 'string' ? { baseUrl: file['url'] } : {}),
|
|
114
202
|
};
|
|
115
203
|
}
|
|
204
|
+
/** The two archive bases the settings hold, as the URL builders want them. */
|
|
205
|
+
export function taxonomyBasesFromSettings(settings) {
|
|
206
|
+
return { tag: settings.tagBase, category: settings.categoryBase };
|
|
207
|
+
}
|
|
116
208
|
/**
|
|
117
209
|
* `content/_data/site.json` as it should read for these settings.
|
|
118
210
|
*
|
|
119
|
-
* Every key the
|
|
120
|
-
* file's shape is stable and an Eleventy template may reference
|
|
121
|
-
* without guarding it. Every other key the file already had is
|
|
122
|
-
* may put anything in there and reach it from its templates, and
|
|
123
|
-
* form is not going to be the thing that throws it away.
|
|
211
|
+
* Every key the settings model is written whether or not it has a value, so
|
|
212
|
+
* the file's shape is stable and an Eleventy template may reference
|
|
213
|
+
* `site.author` without guarding it. Every other key the file already had is
|
|
214
|
+
* kept: a site may put anything in there and reach it from its templates, and
|
|
215
|
+
* the settings form is not going to be the thing that throws it away.
|
|
216
|
+
*
|
|
217
|
+
* `homepage` and `postsPage` are the one exception, and they earn it: a site
|
|
218
|
+
* showing its latest posts has no static front page at all rather than an
|
|
219
|
+
* empty one, so the keys are absent rather than empty — which is also how a
|
|
220
|
+
* `site.json` written before this version reads, and what it should mean.
|
|
124
221
|
*/
|
|
125
222
|
export function siteJsonFor(settings, existing = {}) {
|
|
126
|
-
|
|
223
|
+
const file = {
|
|
127
224
|
...existing,
|
|
128
225
|
title: settings.title,
|
|
129
226
|
tagline: settings.tagline,
|
|
@@ -131,69 +228,167 @@ export function siteJsonFor(settings, existing = {}) {
|
|
|
131
228
|
author: settings.author,
|
|
132
229
|
postsPerPage: settings.postsPerPage,
|
|
133
230
|
timezone: settings.timezone,
|
|
134
|
-
|
|
231
|
+
language: settings.language,
|
|
232
|
+
tagBase: settings.tagBase,
|
|
233
|
+
categoryBase: settings.categoryBase,
|
|
234
|
+
comments: settings.comments,
|
|
235
|
+
commentsCloseAfterDays: settings.commentsCloseAfterDays,
|
|
236
|
+
webmentionsSend: settings.webmentionsSend,
|
|
237
|
+
webmentionsReceive: settings.webmentionsReceive,
|
|
238
|
+
notifyServer: settings.notifyServer,
|
|
239
|
+
mailProvider: settings.mailProvider,
|
|
240
|
+
mailFromName: settings.mailFromName,
|
|
241
|
+
mailFromAddress: settings.mailFromAddress,
|
|
242
|
+
mailReplyTo: settings.mailReplyTo,
|
|
243
|
+
contactEmail: settings.contactEmail,
|
|
244
|
+
relays: [...settings.relays],
|
|
245
|
+
// Every menu the site holds, written whole. A settings page never has
|
|
246
|
+
// them off its own form — it reads them out of the file inside the write
|
|
247
|
+
// and hands them straight back — so the one screen that models them is the
|
|
248
|
+
// one that edits them (TASK-108).
|
|
249
|
+
menus: Object.fromEntries(Object.entries(settings.menus).map(([name, items]) => [
|
|
250
|
+
name,
|
|
251
|
+
items.map((item) => ({ ...item })),
|
|
252
|
+
])),
|
|
253
|
+
taxonomyRedirects: settings.taxonomyRedirects.map((entry) => ({ ...entry })),
|
|
135
254
|
};
|
|
255
|
+
// The key the menu lived under before menus had names. A rename leaves
|
|
256
|
+
// nothing behind: a `navigation` array sitting beside `menus` in a file
|
|
257
|
+
// nothing reads it from is the kind of thing somebody edits for an hour
|
|
258
|
+
// before noticing.
|
|
259
|
+
delete file['navigation'];
|
|
260
|
+
// `theme` is absent for a site on the packaged theme, on the same rule and
|
|
261
|
+
// for the same reason as the two above: running what the package ships is
|
|
262
|
+
// not a choice a site should have to write down, and a `theme` of `""` would
|
|
263
|
+
// be a name no directory has.
|
|
264
|
+
for (const key of ['homepage', 'postsPage', 'theme']) {
|
|
265
|
+
if (settings[key] === '')
|
|
266
|
+
delete file[key];
|
|
267
|
+
else
|
|
268
|
+
file[key] = settings[key];
|
|
269
|
+
}
|
|
270
|
+
// And the WordPress switch, on the same rule and for a sharper reason: it is
|
|
271
|
+
// a temporary accommodation for one migrated site (decision-14), so a site
|
|
272
|
+
// that has never turned it on should have nothing to say about it, and one
|
|
273
|
+
// that has turned it off again should go back to saying nothing.
|
|
274
|
+
if (settings.wordpressActivityPub)
|
|
275
|
+
file['wordpressActivityPub'] = true;
|
|
276
|
+
else
|
|
277
|
+
delete file['wordpressActivityPub'];
|
|
278
|
+
return file;
|
|
136
279
|
}
|
|
137
280
|
/** The absolute path of one site's `content/_data/site.json`. */
|
|
138
281
|
export function siteDataPath(contentDir) {
|
|
139
282
|
return path.join(contentDir, ...SITE_DATA_FILE.split('/'));
|
|
140
283
|
}
|
|
284
|
+
/** The bytes of one settings object, as the file holds them. */
|
|
285
|
+
function siteJsonText(settings, existing) {
|
|
286
|
+
return `${JSON.stringify(siteJsonFor(settings, existing), null, 2)}\n`;
|
|
287
|
+
}
|
|
141
288
|
/**
|
|
142
|
-
* Rewrite `content/_data/site.json`
|
|
289
|
+
* Rewrite `content/_data/site.json` as these settings, keeping the keys they
|
|
290
|
+
* do not model.
|
|
143
291
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* one.
|
|
292
|
+
* {@link updateSiteSettings} for a caller whose new settings depend on the old
|
|
293
|
+
* ones, which every screen's is: this is for the callers that have the whole
|
|
294
|
+
* object already, a test's set-up among them.
|
|
148
295
|
*/
|
|
149
296
|
export async function writeSiteJson(options) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
297
|
+
await updateSiteSettings({ contentDir: options.contentDir, change: () => options.settings });
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Change the settings: re-read the file, apply the change, write it back, with
|
|
301
|
+
* nothing able to write that file in between ({@link updateFileAtomically}).
|
|
302
|
+
*
|
|
303
|
+
* The re-read is what makes the file win the way it wins for a post
|
|
304
|
+
* (decision-9). Two people with the settings screen open both save what they
|
|
305
|
+
* see, and the second save is applied to what the first one actually wrote
|
|
306
|
+
* rather than to what the second one had on screen — so a change to a field
|
|
307
|
+
* the second person did not touch survives. The write is a temporary file and
|
|
308
|
+
* a rename, so a reader — an Eleventy build, the site data source, another
|
|
309
|
+
* process entirely — never sees a half-written file. Keys the settings do not
|
|
310
|
+
* model, `feedSize` and anything a site added, are kept.
|
|
311
|
+
*/
|
|
312
|
+
export async function updateSiteSettings(options) {
|
|
313
|
+
let written = DEFAULT_SITE_SETTINGS;
|
|
314
|
+
await updateFileAtomically(siteDataPath(options.contentDir), (current) => {
|
|
315
|
+
const existing = parseSiteJson(current ?? '');
|
|
316
|
+
written = options.change(settingsFromSiteJson(existing));
|
|
317
|
+
return siteJsonText(written, existing);
|
|
318
|
+
});
|
|
319
|
+
return written;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Turn an older site's settings rows into `content/_data/site.json`, once, and
|
|
323
|
+
* drop the table.
|
|
324
|
+
*
|
|
325
|
+
* TASK-14 made SQLite the source and the file its mirror; decision-9 reversed
|
|
326
|
+
* that, and a site upgrading across the two has rows nothing would ever read
|
|
327
|
+
* again. So the first boot of this version writes them out. Which side wins
|
|
328
|
+
* when both have something to say is decided the way it is decided everywhere
|
|
329
|
+
* else: the file wins when it was written after the last save, because that is
|
|
330
|
+
* a hand edit or a restored checkout, and the rows win otherwise. Either way
|
|
331
|
+
* the actor handle and type come from the rows, because `site.json` never
|
|
332
|
+
* carried them and losing them would rename the site's actor.
|
|
333
|
+
*
|
|
334
|
+
* Synchronous because `createCms` is: it runs before the server is listening
|
|
335
|
+
* and before anything else has touched the file.
|
|
336
|
+
*/
|
|
337
|
+
export function migrateSettingsToFile(options) {
|
|
338
|
+
const { admin, contentDir } = options;
|
|
339
|
+
const rows = admin.legacySettings();
|
|
340
|
+
if (rows !== undefined && rows.length > 0) {
|
|
341
|
+
const stored = settingsFromRows(rows);
|
|
342
|
+
const file = siteDataPath(contentDir);
|
|
343
|
+
const existing = parseSiteJson(readFileIfPresentSync(file) ?? '');
|
|
344
|
+
const settings = fileWasEditedLast(file, rows) ? settingsFromSiteJson(existing) : stored;
|
|
345
|
+
writeFileAtomicallySync(file, siteJsonText(settings, existing));
|
|
159
346
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
347
|
+
// Dropped whether or not it had rows: a fresh database gets the table from
|
|
348
|
+
// migration 3, which has shipped and so is never edited, and a site that has
|
|
349
|
+
// been migrated must not be asked again on the next boot.
|
|
350
|
+
admin.dropLegacyTable('settings');
|
|
351
|
+
}
|
|
352
|
+
/** The old key/value rows as settings, defaults filled in. */
|
|
353
|
+
function settingsFromRows(rows) {
|
|
354
|
+
const stored = {};
|
|
355
|
+
for (const row of rows)
|
|
356
|
+
stored[row.key] = row.value;
|
|
357
|
+
// Through the file's own reader, so one description of what a setting is
|
|
358
|
+
// worth serves both: the rows are spelled as the file spells them and read
|
|
359
|
+
// back the same way. `relays`, `navigation` and `taxonomyRedirects` were
|
|
360
|
+
// stored one entry per line, which is not how the file holds them.
|
|
361
|
+
return settingsFromSiteJson({
|
|
362
|
+
...stored,
|
|
363
|
+
...(stored['baseUrl'] === undefined ? {} : { url: stored['baseUrl'] }),
|
|
364
|
+
...(stored['postsPerPage'] === undefined
|
|
365
|
+
? {}
|
|
366
|
+
: { postsPerPage: Number(stored['postsPerPage']) }),
|
|
367
|
+
relays: relayList(stored['relays'] ?? ''),
|
|
368
|
+
menus: { [DEFAULT_MENU_NAME]: menuItemsFromText(stored['navigation'] ?? '') },
|
|
369
|
+
taxonomyRedirects: redirectList(stored['taxonomyRedirects'] ?? ''),
|
|
370
|
+
});
|
|
163
371
|
}
|
|
164
372
|
/**
|
|
165
|
-
*
|
|
166
|
-
* existed before this screen did — or one `geekity init` just wrote — keeps
|
|
167
|
-
* the values it already had.
|
|
373
|
+
* Whether `site.json` has been written since the newest settings row was.
|
|
168
374
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
375
|
+
* A missing file has not, so the rows win. A file whose modification time
|
|
376
|
+
* cannot be read is treated the same way: the rows are the only thing that is
|
|
377
|
+
* certainly there.
|
|
172
378
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
: {}),
|
|
187
|
-
...(typeof file['avatar'] === 'string' ? { avatar: file['avatar'] } : {}),
|
|
188
|
-
...(Number.isInteger(postsPerPage) && postsPerPage > 0 ? { postsPerPage } : {}),
|
|
189
|
-
// The file's `url` only becomes the setting when the deployment has not
|
|
190
|
-
// named one; otherwise the setting records what is actually in effect.
|
|
191
|
-
baseUrl: config.baseUrlSource === 'default' && typeof file['url'] === 'string' && file['url'] !== ''
|
|
192
|
-
? file['url']
|
|
193
|
-
: config.baseUrl,
|
|
194
|
-
};
|
|
195
|
-
writeSiteSettings(store, seeded);
|
|
196
|
-
return seeded;
|
|
379
|
+
function fileWasEditedLast(file, rows) {
|
|
380
|
+
let modified;
|
|
381
|
+
try {
|
|
382
|
+
modified = statSync(file).mtimeMs;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const newest = rows.reduce((latest, row) => {
|
|
388
|
+
const at = Date.parse(row.updatedAt);
|
|
389
|
+
return Number.isNaN(at) ? latest : Math.max(latest, at);
|
|
390
|
+
}, 0);
|
|
391
|
+
return modified > newest;
|
|
197
392
|
}
|
|
198
393
|
/**
|
|
199
394
|
* The base URL actually in effect, and why.
|
|
@@ -209,28 +404,141 @@ export function effectiveBaseUrl(config, settings) {
|
|
|
209
404
|
return config.baseUrl;
|
|
210
405
|
return settings.baseUrl === '' ? config.baseUrl : settings.baseUrl;
|
|
211
406
|
}
|
|
212
|
-
/**
|
|
213
|
-
|
|
407
|
+
/**
|
|
408
|
+
* What is wrong with each field of a submitted form: one check per field, so a
|
|
409
|
+
* page can run the checks for the fields it carries and no others.
|
|
410
|
+
*
|
|
411
|
+
* A field nothing can be wrong with — a tagline, an author, a checkbox — has a
|
|
412
|
+
* check all the same, because a table with a hole in it is a field somebody
|
|
413
|
+
* added and forgot to think about.
|
|
414
|
+
*/
|
|
415
|
+
const FIELD_CHECKS = {
|
|
416
|
+
title: (form) => (form.title.trim() === '' ? 'The site needs a title.' : undefined),
|
|
417
|
+
tagline: () => undefined,
|
|
418
|
+
author: () => undefined,
|
|
419
|
+
baseUrl: (form) => normalizeBaseUrl(form.baseUrl) === undefined
|
|
420
|
+
? 'The base URL has to be an absolute http:// or https:// URL.'
|
|
421
|
+
: undefined,
|
|
422
|
+
postsPerPage: (form) => {
|
|
423
|
+
const perPage = Number(form.postsPerPage);
|
|
424
|
+
return !Number.isInteger(perPage) || perPage < 1
|
|
425
|
+
? 'Posts per page has to be a whole number of one or more.'
|
|
426
|
+
: undefined;
|
|
427
|
+
},
|
|
428
|
+
// The two picks are checked as a pair, the way the archive bases are, and
|
|
429
|
+
// for the same reason: two of the three rules are about the pair. Neither
|
|
430
|
+
// check asks whether the slug still names a published page — the select only
|
|
431
|
+
// offers ones that do, and a page drafted after it was picked is a site back
|
|
432
|
+
// on its latest posts rather than a settings screen that will not save.
|
|
433
|
+
homepage: (form) => slugProblem(form.homepage, 'A homepage'),
|
|
434
|
+
postsPage: (form) => {
|
|
435
|
+
if (form.postsPage.trim() === '')
|
|
436
|
+
return undefined;
|
|
437
|
+
if (form.homepage.trim() === '') {
|
|
438
|
+
return 'A posts page needs a homepage: pick the page the front page shows first.';
|
|
439
|
+
}
|
|
440
|
+
if (form.postsPage.trim() === form.homepage.trim()) {
|
|
441
|
+
return 'The homepage and the posts page have to be two different pages.';
|
|
442
|
+
}
|
|
443
|
+
return slugProblem(form.postsPage, 'A posts page');
|
|
444
|
+
},
|
|
445
|
+
timezone: (form) => isValidTimezone(form.timezone)
|
|
446
|
+
? undefined
|
|
447
|
+
: 'That is not an IANA time zone name, such as Europe/London.',
|
|
448
|
+
language: (form) => LANGUAGE_TAG_PATTERN.test(form.language.trim())
|
|
449
|
+
? undefined
|
|
450
|
+
: 'That is not a language tag, such as en, en-GB or pt-BR.',
|
|
451
|
+
// Empty is a value here — it is how a site says "the theme the package
|
|
452
|
+
// ships" — so only a name has anything to check. A name that is not a theme
|
|
453
|
+
// is refused rather than saved and warned about later: the screen has a list
|
|
454
|
+
// of the themes that are there, so the only way to type one that is not is a
|
|
455
|
+
// theme deleted between drawing the list and saving it, and the person is in
|
|
456
|
+
// front of the form and can be told.
|
|
457
|
+
theme: (form, context) => {
|
|
458
|
+
const name = form.theme.trim();
|
|
459
|
+
if (name === '')
|
|
460
|
+
return undefined;
|
|
461
|
+
const badName = themeNameProblem(name);
|
|
462
|
+
if (badName !== undefined)
|
|
463
|
+
return badName;
|
|
464
|
+
if (context.themesDir === undefined)
|
|
465
|
+
return undefined;
|
|
466
|
+
const read = readTheme(path.join(context.themesDir, name));
|
|
467
|
+
return read.ok ? undefined : `There is no theme called "${name}": ${read.reason}`;
|
|
468
|
+
},
|
|
469
|
+
comments: () => undefined,
|
|
470
|
+
// The empty string is refused rather than read as zero, which is what
|
|
471
|
+
// `Number('')` would make it: a cleared field is a mistake, and "never close
|
|
472
|
+
// comments" should have to be typed.
|
|
473
|
+
commentsCloseAfterDays: (form) => {
|
|
474
|
+
const typed = form.commentsCloseAfterDays.trim();
|
|
475
|
+
const days = Number(typed);
|
|
476
|
+
return typed === '' || !Number.isInteger(days) || days < 0
|
|
477
|
+
? 'Comments close after a whole number of days, or 0 for never.'
|
|
478
|
+
: undefined;
|
|
479
|
+
},
|
|
480
|
+
webmentionsSend: () => undefined,
|
|
481
|
+
webmentionsReceive: () => undefined,
|
|
482
|
+
// Empty is a value here — it is how a site turns real-time notification off
|
|
483
|
+
// — so only a non-empty one has to be a URL. http as well as https, because
|
|
484
|
+
// a notify server on a private network or a loopback port is a real one.
|
|
485
|
+
notifyServer: (form) => form.notifyServer.trim() !== '' && normalizeBaseUrl(form.notifyServer) === undefined
|
|
486
|
+
? 'A notify server is an absolute http:// or https:// URL, or empty for none.'
|
|
487
|
+
: undefined,
|
|
488
|
+
mailProvider: (form) => MAIL_PROVIDERS.includes(form.mailProvider)
|
|
489
|
+
? undefined
|
|
490
|
+
: `A mail provider is one of ${MAIL_PROVIDERS.join(', ')}.`,
|
|
491
|
+
mailFromName: () => undefined,
|
|
492
|
+
// Empty is a value for both of these — no From address falls back to
|
|
493
|
+
// `no-reply@` at the site's host, and no reply-to means replies go to the
|
|
494
|
+
// From address — so only a non-empty one has to look like an address.
|
|
495
|
+
mailFromAddress: (form) => form.mailFromAddress.trim() !== '' && !EMAIL_PATTERN.test(form.mailFromAddress.trim())
|
|
496
|
+
? 'A From address is an email address, such as blog@example.com, or empty for the default.'
|
|
497
|
+
: undefined,
|
|
498
|
+
mailReplyTo: (form) => form.mailReplyTo.trim() !== '' && !EMAIL_PATTERN.test(form.mailReplyTo.trim())
|
|
499
|
+
? 'A reply-to is an email address, such as hello@example.com, or empty to reply to the From address.'
|
|
500
|
+
: undefined,
|
|
501
|
+
// Empty is a value here too — it is how a site says "whichever admin has an
|
|
502
|
+
// address" — so only a non-empty one has to look like one.
|
|
503
|
+
contactEmail: (form) => form.contactEmail.trim() !== '' && !EMAIL_PATTERN.test(form.contactEmail.trim())
|
|
504
|
+
? 'A contact address is an email address, such as hello@example.com, or empty for the first admin with one.'
|
|
505
|
+
: undefined,
|
|
506
|
+
// A relay list is checked line by line, and the first bad line is what the
|
|
507
|
+
// field says: a textarea has one message, and pointing at the line somebody
|
|
508
|
+
// has to fix is more use than counting how many are wrong.
|
|
509
|
+
relays: (form) => {
|
|
510
|
+
const bad = relayLines(form.relays).find((line) => normalizeRelayInbox(line) === undefined);
|
|
511
|
+
return bad === undefined
|
|
512
|
+
? undefined
|
|
513
|
+
: `A relay is its inbox as an absolute http:// or https:// URL, one per line. ` +
|
|
514
|
+
`"${bad}" is not one.`;
|
|
515
|
+
},
|
|
516
|
+
// A checkbox is either submitted or not, so there is nothing a person could
|
|
517
|
+
// get wrong about it.
|
|
518
|
+
wordpressActivityPub: () => undefined,
|
|
519
|
+
// The two archive bases are checked as a pair, because two of the rules —
|
|
520
|
+
// that they differ, and that neither takes a path the site already answers
|
|
521
|
+
// on — are about the pair rather than either one. They are on one page for
|
|
522
|
+
// exactly that reason.
|
|
523
|
+
tagBase: (form) => taxonomyBaseProblems({ tag: form.tagBase, category: form.categoryBase }).tag,
|
|
524
|
+
categoryBase: (form) => taxonomyBaseProblems({ tag: form.tagBase, category: form.categoryBase }).category,
|
|
525
|
+
};
|
|
526
|
+
/** Every field of the settings, in the order the form fields name them. */
|
|
527
|
+
export const SETTINGS_FIELD_NAMES = Object.keys(SETTINGS_FIELDS);
|
|
528
|
+
/**
|
|
529
|
+
* What is wrong with a submitted settings form, one message per field.
|
|
530
|
+
*
|
|
531
|
+
* `fields` is what a page carries: it validates its own fields and says
|
|
532
|
+
* nothing about the rest, so a page cannot refuse a save over a field it does
|
|
533
|
+
* not show and gives no way to fix. `context` is what a check needs beyond the
|
|
534
|
+
* form; see {@link SettingsContext}.
|
|
535
|
+
*/
|
|
536
|
+
export function settingsProblems(form, fields = SETTINGS_FIELD_NAMES, context = {}) {
|
|
214
537
|
const problems = {};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
problems.baseUrl = 'The base URL has to be an absolute http:// or https:// URL.';
|
|
220
|
-
}
|
|
221
|
-
const perPage = Number(form.postsPerPage);
|
|
222
|
-
if (!Number.isInteger(perPage) || perPage < 1) {
|
|
223
|
-
problems.postsPerPage = 'Posts per page has to be a whole number of one or more.';
|
|
224
|
-
}
|
|
225
|
-
if (!isValidTimezone(form.timezone)) {
|
|
226
|
-
problems.timezone = 'That is not an IANA time zone name, such as Europe/London.';
|
|
227
|
-
}
|
|
228
|
-
if (!ACTOR_HANDLE_PATTERN.test(form.actorHandle)) {
|
|
229
|
-
problems.actorHandle =
|
|
230
|
-
'An actor handle is 1 to 64 letters, digits, dashes or underscores, with no @ and no dots.';
|
|
231
|
-
}
|
|
232
|
-
if (!ACTOR_TYPES.includes(form.actorType)) {
|
|
233
|
-
problems.actorType = `An actor type is one of ${ACTOR_TYPES.join(', ')}.`;
|
|
538
|
+
for (const name of fields) {
|
|
539
|
+
const problem = FIELD_CHECKS[name](form, context);
|
|
540
|
+
if (problem !== undefined)
|
|
541
|
+
problems[name] = problem;
|
|
234
542
|
}
|
|
235
543
|
return problems;
|
|
236
544
|
}
|
|
@@ -238,21 +546,47 @@ export function settingsProblems(form) {
|
|
|
238
546
|
* A validated form as settings. Only call it on a form
|
|
239
547
|
* {@link settingsProblems} found nothing wrong with.
|
|
240
548
|
*
|
|
241
|
-
* The
|
|
242
|
-
* it: the
|
|
243
|
-
* save of the
|
|
549
|
+
* The archive renames and the menus are carried in rather than read off the
|
|
550
|
+
* form, because they are not on it: the taxonomy screens write the one and the
|
|
551
|
+
* Navigation screen the other, and a save of the fields that are on the form
|
|
552
|
+
* keeps whatever is stored. Passing the settings read inside the write is all
|
|
553
|
+
* a caller has to do, since they hold both.
|
|
244
554
|
*/
|
|
245
|
-
export function settingsFromForm(form,
|
|
555
|
+
export function settingsFromForm(form, carried = {}) {
|
|
246
556
|
return {
|
|
247
|
-
|
|
557
|
+
taxonomyRedirects: carried.taxonomyRedirects ?? DEFAULT_SITE_SETTINGS.taxonomyRedirects,
|
|
558
|
+
menus: carried.menus ?? DEFAULT_SITE_SETTINGS.menus,
|
|
248
559
|
title: form.title.trim(),
|
|
249
560
|
tagline: form.tagline.trim(),
|
|
250
561
|
baseUrl: normalizeBaseUrl(form.baseUrl) ?? '',
|
|
251
562
|
timezone: form.timezone.trim(),
|
|
563
|
+
language: form.language.trim(),
|
|
252
564
|
postsPerPage: Number(form.postsPerPage),
|
|
565
|
+
homepage: form.homepage.trim(),
|
|
566
|
+
// A posts page means nothing without a homepage, and the validator has
|
|
567
|
+
// already refused the pair; this is what makes clearing the homepage clear
|
|
568
|
+
// the listing's own page with it rather than leave it stranded.
|
|
569
|
+
postsPage: form.homepage.trim() === '' ? '' : form.postsPage.trim(),
|
|
253
570
|
author: form.author.trim(),
|
|
254
|
-
|
|
255
|
-
|
|
571
|
+
theme: form.theme.trim(),
|
|
572
|
+
tagBase: form.tagBase.trim(),
|
|
573
|
+
categoryBase: form.categoryBase.trim(),
|
|
574
|
+
// A checkbox submits nothing at all when it is clear, which is what the
|
|
575
|
+
// empty string here means.
|
|
576
|
+
comments: form.comments !== '',
|
|
577
|
+
commentsCloseAfterDays: Number(form.commentsCloseAfterDays),
|
|
578
|
+
webmentionsSend: form.webmentionsSend !== '',
|
|
579
|
+
webmentionsReceive: form.webmentionsReceive !== '',
|
|
580
|
+
notifyServer: normalizeBaseUrl(form.notifyServer) ?? '',
|
|
581
|
+
mailProvider: MAIL_PROVIDERS.includes(form.mailProvider)
|
|
582
|
+
? form.mailProvider
|
|
583
|
+
: 'none',
|
|
584
|
+
mailFromName: form.mailFromName.trim(),
|
|
585
|
+
mailFromAddress: form.mailFromAddress.trim(),
|
|
586
|
+
mailReplyTo: form.mailReplyTo.trim(),
|
|
587
|
+
contactEmail: form.contactEmail.trim(),
|
|
588
|
+
relays: relayList(form.relays),
|
|
589
|
+
wordpressActivityPub: form.wordpressActivityPub !== '',
|
|
256
590
|
};
|
|
257
591
|
}
|
|
258
592
|
/** The settings as the form shows them. */
|
|
@@ -262,169 +596,109 @@ export function formFromSettings(settings) {
|
|
|
262
596
|
tagline: settings.tagline,
|
|
263
597
|
baseUrl: settings.baseUrl,
|
|
264
598
|
timezone: settings.timezone,
|
|
599
|
+
language: settings.language,
|
|
265
600
|
postsPerPage: String(settings.postsPerPage),
|
|
601
|
+
homepage: settings.homepage,
|
|
602
|
+
postsPage: settings.postsPage,
|
|
266
603
|
author: settings.author,
|
|
267
|
-
|
|
268
|
-
|
|
604
|
+
theme: settings.theme,
|
|
605
|
+
tagBase: settings.tagBase,
|
|
606
|
+
categoryBase: settings.categoryBase,
|
|
607
|
+
comments: settings.comments ? '1' : '',
|
|
608
|
+
commentsCloseAfterDays: String(settings.commentsCloseAfterDays),
|
|
609
|
+
webmentionsSend: settings.webmentionsSend ? '1' : '',
|
|
610
|
+
webmentionsReceive: settings.webmentionsReceive ? '1' : '',
|
|
611
|
+
notifyServer: settings.notifyServer,
|
|
612
|
+
mailProvider: settings.mailProvider,
|
|
613
|
+
mailFromName: settings.mailFromName,
|
|
614
|
+
mailFromAddress: settings.mailFromAddress,
|
|
615
|
+
mailReplyTo: settings.mailReplyTo,
|
|
616
|
+
contactEmail: settings.contactEmail,
|
|
617
|
+
relays: settings.relays.join('\n'),
|
|
618
|
+
wordpressActivityPub: settings.wordpressActivityPub ? '1' : '',
|
|
269
619
|
};
|
|
270
620
|
}
|
|
271
621
|
/**
|
|
272
|
-
*
|
|
622
|
+
* A `taxonomy|from|to` block as the renames it names, which is how the old
|
|
623
|
+
* settings table spelled them ({@link migrateSettingsToFile}).
|
|
273
624
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
625
|
+
* The same tolerance the rest of this file reads its lists with: a line that
|
|
626
|
+
* is not three non-empty parts naming a real taxonomy is dropped rather than
|
|
627
|
+
* failing the read.
|
|
277
628
|
*/
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// base URL keeps the value it had rather than being cleared by a save.
|
|
289
|
-
baseUrl: c.var.config.baseUrlSource === 'default'
|
|
290
|
-
? field(body[SETTINGS_FIELDS.baseUrl])
|
|
291
|
-
: stored.baseUrl === ''
|
|
292
|
-
? c.var.config.baseUrl
|
|
293
|
-
: stored.baseUrl,
|
|
294
|
-
timezone: field(body[SETTINGS_FIELDS.timezone]),
|
|
295
|
-
postsPerPage: field(body[SETTINGS_FIELDS.postsPerPage]),
|
|
296
|
-
author: field(body[SETTINGS_FIELDS.author]),
|
|
297
|
-
actorHandle: field(body[SETTINGS_FIELDS.actorHandle]),
|
|
298
|
-
actorType: field(body[SETTINGS_FIELDS.actorType]),
|
|
299
|
-
};
|
|
300
|
-
const problems = settingsProblems(submitted);
|
|
301
|
-
if (Object.keys(problems).length > 0) {
|
|
302
|
-
c.status(400);
|
|
303
|
-
return render(c, ADMIN_TEMPLATES.settings, {
|
|
304
|
-
...screen(c.var.config, stored),
|
|
305
|
-
form: submitted,
|
|
306
|
-
problems,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
const settings = settingsFromForm(submitted, stored.avatar);
|
|
310
|
-
await store(c, settings);
|
|
311
|
-
// The name, the summary and the handle are the actor's profile as much as
|
|
312
|
-
// the avatar is, and a follower's copy of it is only as fresh as the last
|
|
313
|
-
// thing it was told.
|
|
314
|
-
const report = profileChanged(stored, settings)
|
|
315
|
-
? await c.var.delivery.updateActor()
|
|
316
|
-
: undefined;
|
|
317
|
-
flash(c, 'notice', `Settings saved.${toldFollowers(report)}`);
|
|
318
|
-
return c.redirect(SETTINGS_PATH, 303);
|
|
319
|
-
});
|
|
320
|
-
/**
|
|
321
|
-
* The avatar's own endpoint: one multipart form uploads an image, and a
|
|
322
|
-
* second, plain one takes it down again.
|
|
323
|
-
*
|
|
324
|
-
* It is separate from the settings form because a file cannot travel in a
|
|
325
|
-
* urlencoded body, and because the two should not share a fate: a rejected
|
|
326
|
-
* image must not lose an edit to the title, and a rejected title must not
|
|
327
|
-
* lose the avatar. A refusal is a flash and a redirect, so what is stored is
|
|
328
|
-
* exactly what it was and the screen says why.
|
|
329
|
-
*/
|
|
330
|
-
app.post(AVATAR_PATH, async (c) => {
|
|
331
|
-
const body = await c.req.parseBody();
|
|
332
|
-
const stored = readSiteSettings(c.var.admin);
|
|
333
|
-
if (field(body[AVATAR_FIELDS.action]) === AVATAR_REMOVE) {
|
|
334
|
-
if (stored.avatar === '') {
|
|
335
|
-
flash(c, 'notice', 'The site has no avatar.');
|
|
336
|
-
return c.redirect(SETTINGS_PATH, 303);
|
|
337
|
-
}
|
|
338
|
-
await store(c, { ...stored, avatar: '' });
|
|
339
|
-
const removal = await c.var.delivery.updateActor();
|
|
340
|
-
flash(c, 'notice', `Avatar removed.${toldFollowers(removal)}`);
|
|
341
|
-
return c.redirect(SETTINGS_PATH, 303);
|
|
342
|
-
}
|
|
343
|
-
const outcome = await storeUpload(body[AVATAR_FIELDS.file], c.var.config, {
|
|
344
|
-
imagesOnly: true,
|
|
629
|
+
function redirectList(value) {
|
|
630
|
+
const entries = [];
|
|
631
|
+
for (const line of value.split('\n')) {
|
|
632
|
+
const parts = line.split('|');
|
|
633
|
+
if (parts.length !== 3)
|
|
634
|
+
continue;
|
|
635
|
+
entries.push({
|
|
636
|
+
taxonomy: (parts[0] ?? '').trim(),
|
|
637
|
+
from: (parts[1] ?? '').trim(),
|
|
638
|
+
to: (parts[2] ?? '').trim(),
|
|
345
639
|
});
|
|
346
|
-
if (refusedUpload(outcome)) {
|
|
347
|
-
flash(c, 'error', `${outcome.error} The avatar is unchanged.`);
|
|
348
|
-
return c.redirect(SETTINGS_PATH, 303);
|
|
349
|
-
}
|
|
350
|
-
await store(c, { ...stored, avatar: outcome.url });
|
|
351
|
-
const report = await c.var.delivery.updateActor();
|
|
352
|
-
flash(c, 'notice', `Avatar saved.${toldFollowers(report)}`);
|
|
353
|
-
return c.redirect(SETTINGS_PATH, 303);
|
|
354
|
-
});
|
|
355
|
-
/**
|
|
356
|
-
* Write settings to SQLite and then to `content/_data/site.json`, in that
|
|
357
|
-
* order, so the store — which is what the theme reads — is never behind the
|
|
358
|
-
* file.
|
|
359
|
-
*/
|
|
360
|
-
async function store(c, settings) {
|
|
361
|
-
writeSiteSettings(c.var.admin, settings);
|
|
362
|
-
await writeSiteJson({ contentDir: c.var.config.contentDir, settings });
|
|
363
640
|
}
|
|
641
|
+
// Through the same reader `site.json` goes through, so the two spellings of
|
|
642
|
+
// the list cannot mean different things.
|
|
643
|
+
return taxonomyRedirectsOf(entries);
|
|
364
644
|
}
|
|
365
645
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* the settings). The rest — the time zone, the page size — is the site's own
|
|
369
|
-
* business and nobody else's.
|
|
370
|
-
*
|
|
371
|
-
* The base URL is not here either, though the profile is built on it: it is
|
|
372
|
-
* settled at boot ({@link effectiveBaseUrl}), so an actor built the moment it
|
|
373
|
-
* is saved would carry the old one and say nothing new.
|
|
646
|
+
* A base URL, normalised the way `resolveConfig` normalises the configured
|
|
647
|
+
* one, or `undefined` when it is not an absolute http(s) URL.
|
|
374
648
|
*/
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
649
|
+
function normalizeBaseUrl(value) {
|
|
650
|
+
const trimmed = value.trim();
|
|
651
|
+
if (trimmed === '')
|
|
652
|
+
return undefined;
|
|
653
|
+
let parsed;
|
|
654
|
+
try {
|
|
655
|
+
parsed = new URL(trimmed);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
661
|
+
return undefined;
|
|
662
|
+
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
|
|
663
|
+
return parsed.origin + pathname;
|
|
381
664
|
}
|
|
382
665
|
/**
|
|
383
|
-
* The
|
|
384
|
-
*
|
|
385
|
-
*
|
|
666
|
+
* The non-empty lines of a relay textarea, trimmed.
|
|
667
|
+
*
|
|
668
|
+
* Blank lines are not an error: somebody pasting a list leaves them, and a
|
|
669
|
+
* blank line asks for nothing.
|
|
386
670
|
*/
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
? ' One follower has been told.'
|
|
393
|
-
: ` ${String(total)} followers have been told.`;
|
|
394
|
-
}
|
|
395
|
-
/** Everything the settings template renders, for a given set of settings. */
|
|
396
|
-
function screen(config, settings) {
|
|
397
|
-
const overridden = config.baseUrlSource !== 'default';
|
|
398
|
-
return {
|
|
399
|
-
section: 'settings',
|
|
400
|
-
settingsUrl: SETTINGS_PATH,
|
|
401
|
-
fields: SETTINGS_FIELDS,
|
|
402
|
-
actorTypes: ACTOR_TYPES,
|
|
403
|
-
avatar: settings.avatar,
|
|
404
|
-
avatarUrl: AVATAR_PATH,
|
|
405
|
-
avatarFields: AVATAR_FIELDS,
|
|
406
|
-
avatarRemove: AVATAR_REMOVE,
|
|
407
|
-
form: formFromSettings(settings),
|
|
408
|
-
problems: {},
|
|
409
|
-
baseUrlInEffect: effectiveBaseUrl(config, settings),
|
|
410
|
-
baseUrlOverridden: overridden,
|
|
411
|
-
baseUrlSource: config.baseUrlSource,
|
|
412
|
-
baseUrlNote: overridden
|
|
413
|
-
? config.baseUrlSource === 'environment'
|
|
414
|
-
? 'GEEKITY_BASE_URL is set, so it is the base URL in effect and this field is not editable here.'
|
|
415
|
-
: 'The config file sets baseUrl, so it is the base URL in effect and this field is not editable here.'
|
|
416
|
-
: 'Absolute URLs, the feeds and the session cookie pick this up when the site next starts.',
|
|
417
|
-
};
|
|
671
|
+
function relayLines(value) {
|
|
672
|
+
return value
|
|
673
|
+
.split('\n')
|
|
674
|
+
.map((line) => line.trim())
|
|
675
|
+
.filter((line) => line !== '');
|
|
418
676
|
}
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
677
|
+
/**
|
|
678
|
+
* A relay textarea as the list of inboxes it names: normalised, in the order
|
|
679
|
+
* they were given, with repeats dropped.
|
|
680
|
+
*
|
|
681
|
+
* Exported because the same parsing turns a stored setting, a submitted form
|
|
682
|
+
* and a `site.json` array into the one list; a second spelling of it would let
|
|
683
|
+
* the file and the database disagree about what a site subscribes to.
|
|
684
|
+
*/
|
|
685
|
+
export function relayList(value) {
|
|
686
|
+
const relays = [];
|
|
687
|
+
for (const line of relayLines(value)) {
|
|
688
|
+
const inbox = normalizeRelayInbox(line);
|
|
689
|
+
if (inbox !== undefined && !relays.includes(inbox))
|
|
690
|
+
relays.push(inbox);
|
|
691
|
+
}
|
|
692
|
+
return relays;
|
|
422
693
|
}
|
|
423
694
|
/**
|
|
424
|
-
* A
|
|
425
|
-
*
|
|
695
|
+
* A relay inbox URL, or `undefined` when it is not an absolute http(s) one.
|
|
696
|
+
*
|
|
697
|
+
* Unlike {@link normalizeBaseUrl} this keeps the whole URL bar a trailing
|
|
698
|
+
* slash: a relay inbox is a path — `/user/_____relay_____/inbox` — rather than
|
|
699
|
+
* an origin, and some relays hang one off a query string.
|
|
426
700
|
*/
|
|
427
|
-
function
|
|
701
|
+
export function normalizeRelayInbox(value) {
|
|
428
702
|
const trimmed = value.trim();
|
|
429
703
|
if (trimmed === '')
|
|
430
704
|
return undefined;
|
|
@@ -437,8 +711,23 @@ function normalizeBaseUrl(value) {
|
|
|
437
711
|
}
|
|
438
712
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
439
713
|
return undefined;
|
|
440
|
-
const
|
|
441
|
-
return parsed.
|
|
714
|
+
const href = parsed.href;
|
|
715
|
+
return href.endsWith('/') && parsed.pathname !== '/' ? href.slice(0, -1) : href;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* What a page slug may be made of on this screen: the shape
|
|
719
|
+
* {@link slugify} produces, which is what the editor writes.
|
|
720
|
+
*
|
|
721
|
+
* The setting travels no further than a lookup by slug, so this is about
|
|
722
|
+
* catching a hand-typed value rather than about safety.
|
|
723
|
+
*/
|
|
724
|
+
const PAGE_SLUG_PATTERN = /^[A-Za-z0-9._~-]{1,200}$/;
|
|
725
|
+
/** What is wrong with one page pick, or `undefined`. Empty is a value. */
|
|
726
|
+
function slugProblem(value, what) {
|
|
727
|
+
const slug = value.trim();
|
|
728
|
+
return slug === '' || PAGE_SLUG_PATTERN.test(slug)
|
|
729
|
+
? undefined
|
|
730
|
+
: `${what} is the slug of one of the site's pages, such as about.`;
|
|
442
731
|
}
|
|
443
732
|
/** Whether `Intl` knows the zone. An empty name is not a zone. */
|
|
444
733
|
function isValidTimezone(value) {
|
|
@@ -453,16 +742,19 @@ function isValidTimezone(value) {
|
|
|
453
742
|
return false;
|
|
454
743
|
}
|
|
455
744
|
}
|
|
745
|
+
/** One site.json as an object, empty when it is missing or will not parse. */
|
|
456
746
|
function readSiteJsonSync(file) {
|
|
747
|
+
return parseSiteJson(readFileIfPresentSync(file) ?? '');
|
|
748
|
+
}
|
|
749
|
+
/** The same, from bytes already in hand. */
|
|
750
|
+
function parseSiteJson(source) {
|
|
751
|
+
let parsed;
|
|
457
752
|
try {
|
|
458
|
-
|
|
753
|
+
parsed = JSON.parse(source);
|
|
459
754
|
}
|
|
460
755
|
catch {
|
|
461
756
|
return {};
|
|
462
757
|
}
|
|
463
|
-
}
|
|
464
|
-
function parseSiteJson(source) {
|
|
465
|
-
const parsed = JSON.parse(source);
|
|
466
758
|
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
467
759
|
? parsed
|
|
468
760
|
: {};
|