@geekity/cms 0.14.0 → 0.16.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 (209) hide show
  1. package/README.md +270 -52
  2. package/admin/pages/documents/editor.njk +120 -0
  3. package/admin/pages/documents/syndication.njk +110 -0
  4. package/admin/pages/federation/followers.njk +7 -0
  5. package/admin/pages/media/library.njk +4 -3
  6. package/admin/pages/settings/general.njk +44 -0
  7. package/admin/static/admin.css +44 -0
  8. package/dist/admin/documents.d.ts +104 -0
  9. package/dist/admin/documents.d.ts.map +1 -1
  10. package/dist/admin/documents.js +220 -55
  11. package/dist/admin/documents.js.map +1 -1
  12. package/dist/admin/enclosure-field.d.ts +91 -0
  13. package/dist/admin/enclosure-field.d.ts.map +1 -0
  14. package/dist/admin/enclosure-field.js +282 -0
  15. package/dist/admin/enclosure-field.js.map +1 -0
  16. package/dist/admin/federation.d.ts +15 -0
  17. package/dist/admin/federation.d.ts.map +1 -1
  18. package/dist/admin/federation.js +19 -0
  19. package/dist/admin/federation.js.map +1 -1
  20. package/dist/admin/media.d.ts +7 -2
  21. package/dist/admin/media.d.ts.map +1 -1
  22. package/dist/admin/media.js +4 -2
  23. package/dist/admin/media.js.map +1 -1
  24. package/dist/admin/menu.d.ts.map +1 -1
  25. package/dist/admin/menu.js +3 -0
  26. package/dist/admin/menu.js.map +1 -1
  27. package/dist/admin/photo-field.d.ts +39 -0
  28. package/dist/admin/photo-field.d.ts.map +1 -0
  29. package/dist/admin/photo-field.js +91 -0
  30. package/dist/admin/photo-field.js.map +1 -0
  31. package/dist/admin/preview.d.ts.map +1 -1
  32. package/dist/admin/preview.js +6 -1
  33. package/dist/admin/preview.js.map +1 -1
  34. package/dist/admin/routes.d.ts.map +1 -1
  35. package/dist/admin/routes.js +4 -0
  36. package/dist/admin/routes.js.map +1 -1
  37. package/dist/admin/settings-general.d.ts.map +1 -1
  38. package/dist/admin/settings-general.js +36 -2
  39. package/dist/admin/settings-general.js.map +1 -1
  40. package/dist/admin/settings-page.d.ts.map +1 -1
  41. package/dist/admin/settings-page.js +1 -2
  42. package/dist/admin/settings-page.js.map +1 -1
  43. package/dist/admin/settings.d.ts +26 -0
  44. package/dist/admin/settings.d.ts.map +1 -1
  45. package/dist/admin/settings.js +88 -2
  46. package/dist/admin/settings.js.map +1 -1
  47. package/dist/admin/syndication-targets.d.ts +57 -0
  48. package/dist/admin/syndication-targets.d.ts.map +1 -0
  49. package/dist/admin/syndication-targets.js +266 -0
  50. package/dist/admin/syndication-targets.js.map +1 -0
  51. package/dist/admin/templates.d.ts +1 -0
  52. package/dist/admin/templates.d.ts.map +1 -1
  53. package/dist/admin/templates.js +1 -0
  54. package/dist/admin/templates.js.map +1 -1
  55. package/dist/admin/uploads.d.ts +13 -2
  56. package/dist/admin/uploads.d.ts.map +1 -1
  57. package/dist/admin/uploads.js +23 -5
  58. package/dist/admin/uploads.js.map +1 -1
  59. package/dist/config.d.ts +17 -3
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +11 -7
  62. package/dist/config.js.map +1 -1
  63. package/dist/content/citation.d.ts +36 -0
  64. package/dist/content/citation.d.ts.map +1 -0
  65. package/dist/content/citation.js +31 -0
  66. package/dist/content/citation.js.map +1 -0
  67. package/dist/content/enclosure.d.ts +69 -0
  68. package/dist/content/enclosure.d.ts.map +1 -0
  69. package/dist/content/enclosure.js +165 -0
  70. package/dist/content/enclosure.js.map +1 -0
  71. package/dist/content/index.d.ts +5 -1
  72. package/dist/content/index.d.ts.map +1 -1
  73. package/dist/content/index.js +2 -0
  74. package/dist/content/index.js.map +1 -1
  75. package/dist/content/media.d.ts +15 -2
  76. package/dist/content/media.d.ts.map +1 -1
  77. package/dist/content/media.js +92 -9
  78. package/dist/content/media.js.map +1 -1
  79. package/dist/content/photo.d.ts +45 -0
  80. package/dist/content/photo.d.ts.map +1 -0
  81. package/dist/content/photo.js +55 -0
  82. package/dist/content/photo.js.map +1 -0
  83. package/dist/content/post-type.d.ts +20 -8
  84. package/dist/content/post-type.d.ts.map +1 -1
  85. package/dist/content/post-type.js +17 -0
  86. package/dist/content/post-type.js.map +1 -1
  87. package/dist/federation/article.d.ts.map +1 -1
  88. package/dist/federation/article.js +93 -10
  89. package/dist/federation/article.js.map +1 -1
  90. package/dist/federation/citations.d.ts +38 -0
  91. package/dist/federation/citations.d.ts.map +1 -0
  92. package/dist/federation/citations.js +98 -0
  93. package/dist/federation/citations.js.map +1 -0
  94. package/dist/federation/delivery.d.ts.map +1 -1
  95. package/dist/federation/delivery.js +79 -31
  96. package/dist/federation/delivery.js.map +1 -1
  97. package/dist/images/alt-text.d.ts +6 -0
  98. package/dist/images/alt-text.d.ts.map +1 -1
  99. package/dist/images/alt-text.js +10 -0
  100. package/dist/images/alt-text.js.map +1 -1
  101. package/dist/images/icons.d.ts +5 -0
  102. package/dist/images/icons.d.ts.map +1 -1
  103. package/dist/images/icons.js +13 -8
  104. package/dist/images/icons.js.map +1 -1
  105. package/dist/images/paths.d.ts +1 -1
  106. package/dist/images/paths.d.ts.map +1 -1
  107. package/dist/images/paths.js.map +1 -1
  108. package/dist/index.d.ts +7 -7
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +25 -4
  111. package/dist/index.js.map +1 -1
  112. package/dist/indieauth/bearer.d.ts +6 -1
  113. package/dist/indieauth/bearer.d.ts.map +1 -1
  114. package/dist/indieauth/bearer.js +39 -12
  115. package/dist/indieauth/bearer.js.map +1 -1
  116. package/dist/indieauth/discovery.d.ts +9 -4
  117. package/dist/indieauth/discovery.d.ts.map +1 -1
  118. package/dist/indieauth/discovery.js +18 -7
  119. package/dist/indieauth/discovery.js.map +1 -1
  120. package/dist/micropub/create.d.ts +56 -0
  121. package/dist/micropub/create.d.ts.map +1 -0
  122. package/dist/micropub/create.js +200 -0
  123. package/dist/micropub/create.js.map +1 -0
  124. package/dist/micropub/endpoint.d.ts +21 -0
  125. package/dist/micropub/endpoint.d.ts.map +1 -0
  126. package/dist/micropub/endpoint.js +353 -0
  127. package/dist/micropub/endpoint.js.map +1 -0
  128. package/dist/micropub/media.d.ts +14 -0
  129. package/dist/micropub/media.d.ts.map +1 -0
  130. package/dist/micropub/media.js +93 -0
  131. package/dist/micropub/media.js.map +1 -0
  132. package/dist/micropub/update.d.ts +44 -0
  133. package/dist/micropub/update.d.ts.map +1 -0
  134. package/dist/micropub/update.js +133 -0
  135. package/dist/micropub/update.js.map +1 -0
  136. package/dist/web/assets.d.ts +14 -0
  137. package/dist/web/assets.d.ts.map +1 -1
  138. package/dist/web/assets.js +56 -1
  139. package/dist/web/assets.js.map +1 -1
  140. package/dist/web/context.d.ts +68 -3
  141. package/dist/web/context.d.ts.map +1 -1
  142. package/dist/web/context.js +56 -1
  143. package/dist/web/context.js.map +1 -1
  144. package/dist/web/feed-atom.d.ts.map +1 -1
  145. package/dist/web/feed-atom.js +20 -0
  146. package/dist/web/feed-atom.js.map +1 -1
  147. package/dist/web/feed-item.d.ts +19 -0
  148. package/dist/web/feed-item.d.ts.map +1 -1
  149. package/dist/web/feed-item.js +22 -0
  150. package/dist/web/feed-item.js.map +1 -1
  151. package/dist/web/feed-json.d.ts +13 -0
  152. package/dist/web/feed-json.d.ts.map +1 -1
  153. package/dist/web/feed-json.js +15 -0
  154. package/dist/web/feed-json.js.map +1 -1
  155. package/dist/web/feed-rss.d.ts.map +1 -1
  156. package/dist/web/feed-rss.js +59 -2
  157. package/dist/web/feed-rss.js.map +1 -1
  158. package/dist/web/feed-xml.d.ts +7 -1
  159. package/dist/web/feed-xml.d.ts.map +1 -1
  160. package/dist/web/feed-xml.js +8 -2
  161. package/dist/web/feed-xml.js.map +1 -1
  162. package/dist/web/feeds.d.ts +1 -1
  163. package/dist/web/feeds.d.ts.map +1 -1
  164. package/dist/web/feeds.js +6 -0
  165. package/dist/web/feeds.js.map +1 -1
  166. package/dist/web/index.d.ts +4 -1
  167. package/dist/web/index.d.ts.map +1 -1
  168. package/dist/web/index.js +2 -0
  169. package/dist/web/index.js.map +1 -1
  170. package/dist/web/license.d.ts +94 -0
  171. package/dist/web/license.d.ts.map +1 -0
  172. package/dist/web/license.js +116 -0
  173. package/dist/web/license.js.map +1 -0
  174. package/dist/web/oembed.d.ts.map +1 -1
  175. package/dist/web/oembed.js +1 -2
  176. package/dist/web/oembed.js.map +1 -1
  177. package/dist/web/render.d.ts +15 -0
  178. package/dist/web/render.d.ts.map +1 -1
  179. package/dist/web/render.js +27 -2
  180. package/dist/web/render.js.map +1 -1
  181. package/dist/web/routes.d.ts.map +1 -1
  182. package/dist/web/routes.js +10 -6
  183. package/dist/web/routes.js.map +1 -1
  184. package/dist/web/share-image.d.ts +7 -7
  185. package/dist/web/share-image.d.ts.map +1 -1
  186. package/dist/web/share-image.js +8 -7
  187. package/dist/web/share-image.js.map +1 -1
  188. package/dist/webmention/index.d.ts +2 -0
  189. package/dist/webmention/index.d.ts.map +1 -1
  190. package/dist/webmention/index.js +1 -0
  191. package/dist/webmention/index.js.map +1 -1
  192. package/dist/webmention/service.d.ts.map +1 -1
  193. package/dist/webmention/service.js +96 -18
  194. package/dist/webmention/service.js.map +1 -1
  195. package/dist/webmention/syndication.d.ts +135 -0
  196. package/dist/webmention/syndication.d.ts.map +1 -0
  197. package/dist/webmention/syndication.js +337 -0
  198. package/dist/webmention/syndication.js.map +1 -0
  199. package/package.json +1 -1
  200. package/themes/default/README.md +121 -37
  201. package/themes/default/layouts/base.njk +3 -4
  202. package/themes/default/layouts/post.njk +23 -6
  203. package/themes/default/partials/citations.njk +17 -0
  204. package/themes/default/partials/jsonld.njk +15 -6
  205. package/themes/default/partials/photos.njk +10 -0
  206. package/themes/default/partials/post-list.njk +7 -0
  207. package/themes/default/partials/recording.njk +41 -0
  208. package/themes/default/src/style.css +25 -0
  209. package/themes/default/static/style.css +41 -0
package/README.md CHANGED
@@ -1185,6 +1185,113 @@ answers 404 or 410 — takes it away. Closing rules do not apply: a post that
1185
1185
  stopped taking comments still hears about a page that links to it, exactly as it
1186
1186
  still hears a fediverse reply.
1187
1187
 
1188
+ ### Syndication targets
1189
+
1190
+ Some services copy a post when it links to them and sends them a webmention,
1191
+ and answer with the address of the copy. IndieNews lists posts about the
1192
+ IndieWeb; Bridgy Publish posts to Mastodon, Bluesky, GitHub and Flickr. The
1193
+ CMS knows none of them by name. A site lists the ones it uses on the admin's
1194
+ **Posts > Syndication** screen (`/admin/syndication`), which reads
1195
+ and writes `content/_data/syndicationTargets.json`. The file stays the source of
1196
+ truth, so it can still be edited by hand, and it looks like this:
1197
+
1198
+ ```json
1199
+ [
1200
+ {
1201
+ "id": "indienews",
1202
+ "name": "IndieNews",
1203
+ "url": "https://news.indieweb.org/en",
1204
+ "tag": "indienews"
1205
+ },
1206
+ {
1207
+ "id": "mastodon",
1208
+ "name": "Mastodon",
1209
+ "url": "https://brid.gy/publish/mastodon"
1210
+ }
1211
+ ]
1212
+ ```
1213
+
1214
+ | Key | What it is |
1215
+ | ----------- | --------------------------------------------------------------------------------------------- |
1216
+ | `id` | What a post's `syndicate-to` lists. Letters, digits, `.`, `-` and `_`. |
1217
+ | `name` | What the editor's checkbox and the post's link say. |
1218
+ | `url` | The page the post links to and sends its webmention to. May hold `{lang}`. |
1219
+ | `tag` | Optional. A post carrying this tag is sent to the target without listing it. |
1220
+ | `languages` | Optional. The language tags the target takes posts in. A post in any other language skips it. |
1221
+
1222
+ An entry missing any of the first three, repeating an id, or with a `languages`
1223
+ value that is not a list of language tags, is ignored. It is reported in the log
1224
+ when the site starts, and the Syndication screen shows it with its problem so it
1225
+ can be fixed or removed there. No file means no targets.
1226
+
1227
+ The screen has a panel for each entry, in file order, and an **Add a target**
1228
+ form. A save is checked by the same rules the file is read by: a field that
1229
+ breaks one is marked on the form and the file is left as it was. A save that
1230
+ passes is written atomically, and the editor's **Syndicate to** checkboxes and
1231
+ Micropub's `q=syndicate-to` offer the change on the next request. A save or
1232
+ remove is refused when the entry changed in the file since the page was drawn.
1233
+ A file that is not a JSON list is shown with its problem and never saved over.
1234
+ To keep the default theme's old IndieNews link, add a target with the id
1235
+ `indienews`, the url `https://news.indieweb.org/{lang}` and the tag `indienews`.
1236
+
1237
+ Some targets have a page per language. IndieNews has `news.indieweb.org/en`,
1238
+ `/de`, `/fr` and more. Declare one target and put `{lang}` where the language
1239
+ goes:
1240
+
1241
+ ```json
1242
+ {
1243
+ "id": "indienews",
1244
+ "name": "IndieNews",
1245
+ "url": "https://news.indieweb.org/{lang}",
1246
+ "tag": "indienews",
1247
+ "languages": ["en", "de", "fr"]
1248
+ }
1249
+ ```
1250
+
1251
+ The CMS fills `{lang}` with the post's language: its `lang` front matter, or the
1252
+ site's language when the post names none. With `languages`, a post in a
1253
+ language the list does not name neither links to the target nor notifies it. A
1254
+ regional tag counts as its language, so a `de-AT` post goes to
1255
+ `news.indieweb.org/de`, and `{lang}` takes the tag from the list. Without
1256
+ `languages`, `{lang}` takes the post's own tag as written, region and all.
1257
+
1258
+ A post selects targets in one of two ways:
1259
+
1260
+ - **By listing them.** The post editor has a **Syndicate to** checkbox for each
1261
+ target, and saving writes the ticked ids into the front matter as
1262
+ `syndicate-to: [indienews, mastodon]`. An id no target declares is kept as
1263
+ written.
1264
+ - **By its tags.** A post tagged `indienews` is sent to the target whose `tag`
1265
+ is `indienews`, as the WordPress IndieNews plugin did.
1266
+
1267
+ The default theme links to each selected target at the start of the Published
1268
+ line, inside the post's `h-entry`, as `<a class="u-syndication"
1269
+ href="https://news.indieweb.org/en">IndieNews</a>`. That link is what IndieNews
1270
+ and Bridgy Publish check for when they fetch the post. When the post is
1271
+ published or updated, the sender adds each selected target to the pages it
1272
+ notifies, the same way it adds the post a reply answers. Nothing is sent while a
1273
+ page is served.
1274
+
1275
+ A target that answers `201` or `202` with a `Location` header has made a copy.
1276
+ The address is kept in `content/_data/syndication.json`, keyed by the post's
1277
+ permalink (decision-26), and the theme prints it after the dates as **Also on**
1278
+ with a `u-syndication` link. A copy made by hand goes in the post's own front
1279
+ matter, and is printed the same way:
1280
+
1281
+ ```yaml
1282
+ syndication:
1283
+ - https://mastodon.social/@me/113000000000000000
1284
+ ```
1285
+
1286
+ Removing a target from `syndicate-to`, or removing its tag, removes the link.
1287
+ The sender then tells the target, as it tells any page a post stops linking to,
1288
+ and the copy is dropped from `syndication.json`. IndieNews takes the post down
1289
+ when it finds the link gone. Drafting or trashing the post does the same.
1290
+
1291
+ The posts table on `/admin/federation` lists each selected target with how its
1292
+ last webmention went and a link to its copy, and **Resend** sends to the targets
1293
+ again.
1294
+
1188
1295
  ## Email
1189
1296
 
1190
1297
  The CMS sends its mail through one service and one seam. Everything that will
@@ -1671,53 +1778,53 @@ that ship inside the package, deliberately outside the theme search path: a
1671
1778
  site's theme may override any public template, and must not be able to
1672
1779
  shadow the login form.
1673
1780
 
1674
- | Route | What it does |
1675
- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
1676
- | `/admin` | The dashboard: counts, the five most recent posts, the follower count. |
1677
- | `/admin/posts`, `/admin/pages` | The listings and the editors. |
1678
- | `/admin/tags`, `/admin/categories` | Every term in use, with rename, merge and delete. |
1679
- | `/admin/tags/rename`, `/admin/categories/rename` | `POST` only. Renames a term, or merges it into one that exists. |
1680
- | `/admin/tags/delete`, `/admin/categories/delete` | `POST` only. Takes a term out of every file. |
1681
- | `/admin/navigation` | Navigation > Menus: every menu the site holds. `POST` saves one menu's items. |
1682
- | `/admin/navigation/add` | `POST` only. Adds an empty menu under the name the form gives. |
1683
- | `/admin/navigation/delete` | `POST` only. Deletes one menu the active theme renders nowhere. |
1684
- | `/admin/media` | Everything under `content/uploads`, with the URL, the Markdown and what uses it. |
1685
- | `/admin/media/upload` | `POST` only. Stores one file by the rules the editor's upload enforces. |
1686
- | `/admin/media/delete` | `POST` only. Deletes one upload, asking first when a document points at it. |
1687
- | `/admin/media/alt` | `POST` only. Sets a picture's alt text, or marks it decorative, in `media.json`. |
1688
- | `/admin/comments` | Pending, approved and spam, with approve, spam, delete and reply. |
1689
- | `/admin/comments/moderate` | `POST` only. Approves one comment, files it as spam, or deletes it. |
1690
- | `/admin/comments/reply` | `POST` only. Posts an approved reply under the comment it answers. |
1691
- | `/admin/messages` | The contact form's inbox, with a Spam list beside it. |
1692
- | `/admin/messages/read` | `POST` only. Marks one message read, or unread again. |
1693
- | `/admin/messages/delete` | `POST` only. Deletes one message, and its file with it. |
1694
- | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1695
- | `/admin/tools` | Tools > Content index: what the index holds, and the button that rebuilds it. |
1696
- | `/admin/tools/rebuild-index` | `POST` only. Offers the rebuild, then reads every file again on the live site. |
1697
- | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language, locale. |
1698
- | `/admin/settings/reading` | What the homepage displays, posts per page, the notify server. |
1699
- | `/admin/settings/permalinks` | The tag and category bases, and the archive redirects already recorded. |
1700
- | `/admin/settings/discussion` | Comments and the closing window, webmentions, and the Akismet key. |
1701
- | `/admin/settings/akismet` | `POST` only. Saves the Akismet key, or forgets it. |
1702
- | `/admin/settings/email` | The mail provider, the From line, the reply-to and the contact address. |
1703
- | `/admin/settings/mail` | `POST` only. Saves a mail credential, or forgets every one of them. |
1704
- | `/admin/settings/mail/test` | `POST` only. Sends the theme's test message through the whole chain. |
1705
- | `/admin/users` | Who may sign in: a row each, with Edit and Delete. Nothing on it edits anybody. |
1706
- | `/admin/users/new` | Users > Add new: the add form. `POST` adds one. |
1707
- | `/admin/users/<id>` | One user: the account, the profile, the notices, your password, and the delete. |
1708
- | `/admin/users/password` | `POST` only. Changes the signed-in admin's own password. |
1709
- | `/admin/users/email` | `POST` only. Sets or clears the email address on the user the form names. |
1710
- | `/admin/users/profile` | `POST` only. Saves the whole public profile of the user the form names. |
1711
- | `/admin/users/notifications` | `POST` only. Turns one notice on or off for the user the form names. |
1712
- | `/admin/users/notifications/mode` | `POST` only. Sets how often that notice arrives: as they arrive, hourly or daily. |
1713
- | `/admin/users/delete` | `POST` only. Deletes the user the form names. |
1714
- | `/admin/federation` | The actors, their followers, the inbox log, and per-post delivery. |
1715
- | `/admin/federation/settings` | Federation > Settings: the relays the site subscribes to, and the WordPress switch. |
1716
- | `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
1717
- | `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
1718
- | `/admin/login` | Username and password. |
1719
- | `/admin/logout` | `POST` only. Deletes the session row and sends `Clear-Site-Data`. |
1720
- | `/admin/_static/*` | The admin's own stylesheet and scripts, cached for an hour. |
1781
+ | Route | What it does |
1782
+ | ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
1783
+ | `/admin` | The dashboard: counts, the five most recent posts, the follower count. |
1784
+ | `/admin/posts`, `/admin/pages` | The listings and the editors. |
1785
+ | `/admin/tags`, `/admin/categories` | Every term in use, with rename, merge and delete. |
1786
+ | `/admin/tags/rename`, `/admin/categories/rename` | `POST` only. Renames a term, or merges it into one that exists. |
1787
+ | `/admin/tags/delete`, `/admin/categories/delete` | `POST` only. Takes a term out of every file. |
1788
+ | `/admin/navigation` | Navigation > Menus: every menu the site holds. `POST` saves one menu's items. |
1789
+ | `/admin/navigation/add` | `POST` only. Adds an empty menu under the name the form gives. |
1790
+ | `/admin/navigation/delete` | `POST` only. Deletes one menu the active theme renders nowhere. |
1791
+ | `/admin/media` | Everything under `content/uploads`, with the URL, the Markdown and what uses it. |
1792
+ | `/admin/media/upload` | `POST` only. Stores one file by the rules the editor's upload enforces. |
1793
+ | `/admin/media/delete` | `POST` only. Deletes one upload, asking first when a document points at it. |
1794
+ | `/admin/media/alt` | `POST` only. Sets a picture's alt text, or marks it decorative, in `media.json`. |
1795
+ | `/admin/comments` | Pending, approved and spam, with approve, spam, delete and reply. |
1796
+ | `/admin/comments/moderate` | `POST` only. Approves one comment, files it as spam, or deletes it. |
1797
+ | `/admin/comments/reply` | `POST` only. Posts an approved reply under the comment it answers. |
1798
+ | `/admin/messages` | The contact form's inbox, with a Spam list beside it. |
1799
+ | `/admin/messages/read` | `POST` only. Marks one message read, or unread again. |
1800
+ | `/admin/messages/delete` | `POST` only. Deletes one message, and its file with it. |
1801
+ | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1802
+ | `/admin/tools` | Tools > Content index: what the index holds, and the button that rebuilds it. |
1803
+ | `/admin/tools/rebuild-index` | `POST` only. Offers the rebuild, then reads every file again on the live site. |
1804
+ | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language, locale, site icon. |
1805
+ | `/admin/settings/reading` | What the homepage displays, posts per page, the notify server. |
1806
+ | `/admin/settings/permalinks` | The tag and category bases, and the archive redirects already recorded. |
1807
+ | `/admin/settings/discussion` | Comments and the closing window, webmentions, and the Akismet key. |
1808
+ | `/admin/settings/akismet` | `POST` only. Saves the Akismet key, or forgets it. |
1809
+ | `/admin/settings/email` | The mail provider, the From line, the reply-to and the contact address. |
1810
+ | `/admin/settings/mail` | `POST` only. Saves a mail credential, or forgets every one of them. |
1811
+ | `/admin/settings/mail/test` | `POST` only. Sends the theme's test message through the whole chain. |
1812
+ | `/admin/users` | Who may sign in: a row each, with Edit and Delete. Nothing on it edits anybody. |
1813
+ | `/admin/users/new` | Users > Add new: the add form. `POST` adds one. |
1814
+ | `/admin/users/<id>` | One user: the account, the profile, the notices, your password, and the delete. |
1815
+ | `/admin/users/password` | `POST` only. Changes the signed-in admin's own password. |
1816
+ | `/admin/users/email` | `POST` only. Sets or clears the email address on the user the form names. |
1817
+ | `/admin/users/profile` | `POST` only. Saves the whole public profile of the user the form names. |
1818
+ | `/admin/users/notifications` | `POST` only. Turns one notice on or off for the user the form names. |
1819
+ | `/admin/users/notifications/mode` | `POST` only. Sets how often that notice arrives: as they arrive, hourly or daily. |
1820
+ | `/admin/users/delete` | `POST` only. Deletes the user the form names. |
1821
+ | `/admin/federation` | The actors, their followers, the inbox log, and per-post delivery. |
1822
+ | `/admin/federation/settings` | Federation > Settings: the relays the site subscribes to, and the WordPress switch. |
1823
+ | `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
1824
+ | `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
1825
+ | `/admin/login` | Username and password. |
1826
+ | `/admin/logout` | `POST` only. Deletes the session row and sends `Clear-Site-Data`. |
1827
+ | `/admin/_static/*` | The admin's own stylesheet and scripts, cached for an hour. |
1721
1828
 
1722
1829
  The screens behind the login share one layout: a bar across the top with the
1723
1830
  site name and a link to the public site, the menu down the left, and a place for
@@ -1846,7 +1953,8 @@ at once cannot each keep half of what the other kept.
1846
1953
 
1847
1954
  The file carries `title`, `tagline`, `url`, `author` (a username, when the
1848
1955
  site has one author), `postsPerPage`, `homepage`, `postsPage`,
1849
- `timezone`, `language`, `locale` (when it is set), `tagBase`,
1956
+ `timezone`, `language`, `locale` (when it is set), `icon` (when it is set),
1957
+ `license` and `licenseName` (when a license is chosen; see below), `tagBase`,
1850
1958
  `categoryBase`, `notifyServer`, `feedUpdatePeriod`, `feedUpdateFrequency`,
1851
1959
  `webmentionsSend`, `webmentionsReceive`,
1852
1960
  `mailProvider`, `mailFromName`, `mailFromAddress`, `mailReplyTo`,
@@ -1999,7 +2107,7 @@ would have done anyway.
1999
2107
  | `removeImageVariants` | Take a source's derived directory away. |
2000
2108
  | `responsiveImages(html, describe)` | Rewrite `<img src="/uploads/…">` as `<picture>`. Pure: hand it any lookup. |
2001
2109
  | `siteImageMarkup(config, html)` | The same, over one site's own records, deriving in the background for a record miss. |
2002
- | `siteIcons(config, avatar)` | The head's icon links for a site's avatar: `{ rel, sizes, href }`, or none at all. |
2110
+ | `siteIcons(config, setting)` | The head's icon links for a site's icon setting: `{ rel, sizes, href }`, or none. |
2003
2111
 
2004
2112
  `describeImage` is synchronous and cached because it is called once per image
2005
2113
  while a page renders, and decision-10 forbids probing an image file at render
@@ -2009,10 +2117,11 @@ markup that asks for them back, and the first request for each one rebuilds it.
2009
2117
 
2010
2118
  The site's icons are derived copies of the same kind, and `siteIcons` is how a
2011
2119
  head gets at them: three square PNGs, `icon` at 32 and 16 pixels and
2012
- `apple-touch-icon` at 180, cropped from the middle of the site's avatar and
2120
+ `apple-touch-icon` at 180, cropped from the middle of the site's icon (the
2121
+ `icon` that Settings > General writes, else a hand-set `avatar`) and
2013
2122
  encoded the first time a browser asks for one. It answers an empty list — and
2014
- the packaged theme then links no icon at all — when the site has no avatar, when
2015
- its avatar is a file no icon can be made of, or when `imageOptimization` is off,
2123
+ the packaged theme then links no icon at all — when the site has no icon, when
2124
+ its icon is a file no icon can be made of, or when `imageOptimization` is off,
2016
2125
  so a head never advertises a URL this server would answer 404 for.
2017
2126
 
2018
2127
  `documentContext(document, images)` is where the rewrite is applied. Passing the
@@ -2869,8 +2978,22 @@ to read them on, `wfw:commentRss` (the [Well-Formed Web][wfw-ns] comment API) is
2869
2978
  the feed to poll, and `source:comments` is that same feed with a `count`
2870
2979
  attribute, so a reader can say "3 comments" without fetching anything.
2871
2980
 
2981
+ When the site has a license (Settings > General, or `license` in `site.json`),
2982
+ the channel carries a `creativeCommons:license` holding its URL, and every item
2983
+ carries one for its own license: the post's `license` front matter, else the
2984
+ site's. A post whose front matter says `license: none` carries none. The
2985
+ element is the [Creative Commons RSS module][cc-rss], declared as
2986
+ `xmlns:creativeCommons` only when some license is printed. It names a license
2987
+ by URL, Creative Commons or not, which a reader can act on; `dc:rights` would be
2988
+ free text. Nothing about a license is printed on a site and posts without one.
2989
+ RSS has no element that says "no license", so a post saying `license: none`
2990
+ on a licensed site carries no item license while the channel still names the
2991
+ site's; a reader that applies the channel's license to every item reads that
2992
+ post as under it.
2993
+
2872
2994
  [source-ns]: https://source.scripting.com/
2873
2995
  [wfw-ns]: http://wellformedweb.org/CommentAPI/
2996
+ [cc-rss]: https://cyber.harvard.edu/rss/creativeCommonsRssModule.html
2874
2997
 
2875
2998
  ### Comments
2876
2999
 
@@ -2925,6 +3048,14 @@ post as `content type="html"`. The feed itself carries `id`, `title`,
2925
3048
  `xml:lang` from the `language` setting, and — when the site names a notify
2926
3049
  server — a `source:cloud` and a `link rel="hub"`.
2927
3050
 
3051
+ With a license, the feed carries an [RFC 4946][rfc4946] `link rel="license"`
3052
+ to the site's license, its name as the `title`, and every entry carries one for
3053
+ its own license, the post's `license` front matter else the site's. An entry
3054
+ states its own because RFC 4946 does not let an entry inherit the feed's. JSON
3055
+ Feed has no field for a license and says nothing about one.
3056
+
3057
+ [rfc4946]: https://www.rfc-editor.org/rfc/rfc4946
3058
+
2928
3059
  A JSON Feed item carries `id` (the object id), `url` (the permalink), `title`,
2929
3060
  `content_html`, `summary`, `date_published`, `date_modified`, `tags` (the
2930
3061
  terms) and `authors`; the feed carries `version`, `title`, `home_page_url`,
@@ -2935,6 +3066,93 @@ The XML is written by this package rather than by a library. Text is escaped;
2935
3066
  `content:encoded` and `source:markdown` are CDATA sections, with any `]]>` in
2936
3067
  the text split across two sections so it cannot end one early.
2937
3068
 
3069
+ ### A recording on a post
3070
+
3071
+ A post can carry one recording: a spoken version of an article, or an episode
3072
+ now and then. The site is a blog that sometimes has audio, not a podcast host,
3073
+ so the feeds carry what a podcast app needs to play, describe and caption an
3074
+ item, and leave the podcast directory tags out. The front matter spells it the
3075
+ way RSS does:
3076
+
3077
+ ```yaml
3078
+ enclosure:
3079
+ url: /uploads/2026/10/episode-12.mp3 # an upload; written by the editor
3080
+ type: audio/mpeg # from the upload table; written by the editor
3081
+ length: 23456789 # bytes, from the file on disk; written by the editor
3082
+ duration: 1834 # seconds, optional
3083
+ transcript: # optional
3084
+ url: /uploads/2026/10/episode-12.vtt # an upload or an http(s) URL
3085
+ type: text/vtt # text/vtt, application/x-subrip, text/html or text/plain
3086
+ alternates: # optional, any number
3087
+ - url: /uploads/2026/10/episode-12.mp4
3088
+ type: video/mp4
3089
+ length: 98765432
3090
+ title: Video # optional, at most 32 characters
3091
+ height: 720 # optional, for video
3092
+ lang: en # optional
3093
+ - url: https://cdn.example.com/episode-12-low.mp3
3094
+ type: audio/mpeg # required for a link
3095
+ title: Low bandwidth # a link's length is optional
3096
+ ```
3097
+
3098
+ **The main file is always an upload** under `/uploads/`, because RSS requires
3099
+ its `url`, `length` and `type`, and an upload is the only file whose length the
3100
+ site knows. A main file without all three is no recording at all. A bad
3101
+ alternate version or transcript is dropped on its own, so one wrong line in a
3102
+ hand-edited file costs that line and not the episode. `enclosureOf(extra)` is
3103
+ the one reading of the key, and the feeds, the theme and the federation read
3104
+ only what it returns.
3105
+
3106
+ **The post editor** has a Recording section on posts. The main file is a select
3107
+ of the audio and video in the media library, and None removes the whole key,
3108
+ transcript and alternate versions included. The duration takes seconds, `30:34`
3109
+ or `1:02:03`. The transcript is an upload (`.vtt`, `.srt` or `.txt`) or an
3110
+ `https` address with its type. Alternate versions are numbered rows with one
3111
+ blank row at the end, so a plain form can add, edit and remove them; emptying a
3112
+ row's address removes it. On save, the type and length of every upload are read
3113
+ from the upload table and from the file on disk, never from the form. A main
3114
+ file that is not an audio or video upload, a linked version without an `http`
3115
+ or `https` address or a media type, or a title over 32 characters is refused
3116
+ with a message, and nothing is written.
3117
+
3118
+ **What the feeds print, and when.** A post with no recording prints none of
3119
+ this, and a feed with no recording in it declares neither namespace, so its
3120
+ bytes and its ETag are what they were before recordings existed.
3121
+
3122
+ | Element | Format | Printed when |
3123
+ | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3124
+ | `<enclosure url length type/>` | RSS item | The post has a main file. `url` is absolute. |
3125
+ | `<podcast:alternateEnclosure>` with `<podcast:source uri/>` | RSS item | Once per alternate version, with `type`, and `length`, `title`, `height` and `lang` when known. |
3126
+ | `<podcast:transcript url type/>` | RSS item | The post has a transcript, with `rel="captions"` for WebVTT or SubRip. |
3127
+ | `<itunes:duration>` | RSS item | The duration is known, in whole seconds. |
3128
+ | `<podcast:medium>blog</podcast:medium>` | RSS channel | Any item in the feed has a recording. Without it an app assumes `podcast`; `blog` is the spec's word for articles that sometimes have audio. |
3129
+ | `<link rel="enclosure" type length href/>` | Atom entry | The post has a main file. Atom has no way to say the others are versions of it. |
3130
+ | `attachments` | JSON Feed | The main file and then each alternate version, with `url`, `mime_type`, `size_in_bytes` and `title` when known, and `duration_in_seconds` on audio or video when the duration is known. |
3131
+
3132
+ `xmlns:podcast="https://podcastindex.org/namespace/1.0"` is declared only on a
3133
+ feed that prints a podcast element, and
3134
+ `xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"` only on one that
3135
+ prints `itunes:duration`.
3136
+
3137
+ **Left out on purpose:** the Apple directory tags (`itunes:image`,
3138
+ `itunes:category`, `itunes:explicit`, `itunes:owner`), and `podcast:guid`,
3139
+ `podcast:locked`, `podcast:person`, `podcast:season`, `podcast:episode`,
3140
+ `podcast:value` and `podcast:chapters`. Each one matters only to a podcast
3141
+ directory or a dedicated show. Adding them is a follow-up for a site that wants
3142
+ to be listed in Apple Podcasts, and so is `podcast:license`, which could reuse
3143
+ the [content license](#rss-20).
3144
+
3145
+ **The fediverse** gets the main file as an `Audio` or `Video` attachment with
3146
+ its absolute `url`, its `mediaType`, and the post's title, or a note's first
3147
+ words, as its `name`. It goes before the post's `Image` attachments, because
3148
+ Mastodon shows a player or a gallery by the type of the first attachment and
3149
+ keeps only four. Alternate versions and linked files stay out: a remote server
3150
+ fetches and re-encodes what it is given, and a link to another host is a type
3151
+ this site cannot check. `playsAsVideo(type)` decides `Video` against `Audio`,
3152
+ and the default theme's `<video>` against `<audio>`, so the two never disagree.
3153
+
3154
+ The default theme plays the main file on the post page; see its README.
3155
+
2938
3156
  ### Real-time notification
2939
3157
 
2940
3158
  A feed reader that polls hears about a post when it next polls. An [rssCloud][]
@@ -62,6 +62,97 @@
62
62
  formnovalidate
63
63
  >Preview</button>
64
64
  </div>
65
+
66
+ {% if kind.type == "post" %}
67
+ <fieldset class="admin-editor-enclosure admin-editor-photos">
68
+ <legend>Photos</legend>
69
+ <p class="admin-hint">
70
+ Each is an image in the media library or an address starting https://. Leave the alt text
71
+ empty to use the media library’s. Empty an address to remove that photo.
72
+ </p>
73
+ {% for row in photoRows %}
74
+ {% set n = loop.index0 %}
75
+ <fieldset class="admin-editor-alternate">
76
+ <legend>{% if row.url %}Photo {{ loop.index }}{% else %}Add a photo{% endif %}</legend>
77
+ <label for="editor-photo-url-{{ n }}">Address</label>
78
+ <input id="editor-photo-url-{{ n }}" name="{{ photoFields.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-photo-uploads" autocomplete="off" />
79
+ <label for="editor-photo-alt-{{ n }}">Alt text</label>
80
+ <input id="editor-photo-alt-{{ n }}" name="{{ photoFields.alt }}{{ n }}" type="text" value="{{ row.alt }}"{% if row.libraryAlt %} placeholder="{{ row.libraryAlt }}"{% endif %} />
81
+ </fieldset>
82
+ {% endfor %}
83
+ <datalist id="editor-photo-uploads">
84
+ {% for url in photoChoices %}
85
+ <option value="{{ url }}"></option>
86
+ {% endfor %}
87
+ </datalist>
88
+ </fieldset>
89
+ <fieldset class="admin-editor-enclosure">
90
+ <legend>Recording</legend>
91
+
92
+ <label for="editor-enclosure-url">Audio or video file</label>
93
+ <select id="editor-enclosure-url" name="{{ enclosureFields.url }}" aria-describedby="editor-enclosure-url-hint">
94
+ <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
95
+ {% for choice in enclosureChoices.media %}
96
+ <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
97
+ {% endfor %}
98
+ </select>
99
+ <p class="admin-hint" id="editor-enclosure-url-hint">
100
+ Upload the file to the media library first. Feeds carry it as the post’s enclosure and the
101
+ post page plays it. None removes the recording, its transcript and its other versions.
102
+ </p>
103
+
104
+ <label for="editor-enclosure-duration">Duration</label>
105
+ <input id="editor-enclosure-duration" name="{{ enclosureFields.duration }}" type="text" inputmode="numeric" value="{{ form.enclosure.duration }}" autocomplete="off" aria-describedby="editor-enclosure-duration-hint" />
106
+ <p class="admin-hint" id="editor-enclosure-duration-hint">Seconds, or 30:34, or 1:02:03. Leave it empty if you do not know.</p>
107
+
108
+ <label for="editor-enclosure-transcript">Transcript or captions</label>
109
+ <input id="editor-enclosure-transcript" name="{{ enclosureFields.transcriptUrl }}" type="text" value="{{ form.enclosure.transcriptUrl }}" list="editor-transcript-uploads" autocomplete="off" aria-describedby="editor-enclosure-transcript-hint" />
110
+ <datalist id="editor-transcript-uploads">
111
+ {% for url in enclosureChoices.transcripts %}
112
+ <option value="{{ url }}"></option>
113
+ {% endfor %}
114
+ </datalist>
115
+ <label for="editor-enclosure-transcript-type">Transcript type</label>
116
+ <select id="editor-enclosure-transcript-type" name="{{ enclosureFields.transcriptType }}" aria-describedby="editor-enclosure-transcript-hint">
117
+ <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
118
+ {% for type in transcriptTypes %}
119
+ <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
120
+ {% endfor %}
121
+ </select>
122
+ <p class="admin-hint" id="editor-enclosure-transcript-hint">
123
+ A .vtt, .srt or .txt file in the media library, or an address starting https://. A linked
124
+ transcript needs its type.
125
+ </p>
126
+
127
+ <h2 class="admin-editor-enclosure-heading">Other versions</h2>
128
+ <p class="admin-hint">
129
+ A video of an audio episode, a smaller file, another language. Each is a file in the media
130
+ library or an address starting https://, which needs its type. Empty an address to remove
131
+ that version.
132
+ </p>
133
+ {% for row in alternateRows %}
134
+ {% set n = loop.index0 %}
135
+ <fieldset class="admin-editor-alternate">
136
+ <legend>{% if row.url %}Version {{ loop.index }}{% else %}Add a version{% endif %}</legend>
137
+ <label for="editor-alternate-url-{{ n }}">Address</label>
138
+ <input id="editor-alternate-url-{{ n }}" name="{{ enclosureFields.alternate.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-media-uploads" autocomplete="off" />
139
+ <label for="editor-alternate-type-{{ n }}">Type</label>
140
+ <input id="editor-alternate-type-{{ n }}" name="{{ enclosureFields.alternate.type }}{{ n }}" type="text" value="{{ row.type }}" placeholder="video/mp4" autocomplete="off" spellcheck="false" />
141
+ <label for="editor-alternate-title-{{ n }}">Title</label>
142
+ <input id="editor-alternate-title-{{ n }}" name="{{ enclosureFields.alternate.title }}{{ n }}" type="text" value="{{ row.title }}" maxlength="32" />
143
+ <label for="editor-alternate-height-{{ n }}">Height</label>
144
+ <input id="editor-alternate-height-{{ n }}" name="{{ enclosureFields.alternate.height }}{{ n }}" type="text" inputmode="numeric" value="{{ row.height }}" autocomplete="off" />
145
+ <label for="editor-alternate-lang-{{ n }}">Language</label>
146
+ <input id="editor-alternate-lang-{{ n }}" name="{{ enclosureFields.alternate.lang }}{{ n }}" type="text" value="{{ row.lang }}" autocomplete="off" spellcheck="false" />
147
+ </fieldset>
148
+ {% endfor %}
149
+ <datalist id="editor-media-uploads">
150
+ {% for choice in enclosureChoices.media %}
151
+ <option value="{{ choice.url }}"></option>
152
+ {% endfor %}
153
+ </datalist>
154
+ </fieldset>
155
+ {% endif %}
65
156
  </div>
66
157
 
67
158
  <div class="admin-editor-side">
@@ -98,6 +189,16 @@
98
189
  <label for="editor-in-reply-to">In reply to</label>
99
190
  <input id="editor-in-reply-to" name="in-reply-to" type="url" value="{{ form.inReplyTo }}" />
100
191
  <p class="admin-hint">The address of the post this one answers. Leave it empty for a post that answers nobody.</p>
192
+
193
+ {#- The mf2 `like-of`, `repost-of` and `bookmark-of` (TASK-169): each
194
+ cites a page the way a reply does, and sends it a webmention. -#}
195
+ <label for="editor-like-of">Like of</label>
196
+ <input id="editor-like-of" name="like-of" type="url" value="{{ form.likeOf }}" />
197
+ <label for="editor-repost-of">Repost of</label>
198
+ <input id="editor-repost-of" name="repost-of" type="url" value="{{ form.repostOf }}" />
199
+ <label for="editor-bookmark-of">Bookmark of</label>
200
+ <input id="editor-bookmark-of" name="bookmark-of" type="url" value="{{ form.bookmarkOf }}" />
201
+ <p class="admin-hint">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
101
202
  {% endif %}
102
203
 
103
204
  <label for="editor-description">Description</label>
@@ -141,6 +242,25 @@
141
242
  </p>
142
243
  {% endif %}
143
244
 
245
+ {% if kind.type == "post" and syndicationTargets.length %}
246
+ {# TASK-155: the targets content/_data/syndicationTargets.json declares.
247
+ A target whose tag the post carries is selected without a tick. #}
248
+ <fieldset class="admin-editor-syndication">
249
+ <legend>Syndicate to</legend>
250
+ {% for target in syndicationTargets %}
251
+ <p class="admin-check">
252
+ <input id="editor-{{ target.field }}" name="{{ target.field }}" type="checkbox" value="1"{% if target.checked %} checked{% endif %} />
253
+ <label for="editor-{{ target.field }}">{{ target.name }}</label>
254
+ </p>
255
+ {% endfor %}
256
+ <p class="admin-hint">
257
+ Writes <code>syndicate-to:</code> into the front matter. The post links to each target and
258
+ sends it a webmention when it is published or updated. A post tagged with a target’s tag is
259
+ sent there without a tick.
260
+ </p>
261
+ </fieldset>
262
+ {% endif %}
263
+
144
264
  {# What this document says about comments, if anything. Three answers
145
265
  rather than two: the site's own rules, always open, always closed. #}
146
266
  <label for="editor-comments">Comments</label>