@geekity/cms 0.4.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.
Files changed (143) hide show
  1. package/README.md +234 -108
  2. package/admin/pages/documents/editor.njk +0 -13
  3. package/admin/pages/{settings/federation.njk → federation/settings.njk} +7 -5
  4. package/admin/pages/navigation/menus.njk +129 -0
  5. package/admin/pages/settings/reading.njk +2 -10
  6. package/admin/pages/tools/content-index.njk +89 -0
  7. package/admin/pages/users/list.njk +5 -2
  8. package/admin/static/admin.css +60 -0
  9. package/dist/access-log.d.ts +44 -0
  10. package/dist/access-log.d.ts.map +1 -0
  11. package/dist/access-log.js +71 -0
  12. package/dist/access-log.js.map +1 -0
  13. package/dist/admin/documents.d.ts +3 -13
  14. package/dist/admin/documents.d.ts.map +1 -1
  15. package/dist/admin/documents.js +2 -34
  16. package/dist/admin/documents.js.map +1 -1
  17. package/dist/admin/federation.d.ts +10 -0
  18. package/dist/admin/federation.d.ts.map +1 -1
  19. package/dist/admin/federation.js +10 -0
  20. package/dist/admin/federation.js.map +1 -1
  21. package/dist/admin/index.d.ts +3 -3
  22. package/dist/admin/index.d.ts.map +1 -1
  23. package/dist/admin/index.js +2 -2
  24. package/dist/admin/index.js.map +1 -1
  25. package/dist/admin/menu.d.ts.map +1 -1
  26. package/dist/admin/menu.js +19 -2
  27. package/dist/admin/menu.js.map +1 -1
  28. package/dist/admin/navigation.d.ts +155 -0
  29. package/dist/admin/navigation.d.ts.map +1 -0
  30. package/dist/admin/navigation.js +292 -0
  31. package/dist/admin/navigation.js.map +1 -0
  32. package/dist/admin/routes.d.ts.map +1 -1
  33. package/dist/admin/routes.js +9 -0
  34. package/dist/admin/routes.js.map +1 -1
  35. package/dist/admin/session.d.ts +17 -3
  36. package/dist/admin/session.d.ts.map +1 -1
  37. package/dist/admin/session.js +19 -5
  38. package/dist/admin/session.js.map +1 -1
  39. package/dist/admin/settings-federation.d.ts +10 -1
  40. package/dist/admin/settings-federation.d.ts.map +1 -1
  41. package/dist/admin/settings-federation.js +26 -6
  42. package/dist/admin/settings-federation.js.map +1 -1
  43. package/dist/admin/settings-page.d.ts +26 -9
  44. package/dist/admin/settings-page.d.ts.map +1 -1
  45. package/dist/admin/settings-page.js +18 -12
  46. package/dist/admin/settings-page.js.map +1 -1
  47. package/dist/admin/settings-pages.d.ts +14 -3
  48. package/dist/admin/settings-pages.d.ts.map +1 -1
  49. package/dist/admin/settings-pages.js +15 -5
  50. package/dist/admin/settings-pages.js.map +1 -1
  51. package/dist/admin/settings-reading.d.ts +5 -4
  52. package/dist/admin/settings-reading.d.ts.map +1 -1
  53. package/dist/admin/settings-reading.js +6 -5
  54. package/dist/admin/settings-reading.js.map +1 -1
  55. package/dist/admin/settings.d.ts +29 -15
  56. package/dist/admin/settings.d.ts.map +1 -1
  57. package/dist/admin/settings.js +28 -78
  58. package/dist/admin/settings.js.map +1 -1
  59. package/dist/admin/templates.d.ts +3 -1
  60. package/dist/admin/templates.d.ts.map +1 -1
  61. package/dist/admin/templates.js +3 -1
  62. package/dist/admin/templates.js.map +1 -1
  63. package/dist/admin/tools.d.ts +111 -0
  64. package/dist/admin/tools.d.ts.map +1 -0
  65. package/dist/admin/tools.js +182 -0
  66. package/dist/admin/tools.js.map +1 -0
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +6 -1
  69. package/dist/cli.js.map +1 -1
  70. package/dist/comments/form.d.ts +48 -0
  71. package/dist/comments/form.d.ts.map +1 -1
  72. package/dist/comments/form.js +15 -0
  73. package/dist/comments/form.js.map +1 -1
  74. package/dist/comments/index.d.ts +4 -3
  75. package/dist/comments/index.d.ts.map +1 -1
  76. package/dist/comments/index.js +2 -1
  77. package/dist/comments/index.js.map +1 -1
  78. package/dist/comments/routes.d.ts +3 -1
  79. package/dist/comments/routes.d.ts.map +1 -1
  80. package/dist/comments/routes.js +46 -11
  81. package/dist/comments/routes.js.map +1 -1
  82. package/dist/comments/submission.d.ts +37 -3
  83. package/dist/comments/submission.d.ts.map +1 -1
  84. package/dist/comments/submission.js +59 -20
  85. package/dist/comments/submission.js.map +1 -1
  86. package/dist/comments/viewer.d.ts +21 -0
  87. package/dist/comments/viewer.d.ts.map +1 -0
  88. package/dist/comments/viewer.js +45 -0
  89. package/dist/comments/viewer.js.map +1 -0
  90. package/dist/config.d.ts +35 -0
  91. package/dist/config.d.ts.map +1 -1
  92. package/dist/config.js +3 -0
  93. package/dist/config.js.map +1 -1
  94. package/dist/content/store.d.ts +18 -0
  95. package/dist/content/store.d.ts.map +1 -1
  96. package/dist/content/store.js +18 -0
  97. package/dist/content/store.js.map +1 -1
  98. package/dist/env.d.ts +12 -1
  99. package/dist/env.d.ts.map +1 -1
  100. package/dist/federation/mount.d.ts +4 -0
  101. package/dist/federation/mount.d.ts.map +1 -1
  102. package/dist/federation/mount.js +44 -12
  103. package/dist/federation/mount.js.map +1 -1
  104. package/dist/index.d.ts +5 -5
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +21 -4
  107. package/dist/index.js.map +1 -1
  108. package/dist/web/index.d.ts +3 -3
  109. package/dist/web/index.d.ts.map +1 -1
  110. package/dist/web/index.js +1 -1
  111. package/dist/web/index.js.map +1 -1
  112. package/dist/web/navigation.d.ts +158 -36
  113. package/dist/web/navigation.d.ts.map +1 -1
  114. package/dist/web/navigation.js +250 -60
  115. package/dist/web/navigation.js.map +1 -1
  116. package/dist/web/negotiate.d.ts +22 -0
  117. package/dist/web/negotiate.d.ts.map +1 -1
  118. package/dist/web/negotiate.js +25 -10
  119. package/dist/web/negotiate.js.map +1 -1
  120. package/dist/web/render.d.ts +16 -10
  121. package/dist/web/render.d.ts.map +1 -1
  122. package/dist/web/render.js +21 -14
  123. package/dist/web/render.js.map +1 -1
  124. package/dist/web/routes.d.ts.map +1 -1
  125. package/dist/web/routes.js +11 -2
  126. package/dist/web/routes.js.map +1 -1
  127. package/dist/web/themes.d.ts +21 -0
  128. package/dist/web/themes.d.ts.map +1 -1
  129. package/dist/web/themes.js +37 -0
  130. package/dist/web/themes.js.map +1 -1
  131. package/package.json +1 -1
  132. package/templates/site/content/_data/site.json +7 -0
  133. package/templates/site/content/pages/about.md +7 -0
  134. package/themes/default/README.md +200 -80
  135. package/themes/default/layouts/author.njk +3 -4
  136. package/themes/default/layouts/base.njk +38 -49
  137. package/themes/default/layouts/front-page.njk +1 -2
  138. package/themes/default/partials/bio.njk +15 -21
  139. package/themes/default/partials/comment-form.njk +31 -3
  140. package/themes/default/partials/menu.njk +32 -0
  141. package/themes/default/partials/search-form.njk +5 -0
  142. package/themes/default/static/style.css +31 -8
  143. package/themes/default/theme.json +5 -1
package/README.md CHANGED
@@ -131,6 +131,16 @@ It refuses while the site is running, because deleting the file under a live
131
131
  server would leave it writing to a database nothing can find. Stop the site
132
132
  first. A file that will not parse is reported and exits `1`, as with `sync`.
133
133
 
134
+ That is why it is not the usual repair. **Tools > Content index** in the admin
135
+ empties the index and reads every file again in place, on the live connections:
136
+ no downtime, nobody signed out, and the delivery log, the relay handshakes and
137
+ the scheduler's watermark all kept. It is behind a confirm step, because the
138
+ site answers 404 for its documents between the emptying and the end of the
139
+ scan, and it federates nothing — a change a scan makes carries `origin: 'scan'`,
140
+ which delivery, the webmentions and the feed pings ignore. Use the command for
141
+ the one case the screen cannot be the door for: a database this version refuses
142
+ to open, where there is no site running to press a button in.
143
+
134
144
  ### Creating an admin from the command line
135
145
 
136
146
  `geekity user add <username>` writes a user straight into `data/users.json`,
@@ -248,29 +258,76 @@ export default defineConfig({
248
258
  Every field is optional. Relative directories resolve against the working
249
259
  directory; absolute ones are used as given.
250
260
 
251
- | Field | Default | Environment override | Meaning |
252
- | ------------------ | ------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
- | `port` | `3000` | `GEEKITY_PORT`, then `PORT` | Port the HTTP server listens on. `0` picks a free one. |
254
- | `contentDir` | `<cwd>/content` | `GEEKITY_CONTENT_DIR` | Markdown content. |
255
- | `dataDir` | `<cwd>/data` | `GEEKITY_DATA_DIR` | Derived state — the SQLite index, the image variants — and the two things in it that are not derived and must be backed up: `users.json` and, under `keys/`, each user's actor key pairs. |
256
- | `themesDir` | `<cwd>/themes` | `GEEKITY_THEMES_DIR` | The site's themes, one directory per theme, each with a `theme.json`. Which one is in use is the `theme` setting in `site.json`, not a path. Need not exist. |
257
- | `baseUrl` | `http://localhost:<port>` | `GEEKITY_BASE_URL` | Public origin for canonical URLs, feeds and ActivityPub ids. A trailing slash is stripped. |
258
- | `watch` | `true` | `GEEKITY_WATCH` | Watch `contentDir` while serving and keep the index in step. |
259
- | `sessionLifetime` | `1209600` (14 days) | `GEEKITY_SESSION_LIFETIME` | How long an admin login lasts, in seconds. |
260
- | `loginAttempts` | `5` | `GEEKITY_LOGIN_ATTEMPTS` | Failed sign-ins a username or an address may make before it is locked out. |
261
- | `loginLockout` | `900` (15 minutes) | `GEEKITY_LOGIN_LOCKOUT` | How long the first lockout lasts, in seconds. See [Login hardening](#login-hardening). |
262
- | `trustProxy` | `false` | `GEEKITY_TRUST_PROXY` | Believe `X-Forwarded-For` when deciding which address a sign-in came from. |
263
- | `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
264
- | `onPublish` | none | — | Hook run when a document becomes visible. See [Hooks](#hooks). |
265
- | `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
266
- | `commentChecker` | Akismet | — | A spam checker of the site's own, which wins over the key in `data/akismet.json`. See [Akismet](#akismet). |
267
- | `mail` | `{}` | — | Mail provider, retries, backoff and logger. See [Email](#email). |
261
+ | Field | Default | Environment override | Meaning |
262
+ | ------------------ | ------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
+ | `port` | `3000` | `GEEKITY_PORT`, then `PORT` | Port the HTTP server listens on. `0` picks a free one. |
264
+ | `contentDir` | `<cwd>/content` | `GEEKITY_CONTENT_DIR` | Markdown content. |
265
+ | `dataDir` | `<cwd>/data` | `GEEKITY_DATA_DIR` | Derived state — the SQLite index, the image variants — and the two things in it that are not derived and must be backed up: `users.json` and, under `keys/`, each user's actor key pairs. |
266
+ | `themesDir` | `<cwd>/themes` | `GEEKITY_THEMES_DIR` | The site's themes, one directory per theme, each with a `theme.json`. Which one is in use is the `theme` setting in `site.json`, not a path. Need not exist. |
267
+ | `baseUrl` | `http://localhost:<port>` | `GEEKITY_BASE_URL` | Public origin for canonical URLs, feeds and ActivityPub ids. A trailing slash is stripped. |
268
+ | `watch` | `true` | `GEEKITY_WATCH` | Watch `contentDir` while serving and keep the index in step. |
269
+ | `sessionLifetime` | `1209600` (14 days) | `GEEKITY_SESSION_LIFETIME` | How long an admin login lasts, in seconds. |
270
+ | `loginAttempts` | `5` | `GEEKITY_LOGIN_ATTEMPTS` | Failed sign-ins a username or an address may make before it is locked out. |
271
+ | `loginLockout` | `900` (15 minutes) | `GEEKITY_LOGIN_LOCKOUT` | How long the first lockout lasts, in seconds. See [Login hardening](#login-hardening). |
272
+ | `trustProxy` | `false` | `GEEKITY_TRUST_PROXY` | Believe `X-Forwarded-For` when deciding which address a sign-in came from. |
273
+ | `accessLog` | `false`; `true` under `geekity serve` | `GEEKITY_ACCESS_LOG` | One line per request on stdout: method, path with query, status, duration. See [The access log](#the-access-log). |
274
+ | `accessLogAddress` | `false` | `GEEKITY_ACCESS_LOG_ADDRESS` | Put the client address on the end of each access-log line. `trustProxy` decides which address that is. |
275
+ | `accessLogWriter` | stdout | — | Where the lines go instead. See [The access log](#the-access-log). |
276
+ | `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
277
+ | `onPublish` | none | — | Hook run when a document becomes visible. See [Hooks](#hooks). |
278
+ | `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
279
+ | `commentChecker` | Akismet | — | A spam checker of the site's own, which wins over the key in `data/akismet.json`. See [Akismet](#akismet). |
280
+ | `mail` | `{}` | — | Mail provider, retries, backoff and logger. See [Email](#email). |
268
281
 
269
282
  Precedence is environment variable, then config file, then default, so a host
270
283
  can override anything without editing the site. A boolean environment variable
271
284
  takes `true`, `1`, `yes` and `on`, or their opposites; anything else is an error
272
285
  rather than a silent `false`.
273
286
 
287
+ ## The access log
288
+
289
+ `geekity serve` writes one line per request to stdout. `createCms` does not:
290
+ a CMS mounted inside somebody else's server has no business writing to its
291
+ stdout uninvited, so an embedder asks for it with `accessLog: true` — in the
292
+ config, no environment variable needed:
293
+
294
+ ```ts
295
+ const cms = createCms({ accessLog: true });
296
+ ```
297
+
298
+ The line is the method, the path with its query string, the status and how
299
+ long the request took, in that order, separated by single spaces:
300
+
301
+ ```
302
+ GET / 200 4.2ms
303
+ GET /.well-known/webfinger?resource=acct:ada@blog.example 200 1.9ms
304
+ GET /nothing-here 404 2.1ms
305
+ POST /admin/login 303 41.3ms
306
+ ```
307
+
308
+ Every route is on it — `/healthz`, the federation endpoints, the admin and the
309
+ public site — and a request whose handler threw gets its `500` line. Nothing
310
+ but the request line is ever read: no body is touched, so a password posted to
311
+ the login form cannot reach the log, and no cookie or `Authorization` header
312
+ is looked at either.
313
+
314
+ With `accessLogAddress: true` the client address goes on the end, after the
315
+ duration. It is off by default because an address is personal data. Which
316
+ address is right is `trustProxy`'s answer — the leftmost `X-Forwarded-For`
317
+ entry behind a proxy that sets it, the socket's own address otherwise — the
318
+ same one the login throttle counts a failed sign-in against.
319
+
320
+ `accessLogWriter` takes the lines somewhere else, one call per line, without
321
+ the newline:
322
+
323
+ ```ts
324
+ const lines: string[] = [];
325
+ const cms = createCms({
326
+ accessLog: true,
327
+ accessLogWriter: (line) => lines.push(line),
328
+ });
329
+ ```
330
+
274
331
  ## Two directories: `content/` and `data/`
275
332
 
276
333
  Everything a site cannot afford to lose is a file, and every one of those files
@@ -508,7 +565,7 @@ rather than in the trash: there is no file left to build the `Tombstone` from.
508
565
  A [Mastodon-style relay][fepae0c] boosts every public activity it is sent on to
509
566
  the instances subscribed to it, which is how a site nobody follows yet reaches
510
567
  people. `relays` is the setting: one relay inbox per line on
511
- `/admin/settings/federation`, kept in `site.json` like every other setting, and
568
+ `/admin/federation/settings`, kept in `site.json` like every other setting, and
512
569
  `https://tags.pub/user/_____relay_____/inbox` is one worth knowing about — it
513
570
  boosts any public post carrying a hashtag it tracks, which every `Article` this
514
571
  CMS builds already carries one of per tag and per category.
@@ -616,7 +673,7 @@ next time it refetches the actor. So the CMS can carry them for a while, behind
616
673
  a switch, and is meant to stop.
617
674
 
618
675
  Turn **WordPress ActivityPub compatibility** on under
619
- `/admin/settings/federation`. It is off by default and `site.json` says nothing
676
+ `/admin/federation/settings`. It is off by default and `site.json` says nothing
620
677
  about it until it is on. It needs one thing on the user record in
621
678
  `data/users.json` besides the stored actor id above: the number WordPress gave
622
679
  that person, which is what its paths are built from.
@@ -719,11 +776,11 @@ followers.json` — which is worth doing if the old site is going away before
719
776
  you took one. Check the report: every follower should be added, and any that
720
777
  were skipped should be re-run once their servers answer.
721
778
  3. **Switch on.** Turn **WordPress ActivityPub compatibility** on under
722
- `/admin/settings/federation`, then move the DNS. Followers' servers go on
779
+ `/admin/federation/settings`, then move the DNS. Followers' servers go on
723
780
  delivering to the plugin's old inbox paths until they next refetch the
724
781
  actor, and the switch is what catches those deliveries.
725
782
  4. **Watch.** `/admin/federation` lists the users with their actor ids and
726
- followers; `/admin/settings/federation` lists each compatibility path with
783
+ followers; `/admin/federation/settings` lists each compatibility path with
727
784
  the instant it was last asked for. Deliveries should thin out as each
728
785
  follower's server refetches the actor and learns the new endpoints.
729
786
  5. **Switch off.** Once every path says _Never_ again for long enough — weeks
@@ -1406,47 +1463,52 @@ that ship inside the package, deliberately outside the theme search path: a
1406
1463
  site's theme may override any public template, and must not be able to
1407
1464
  shadow the login form.
1408
1465
 
1409
- | Route | What it does |
1410
- | ------------------------------------------------ | --------------------------------------------------------------------------------- |
1411
- | `/admin` | The dashboard: counts, the five most recent posts, the follower count. |
1412
- | `/admin/posts`, `/admin/pages` | The listings and the editors. |
1413
- | `/admin/tags`, `/admin/categories` | Every term in use, with rename, merge and delete. |
1414
- | `/admin/tags/rename`, `/admin/categories/rename` | `POST` only. Renames a term, or merges it into one that exists. |
1415
- | `/admin/tags/delete`, `/admin/categories/delete` | `POST` only. Takes a term out of every file. |
1416
- | `/admin/media` | Everything under `content/uploads`, with the URL, the Markdown and what uses it. |
1417
- | `/admin/media/upload` | `POST` only. Stores one file by the rules the editor's upload enforces. |
1418
- | `/admin/media/delete` | `POST` only. Deletes one upload, asking first when a document points at it. |
1419
- | `/admin/comments` | Pending, approved and spam, with approve, spam, delete and reply. |
1420
- | `/admin/comments/moderate` | `POST` only. Approves one comment, files it as spam, or deletes it. |
1421
- | `/admin/comments/reply` | `POST` only. Posts an approved reply under the comment it answers. |
1422
- | `/admin/messages` | The contact form's inbox, with a Spam list beside it. |
1423
- | `/admin/messages/read` | `POST` only. Marks one message read, or unread again. |
1424
- | `/admin/messages/delete` | `POST` only. Deletes one message, and its file with it. |
1425
- | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1426
- | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language. |
1427
- | `/admin/settings/reading` | Posts per page, the site menu, the notify server. |
1428
- | `/admin/settings/permalinks` | The tag and category bases, and the archive redirects already recorded. |
1429
- | `/admin/settings/discussion` | Comments and the closing window, webmentions, and the Akismet key. |
1430
- | `/admin/settings/akismet` | `POST` only. Saves the Akismet key, or forgets it. |
1431
- | `/admin/settings/email` | The mail provider, the From line, the reply-to and the contact address. |
1432
- | `/admin/settings/mail` | `POST` only. Saves a mail credential, or forgets every one of them. |
1433
- | `/admin/settings/mail/test` | `POST` only. Sends the theme's test message through the whole chain. |
1434
- | `/admin/settings/federation` | The relays the site subscribes to, and the WordPress compatibility switch. |
1435
- | `/admin/users` | Who may sign in: a row each, with Edit and Delete. Nothing on it edits anybody. |
1436
- | `/admin/users/new` | Users > Add new: the add form. `POST` adds one. |
1437
- | `/admin/users/<id>` | One user: the account, the profile, the notices, your password, and the delete. |
1438
- | `/admin/users/password` | `POST` only. Changes the signed-in admin's own password. |
1439
- | `/admin/users/email` | `POST` only. Sets or clears the email address on the user the form names. |
1440
- | `/admin/users/profile` | `POST` only. Saves the whole public profile of the user the form names. |
1441
- | `/admin/users/notifications` | `POST` only. Turns one notice on or off for the user the form names. |
1442
- | `/admin/users/notifications/mode` | `POST` only. Sets how often that notice arrives: as they arrive, hourly or daily. |
1443
- | `/admin/users/delete` | `POST` only. Deletes the user the form names. |
1444
- | `/admin/federation` | The actors, their followers, the inbox log, and per-post delivery. |
1445
- | `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
1446
- | `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
1447
- | `/admin/login` | Username and password. |
1448
- | `/admin/logout` | `POST` only. Deletes the session row. |
1449
- | `/admin/_static/*` | The admin's own stylesheet and scripts, cached for an hour. |
1466
+ | Route | What it does |
1467
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
1468
+ | `/admin` | The dashboard: counts, the five most recent posts, the follower count. |
1469
+ | `/admin/posts`, `/admin/pages` | The listings and the editors. |
1470
+ | `/admin/tags`, `/admin/categories` | Every term in use, with rename, merge and delete. |
1471
+ | `/admin/tags/rename`, `/admin/categories/rename` | `POST` only. Renames a term, or merges it into one that exists. |
1472
+ | `/admin/tags/delete`, `/admin/categories/delete` | `POST` only. Takes a term out of every file. |
1473
+ | `/admin/navigation` | Navigation > Menus: every menu the site holds. `POST` saves one menu's items. |
1474
+ | `/admin/navigation/add` | `POST` only. Adds an empty menu under the name the form gives. |
1475
+ | `/admin/navigation/delete` | `POST` only. Deletes one menu the active theme renders nowhere. |
1476
+ | `/admin/media` | Everything under `content/uploads`, with the URL, the Markdown and what uses it. |
1477
+ | `/admin/media/upload` | `POST` only. Stores one file by the rules the editor's upload enforces. |
1478
+ | `/admin/media/delete` | `POST` only. Deletes one upload, asking first when a document points at it. |
1479
+ | `/admin/comments` | Pending, approved and spam, with approve, spam, delete and reply. |
1480
+ | `/admin/comments/moderate` | `POST` only. Approves one comment, files it as spam, or deletes it. |
1481
+ | `/admin/comments/reply` | `POST` only. Posts an approved reply under the comment it answers. |
1482
+ | `/admin/messages` | The contact form's inbox, with a Spam list beside it. |
1483
+ | `/admin/messages/read` | `POST` only. Marks one message read, or unread again. |
1484
+ | `/admin/messages/delete` | `POST` only. Deletes one message, and its file with it. |
1485
+ | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1486
+ | `/admin/tools` | Tools > Content index: what the index holds, and the button that rebuilds it. |
1487
+ | `/admin/tools/rebuild-index` | `POST` only. Offers the rebuild, then reads every file again on the live site. |
1488
+ | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language. |
1489
+ | `/admin/settings/reading` | What the homepage displays, posts per page, the notify server. |
1490
+ | `/admin/settings/permalinks` | The tag and category bases, and the archive redirects already recorded. |
1491
+ | `/admin/settings/discussion` | Comments and the closing window, webmentions, and the Akismet key. |
1492
+ | `/admin/settings/akismet` | `POST` only. Saves the Akismet key, or forgets it. |
1493
+ | `/admin/settings/email` | The mail provider, the From line, the reply-to and the contact address. |
1494
+ | `/admin/settings/mail` | `POST` only. Saves a mail credential, or forgets every one of them. |
1495
+ | `/admin/settings/mail/test` | `POST` only. Sends the theme's test message through the whole chain. |
1496
+ | `/admin/users` | Who may sign in: a row each, with Edit and Delete. Nothing on it edits anybody. |
1497
+ | `/admin/users/new` | Users > Add new: the add form. `POST` adds one. |
1498
+ | `/admin/users/<id>` | One user: the account, the profile, the notices, your password, and the delete. |
1499
+ | `/admin/users/password` | `POST` only. Changes the signed-in admin's own password. |
1500
+ | `/admin/users/email` | `POST` only. Sets or clears the email address on the user the form names. |
1501
+ | `/admin/users/profile` | `POST` only. Saves the whole public profile of the user the form names. |
1502
+ | `/admin/users/notifications` | `POST` only. Turns one notice on or off for the user the form names. |
1503
+ | `/admin/users/notifications/mode` | `POST` only. Sets how often that notice arrives: as they arrive, hourly or daily. |
1504
+ | `/admin/users/delete` | `POST` only. Deletes the user the form names. |
1505
+ | `/admin/federation` | The actors, their followers, the inbox log, and per-post delivery. |
1506
+ | `/admin/federation/settings` | Federation > Settings: the relays the site subscribes to, and the WordPress switch. |
1507
+ | `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
1508
+ | `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
1509
+ | `/admin/login` | Username and password. |
1510
+ | `/admin/logout` | `POST` only. Deletes the session row. |
1511
+ | `/admin/_static/*` | The admin's own stylesheet and scripts, cached for an hour. |
1450
1512
 
1451
1513
  The screens behind the login share one layout: a bar across the top with the
1452
1514
  site name and a link to the public site, the menu down the left, and a place for
@@ -1456,9 +1518,9 @@ read, so it survives exactly one redirect.
1456
1518
 
1457
1519
  ### The menu
1458
1520
 
1459
- The menu is WordPress classic: ten sections — Dashboard, Posts, Pages, Media,
1460
- Comments, Messages, Appearance, Users, Settings, Federation — each a heading
1461
- over one or more children. Clicking a heading opens the section and lands on its first
1521
+ The menu is WordPress classic: twelve sections — Dashboard, Posts, Pages,
1522
+ Navigation, Media, Comments, Messages, Appearance, Users, Tools, Settings,
1523
+ Federation — each a heading over one or more children. Clicking a heading opens the section and lands on its first
1462
1524
  child; the open section shows its children and the one you are on carries
1463
1525
  `aria-current="page"`, so Users tells you that you are on Users > All users.
1464
1526
  Tags and categories are children of Posts, because a tag with no post on it is
@@ -1552,10 +1614,12 @@ await createUser({
1552
1614
  ### Settings
1553
1615
 
1554
1616
  `content/_data/site.json` is the source of truth for a site's settings. The
1555
- pages under `/admin/settings` — General, Reading, Permalinks, Discussion, Email
1556
- and Federation — each read that file, validate what was typed and write it
1557
- back; nothing else remembers a setting, and `data/geekity.db` holds none of
1558
- them.
1617
+ pages under `/admin/settings` — General, Reading, Permalinks, Discussion and
1618
+ Email — each read that file, validate what was typed and write it back; nothing
1619
+ else remembers a setting, and `data/geekity.db` holds none of them. Federation's
1620
+ page is a sixth one of exactly the same kind, filed under its own section at
1621
+ `/admin/federation/settings` rather than under Settings, because the relays and
1622
+ the compatibility switch are about the section that holds the followers.
1559
1623
 
1560
1624
  Each page saves its own fields and no others, onto the file as re-read inside
1561
1625
  the write, so two people saving two different pages at the same moment both
@@ -1574,7 +1638,7 @@ The file carries `title`, `tagline`, `url`, `author`, `postsPerPage`,
1574
1638
  `timezone`, `language`, `tagBase`,
1575
1639
  `categoryBase`, `notifyServer`, `webmentionsSend`, `webmentionsReceive`,
1576
1640
  `mailProvider`, `mailFromName`, `mailFromAddress`, `mailReplyTo`,
1577
- `contactEmail`, `relays`, `navigation` and `taxonomyRedirects`,
1641
+ `contactEmail`, `relays`, `menus` and `taxonomyRedirects`,
1578
1642
  and every other key it already had is kept, `feedSize` and anything a site put
1579
1643
  there included. A key it does not carry is the default, and a key of the wrong
1580
1644
  type is the default too: a hand-edited `site.json` cannot take the site down.
@@ -1961,37 +2025,96 @@ can put its archives at the same URLs.
1961
2025
 
1962
2026
  ### Navigation
1963
2027
 
1964
- Every page carries the site menu, which the theme renders in the header. It is
1965
- two things joined:
1966
-
1967
- 1. The `navigation` setting, edited on `/admin/settings/reading` as one
1968
- `Label | URL`
1969
- per line — `About | /about/`, `Mastodon | https://example.social/@me` — in
1970
- the order it is typed. The URL is a site-root path or an absolute
1971
- `http(s)` URL; anything else is refused with the offending line quoted.
1972
- 2. Every published page whose front matter says `navigation: true`, ordered by
1973
- `navigationOrder` and then by title. A page that names no order sorts after
1974
- every page that does, and the flagged pages always come after the items the
1975
- setting names.
1976
-
1977
- The editor writes both keys: **Show in navigation** on a page's editor writes
1978
- `navigation: true`, **Menu order** writes `navigationOrder`, and clearing the
1979
- box takes both back out of the file. Posts have neither field — a post is in
1980
- the archive and in the feeds, which is where a post belongs.
1981
-
1982
- Templates read it as `menu`, a list of `{ label, url, current }`, with
1983
- `current` true for the item whose path is the one being rendered. It is `menu`
1984
- rather than `navigation` because `navigation` is the front-matter key a page
1985
- opts in with, and a document's own front matter goes on top of the globals as
1986
- Eleventy's data cascade does. The scheduled, drafted and trashed pages are not
1987
- in it, for the same reason they are not on the site.
1988
-
1989
- The setting is `navigation` in `content/_data/site.json`, a list of
1990
- `{ label, url }`, so an Eleventy build renders the same menu; the example
1991
- config assembles it as `collections.menu`. A `navigation` in a hand-edited
1992
- `site.json` that is not a list of items yields an empty menu rather than an
1993
- error, exactly as a bad archive base falls back rather than taking the site
1994
- down.
2028
+ A menu is a named thing the site stores and the theme asks for.
2029
+
2030
+ **The site stores menus by name.** `content/_data/site.json` holds a `menus`
2031
+ object keyed by the name of each menu, each an ordered list of items:
2032
+
2033
+ ```json
2034
+ {
2035
+ "menus": {
2036
+ "primary": [{ "label": "About", "url": "/about/" }],
2037
+ "footer": [
2038
+ { "label": "Colophon", "url": "/colophon/" },
2039
+ { "label": "Mastodon", "url": "https://example.social/@me", "me": true }
2040
+ ]
2041
+ }
2042
+ }
2043
+ ```
2044
+
2045
+ An item is a label, a URL and the flags it carries. The URL is a site-root path
2046
+ or an absolute `http(s)` URL; anything else is refused. There is one flag,
2047
+ `me`, present only when it is set and read only when it is spelled exactly
2048
+ `true`: it gives the link `rel="me"`, which is how Mastodon and the rest of the
2049
+ IndieWeb verify that the site and the profile it links are the same person.
2050
+
2051
+ **The theme declares the areas it renders.** `theme.json` carries an `areas`
2052
+ list, each entry a `name` — the key under `menus` — and a `label` a person
2053
+ reads:
2054
+
2055
+ ```json
2056
+ { "areas": [{ "name": "primary", "label": "Site menu" }] }
2057
+ ```
2058
+
2059
+ The packaged theme declares `primary` ("Site menu") and `footer` ("Footer
2060
+ links"). A theme that wants a third declares it; an `areas` that is missing or
2061
+ unreadable leaves that theme declaring none rather than failing to load it, and
2062
+ the Navigation screen then offers the packaged theme's — a site theme is laid
2063
+ over the packaged one a file at a time, so a theme that has declared nothing has
2064
+ not overridden the declaration, and its inherited `layouts/base.njk` really is
2065
+ still rendering `menus.primary` and `menus.footer`.
2066
+
2067
+ **A theme renders a menu by name.** Every template gets `menus`, keyed by the
2068
+ same names, each item already marked `current` for the path being rendered.
2069
+ `{% for item in menus.footer %}` is the whole interface, so a site theme can
2070
+ render a menu the packaged theme has never heard of. Every stored menu is on
2071
+ the context, not only the declared ones: the declaration is what the screen
2072
+ that edits menus reads, and a menu nothing loops over is rendered nowhere and
2073
+ kept — which is how somebody writes the menu a theme will use before switching
2074
+ to that theme.
2075
+
2076
+ **The menus are managed on `/admin/navigation`.** It is a top-level section
2077
+ after Pages rather than a settings page: a menu is content a site arranges, the
2078
+ way its pages are, and the shape of the screen comes from the active theme.
2079
+ The screen is two lists. First the areas that theme declares, in the theme's
2080
+ order and under its labels — an area the site has never filled in is an empty
2081
+ box, not a missing one. Then every other stored menu, under a heading saying
2082
+ this theme renders them nowhere; those are still editable, which is how the
2083
+ menu a theme will use is written before the site switches to it, and each has
2084
+ the Delete that is the only way a menu is removed. An area the theme declares
2085
+ is emptied rather than deleted: the theme goes on asking for the name, and the
2086
+ box has to stay for the next thing typed into it.
2087
+
2088
+ Each menu is one box of `Label | URL` lines, in the order they are typed, with
2089
+ a trailing `| me` marking the flag. Only a flag this CMS has is taken off the
2090
+ end of a line, so a URL holding a bar survives and a trailing word that is not
2091
+ a flag stays part of the URL. A line that is not an item is refused by name,
2092
+ with the box still holding every line of what was typed, and nothing written.
2093
+
2094
+ **A menu name** is lower-case ASCII letters, digits and underscores, starting
2095
+ with a letter, at most 32 characters. It is the word a theme writes after the
2096
+ dot — `{% for item in menus.footer %}` — so a dash is refused: after a dot it
2097
+ is a minus sign, and `menus.top-bar` renders nothing and raises nothing. Lower
2098
+ case is the rest of the rule and it is what refuses the confusable spellings:
2099
+ `Footer` is refused rather than quietly lowered, because `footer` and `Footer`
2100
+ would be two menus on one screen that nothing could tell apart. A name the site
2101
+ already holds is refused too, and the message says its box is already on the
2102
+ page. The rule governs a name somebody types; a name a `theme.json` declares is
2103
+ a menu name by declaration and is shown as the theme spells it, because a
2104
+ screen that hid a declared area over a spelling would leave somebody unable to
2105
+ fill in a menu their site renders.
2106
+
2107
+ There is no second source. A page cannot put itself in a menu, so there is one
2108
+ screen to edit it on, one order, and no way for the same link to appear twice.
2109
+ A page that should be linked is linked by typing a line for it — including the
2110
+ page a site serves as its front page, which is typed `Home | /`, the URL a
2111
+ reader lands on, rather than at the permalink that redirects there.
2112
+
2113
+ Because the menus live in `site.json`, an Eleventy build renders the same ones;
2114
+ the example config assembles them as `collections.menus`. A `menus` in a
2115
+ hand-edited `site.json` that is not an object of lists of items yields no menu
2116
+ rather than an error, exactly as a bad archive base falls back rather than
2117
+ taking the site down.
1995
2118
 
1996
2119
  ## Content negotiation
1997
2120
 
@@ -2572,10 +2695,13 @@ theme can shadow the login form or the CSRF field inside it.
2572
2695
  The packaged theme is the andrewshell.org design (decision-16). `base.njk` is
2573
2696
  its shell: a skip link, a `.global-wrapper` carrying `data-is-root-path="true"`
2574
2697
  at `/` only, a header that is the site title and tagline on the front page and a
2575
- small link home on every other, `<main id="main">`, and a footer with the
2576
- copyright year, the site author, the colophon, an RSS link and one `rel="me"`
2577
- link per link on the site author's profile. Nothing particular to one site is in
2578
- it — webrings and badges belong in a site theme's `footer` block. Its head
2698
+ small link home on every other — with `menus.primary` inside it on every page
2699
+ — `<main id="main">`, and a footer with the copyright year, the site author,
2700
+ the colophon and `menus.footer`. The footer reads nothing off an account: it
2701
+ used to print an RSS link and one `rel="me"` link per link on the site author's
2702
+ profile, and a site that wants either types it into its footer menu. Nothing
2703
+ particular to one site is in it — webrings and badges belong in a site theme's
2704
+ `footer` block. Its head
2579
2705
  carries a description, Open Graph and Twitter card tags, icons derived from the
2580
2706
  site's avatar, and one JSON-LD `@graph` from `partials/jsonld.njk`: that partial
2581
2707
  is all the structured data the theme emits, there is no Microdata anywhere, and
@@ -2668,9 +2794,9 @@ the rules the CMS follows that Eleventy does not know about on its own:
2668
2794
  It also builds two collections Eleventy has no notion of. `collections.categories`
2669
2795
  is the second taxonomy, one entry of `{ name, posts }` per category in use, for
2670
2796
  paginating into archives at `/{{ site.categoryBase }}/{name}/`.
2671
- `collections.menu` is the site menu — the `navigation` array of `site.json`
2672
- followed by the pages whose front matter says `navigation: true` — as
2673
- `{ label, url }` entries in the order the header should render them; a layout
2797
+ `collections.menus` is the site's named menus — the `menus` object of
2798
+ `site.json`, and nothing else — each an ordered list of `{ label, url }`
2799
+ entries, plus `me: true` on a link that should carry `rel="me"`; a layout
2674
2800
  marks the current one itself by comparing `item.url` with `page.url`, because a
2675
2801
  collection is built once for the whole site.
2676
2802
 
@@ -133,19 +133,6 @@
133
133
  <p class="admin-hint">Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.</p>
134
134
  {% endif %}
135
135
 
136
- {% if kind.navigable %}
137
- {# The page puts itself in the site menu, after the items the settings
138
- screen names. #}
139
- <p class="admin-check">
140
- <input id="editor-navigation" name="navigation" type="checkbox" value="1"{% if form.navigation %} checked{% endif %} />
141
- <label for="editor-navigation">Show in navigation</label>
142
- </p>
143
-
144
- <label for="editor-navigation-order">Menu order</label>
145
- <input id="editor-navigation-order" name="navigation_order" type="text" inputmode="numeric" value="{{ form.navigationOrder }}" />
146
- <p class="admin-hint">Puts this {{ kind.singular }} in the site header, after the items on the settings screen. A lower order comes first; leave it empty and it sorts by title after the ordered ones.</p>
147
- {% endif %}
148
-
149
136
  {% if kind.contactable %}
150
137
  {# The page offers a contact form under its content (TASK-56). Where a
151
138
  message goes is the contact address on the Settings screen; it is
@@ -1,7 +1,9 @@
1
- {# /admin/settings/federation: which relays boost the site, and whether it is
2
- still answering the paths a WordPress site arrived with. Who the site's
3
- people are on the fediverse is a user's profile now (decision-14); the
4
- Federation section in the menu is where the actors and their followers are. #}
1
+ {# /admin/federation/settings: which relays boost the site, and whether it is
2
+ still answering the paths a WordPress site arrived with. It is Federation >
3
+ Settings rather than Settings > Federation (TASK-109): the section holds
4
+ everything about federation, and Followers beside it is where the actors and
5
+ the people following them are. Who the site's people are on the fediverse is
6
+ a user's profile now (decision-14). #}
5
7
  {% extends "layouts/settings-page.njk" %}
6
8
  {% import "components/fields.njk" as field %}
7
9
 
@@ -11,7 +13,7 @@
11
13
  <code>https://tags.pub/user/_____relay_____/inbox</code>. Adding one sends
12
14
  it a follow; every public post then goes to it as well as to the
13
15
  followers, and it boosts them on to instances nobody here follows. The
14
- Federation screen says where each subscription stands.
16
+ Followers screen says where each subscription stands.
15
17
  {% endset %}
16
18
  {{ field.textarea('settings-relays', fields.relays, 'Relays', form.relays,
17
19
  error=problems.relays, hint=relaysHint, rows='3', spellcheck='false', autofocus=true) }}