@geekity/cms 0.27.0 → 0.28.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 (187) hide show
  1. package/README.md +168 -33
  2. package/admin/layouts/shell.njk +6 -0
  3. package/admin/pages/documents/editor.njk +4 -0
  4. package/admin/pages/plugins/installed.njk +1 -1
  5. package/admin/pages/tools/dev-mode.njk +47 -0
  6. package/admin/static/admin.css +8 -0
  7. package/admin/static/editor-actions.js +13 -9
  8. package/admin/static/editor.js +31 -17
  9. package/dist/admin/dev-mode.d.ts +9 -0
  10. package/dist/admin/dev-mode.d.ts.map +1 -0
  11. package/dist/admin/dev-mode.js +42 -0
  12. package/dist/admin/dev-mode.js.map +1 -0
  13. package/dist/admin/editor-actions.js +3 -1
  14. package/dist/admin/editor-actions.js.map +1 -1
  15. package/dist/admin/federation.d.ts.map +1 -1
  16. package/dist/admin/federation.js +2 -0
  17. package/dist/admin/federation.js.map +1 -1
  18. package/dist/admin/menu.d.ts.map +1 -1
  19. package/dist/admin/menu.js +2 -0
  20. package/dist/admin/menu.js.map +1 -1
  21. package/dist/admin/own-paragraph.d.ts +7 -0
  22. package/dist/admin/own-paragraph.d.ts.map +1 -0
  23. package/dist/admin/own-paragraph.js +9 -0
  24. package/dist/admin/own-paragraph.js.map +1 -0
  25. package/dist/admin/plugins.d.ts.map +1 -1
  26. package/dist/admin/plugins.js +19 -8
  27. package/dist/admin/plugins.js.map +1 -1
  28. package/dist/admin/preview.d.ts.map +1 -1
  29. package/dist/admin/preview.js +4 -1
  30. package/dist/admin/preview.js.map +1 -1
  31. package/dist/admin/routes.d.ts.map +1 -1
  32. package/dist/admin/routes.js +4 -0
  33. package/dist/admin/routes.js.map +1 -1
  34. package/dist/admin/settings-email.d.ts.map +1 -1
  35. package/dist/admin/settings-email.js +4 -0
  36. package/dist/admin/settings-email.js.map +1 -1
  37. package/dist/admin/store.d.ts +4 -2
  38. package/dist/admin/store.d.ts.map +1 -1
  39. package/dist/admin/store.js +1 -1
  40. package/dist/admin/store.js.map +1 -1
  41. package/dist/admin/templates.d.ts +1 -0
  42. package/dist/admin/templates.d.ts.map +1 -1
  43. package/dist/admin/templates.js +1 -0
  44. package/dist/admin/templates.js.map +1 -1
  45. package/dist/admin/uploads.d.ts +7 -1
  46. package/dist/admin/uploads.d.ts.map +1 -1
  47. package/dist/admin/uploads.js +33 -30
  48. package/dist/admin/uploads.js.map +1 -1
  49. package/dist/cli.d.ts +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +76 -10
  52. package/dist/cli.js.map +1 -1
  53. package/dist/comments/index.d.ts +1 -1
  54. package/dist/comments/index.d.ts.map +1 -1
  55. package/dist/comments/index.js +1 -1
  56. package/dist/comments/index.js.map +1 -1
  57. package/dist/comments/records.d.ts +1 -0
  58. package/dist/comments/records.d.ts.map +1 -1
  59. package/dist/comments/records.js +39 -10
  60. package/dist/comments/records.js.map +1 -1
  61. package/dist/config.d.ts +21 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +2 -0
  64. package/dist/config.js.map +1 -1
  65. package/dist/content/markdown.d.ts.map +1 -1
  66. package/dist/content/markdown.js +33 -1
  67. package/dist/content/markdown.js.map +1 -1
  68. package/dist/content/migrated.d.ts +6 -0
  69. package/dist/content/migrated.d.ts.map +1 -0
  70. package/dist/content/migrated.js +12 -0
  71. package/dist/content/migrated.js.map +1 -0
  72. package/dist/content/original.d.ts +3 -0
  73. package/dist/content/original.d.ts.map +1 -0
  74. package/dist/content/original.js +12 -0
  75. package/dist/content/original.js.map +1 -0
  76. package/dist/content/parser.d.ts +5 -0
  77. package/dist/content/parser.d.ts.map +1 -1
  78. package/dist/content/parser.js +1 -1
  79. package/dist/content/parser.js.map +1 -1
  80. package/dist/content/schedule.d.ts +7 -0
  81. package/dist/content/schedule.d.ts.map +1 -1
  82. package/dist/content/schedule.js +11 -2
  83. package/dist/content/schedule.js.map +1 -1
  84. package/dist/content/sync.d.ts.map +1 -1
  85. package/dist/content/sync.js +27 -5
  86. package/dist/content/sync.js.map +1 -1
  87. package/dist/content/video.d.ts +12 -0
  88. package/dist/content/video.d.ts.map +1 -0
  89. package/dist/content/video.js +91 -0
  90. package/dist/content/video.js.map +1 -0
  91. package/dist/dev-mode.d.ts +28 -0
  92. package/dist/dev-mode.d.ts.map +1 -0
  93. package/dist/dev-mode.js +64 -0
  94. package/dist/dev-mode.js.map +1 -0
  95. package/dist/drain.d.ts +9 -3
  96. package/dist/drain.d.ts.map +1 -1
  97. package/dist/drain.js +24 -3
  98. package/dist/drain.js.map +1 -1
  99. package/dist/federation/delivery.d.ts +2 -0
  100. package/dist/federation/delivery.d.ts.map +1 -1
  101. package/dist/federation/delivery.js +46 -16
  102. package/dist/federation/delivery.js.map +1 -1
  103. package/dist/federation/inbox.d.ts.map +1 -1
  104. package/dist/federation/inbox.js +13 -3
  105. package/dist/federation/inbox.js.map +1 -1
  106. package/dist/federation/mount.d.ts.map +1 -1
  107. package/dist/federation/mount.js +8 -4
  108. package/dist/federation/mount.js.map +1 -1
  109. package/dist/federation/relays.d.ts.map +1 -1
  110. package/dist/federation/relays.js +16 -1
  111. package/dist/federation/relays.js.map +1 -1
  112. package/dist/index.d.ts +2 -0
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +16 -4
  115. package/dist/index.js.map +1 -1
  116. package/dist/indexnow.d.ts +1 -1
  117. package/dist/indexnow.d.ts.map +1 -1
  118. package/dist/indexnow.js +7 -0
  119. package/dist/indexnow.js.map +1 -1
  120. package/dist/mail/service.d.ts +3 -1
  121. package/dist/mail/service.d.ts.map +1 -1
  122. package/dist/mail/service.js +17 -0
  123. package/dist/mail/service.js.map +1 -1
  124. package/dist/notify.d.ts +1 -1
  125. package/dist/notify.d.ts.map +1 -1
  126. package/dist/notify.js +7 -0
  127. package/dist/notify.js.map +1 -1
  128. package/dist/plugin.d.ts +67 -4
  129. package/dist/plugin.d.ts.map +1 -1
  130. package/dist/plugin.js.map +1 -1
  131. package/dist/plugins/folder.d.ts +2 -0
  132. package/dist/plugins/folder.d.ts.map +1 -1
  133. package/dist/plugins/folder.js +26 -5
  134. package/dist/plugins/folder.js.map +1 -1
  135. package/dist/plugins/manage.d.ts +8 -6
  136. package/dist/plugins/manage.d.ts.map +1 -1
  137. package/dist/plugins/manage.js +1 -4
  138. package/dist/plugins/manage.js.map +1 -1
  139. package/dist/plugins/site.d.ts.map +1 -1
  140. package/dist/plugins/site.js +56 -0
  141. package/dist/plugins/site.js.map +1 -1
  142. package/dist/supervisor/control.d.ts +15 -0
  143. package/dist/supervisor/control.d.ts.map +1 -0
  144. package/dist/supervisor/control.js +89 -0
  145. package/dist/supervisor/control.js.map +1 -0
  146. package/dist/supervisor/primary.d.ts +18 -4
  147. package/dist/supervisor/primary.d.ts.map +1 -1
  148. package/dist/supervisor/primary.js +128 -16
  149. package/dist/supervisor/primary.js.map +1 -1
  150. package/dist/supervisor/protocol.d.ts +12 -0
  151. package/dist/supervisor/protocol.d.ts.map +1 -1
  152. package/dist/supervisor/protocol.js +21 -2
  153. package/dist/supervisor/protocol.js.map +1 -1
  154. package/dist/supervisor/supervision.d.ts +1 -0
  155. package/dist/supervisor/supervision.d.ts.map +1 -1
  156. package/dist/supervisor/supervision.js +14 -1
  157. package/dist/supervisor/supervision.js.map +1 -1
  158. package/dist/supervisor/worker.d.ts.map +1 -1
  159. package/dist/supervisor/worker.js +31 -6
  160. package/dist/supervisor/worker.js.map +1 -1
  161. package/dist/web/context.d.ts +7 -0
  162. package/dist/web/context.d.ts.map +1 -1
  163. package/dist/web/context.js +7 -4
  164. package/dist/web/context.js.map +1 -1
  165. package/dist/web/feed-item.d.ts +7 -6
  166. package/dist/web/feed-item.d.ts.map +1 -1
  167. package/dist/web/feed-item.js +5 -4
  168. package/dist/web/feed-item.js.map +1 -1
  169. package/dist/web/feed-rss.d.ts +2 -2
  170. package/dist/web/feed-rss.js +2 -2
  171. package/dist/web/redirects.d.ts +17 -10
  172. package/dist/web/redirects.d.ts.map +1 -1
  173. package/dist/web/redirects.js +83 -49
  174. package/dist/web/redirects.js.map +1 -1
  175. package/dist/webmention/service.d.ts +1 -1
  176. package/dist/webmention/service.d.ts.map +1 -1
  177. package/dist/webmention/service.js +12 -2
  178. package/dist/webmention/service.js.map +1 -1
  179. package/package.json +1 -1
  180. package/themes/default/README.md +152 -1
  181. package/themes/default/layouts/base.njk +11 -2
  182. package/themes/default/layouts/post.njk +8 -2
  183. package/themes/default/partials/body-end.njk +1 -0
  184. package/themes/default/partials/conversation.njk +1 -1
  185. package/themes/default/partials/head-end.njk +1 -0
  186. package/themes/default/src/style.css +4 -0
  187. package/themes/default/static/style.css +8 -0
package/README.md CHANGED
@@ -86,26 +86,27 @@ message on a site whose `site.json` has not set a period.
86
86
  site (or `npx geekity`, or a `package.json` script, which is how the generated
87
87
  `sync` script calls it).
88
88
 
89
- | Command | What it does |
90
- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `geekity serve` | Boot from the config file and listen, in a worker the command supervises: Reload on the Plugins screen replaces it, and a crash respawns it. The default when no command is given. |
92
- | `geekity init <dir>` | Create a new site in `<dir>`. Refuses a directory that is not empty. |
93
- | `geekity sync` | Rebuild the content index once and exit. Exits non-zero if any file could not be parsed. |
94
- | `geekity rebuild` | Delete `data/geekity.db` and build it again from the files. |
95
- | `geekity resend --all`, `<slug>...` | Send announced posts to every follower and relay again, as they now read. See [Quote posts](#quote-posts). |
96
- | `geekity maintenance on`, `off`, `status` | Take the public site down on purpose with a 503 and `Retry-After`, or bring it back, without a restart. `on --until <time>` names when it should be back. |
97
- | `geekity strip-metadata` | Remove location and camera metadata from files already in `content/uploads`. See [The media library](#the-media-library). |
98
- | `geekity user add <name>` | Create an admin account, so a site can get its first login without the setup screen. |
99
- | `geekity plugin add <package>`, `remove` | Install a plugin package's bundle from the npm registry into the plugins folder (`GEEKITY_PLUGINS_DIR`), or delete a disabled one. Reload on the Plugins screen loads the change. |
100
- | `geekity --help`, `-h` | The same table, on the terminal. |
101
- | `geekity --version` | The installed version. |
89
+ | Command | What it does |
90
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `geekity serve` | Boot from the config file and listen, in a worker the command supervises: a change to the plugins folder replaces it, and a crash respawns it. The default when no command is given. |
92
+ | `geekity init <dir>` | Create a new site in `<dir>`. Refuses a directory that is not empty. |
93
+ | `geekity sync` | Rebuild the content index once and exit. Exits non-zero if any file could not be parsed. |
94
+ | `geekity rebuild` | Delete `data/geekity.db` and build it again from the files. |
95
+ | `geekity resend --all`, `<slug>...` | Send announced posts to every follower and relay again, as they now read. See [Quote posts](#quote-posts). |
96
+ | `geekity maintenance on`, `off`, `status` | Take the public site down on purpose with a 503 and `Retry-After`, or bring it back, without a restart. `on --until <time>` names when it should be back. |
97
+ | `geekity dev-mode on`, `off`, `status` | Hold everything the site would send to the outside world while it is built or migrated, or take it live. See [Dev mode](#dev-mode). |
98
+ | `geekity strip-metadata` | Remove location and camera metadata from files already in `content/uploads`. See [The media library](#the-media-library). |
99
+ | `geekity user add <name>` | Create an admin account, so a site can get its first login without the setup screen. |
100
+ | `geekity plugin add <package>`, `remove` | Install a plugin package's bundle from the npm registry into the plugins folder (`GEEKITY_PLUGINS_DIR`), or delete a disabled one. A running `geekity serve` reloads with the change. |
101
+ | `geekity --help`, `-h` | The same table, on the terminal. |
102
+ | `geekity --version` | The installed version. |
102
103
 
103
104
  An installed plugin may add commands of its own, which `geekity --help` lists
104
105
  under _Plugin commands_ and which run whether or not the plugin is enabled.
105
106
  That includes a plugin in the plugins folder. The repository README's
106
107
  _Plugins_ section describes `geekity plugin add` and writing a plugin.
107
108
 
108
- `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `strip-metadata`, `user add`, `plugin` and every plugin command take
109
+ `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `dev-mode`, `strip-metadata`, `user add`, `plugin` and every plugin command take
109
110
  `--config <file>`; without it they look for `geekity.config.ts`, then
110
111
  `geekity.config.js`, then `geekity.config.mjs` in the working directory, and run
111
112
  on defaults if there is none.
@@ -289,6 +290,7 @@ directory; absolute ones are used as given.
289
290
  | `accessLogAddress` | `false` | `GEEKITY_ACCESS_LOG_ADDRESS` | Put the client address on the end of each access-log line. `trustProxy` decides which address that is. |
290
291
  | `accessLogWriter` | stdout | — | Where the lines go instead. See [The access log](#the-access-log). |
291
292
  | `maintenance` | `false` | `GEEKITY_MAINTENANCE` | Keep the site in maintenance mode, answering 503, until a restart without it. |
293
+ | `devMode` | `false` | `GEEKITY_DEV_MODE` | Start the site in dev mode, which holds every outbound side effect until `geekity dev-mode off`. See [Dev mode](#dev-mode). |
292
294
  | `compression` | `true` | `GEEKITY_COMPRESSION` | Compress text responses with brotli or gzip. Turn it off behind a proxy that compresses. See [Compression](#compression). |
293
295
  | `securityHeaders` | [see below](#security-headers) | — | Headers every response carries. A string replaces a default or adds a header, `false` removes one. |
294
296
  | `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
@@ -426,7 +428,7 @@ something a site is told it may lose:
426
428
  | `ap_deliveries` | Nothing. The federation screen shows its posts with "Nothing recorded." |
427
429
  | `ap_relays` | The relay list in `site.json`: boot sends each of them a fresh `Follow`. |
428
430
  | `password_resets`, `spent_tokens` | Nothing. Every reset link and every one-click moderation link stops working. |
429
- | `cms_state` | Nothing. One key, the scheduler's watermark. |
431
+ | `cms_state` | Nothing. The scheduler's watermark and its first run. |
430
432
  | `migrations`, `admin_migrations` | The package. They record which schema versions have run. |
431
433
 
432
434
  So four things are actually lost:
@@ -790,9 +792,9 @@ ignored.
790
792
 
791
793
  A site that moved here from the WordPress ActivityPub plugin keeps its people's
792
794
  actor ids and its posts' object ids, which are identity and are the CMS's own
793
- (above). What it needs for a while besides is the plugin's old paths, which
795
+ (above). What it needs besides is its content, the plugin's old paths, which
794
796
  followers' servers keep delivering to until they refetch the actor, and an
795
- import that brings each person's key pair and followers across. Both are the
797
+ import that brings each person's key pair and followers across. All three are the
796
798
  [`@geekity/plugin-wordpress`](../plugin-wordpress/README.md) package, which a
797
799
  site born on the CMS never installs. decision-14's switch is that plugin now:
798
800
  where the cutover used to say "turn the switch on", it says install and enable
@@ -800,27 +802,43 @@ the WordPress plugin.
800
802
 
801
803
  #### The cutover, end to end
802
804
 
803
- 1. **Export, while the WordPress site is still up.** Each person's key pair,
805
+ 1. **Start the new site in dev mode.** Set `GEEKITY_DEV_MODE=true` before
806
+ its first boot (see [Dev mode](#dev-mode)). Followers, relays, links and
807
+ mail are all real from here on, and dev mode keeps every one of them from
808
+ hearing about the import, the edits or a scheduled post until step 4.
809
+ 2. **Export, while the WordPress site is still up.** Each person's key pair,
804
810
  and the followers if the old site is going away before the import runs. The
805
811
  plugin's README says how. Note the actor id and the numeric actor id off
806
- `https://example.com/?author=2`, and bring the content across.
807
- 2. **Install the WordPress plugin and import.** Add `@geekity/plugin-wordpress`
808
- to the site's dependencies and to `plugins` in its config, then run `geekity import
809
- wordpress-actor ada --actor-id … --wordpress-id … --keypair ada.keypair.json`.
810
- The command works before the plugin is enabled. Check the report: every
811
- follower should be added, and any that were skipped should be re-run once
812
- their servers answer.
813
- 3. **Enable the WordPress plugin.** Enable it under `/admin/plugins`, then move
814
- the DNS. Followers' servers go on delivering to
815
- the plugin's old inbox paths until they next refetch the actor, and the
816
- plugin is what catches those deliveries.
817
- 4. **Watch.** `/admin/federation` lists the users with their actor ids and
812
+ `https://example.com/?author=2`, take a WXR export under Tools > Export,
813
+ and copy `wp-content/uploads`, which the export does not carry.
814
+ 3. **Install the WordPress plugin and import.** Add `@geekity/plugin-wordpress`
815
+ to the site's dependencies and to `plugins` in its config, then run
816
+ `geekity import wordpress example.WordPress.xml --uploads wp-content/uploads`
817
+ for the content and media, and
818
+ `geekity import wordpress-actor ada --actor-id … --wordpress-id … --keypair ada.keypair.json`
819
+ for each person. Both work before the plugin is enabled. Check the reports:
820
+ every post should be written, every conflict or clash resolved, every
821
+ follower added, and any follower that was skipped re-run once its server
822
+ answers. Tools > Dev mode shows that the imports sent nothing. Both imports
823
+ are safe to run again: run them over a fresh export just before step 4 to
824
+ pick up what WordPress received in the meantime.
825
+ 4. **Enable the WordPress plugin, move the DNS and go live.** Enable it under
826
+ `/admin/plugins`, then move the DNS. Remove `GEEKITY_DEV_MODE` and run
827
+ `geekity dev-mode off`: nothing held during the migration is sent.
828
+ Followers' servers go on delivering to the plugin's old inbox paths until
829
+ they next refetch the actor, and the plugin is what catches those
830
+ deliveries. Once WordPress no longer answers, run both imports one last time
831
+ over its final export: they merge what arrived between the export and the
832
+ DNS move, and leave alone whatever this site wrote or edited since.
833
+ 5. **Watch.** `/admin/federation` lists the users with their actor ids and
818
834
  followers; the plugin's own screen under Plugins lists each old path with
819
835
  the instant it was last asked for. Deliveries should thin out as each
820
836
  follower's server refetches the actor and learns the new endpoints.
821
- 5. **Disable it.** Once every path says _Never_ again for long enough — weeks
837
+ 6. **Disable it.** Once every path says _Never_ again for long enough — weeks
822
838
  rather than days, since a quiet instance refetches rarely — disable the
823
- plugin. The paths go away on the very next request, with nothing restarted,
839
+ plugin. Disabling it also ends the redirects from the old
840
+ `/wp-content/uploads/` media URLs, so a site that still gets links to
841
+ those keeps it enabled. The paths go away on the very next request, with nothing restarted,
824
842
  and if something was still using them you can enable it again just as
825
843
  quickly.
826
844
 
@@ -918,13 +936,95 @@ half is sent by a peer built in the script rather than by `fedify inbox`.
918
936
  [Fedify]: https://fedify.dev/
919
937
  [`fedify tunnel`]: https://fedify.dev/cli
920
938
 
939
+ ## Dev mode
940
+
941
+ Building or migrating a site means running it with production data: the real
942
+ base URL, the real actor and keys, real followers, real links. Dev mode keeps
943
+ that site quiet until you take it live (decision-35). While it is on, nothing
944
+ the site does reaches the outside world:
945
+
946
+ - No ActivityPub delivery to followers or relays, no relay `Follow` or `Undo`,
947
+ and no `Accept` or `Reject` answering an inbox. This holds whatever caused
948
+ the change: the watcher, the admin, the scheduler, `geekity resend` or a
949
+ reply context arriving.
950
+ - No webmention. Endpoint discovery is skipped too.
951
+ - No rssCloud or WebSub ping.
952
+ - No IndexNow submission.
953
+ - No email.
954
+
955
+ Reads still go out: reply contexts and cited pages, actor profiles, oEmbed,
956
+ avatars, the check of an incoming webmention, and a plugin's `host.fetch`. A
957
+ read tells nobody about the site, and without them it could not show its
958
+ replies or profiles. Inbound federation is accepted: follows, replies, likes
959
+ and webmentions are stored as on a live site, and the `Accept` a follow would
960
+ get is held. Plugins need no switch of their own. A plugin's federation hands
961
+ what it receives to the site's inbox, so its answers are held the same way.
962
+
963
+ Each held side effect is a line in `data/dev-mode.jsonl`: when, what kind,
964
+ what would have been sent, and to whom. Tools > Dev mode in the admin lists
965
+ them, newest first. Every admin screen shows a banner while the mode is on,
966
+ and `geekity serve` says so when it boots.
967
+
968
+ ```sh
969
+ $ geekity dev-mode on
970
+ Dev mode is on: nothing the site does is sent until `geekity dev-mode off`.
971
+ $ geekity dev-mode status
972
+ Dev mode is on.
973
+ $ geekity dev-mode off
974
+ Dev mode is off: the site is live. Nothing held while it was on will be sent.
975
+ ```
976
+
977
+ `devMode: true` in the config, or `GEEKITY_DEV_MODE=true`, starts a site in
978
+ dev mode: boot writes `data/dev-mode.json`, and the file keeps the mode on
979
+ from then on. Losing the variable does not take the site live. Only
980
+ `geekity dev-mode off` does, and it refuses while the config or the
981
+ environment still asks for dev mode, because the next boot would turn it on
982
+ again. The running site checks the file before every send, so going live
983
+ needs no restart, unless the server was started with the variable: that
984
+ process stays in dev mode until it restarts without it. Going live is a line
985
+ in the record too.
986
+
987
+ Going live replays nothing. A held side effect is dropped, not queued, and no
988
+ delivery or webmention is recorded as failed, so neither the Federation screen
989
+ nor `geekity resend` offers it again. A post announced while the mode was on
990
+ counts as announced, so its next edit goes out as an `Update`. A relay listed
991
+ while the mode was on stays pending: press Retry on the Federation screen once
992
+ the site is live.
993
+
994
+ Password recovery mail is held too, so in dev mode a password is reset from
995
+ the shell. A peer that followed while the mode was on never got its `Accept`,
996
+ and has to follow again.
997
+
998
+ ### Dev mode on Docker
999
+
1000
+ Set the variable in the service's environment before the first boot, so the
1001
+ container never starts live:
1002
+
1003
+ ```yaml
1004
+ services:
1005
+ server:
1006
+ environment:
1007
+ GEEKITY_DEV_MODE: 'true'
1008
+ ```
1009
+
1010
+ To go live, remove the variable, recreate the container (the file in the
1011
+ `data` volume keeps the mode on through the restart), then run:
1012
+
1013
+ ```sh
1014
+ docker compose exec server geekity dev-mode off
1015
+ ```
1016
+
1017
+ `geekity dev-mode status` in the same container says which state the site is
1018
+ in. The record is in the `data` volume at `data/dev-mode.jsonl`.
1019
+
921
1020
  ## Comments
922
1021
 
923
1022
  A reader can answer a post on the page, and what they leave joins the same
924
1023
  thread as the fediverse replies rather than sitting in a section of its own.
925
1024
 
926
1025
  A comment is a file. `content/_data/comments/{slug}.json` holds one post's
927
- comments — id, source, kind, status, author, the Markdown and the HTML it
1026
+ comments (a slug that is not plain ASCII is percent-encoded in the name, so
1027
+ `i-♥-rss` is `i-%E2%99%A5-rss.json`) — id, source, kind, status, author, the Markdown and the HTML it
928
1028
  rendered to, when it was submitted, a salted hash of the address, and what it
929
1029
  answers — so they are in git beside the posts, an Eleventy build of the same
930
1030
  directory shows them, and the `comments` table is an index emptied and read
@@ -2439,6 +2539,11 @@ export default defineConfig({
2439
2539
  A name that is not a header name, or a value that is empty or has a line break
2440
2540
  in it, stops the site at boot. There is no environment variable for these.
2441
2541
 
2542
+ A policy that limits scripts has to allow whatever the site's theme adds to
2543
+ every page. For the Umami and Google Analytics snippets, the theme README's
2544
+ [Adding to every page](./themes/default/README.md#adding-to-every-page) lists
2545
+ the directives each one needs.
2546
+
2442
2547
  Every response under `/admin`, static files and redirects included, gets three
2443
2548
  stricter values in their place, whatever `securityHeaders` says:
2444
2549
 
@@ -2797,6 +2902,25 @@ first request after the file changes, and it is skipped. The following entries a
2797
2902
  A file that is not a JSON list serves no redirects and is reported the same
2798
2903
  way.
2799
2904
 
2905
+ #### More than one file
2906
+
2907
+ Every `*.json` file in `content/_data/redirects/` is read after
2908
+ `redirects.json`, in name order, and served the same way. A tool that generates
2909
+ redirects, such as an import, writes a file of its own there and never touches
2910
+ the list a person keeps. A file there, or `redirects.json` itself, may also be
2911
+ an object that maps each `from` to its `to`, every one a `301`:
2912
+
2913
+ ```json
2914
+ {
2915
+ "/?p=123": "/2026/01/hello/",
2916
+ "/old-section/": "/new-section/"
2917
+ }
2918
+ ```
2919
+
2920
+ Where two files declare the same `from`, the one read first wins, so
2921
+ `redirects.json` wins over every file in the folder, and the other declaration
2922
+ is reported. A loop across files is found and skipped as within one.
2923
+
2800
2924
  Every redirect the CMS sends carries `X-Redirect-By: Geekity CMS`. This
2801
2925
  includes the declared redirects, moved URLs, trailing slashes, feed spellings
2802
2926
  and the admin's redirects. Someone who traces a chain of redirects through a
@@ -3677,6 +3801,17 @@ The admin is not themed. Its templates and its static files live in a tree of
3677
3801
  their own with a loader of their own, deliberately off this search path, so no
3678
3802
  theme can shadow the login form or the CSRF field inside it.
3679
3803
 
3804
+ Analytics and other markup that goes on every page need no layout at all. The
3805
+ packaged `layouts/base.njk` includes `partials/head-end.njk` last in `<head>`
3806
+ and `partials/body-end.njk` last in `<body>`, both empty in the package, so a
3807
+ theme of `theme.json` and `partials/head-end.njk` holding the Umami or Google
3808
+ Analytics snippet adds it to every public HTML page and keeps every other
3809
+ packaged template. Neither is printed in a feed, a Markdown, text or JSON
3810
+ representation, mail, the admin or the editor's preview. The theme README's
3811
+ [Adding to every page](./themes/default/README.md#adding-to-every-page) has the
3812
+ Umami and GA4 example, the Docker volume, and what a Content-Security-Policy
3813
+ has to allow for them.
3814
+
3680
3815
  The packaged theme is the Paper design (doc-9) on the andrewshell.org shell
3681
3816
  (decision-16). Its stylesheet is written in Tailwind v4 in
3682
3817
  `themes/default/src/style.css` and compiled to `themes/default/static/style.css`
@@ -32,6 +32,12 @@
32
32
  <div class="drawer-content">
33
33
  <label for="admin-menu" class="btn btn-sm drawer-button mx-4 mt-4 lg:hidden">Menu</label>
34
34
  <main id="main" class="p-4 sm:p-6 lg:p-8">
35
+ {% if devModeUrl %}
36
+ {% from "components/alert.njk" import alert %}
37
+ <div class="mb-6">
38
+ {% call alert(color='warning', polite=true) %}<span>Dev mode is on: nothing this site does is sent to followers, relays, linked sites, feed hubs, search engines or inboxes. <a class="link" href="{{ devModeUrl }}">See what was held</a>.</span>{% endcall %}
39
+ </div>
40
+ {% endif %}
35
41
  {% include "components/flash.njk" %}
36
42
  {% block content %}{% endblock %}
37
43
  </main>
@@ -46,7 +46,11 @@
46
46
  <template data-editor-choice>
47
47
  <li>
48
48
  <label class="flex items-start gap-2">
49
+ {% if fieldId == 'editor-tags' %}
49
50
  <input type="checkbox" class="checkbox checkbox-sm mt-0.5" form="editor-suggestions">
51
+ {% else %}
52
+ <input type="radio" class="radio radio-sm mt-0.5" name="{{ action.url }}" form="editor-suggestions">
53
+ {% endif %}
50
54
  <span class="flex flex-col">
51
55
  <span class="flex flex-wrap items-center gap-2">
52
56
  <span class="break-all" data-choice-value></span>
@@ -32,7 +32,7 @@
32
32
  {% if reload %}
33
33
  <div id="plugins-reload" class="mb-8 max-w-prose">
34
34
  {% call card(title="The plugins folder has changed") %}
35
- <p>Reload starts a new server with the plugins folder as it is now, then retires this one. No request is turned away.</p>
35
+ <p>Reload starts a new server with the plugins folder as it is now, then retires this one. No request is turned away.{% if reload.watching %} The site also reloads by itself once the folder has stood still for a few seconds.{% endif %}</p>
36
36
  <dl class="text-sm">
37
37
  {% for heading, names in [["Added", reload.changes.added], ["Removed", reload.changes.removed], ["Updated", reload.changes.updated]] %}
38
38
  {% if names.length %}
@@ -0,0 +1,47 @@
1
+ {% extends "layouts/shell.njk" %}
2
+ {% from "components/table.njk" import table %}
3
+
4
+ {% block title %}{{ heading }}{% endblock %}
5
+
6
+ {% block content %}
7
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
8
+
9
+ <div class="flex flex-col gap-6">
10
+ <p class="max-w-prose">
11
+ {% if on %}
12
+ Dev mode is on. Nothing this site does reaches the outside world: no ActivityPub
13
+ delivery or relay follow, no webmention, no rssCloud or WebSub ping, no IndexNow
14
+ submission and no email. Each one is recorded here instead. Run
15
+ <code>geekity dev-mode off</code> to take the site live; nothing held is sent then.
16
+ {% else %}
17
+ Dev mode is off, and the site is live. This is what it held while it was on. To hold
18
+ everything again, run <code>geekity dev-mode on</code>.
19
+ {% endif %}
20
+ </p>
21
+
22
+ <p>{{ heldCount }} held in all{% if rows.length >= shown %}; the newest {{ shown }} entries are listed, and <code>data/dev-mode.jsonl</code> has the rest{% endif %}.</p>
23
+
24
+ {% if rows.length %}
25
+ {% call table('What dev mode held') %}
26
+ <thead>
27
+ <tr>
28
+ <th scope="col">When</th>
29
+ <th scope="col">What</th>
30
+ <th scope="col">Detail</th>
31
+ <th scope="col">Would have gone to</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ {% for row in rows %}
36
+ <tr>
37
+ <td class="whitespace-nowrap"><time datetime="{{ row.at }}">{{ row.at }}</time></td>
38
+ <td class="whitespace-nowrap">{{ row.what }}</td>
39
+ <td class="break-all">{{ row.detail }}</td>
40
+ <td class="break-all">{% for to in row.to %}<div>{{ to }}</div>{% endfor %}</td>
41
+ </tr>
42
+ {% endfor %}
43
+ </tbody>
44
+ {% endcall %}
45
+ {% endif %}
46
+ </div>
47
+ {% endblock %}
@@ -5728,6 +5728,14 @@
5728
5728
  padding: .1875rem;
5729
5729
  }
5730
5730
 
5731
+ .radio-sm {
5732
+ padding: .1875rem;
5733
+ }
5734
+
5735
+ .radio-sm[type="radio"] {
5736
+ --size: calc(var(--size-selector, .25rem) * 5);
5737
+ }
5738
+
5731
5739
  .table-lg :not(thead, tfoot) tr {
5732
5740
  font-size: 1.125rem;
5733
5741
  }
@@ -1,11 +1,13 @@
1
1
  /* The buttons running plugins put beside the editor's title, description and
2
2
  tags (decision-33). Each posts the form as it stands to its plugin's
3
- endpoint, which answers { ok, value }, { ok, choices } or { ok: false,
4
- message }, and shows the suggestion with Accept and Dismiss. Choices come
5
- only beside the tags field, each drawn from the page's template row with a
6
- box to tick, under a heading row wherever its group changes, and Accept
7
- adds the ticked ones. Accept fills the field and saves nothing; a save is
8
- still only the form's own buttons.
3
+ endpoint, which answers { ok, value }, { ok, choices }, { ok, message }
4
+ or { ok: false, message }, and shows a value or choices with Accept and
5
+ Dismiss, and a message in the status line alone. Choices come beside the
6
+ tags field, each drawn from the page's template row with a box to tick,
7
+ under a heading row wherever its group changes, and Accept adds the ticked
8
+ ones; or beside the title, as radio buttons with the first picked, and
9
+ Accept puts the picked one in the title. Accept fills the field and saves
10
+ nothing; a save is still only the form's own buttons.
9
11
 
10
12
  A file rather than an inline script, loaded from the admin's own origin
11
13
  like slug.js, so the admin's policy stays `script-src 'self'` and
@@ -146,10 +148,12 @@
146
148
  list.hidden = false;
147
149
  suggestion.hidden = false;
148
150
  say('');
149
- list.querySelector('input').focus();
151
+ var first = list.querySelector('input');
152
+ if (first.type === 'radio') first.checked = true;
153
+ first.focus();
150
154
  return;
151
155
  }
152
- if (answer.ok) {
156
+ if (answer.ok && typeof answer.value === 'string') {
153
157
  offered = String(answer.value);
154
158
  text.textContent = offered;
155
159
  text.hidden = false;
@@ -159,7 +163,7 @@
159
163
  accept.focus();
160
164
  return;
161
165
  }
162
- say(String(answer.message), true);
166
+ say(String(answer.message), !answer.ok);
163
167
  if (answer.withdrawn) press.hidden = true;
164
168
  })
165
169
  .catch(function (error) {