@geekity/cms 0.12.0 → 0.14.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 (278) hide show
  1. package/README.md +188 -9
  2. package/admin/pages/account/login.njk +3 -0
  3. package/admin/pages/documents/editor.njk +20 -0
  4. package/admin/pages/indieauth/consent.njk +51 -0
  5. package/admin/pages/indieauth/refused.njk +11 -0
  6. package/admin/pages/settings/general.njk +12 -2
  7. package/admin/pages/settings/reading.njk +93 -0
  8. package/admin/pages/users/apps.njk +74 -0
  9. package/admin/pages/users/edit.njk +12 -0
  10. package/admin/static/admin.css +27 -0
  11. package/dist/admin/accounts.d.ts +3 -0
  12. package/dist/admin/accounts.d.ts.map +1 -1
  13. package/dist/admin/accounts.js +5 -0
  14. package/dist/admin/accounts.js.map +1 -1
  15. package/dist/admin/documents.d.ts +4 -0
  16. package/dist/admin/documents.d.ts.map +1 -1
  17. package/dist/admin/documents.js +45 -5
  18. package/dist/admin/documents.js.map +1 -1
  19. package/dist/admin/federation.js +2 -2
  20. package/dist/admin/federation.js.map +1 -1
  21. package/dist/admin/headers.d.ts +9 -2
  22. package/dist/admin/headers.d.ts.map +1 -1
  23. package/dist/admin/headers.js +16 -7
  24. package/dist/admin/headers.js.map +1 -1
  25. package/dist/admin/menu.d.ts.map +1 -1
  26. package/dist/admin/menu.js +2 -1
  27. package/dist/admin/menu.js.map +1 -1
  28. package/dist/admin/routes.d.ts.map +1 -1
  29. package/dist/admin/routes.js +48 -5
  30. package/dist/admin/routes.js.map +1 -1
  31. package/dist/admin/settings-general.d.ts.map +1 -1
  32. package/dist/admin/settings-general.js +28 -3
  33. package/dist/admin/settings-general.js.map +1 -1
  34. package/dist/admin/settings-reading.d.ts.map +1 -1
  35. package/dist/admin/settings-reading.js +15 -1
  36. package/dist/admin/settings-reading.js.map +1 -1
  37. package/dist/admin/settings.d.ts +83 -4
  38. package/dist/admin/settings.d.ts.map +1 -1
  39. package/dist/admin/settings.js +166 -4
  40. package/dist/admin/settings.js.map +1 -1
  41. package/dist/admin/templates.d.ts +3 -0
  42. package/dist/admin/templates.d.ts.map +1 -1
  43. package/dist/admin/templates.js +3 -0
  44. package/dist/admin/templates.js.map +1 -1
  45. package/dist/admin/users.d.ts +8 -0
  46. package/dist/admin/users.d.ts.map +1 -1
  47. package/dist/admin/users.js +9 -0
  48. package/dist/admin/users.js.map +1 -1
  49. package/dist/config.d.ts +34 -0
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/content/pinned.d.ts +26 -0
  54. package/dist/content/pinned.d.ts.map +1 -0
  55. package/dist/content/pinned.js +42 -0
  56. package/dist/content/pinned.js.map +1 -0
  57. package/dist/content/store.d.ts +7 -0
  58. package/dist/content/store.d.ts.map +1 -1
  59. package/dist/content/store.js +20 -0
  60. package/dist/content/store.js.map +1 -1
  61. package/dist/env.d.ts +21 -0
  62. package/dist/env.d.ts.map +1 -1
  63. package/dist/federation/actor.d.ts +15 -0
  64. package/dist/federation/actor.d.ts.map +1 -1
  65. package/dist/federation/actor.js +94 -3
  66. package/dist/federation/actor.js.map +1 -1
  67. package/dist/federation/article.d.ts +13 -1
  68. package/dist/federation/article.d.ts.map +1 -1
  69. package/dist/federation/article.js +45 -6
  70. package/dist/federation/article.js.map +1 -1
  71. package/dist/federation/delivery.d.ts.map +1 -1
  72. package/dist/federation/delivery.js +36 -4
  73. package/dist/federation/delivery.js.map +1 -1
  74. package/dist/federation/federation.d.ts.map +1 -1
  75. package/dist/federation/federation.js +24 -2
  76. package/dist/federation/federation.js.map +1 -1
  77. package/dist/federation/mount.js +2 -2
  78. package/dist/federation/mount.js.map +1 -1
  79. package/dist/federation/paths.d.ts +30 -0
  80. package/dist/federation/paths.d.ts.map +1 -1
  81. package/dist/federation/paths.js +36 -0
  82. package/dist/federation/paths.js.map +1 -1
  83. package/dist/images/icons.d.ts +44 -25
  84. package/dist/images/icons.d.ts.map +1 -1
  85. package/dist/images/icons.js +205 -60
  86. package/dist/images/icons.js.map +1 -1
  87. package/dist/images/index.d.ts +2 -2
  88. package/dist/images/index.d.ts.map +1 -1
  89. package/dist/images/index.js +1 -1
  90. package/dist/images/index.js.map +1 -1
  91. package/dist/images/markup.d.ts +14 -0
  92. package/dist/images/markup.d.ts.map +1 -1
  93. package/dist/images/markup.js +16 -11
  94. package/dist/images/markup.js.map +1 -1
  95. package/dist/images/variants.d.ts +11 -0
  96. package/dist/images/variants.d.ts.map +1 -1
  97. package/dist/images/variants.js +24 -4
  98. package/dist/images/variants.js.map +1 -1
  99. package/dist/index.d.ts +15 -5
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +33 -2
  102. package/dist/index.js.map +1 -1
  103. package/dist/indexnow.d.ts +34 -0
  104. package/dist/indexnow.d.ts.map +1 -0
  105. package/dist/indexnow.js +141 -0
  106. package/dist/indexnow.js.map +1 -0
  107. package/dist/indieauth/bearer.d.ts +39 -0
  108. package/dist/indieauth/bearer.d.ts.map +1 -0
  109. package/dist/indieauth/bearer.js +63 -0
  110. package/dist/indieauth/bearer.js.map +1 -0
  111. package/dist/indieauth/client.d.ts +61 -0
  112. package/dist/indieauth/client.d.ts.map +1 -0
  113. package/dist/indieauth/client.js +139 -0
  114. package/dist/indieauth/client.js.map +1 -0
  115. package/dist/indieauth/connected-apps.d.ts +23 -0
  116. package/dist/indieauth/connected-apps.d.ts.map +1 -0
  117. package/dist/indieauth/connected-apps.js +73 -0
  118. package/dist/indieauth/connected-apps.js.map +1 -0
  119. package/dist/indieauth/consent.d.ts +28 -0
  120. package/dist/indieauth/consent.d.ts.map +1 -0
  121. package/dist/indieauth/consent.js +185 -0
  122. package/dist/indieauth/consent.js.map +1 -0
  123. package/dist/indieauth/discovery.d.ts +87 -0
  124. package/dist/indieauth/discovery.d.ts.map +1 -0
  125. package/dist/indieauth/discovery.js +127 -0
  126. package/dist/indieauth/discovery.js.map +1 -0
  127. package/dist/indieauth/grants.d.ts +57 -0
  128. package/dist/indieauth/grants.d.ts.map +1 -0
  129. package/dist/indieauth/grants.js +35 -0
  130. package/dist/indieauth/grants.js.map +1 -0
  131. package/dist/indieauth/identity.d.ts +36 -0
  132. package/dist/indieauth/identity.d.ts.map +1 -0
  133. package/dist/indieauth/identity.js +77 -0
  134. package/dist/indieauth/identity.js.map +1 -0
  135. package/dist/indieauth/redeem.d.ts +47 -0
  136. package/dist/indieauth/redeem.d.ts.map +1 -0
  137. package/dist/indieauth/redeem.js +69 -0
  138. package/dist/indieauth/redeem.js.map +1 -0
  139. package/dist/indieauth/request.d.ts +47 -0
  140. package/dist/indieauth/request.d.ts.map +1 -0
  141. package/dist/indieauth/request.js +104 -0
  142. package/dist/indieauth/request.js.map +1 -0
  143. package/dist/indieauth/token.d.ts +19 -0
  144. package/dist/indieauth/token.d.ts.map +1 -0
  145. package/dist/indieauth/token.js +110 -0
  146. package/dist/indieauth/token.js.map +1 -0
  147. package/dist/indieauth/tokens.d.ts +131 -0
  148. package/dist/indieauth/tokens.d.ts.map +1 -0
  149. package/dist/indieauth/tokens.js +205 -0
  150. package/dist/indieauth/tokens.js.map +1 -0
  151. package/dist/web/admin-bar.d.ts +3 -1
  152. package/dist/web/admin-bar.d.ts.map +1 -1
  153. package/dist/web/admin-bar.js +5 -1
  154. package/dist/web/admin-bar.js.map +1 -1
  155. package/dist/web/archive.d.ts +1 -1
  156. package/dist/web/archive.d.ts.map +1 -1
  157. package/dist/web/archive.js +3 -2
  158. package/dist/web/archive.js.map +1 -1
  159. package/dist/web/authors.d.ts +19 -0
  160. package/dist/web/authors.d.ts.map +1 -1
  161. package/dist/web/authors.js +25 -0
  162. package/dist/web/authors.js.map +1 -1
  163. package/dist/web/context.d.ts +25 -0
  164. package/dist/web/context.d.ts.map +1 -1
  165. package/dist/web/context.js +18 -0
  166. package/dist/web/context.js.map +1 -1
  167. package/dist/web/feed-atom.d.ts.map +1 -1
  168. package/dist/web/feed-atom.js +5 -2
  169. package/dist/web/feed-atom.js.map +1 -1
  170. package/dist/web/feed-item.d.ts +24 -6
  171. package/dist/web/feed-item.d.ts.map +1 -1
  172. package/dist/web/feed-item.js +16 -8
  173. package/dist/web/feed-item.js.map +1 -1
  174. package/dist/web/feed-json.d.ts +6 -2
  175. package/dist/web/feed-json.d.ts.map +1 -1
  176. package/dist/web/feed-json.js +6 -2
  177. package/dist/web/feed-json.js.map +1 -1
  178. package/dist/web/feed-rss.d.ts.map +1 -1
  179. package/dist/web/feed-rss.js +17 -4
  180. package/dist/web/feed-rss.js.map +1 -1
  181. package/dist/web/feed-source.d.ts +31 -0
  182. package/dist/web/feed-source.d.ts.map +1 -1
  183. package/dist/web/feed-source.js +31 -0
  184. package/dist/web/feed-source.js.map +1 -1
  185. package/dist/web/feed-xml.d.ts +2 -0
  186. package/dist/web/feed-xml.d.ts.map +1 -1
  187. package/dist/web/feed-xml.js +2 -0
  188. package/dist/web/feed-xml.js.map +1 -1
  189. package/dist/web/feeds.d.ts.map +1 -1
  190. package/dist/web/feeds.js +15 -3
  191. package/dist/web/feeds.js.map +1 -1
  192. package/dist/web/index.d.ts +4 -2
  193. package/dist/web/index.d.ts.map +1 -1
  194. package/dist/web/index.js +3 -2
  195. package/dist/web/index.js.map +1 -1
  196. package/dist/web/indexnow.d.ts +9 -0
  197. package/dist/web/indexnow.d.ts.map +1 -0
  198. package/dist/web/indexnow.js +27 -0
  199. package/dist/web/indexnow.js.map +1 -0
  200. package/dist/web/llms.d.ts +58 -0
  201. package/dist/web/llms.d.ts.map +1 -0
  202. package/dist/web/llms.js +91 -0
  203. package/dist/web/llms.js.map +1 -0
  204. package/dist/web/locale.d.ts +41 -0
  205. package/dist/web/locale.d.ts.map +1 -0
  206. package/dist/web/locale.js +56 -0
  207. package/dist/web/locale.js.map +1 -0
  208. package/dist/web/manifest.d.ts +50 -0
  209. package/dist/web/manifest.d.ts.map +1 -0
  210. package/dist/web/manifest.js +56 -0
  211. package/dist/web/manifest.js.map +1 -0
  212. package/dist/web/negotiate.d.ts.map +1 -1
  213. package/dist/web/negotiate.js +6 -0
  214. package/dist/web/negotiate.js.map +1 -1
  215. package/dist/web/oembed.d.ts +104 -0
  216. package/dist/web/oembed.d.ts.map +1 -0
  217. package/dist/web/oembed.js +290 -0
  218. package/dist/web/oembed.js.map +1 -0
  219. package/dist/web/opensearch.d.ts +35 -0
  220. package/dist/web/opensearch.d.ts.map +1 -0
  221. package/dist/web/opensearch.js +65 -0
  222. package/dist/web/opensearch.js.map +1 -0
  223. package/dist/web/render.d.ts +6 -1
  224. package/dist/web/render.d.ts.map +1 -1
  225. package/dist/web/render.js +40 -10
  226. package/dist/web/render.js.map +1 -1
  227. package/dist/web/robots.d.ts +89 -0
  228. package/dist/web/robots.d.ts.map +1 -0
  229. package/dist/web/robots.js +259 -0
  230. package/dist/web/robots.js.map +1 -0
  231. package/dist/web/routes.d.ts.map +1 -1
  232. package/dist/web/routes.js +179 -8
  233. package/dist/web/routes.js.map +1 -1
  234. package/dist/web/share-image.d.ts +51 -0
  235. package/dist/web/share-image.d.ts.map +1 -0
  236. package/dist/web/share-image.js +43 -0
  237. package/dist/web/share-image.js.map +1 -0
  238. package/dist/web/sitemap.d.ts +0 -26
  239. package/dist/web/sitemap.d.ts.map +1 -1
  240. package/dist/web/sitemap.js +0 -44
  241. package/dist/web/sitemap.js.map +1 -1
  242. package/dist/web/templates.d.ts +16 -9
  243. package/dist/web/templates.d.ts.map +1 -1
  244. package/dist/web/templates.js +88 -55
  245. package/dist/web/templates.js.map +1 -1
  246. package/dist/web/themes.d.ts +33 -0
  247. package/dist/web/themes.d.ts.map +1 -1
  248. package/dist/web/themes.js +57 -0
  249. package/dist/web/themes.js.map +1 -1
  250. package/dist/webmention/discovery.d.ts +4 -2
  251. package/dist/webmention/discovery.d.ts.map +1 -1
  252. package/dist/webmention/discovery.js +14 -8
  253. package/dist/webmention/discovery.js.map +1 -1
  254. package/dist/webmention/fetch-public.d.ts +2 -0
  255. package/dist/webmention/fetch-public.d.ts.map +1 -1
  256. package/dist/webmention/fetch-public.js +1 -1
  257. package/dist/webmention/fetch-public.js.map +1 -1
  258. package/package.json +2 -1
  259. package/templates/site/content/_data/site.json +0 -1
  260. package/themes/default/README.md +174 -43
  261. package/themes/default/layouts/base.njk +116 -17
  262. package/themes/default/layouts/front-page.njk +7 -1
  263. package/themes/default/layouts/home.njk +11 -0
  264. package/themes/default/layouts/page.njk +4 -3
  265. package/themes/default/layouts/post.njk +5 -4
  266. package/themes/default/layouts/search.njk +4 -5
  267. package/themes/default/partials/archive.njk +3 -1
  268. package/themes/default/partials/bio.njk +16 -2
  269. package/themes/default/partials/breadcrumbs.njk +20 -0
  270. package/themes/default/partials/conversation.njk +4 -1
  271. package/themes/default/partials/jsonld.njk +49 -9
  272. package/themes/default/partials/kicker.njk +6 -4
  273. package/themes/default/partials/lang.njk +7 -0
  274. package/themes/default/partials/post-list.njk +5 -3
  275. package/themes/default/partials/reply-context.njk +4 -2
  276. package/themes/default/src/style.css +14 -0
  277. package/themes/default/static/style.css +57 -0
  278. package/themes/default/theme.json +2 -0
package/README.md CHANGED
@@ -370,6 +370,7 @@ the same directory reads all of it, and everything in it is meant to be public:
370
370
  | `data/mail.json` | The mail credential: a Brevo API key, an SMTP connection, or both. Mode `0600`. |
371
371
  | `data/notification-secret` | What signs the one-click links in a notification. Mode `0600`. Losing it kills every link already in an inbox and nothing else. |
372
372
  | `data/comment-optouts.json` | The addresses that have unsubscribed from reply notices. Mode `0600`. |
373
+ | `data/indieauth-tokens.json` | A SHA-256 hash of each IndieAuth access and refresh token, with its user, app, scopes and expiry (decision-24). Mode `0600`. |
373
374
  | `data/notification-digests.json` | When each user was last sent a digest. Mode `0600`. Losing it sends one digest early and nothing worse. |
374
375
  | `data/wordpress-activitypub.json` | When each WordPress compatibility path was last asked for. Losing it resets the answer the switch is watched by. |
375
376
 
@@ -535,6 +536,12 @@ arrives as its title, its excerpt and a link, so it sends `name` and a
535
536
  would show a `summary` as a content warning, so a note sends neither and puts a
536
537
  title its text does not open with at the top of its content.
537
538
 
539
+ Both carry their text twice: `content` as it always was, and `contentMap` keyed
540
+ by the post's language, with `summaryMap` beside an `Article`'s `summary`. The
541
+ language is the post's `lang` front matter when it names one, and the site's
542
+ `language` otherwise, so a client can filter and translate by it. Mastodon
543
+ reads a status's language from that key.
544
+
538
545
  An author can choose the type for one post in its front matter:
539
546
 
540
547
  ```yaml
@@ -1417,6 +1424,7 @@ says otherwise.
1417
1424
  | Other actors who liked, boosted, answered or quoted: handle, display name, avatar URL, profile URL | `data/geekity.db`, fetched from their actor document | A cache. Refetched weekly while the inbox log names them. |
1418
1425
  | Remote avatars, shrunk | `data/avatars/` | Deleted by the avatar sweep once nothing shown names them. |
1419
1426
  | Users: username, email, argon2id password hash, profile | `data/users.json`, mode `0600` | Until the user is deleted. |
1427
+ | The apps a user signed in to with IndieAuth: app, scopes, hashes of its tokens, when they expire | `data/indieauth-tokens.json`, mode `0600` | Until the user is deleted, or 60 days after the app last refreshed. |
1420
1428
  | An index of all of the above, and sessions, reset tokens and spent link tokens | `data/geekity.db` | A cache of the files. Sessions and tokens are pruned when they expire. |
1421
1429
  | Client addresses in the rate limits | Memory | Until the window passes or the site restarts. |
1422
1430
  | Client addresses in the access log | stdout, and whatever collects it | Only with `accessLogAddress` on. The collector keeps them. |
@@ -1686,7 +1694,7 @@ shadow the login form.
1686
1694
  | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1687
1695
  | `/admin/tools` | Tools > Content index: what the index holds, and the button that rebuilds it. |
1688
1696
  | `/admin/tools/rebuild-index` | `POST` only. Offers the rebuild, then reads every file again on the live site. |
1689
- | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language. |
1697
+ | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language, locale. |
1690
1698
  | `/admin/settings/reading` | What the homepage displays, posts per page, the notify server. |
1691
1699
  | `/admin/settings/permalinks` | The tag and category bases, and the archive redirects already recorded. |
1692
1700
  | `/admin/settings/discussion` | Comments and the closing window, webmentions, and the Akismet key. |
@@ -1836,10 +1844,11 @@ can get between. So a reader — an Eleventy build, the CMS's own site data
1836
1844
  source, another process entirely — always sees one whole version, and two saves
1837
1845
  at once cannot each keep half of what the other kept.
1838
1846
 
1839
- The file carries `title`, `tagline`, `url`, `author`, `postsPerPage`,
1840
- `homepage`, `postsPage`,
1841
- `timezone`, `language`, `tagBase`,
1842
- `categoryBase`, `notifyServer`, `webmentionsSend`, `webmentionsReceive`,
1847
+ The file carries `title`, `tagline`, `url`, `author` (a username, when the
1848
+ site has one author), `postsPerPage`, `homepage`, `postsPage`,
1849
+ `timezone`, `language`, `locale` (when it is set), `tagBase`,
1850
+ `categoryBase`, `notifyServer`, `feedUpdatePeriod`, `feedUpdateFrequency`,
1851
+ `webmentionsSend`, `webmentionsReceive`,
1843
1852
  `mailProvider`, `mailFromName`, `mailFromAddress`, `mailReplyTo`,
1844
1853
  `contactEmail`, `securityContacts`, `securityPolicy`, `securityLanguages`,
1845
1854
  `relays`, `menus` and `taxonomyRedirects`,
@@ -2214,6 +2223,15 @@ name another host. A theme that signs readers in or takes payment through a
2214
2223
  popup on another origin needs `same-origin-allow-popups` for COOP. The CMS
2215
2224
  itself does neither.
2216
2225
 
2226
+ One response is framed elsewhere on purpose: the embed view at
2227
+ `/_geekity/embed?url=…`, the card that the oEmbed `html` frames so that
2228
+ WordPress keeps it. It sends a complete policy of its own instead of the
2229
+ default: `frame-ancestors *`, `default-src 'none'`, and its one inline style and
2230
+ one inline script by hash. It never sends `X-Frame-Options`, even one set in
2231
+ `securityHeaders`. It shows nothing from a session, so a signed-in user who is
2232
+ framed sees the same card as anyone else. Every other response keeps the
2233
+ framing headers above.
2234
+
2217
2235
  A site changes them with `securityHeaders` in its config. Names are matched
2218
2236
  without regard to case. A string replaces a default or adds a header of the
2219
2237
  site's own, and `false` removes a default:
@@ -2354,6 +2372,11 @@ Booting mounts the public site on the app. The routes are:
2354
2372
  | `/sitemap.xml` | Every public URL, for a search engine. |
2355
2373
  | `/sitemap-{n}.xml` | One file of a sitemap too big to be a single one. |
2356
2374
  | `/robots.txt` | What a crawler may have, and where the sitemap is. |
2375
+ | `/opensearch.xml` | The OpenSearch description, so a browser can offer the site's search from its address bar. |
2376
+ | `/_geekity/oembed?url=…` | The oEmbed card of the post or page at `url`, as JSON or with `format=xml`; a 404 for anything else. |
2377
+ | `/_geekity/embed?url=…` | The same card as a page any site may frame, which the oEmbed `html` puts in an iframe; a 404 for anything else. |
2378
+ | `/llms.txt` | The site's pages and recent posts for a language model, each linked to its Markdown. See [below](#llmstxt). |
2379
+ | `/{key}.txt` | The IndexNow key, while the site has IndexNow on. See [below](#indexnow). |
2357
2380
  | `/.well-known/security.txt` | Where to report a vulnerability; a 404 until a security contact is set. See [above](#securitytxt-and-change-password). |
2358
2381
  | `/.well-known/change-password` | A redirect to the signed-in user's change-password form, or to the login form. |
2359
2382
  | `/_geekity/comments` | `POST` only. Where the comment form under a post submits. |
@@ -2705,6 +2728,10 @@ without changing the document, so a development server would otherwise answer
2705
2728
  `304` with a page that had already moved on. The `.md` and `.json`
2706
2729
  representations are validated either way.
2707
2730
 
2731
+ Every representation but the HTML carries `X-Robots-Tag: noindex`, whether it
2732
+ was asked for by extension or by `Accept`. They say what the page says, and a
2733
+ search engine that indexed them would list one page two or three times.
2734
+
2708
2735
  Adding a representation — an ActivityStreams object, say — means adding it to
2709
2736
  `Representation` in `src/web/negotiate.ts` with its media type and, if it wants
2710
2737
  one, its extension. The selection, the `Link` alternates, the `ETag` and the
@@ -2745,9 +2772,19 @@ more than an archive page, so a reader that polls once a day does not miss a
2745
2772
  post on a site that publishes several.
2746
2773
 
2747
2774
  ```json
2748
- { "title": "My Site", "tagline": "Notes", "author": "Me", "feedSize": 20 }
2775
+ { "title": "My Site", "tagline": "Notes", "author": "me", "feedSize": 20 }
2749
2776
  ```
2750
2777
 
2778
+ Every RSS feed, the comments feeds included, tells a reader how often to poll
2779
+ it with the Syndication module's `sy:updatePeriod` and `sy:updateFrequency`.
2780
+ Settings → Reading sets both, as `feedUpdatePeriod` (`hourly`, `daily`,
2781
+ `weekly`, `monthly` or `yearly`) and `feedUpdateFrequency` (a whole number, 1 or
2782
+ more) in `site.json`. The default is `hourly` and `1`, which is what WordPress
2783
+ declares, so the subscribers of a migrated site keep polling as they did. A
2784
+ reader may back off from a feed that stays quiet much longer than it says, so
2785
+ a site that posts weekly does better to say `weekly`. Atom and JSON Feed have no
2786
+ element for it.
2787
+
2751
2788
  ### What every format says about a post
2752
2789
 
2753
2790
  The three formats render one **feed item**, derived once per post, rather than
@@ -2802,6 +2839,13 @@ post that is not a reply, or whose `in-reply-to` is not such a URL, carries
2802
2839
  neither. RSS 2.0 has no equivalent element, so an RSS item for a reply is
2803
2840
  written the same as any other.
2804
2841
 
2842
+ **Languages.** Every feed declares the site's `language` once: RSS as the
2843
+ channel's `language`, Atom as the feed's `xml:lang` and JSON Feed as its
2844
+ `language`. A post whose `lang` front matter names another language says so on
2845
+ its own item: RSS as `dc:language`, Atom as `xml:lang` on the `entry` and JSON
2846
+ Feed as the item's `language`. A post in the site's language, or whose `lang`
2847
+ is not a language tag, adds nothing.
2848
+
2805
2849
  ### RSS 2.0
2806
2850
 
2807
2851
  The channel carries `title`, `link`, `description` (the tagline), `language`
@@ -3096,7 +3140,7 @@ holding its own slice and dated by the newest URL in it. The address a search
3096
3140
  engine holds does not change, which is the point of the index living there.
3097
3141
  While the whole sitemap fits in one file the children name nothing and 404.
3098
3142
 
3099
- `/robots.txt` is short:
3143
+ `/robots.txt` starts short:
3100
3144
 
3101
3145
  ```
3102
3146
  User-agent: *
@@ -3111,6 +3155,44 @@ fetched, they carry the same
3111
3155
  content as the pages that link to them, and a crawler that follows one gets
3112
3156
  JSON it will ignore.
3113
3157
 
3158
+ The Crawlers section of Settings → Reading adds to it, and writes what it sets
3159
+ to `site.json`:
3160
+
3161
+ | Setting | `site.json` key | What the file gains |
3162
+ | --------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3163
+ | AI crawlers | `aiCrawlers` | `allow` (the default) adds nothing. `block-training` adds a `Disallow: /` group for each training crawler: GPTBot, ClaudeBot, anthropic-ai, Google-Extended, Applebot-Extended, Bytespider, CCBot and meta-externalagent. `block-all` also blocks the retrieval crawlers: OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User, Claude-SearchBot, Claude-User, meta-webindexer and meta-externalfetcher. |
3164
+ | Content signals | `contentSignalSearch`, `contentSignalAiInput`, `contentSignalAiTrain` | Each `yes`, `no` or empty. The ones set become one line, such as `Content-Signal: search=yes, ai-input=yes, ai-train=no`. |
3165
+ | More robots.txt rules | `robotsRules` | Groups of the site's own, one line per array entry: `User-agent`, then `Allow`, `Disallow`, `Crawl-delay`, `Content-Signal` or `Sitemap` lines. |
3166
+
3167
+ A crawler obeys only the one group that names it (RFC 9309), so the CMS puts
3168
+ `Disallow: /admin/` and the content signal into every group, its own and the
3169
+ site's, unless the group already disallows everything or carries a signal of
3170
+ its own. The form refuses a line that is not a robots.txt field, a rule with no
3171
+ `User-agent` above it, and an `Allow` under `/admin/`; a hand edit of
3172
+ `site.json` that does any of these loses that line. The site's own `Sitemap:`
3173
+ line is always there.
3174
+
3175
+ For example, `block-training`, `ai-train` set to `no` and a rule of
3176
+ `User-agent: SlowBot` / `Crawl-delay: 10` serve:
3177
+
3178
+ ```
3179
+ User-agent: *
3180
+ Disallow: /admin/
3181
+ Content-Signal: ai-train=no
3182
+
3183
+ User-agent: GPTBot
3184
+ Disallow: /
3185
+
3186
+ …one group per training crawler…
3187
+
3188
+ User-agent: SlowBot
3189
+ Disallow: /admin/
3190
+ Crawl-delay: 10
3191
+ Content-Signal: ai-train=no
3192
+
3193
+ Sitemap: https://example.com/sitemap.xml
3194
+ ```
3195
+
3114
3196
  Both are registered routes rather than anything resolved from the content
3115
3197
  index, so a document permalinked at `/sitemap.xml` cannot take the URL a
3116
3198
  search engine polls, and both carry an `ETag` and answer a conditional request
@@ -3120,6 +3202,102 @@ robots without one, because nothing dates it.
3120
3202
  Nothing links the sitemap from a page: `robots.txt` names it, which is where a
3121
3203
  crawler looks.
3122
3204
 
3205
+ ## llms.txt
3206
+
3207
+ `/llms.txt` describes the site for a language model, in the
3208
+ [llms.txt](https://llmstxt.org/) format. It holds the site's title as a heading
3209
+ and its tagline as a quote. Then it lists every public page by title and the
3210
+ posts the feeds carry, newest first. Each entry links to the document's
3211
+ Markdown, which is what a model reads best:
3212
+
3213
+ ```markdown
3214
+ # Field Notes
3215
+
3216
+ > Notes from the field.
3217
+
3218
+ ## Pages
3219
+
3220
+ - [About](https://example.com/about/index.md): Who writes this.
3221
+
3222
+ ## Recent posts
3223
+
3224
+ - [Third post](https://example.com/third/index.md)
3225
+ ```
3226
+
3227
+ A document's `description` follows its link. A static homepage is listed at
3228
+ `/index.md`, because `/` is where it is published. The file is served as
3229
+ `text/markdown` and carries an `ETag` and a `Last-Modified`, the newest date
3230
+ among its entries. It answers a conditional request with 304, as the sitemap
3231
+ does.
3232
+
3233
+ The home page advertises the file twice. The response carries
3234
+ `Link: </llms.txt>; rel="describedby"; type="text/markdown"`, for a tool that
3235
+ reads no HTML. The default theme's head carries
3236
+ `<link rel="describedby" type="text/markdown" href="/llms.txt">`. A theme
3237
+ gets the path as `llmsTxt` while the site serves the file.
3238
+
3239
+ To write the file yourself, put it at `content/llms.txt`. The CMS serves it
3240
+ exactly as written in place of the generated one, dated by the file. To serve
3241
+ no file at all, clear **Serve /llms.txt** under Settings → Reading. This
3242
+ writes `"llmsTxt": false` to `site.json`. `/llms.txt` then answers 404, even
3243
+ when `content/llms.txt` exists, and the home page stops advertising it.
3244
+
3245
+ ## IndexNow
3246
+
3247
+ [IndexNow](https://www.indexnow.org/) gets a changed URL recrawled by Bing,
3248
+ Yandex, Naver, Seznam and the other engines that share it within minutes,
3249
+ instead of on their next visit. It is off by default, because it sends the
3250
+ site's URLs to a third party. To turn it on, select **Submit changes to
3251
+ IndexNow** under Settings → Reading.
3252
+
3253
+ The first time you turn it on, the CMS generates a key of 32 hex digits and
3254
+ writes it to `site.json` as `indexNowKey`, beside `"indexNow": true`. The site
3255
+ then serves the key at `/{key}.txt` as plain text, which is how an engine
3256
+ checks that the submissions are yours. Turning IndexNow off takes the file away
3257
+ and keeps the key, so turning it back on reuses the key the engines already
3258
+ checked. To change the key, write a new one of 8 to 128 letters, digits or
3259
+ dashes to `indexNowKey`. A key that does not fit that pattern counts as none,
3260
+ and the site sends nothing.
3261
+
3262
+ When a post or a page is published, changed, moved or deleted, the CMS submits
3263
+ its URL. A move submits the old URL and the new one. Unpublishing, trashing and
3264
+ deleting submit the URL that went away. The changes of the next ten seconds go
3265
+ out together as one `POST` to `https://api.indexnow.org/indexnow`:
3266
+
3267
+ ```json
3268
+ {
3269
+ "host": "example.com",
3270
+ "key": "0123456789abcdef0123456789abcdef",
3271
+ "keyLocation": "https://example.com/0123456789abcdef0123456789abcdef.txt",
3272
+ "urlList": ["https://example.com/hello/"]
3273
+ }
3274
+ ```
3275
+
3276
+ A network failure, a 429 or a 5xx answer is tried again after ten seconds, then
3277
+ after forty. Any other refusal, such as a 403 for a key the engine could not
3278
+ fetch, is not. A batch that still fails is logged as a warning, and the post
3279
+ stays published.
3280
+
3281
+ Nothing is sent for a draft or a scheduled post, for the boot scan, or while
3282
+ the base URL's host is local: `localhost`, a `.local` name, or a private IP
3283
+ address.
3284
+
3285
+ A site can change how the CMS sends with `indexNow` in its config. Every field
3286
+ is optional:
3287
+
3288
+ ```ts
3289
+ export default defineConfig({
3290
+ indexNow: {
3291
+ fetch, // what submits the URLs; a test names one
3292
+ endpoint: 'https://www.bing.com/indexnow',
3293
+ batchMs: 10_000,
3294
+ attempts: 3,
3295
+ backoffMs: (attempt) => attempt * attempt * 10_000,
3296
+ logger: console,
3297
+ },
3298
+ });
3299
+ ```
3300
+
3123
3301
  ## Theme overrides
3124
3302
 
3125
3303
  A site's themes live one directory per theme under `themesDir` (`themes/` by
@@ -3199,8 +3377,9 @@ Tailwind. Every colour is a `--color-*` custom property a site can override.
3199
3377
  `base.njk` is its shell: a skip link, a `.global-wrapper` carrying `data-is-root-path="true"`
3200
3378
  at `/` only, a header that is the site title and tagline on the front page and a
3201
3379
  small link home on every other — with `menus.primary` inside it on every page
3202
- — `<main id="main">`, and a footer with the copyright year, the site author,
3203
- the colophon and `menus.footer`. The footer reads nothing off an account: it
3380
+ — `<main id="main">`, and a footer with the copyright year, the site author's
3381
+ display name (the site title on a site with several authors), the colophon and
3382
+ `menus.footer`. The footer reads nothing off an account: it
3204
3383
  used to print an RSS link and one `rel="me"` link per link on the site author's
3205
3384
  profile, and a site that wants either types it into its footer menu. Nothing
3206
3385
  particular to one site is in it — webrings and badges belong in a site theme's
@@ -12,6 +12,9 @@
12
12
 
13
13
  <form method="post" action="{{ loginUrl }}">
14
14
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
15
+ {% if returnTo %}
16
+ <input type="hidden" name="return_to" value="{{ returnTo }}" />
17
+ {% endif %}
15
18
 
16
19
  {{ field.text('username', 'username', 'Username', username,
17
20
  autocomplete='username', required=true, autofocus=true) }}
@@ -103,6 +103,13 @@
103
103
  <label for="editor-description">Description</label>
104
104
  <input id="editor-description" name="description" type="text" value="{{ form.description }}" />
105
105
 
106
+ {#- The `lang` front matter (TASK-154): a post or page written in another
107
+ language than the site's says so, for screen readers, translation
108
+ and fediverse language filters. -#}
109
+ <label for="editor-lang">Language</label>
110
+ <input id="editor-lang" name="lang" type="text" value="{{ form.lang }}" autocomplete="off" spellcheck="false" aria-describedby="editor-lang-hint" />
111
+ <p class="admin-hint" id="editor-lang-hint">A language tag, such as fr or pt-BR. Leave it empty for the site’s language, {{ siteLanguage }}.</p>
112
+
106
113
  {# Who the front matter says wrote this (TASK-67). A select of the site's
107
114
  users rather than a free box, because after decision-14 the value is a
108
115
  person rather than a string: it decides whose archive the post is on
@@ -121,6 +128,19 @@
121
128
  <label for="editor-draft">Draft</label>
122
129
  </p>
123
130
 
131
+ {% if kind.type == "post" %}
132
+ {# TASK-207: shown at the top of the author's fediverse profile, from
133
+ the featured collection their actor links. #}
134
+ <p class="admin-check">
135
+ <input id="editor-pinned" name="pinned" type="checkbox" value="1"{% if form.pinned %} checked{% endif %} />
136
+ <label for="editor-pinned">Pinned</label>
137
+ </p>
138
+ <p class="admin-hint">
139
+ Writes <code>pinned:</code> and the moment you pinned it into the front matter. Pinned posts
140
+ show first on your fediverse profile, newest pin first, up to five.
141
+ </p>
142
+ {% endif %}
143
+
124
144
  {# What this document says about comments, if anything. Three answers
125
145
  rather than two: the site's own rules, always open, always closed. #}
126
146
  <label for="editor-comments">Comments</label>
@@ -0,0 +1,51 @@
1
+ {# The IndieAuth consent screen (TASK-158): an app wants to know who the
2
+ signed-in person is. A bare page like the login form, because the person
3
+ arrived from another site and has one thing to decide here. #}
4
+ {% extends "layouts/base.njk" %}
5
+ {% import "components/fields.njk" as field %}
6
+
7
+ {% block title %}Sign in to {{ client.name }}{% endblock %}
8
+
9
+ {% block content %}
10
+ {# A data: URI the server fetched, never the client's URL (TASK-190). #}
11
+ {% if client.logo %}
12
+ <img class="admin-consent-logo" src="{{ client.logo }}" alt="" width="64" height="64" />
13
+ {% endif %}
14
+ <h1>Sign in to {{ client.name }}?</h1>
15
+
16
+ <dl class="admin-consent">
17
+ <dt>App</dt>
18
+ <dd>{{ client.name }}{% if client.named %} <span class="admin-hint">({{ client.id }})</span>{% endif %}</dd>
19
+ <dt>Sends you back to</dt>
20
+ <dd>{{ redirectHost }}</dd>
21
+ <dt>Signs you in as</dt>
22
+ <dd>{{ identity }}</dd>
23
+ {% if resource %}
24
+ <dt>For use at</dt>
25
+ <dd>{{ resource }}</dd>
26
+ {% endif %}
27
+ </dl>
28
+
29
+ {% if loopback %}
30
+ <p class="admin-hint">This app runs on your own computer or network. Approve only if you started this sign-in yourself.</p>
31
+ {% endif %}
32
+
33
+ <form method="post" action="{{ consentUrl }}">
34
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
35
+ <input type="hidden" name="request" value="{{ pending }}" />
36
+
37
+ {% if scopes | length %}
38
+ <fieldset>
39
+ <legend>It also asks for</legend>
40
+ {% for scope in scopes %}
41
+ {{ field.checkbox('scope-' ~ scope.name, 'scope', scope.label, checked=true, value=scope.name) }}
42
+ {% endfor %}
43
+ </fieldset>
44
+ {% else %}
45
+ <p>It asks only who you are.</p>
46
+ {% endif %}
47
+
48
+ <button type="submit" name="decision" value="approve">Approve</button>
49
+ <button type="submit" name="decision" value="deny">Deny</button>
50
+ </form>
51
+ {% endblock %}
@@ -0,0 +1,11 @@
1
+ {# A sign-in request that cannot go ahead (TASK-158). Shown rather than
2
+ redirected, because the fault is in the address it would redirect to. #}
3
+ {% extends "layouts/base.njk" %}
4
+
5
+ {% block title %}Sign-in refused{% endblock %}
6
+
7
+ {% block content %}
8
+ <h1>This sign-in cannot go ahead</h1>
9
+ <p>{{ message }}</p>
10
+ <p>Go back to the app and try again. If it keeps happening, the app may be set up wrongly.</p>
11
+ {% endblock %}
@@ -7,6 +7,7 @@
7
7
  {{ field.problem('settings-base-url', problems.baseUrl) }}
8
8
  {{ field.problem('settings-timezone', problems.timezone) }}
9
9
  {{ field.problem('settings-language', problems.language) }}
10
+ {{ field.problem('settings-locale', problems.locale) }}
10
11
  {% endblock %}
11
12
 
12
13
  {% block settingsFields %}
@@ -16,8 +17,13 @@
16
17
  {{ field.text('settings-tagline', fields.tagline, 'Tagline', form.tagline,
17
18
  hint='One line, shown under the title and used as the feed subtitle.') }}
18
19
 
19
- {{ field.text('settings-author', fields.author, 'Author', form.author,
20
- hint='The name on the feeds.') }}
20
+ {% set authorHint %}A user makes this their blog: the homepage shows their bio and links, claims their author archive and profiles with <code>rel="me"</code>, and is their IndieAuth sign-in URL. The feeds and the footer credit them by name. Several authors credits the site title instead.{% endset %}
21
+ {% call field.select('settings-author', fields.author, 'Site author', hint=authorHint) %}
22
+ <option value=""{% if not form.author %} selected{% endif %}>Several authors</option>
23
+ {% for choice in authorChoices %}
24
+ <option value="{{ choice.username }}"{% if choice.username == form.author %} selected{% endif %}>{{ choice.name }}</option>
25
+ {% endfor %}
26
+ {% endcall %}
21
27
 
22
28
  {% set baseUrlHint %}In effect: <code>{{ baseUrlInEffect }}</code>. {{ baseUrlNote }}{% endset %}
23
29
  {{ field.text('settings-base-url', fields.baseUrl, 'Base URL', form.baseUrl,
@@ -31,6 +37,10 @@
31
37
  {% set languageHint %}A BCP 47 tag, such as <code>en</code> or <code>en-GB</code>. It is the page's <code>lang</code> and the feeds' language.{% endset %}
32
38
  {{ field.text('settings-language', fields.language, 'Language', form.language,
33
39
  error=problems.language, hint=languageHint, required=true) }}
40
+
41
+ {% set localeHint %}How the public site writes dates and counts, such as <code>en-US</code> for <code>September 2, 2026</code>. Leave it empty to follow the language.{% endset %}
42
+ {{ field.text('settings-locale', fields.locale, 'Locale', form.locale,
43
+ error=problems.locale, hint=localeHint) }}
34
44
  {% endblock %}
35
45
 
36
46
  {% block settingsPanels %}{% endblock %}
@@ -9,6 +9,13 @@
9
9
  {{ field.problem('settings-posts-page', problems.postsPage) }}
10
10
  {{ field.problem('settings-posts-per-page', problems.postsPerPage) }}
11
11
  {{ field.problem('settings-notify-server', problems.notifyServer) }}
12
+ {{ field.problem('settings-feed-update-period', problems.feedUpdatePeriod) }}
13
+ {{ field.problem('settings-feed-update-frequency', problems.feedUpdateFrequency) }}
14
+ {{ field.problem('settings-ai-crawlers', problems.aiCrawlers) }}
15
+ {{ field.problem('settings-content-signal-search', problems.contentSignalSearch) }}
16
+ {{ field.problem('settings-content-signal-ai-input', problems.contentSignalAiInput) }}
17
+ {{ field.problem('settings-content-signal-ai-train', problems.contentSignalAiTrain) }}
18
+ {{ field.problem('settings-robots-rules', problems.robotsRules) }}
12
19
  {% endblock %}
13
20
 
14
21
  {% block settingsFields %}
@@ -68,4 +75,90 @@
68
75
  {% endset %}
69
76
  {{ field.text('settings-notify-server', fields.notifyServer, 'Notify server', form.notifyServer,
70
77
  error=problems.notifyServer, hint=notifyHint, inputmode='url') }}
78
+
79
+ {# TASK-152. The Syndication module's polling hint, which only RSS has an
80
+ element for. #}
81
+ {% set periodHint %}
82
+ How often RSS readers should check the feeds. Readers that see no new post
83
+ for much longer than this may check less often, so pick a pace the site keeps.
84
+ {% endset %}
85
+ {% call field.select('settings-feed-update-period', fields.feedUpdatePeriod, 'Feed update period',
86
+ error=problems.feedUpdatePeriod, hint=periodHint) %}
87
+ {% for option in [
88
+ ['hourly', 'Hourly'],
89
+ ['daily', 'Daily'],
90
+ ['weekly', 'Weekly'],
91
+ ['monthly', 'Monthly'],
92
+ ['yearly', 'Yearly']] %}
93
+ <option value="{{ option[0] }}"{% if option[0] == form.feedUpdatePeriod %} selected{% endif %}>{{ option[1] }}</option>
94
+ {% endfor %}
95
+ {% endcall %}
96
+
97
+ {{ field.text('settings-feed-update-frequency', fields.feedUpdateFrequency, 'Checks per period',
98
+ form.feedUpdateFrequency, error=problems.feedUpdateFrequency, inputmode='numeric', required=true,
99
+ hint='How many times in each period. Daily and 2 means twice a day.') }}
100
+
101
+ {# TASK-148. What /robots.txt tells crawlers beyond keeping them out of the
102
+ admin, which it does whatever is set here. #}
103
+ <h2>Crawlers</h2>
104
+
105
+ {% set aiHint %}
106
+ Training crawlers gather pages to train AI models. Retrieval crawlers fetch a
107
+ page when somebody asks an assistant about it, so blocking them also stops the
108
+ site being cited. Each choice writes its own groups into
109
+ <a href="/robots.txt"><code>/robots.txt</code></a>.
110
+ {% endset %}
111
+ {% call field.select('settings-ai-crawlers', fields.aiCrawlers, 'AI crawlers',
112
+ error=problems.aiCrawlers, hint=aiHint) %}
113
+ {% for option in [
114
+ ['allow', 'Allow all'],
115
+ ['block-training', 'Block training crawlers'],
116
+ ['block-all', 'Block all AI crawlers']] %}
117
+ <option value="{{ option[0] }}"{% if option[0] == form.aiCrawlers %} selected{% endif %}>{{ option[1] }}</option>
118
+ {% endfor %}
119
+ {% endcall %}
120
+
121
+ {% for signal in [
122
+ ['contentSignalSearch', 'settings-content-signal-search', 'Content signal: search',
123
+ 'Whether pages may be indexed and shown as search results.'],
124
+ ['contentSignalAiInput', 'settings-content-signal-ai-input', 'Content signal: AI input',
125
+ 'Whether pages may be fed to an AI model to answer a question.'],
126
+ ['contentSignalAiTrain', 'settings-content-signal-ai-train', 'Content signal: AI training',
127
+ 'Whether pages may be used to train or fine-tune an AI model.']] %}
128
+ {% call field.select(signal[1], fields[signal[0]], signal[2],
129
+ error=problems[signal[0]], hint=signal[3]) %}
130
+ {% for option in [['', 'Not said'], ['yes', 'Yes'], ['no', 'No']] %}
131
+ <option value="{{ option[0] }}"{% if option[0] == form[signal[0]] %} selected{% endif %}>{{ option[1] }}</option>
132
+ {% endfor %}
133
+ {% endcall %}
134
+ {% endfor %}
135
+
136
+ {% set rulesHint %}
137
+ More <code>robots.txt</code> groups, written as they would be in the file: one or
138
+ more <code>User-agent</code> lines, then <code>Allow</code>, <code>Disallow</code>,
139
+ <code>Crawl-delay</code> or <code>Content-Signal</code> lines. Every group still
140
+ keeps crawlers out of <code>/admin/</code>, and the sitemap is always named.
141
+ {% endset %}
142
+ {{ field.textarea('settings-robots-rules', fields.robotsRules, 'More robots.txt rules',
143
+ form.robotsRules, error=problems.robotsRules, hint=rulesHint,
144
+ rows='5', spellcheck='false') }}
145
+
146
+ {# TASK-149. The index for language models, beside the file for crawlers. #}
147
+ {% set llmsHint %}
148
+ <a href="/llms.txt"><code>/llms.txt</code></a> lists the site's pages and recent
149
+ posts, each linked to its Markdown, for AI tools to read. The home page points
150
+ to it. To write it yourself, put your own file at <code>content/llms.txt</code>.
151
+ {% endset %}
152
+ {{ field.checkbox('settings-llms-txt', fields.llmsTxt, 'Serve /llms.txt',
153
+ checked=form.llmsTxt, hint=llmsHint) }}
154
+
155
+ {# TASK-151. Telling the search engines, beside the files they read. #}
156
+ {% set indexNowHint %}
157
+ Tells Bing, Yandex, Naver, Seznam and the other IndexNow search engines about
158
+ every post or page you publish, change or delete, so they recrawl it within
159
+ minutes. Turning it on creates a key the site serves at
160
+ <code>/{key}.txt</code>. Nothing is sent while the site's URL is local.
161
+ {% endset %}
162
+ {{ field.checkbox('settings-index-now', fields.indexNow, 'Submit changes to IndexNow',
163
+ checked=form.indexNow, hint=indexNowHint) }}
71
164
  {% endblock %}
@@ -0,0 +1,74 @@
1
+ {# /admin/users/apps: the apps that can act as whoever is signed in (TASK-162).
2
+
3
+ Only your own connections, each with a Revoke button that names the app,
4
+ so a screen reader moving between buttons hears which one it is on. #}
5
+ {% extends "layouts/shell.njk" %}
6
+
7
+ {% block title %}Connected apps{% endblock %}
8
+
9
+ {% block content %}
10
+ <h1>Connected apps</h1>
11
+
12
+ {% if connections | length %}
13
+ <p class="admin-hint">
14
+ These apps signed in as you and can act as you, within the permissions
15
+ listed. An app that keeps using its connection stays connected; one left
16
+ unused is disconnected on the date shown. Revoking disconnects it now:
17
+ its next request is refused, and it has to ask you again.
18
+ </p>
19
+
20
+ <table class="admin-list">
21
+ <caption class="admin-visually-hidden">Connected apps</caption>
22
+ <thead>
23
+ <tr>
24
+ <th scope="col">App</th>
25
+ <th scope="col">Can</th>
26
+ <th scope="col">Connected</th>
27
+ <th scope="col">Last used</th>
28
+ <th scope="col">Expires</th>
29
+ <th scope="col">Actions</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ {% for connection in connections %}
34
+ <tr>
35
+ <td><a href="{{ connection.url }}">{{ connection.name }}</a></td>
36
+ <td>
37
+ <ul>
38
+ {% for scope in connection.scopes %}
39
+ <li>{{ scope }}</li>
40
+ {% else %}
41
+ <li>Confirm who you are</li>
42
+ {% endfor %}
43
+ </ul>
44
+ </td>
45
+ <td><time datetime="{{ connection.issued.iso }}">{{ connection.issued.text }}</time></td>
46
+ <td>
47
+ {% if connection.lastUsed %}
48
+ <time datetime="{{ connection.lastUsed.iso }}">{{ connection.lastUsed.text }}</time>
49
+ {% else %}
50
+ <span class="admin-status">Not yet</span>
51
+ {% endif %}
52
+ </td>
53
+ <td><time datetime="{{ connection.expires.iso }}">{{ connection.expires.text }}</time></td>
54
+ <td class="admin-actions">
55
+ <form method="post" action="{{ revokeUrl }}">
56
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
57
+ <input type="hidden" name="{{ connectionField }}" value="{{ connection.id }}" />
58
+ <button type="submit">Revoke {{ connection.name }}</button>
59
+ </form>
60
+ </td>
61
+ </tr>
62
+ {% endfor %}
63
+ </tbody>
64
+ </table>
65
+ {% else %}
66
+ <p>No apps are connected to your account.</p>
67
+ <p class="admin-hint">
68
+ An app connects here when you sign in to it with your site's address and
69
+ approve what it asks for: a Micropub app such as Quill that posts to
70
+ your site, or an MCP client such as an AI assistant. Each one you approve
71
+ is listed here, with a button to disconnect it.
72
+ </p>
73
+ {% endif %}
74
+ {% endblock %}
@@ -211,6 +211,18 @@
211
211
  whoever is signed in and means nothing on somebody else's screen.
212
212
  Resetting a colleague's password from under them is deliberately not
213
213
  offered — a fresh account is what that is for. #}
214
+ {# Only on your own page, as the screen it links to only ever lists your
215
+ own connections (TASK-162). #}
216
+ {% if account.you %}
217
+ <section class="admin-settings">
218
+ <h2>Connected apps</h2>
219
+ <p class="admin-hint">
220
+ Apps you signed in to with your site's address can act as you.
221
+ <a href="{{ connectedAppsUrl }}">Review or revoke them</a>.
222
+ </p>
223
+ </section>
224
+ {% endif %}
225
+
214
226
  {% if account.you %}
215
227
  <form class="admin-settings" id="change-password" method="post" action="{{ changePasswordUrl }}">
216
228
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />