@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.
- package/README.md +188 -9
- package/admin/pages/account/login.njk +3 -0
- package/admin/pages/documents/editor.njk +20 -0
- package/admin/pages/indieauth/consent.njk +51 -0
- package/admin/pages/indieauth/refused.njk +11 -0
- package/admin/pages/settings/general.njk +12 -2
- package/admin/pages/settings/reading.njk +93 -0
- package/admin/pages/users/apps.njk +74 -0
- package/admin/pages/users/edit.njk +12 -0
- package/admin/static/admin.css +27 -0
- package/dist/admin/accounts.d.ts +3 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +5 -0
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/documents.d.ts +4 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +45 -5
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.js +2 -2
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/headers.d.ts +9 -2
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +16 -7
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +2 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +48 -5
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-general.d.ts.map +1 -1
- package/dist/admin/settings-general.js +28 -3
- package/dist/admin/settings-general.js.map +1 -1
- package/dist/admin/settings-reading.d.ts.map +1 -1
- package/dist/admin/settings-reading.js +15 -1
- package/dist/admin/settings-reading.js.map +1 -1
- package/dist/admin/settings.d.ts +83 -4
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +166 -4
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/templates.d.ts +3 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +3 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/users.d.ts +8 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +9 -0
- package/dist/admin/users.js.map +1 -1
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/content/pinned.d.ts +26 -0
- package/dist/content/pinned.d.ts.map +1 -0
- package/dist/content/pinned.js +42 -0
- package/dist/content/pinned.js.map +1 -0
- package/dist/content/store.d.ts +7 -0
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +20 -0
- package/dist/content/store.js.map +1 -1
- package/dist/env.d.ts +21 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/actor.d.ts +15 -0
- package/dist/federation/actor.d.ts.map +1 -1
- package/dist/federation/actor.js +94 -3
- package/dist/federation/actor.js.map +1 -1
- package/dist/federation/article.d.ts +13 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +45 -6
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +36 -4
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +24 -2
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/mount.js +2 -2
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/paths.d.ts +30 -0
- package/dist/federation/paths.d.ts.map +1 -1
- package/dist/federation/paths.js +36 -0
- package/dist/federation/paths.js.map +1 -1
- package/dist/images/icons.d.ts +44 -25
- package/dist/images/icons.d.ts.map +1 -1
- package/dist/images/icons.js +205 -60
- package/dist/images/icons.js.map +1 -1
- package/dist/images/index.d.ts +2 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +1 -1
- package/dist/images/index.js.map +1 -1
- package/dist/images/markup.d.ts +14 -0
- package/dist/images/markup.d.ts.map +1 -1
- package/dist/images/markup.js +16 -11
- package/dist/images/markup.js.map +1 -1
- package/dist/images/variants.d.ts +11 -0
- package/dist/images/variants.d.ts.map +1 -1
- package/dist/images/variants.js +24 -4
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/indexnow.d.ts +34 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +141 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/indieauth/bearer.d.ts +39 -0
- package/dist/indieauth/bearer.d.ts.map +1 -0
- package/dist/indieauth/bearer.js +63 -0
- package/dist/indieauth/bearer.js.map +1 -0
- package/dist/indieauth/client.d.ts +61 -0
- package/dist/indieauth/client.d.ts.map +1 -0
- package/dist/indieauth/client.js +139 -0
- package/dist/indieauth/client.js.map +1 -0
- package/dist/indieauth/connected-apps.d.ts +23 -0
- package/dist/indieauth/connected-apps.d.ts.map +1 -0
- package/dist/indieauth/connected-apps.js +73 -0
- package/dist/indieauth/connected-apps.js.map +1 -0
- package/dist/indieauth/consent.d.ts +28 -0
- package/dist/indieauth/consent.d.ts.map +1 -0
- package/dist/indieauth/consent.js +185 -0
- package/dist/indieauth/consent.js.map +1 -0
- package/dist/indieauth/discovery.d.ts +87 -0
- package/dist/indieauth/discovery.d.ts.map +1 -0
- package/dist/indieauth/discovery.js +127 -0
- package/dist/indieauth/discovery.js.map +1 -0
- package/dist/indieauth/grants.d.ts +57 -0
- package/dist/indieauth/grants.d.ts.map +1 -0
- package/dist/indieauth/grants.js +35 -0
- package/dist/indieauth/grants.js.map +1 -0
- package/dist/indieauth/identity.d.ts +36 -0
- package/dist/indieauth/identity.d.ts.map +1 -0
- package/dist/indieauth/identity.js +77 -0
- package/dist/indieauth/identity.js.map +1 -0
- package/dist/indieauth/redeem.d.ts +47 -0
- package/dist/indieauth/redeem.d.ts.map +1 -0
- package/dist/indieauth/redeem.js +69 -0
- package/dist/indieauth/redeem.js.map +1 -0
- package/dist/indieauth/request.d.ts +47 -0
- package/dist/indieauth/request.d.ts.map +1 -0
- package/dist/indieauth/request.js +104 -0
- package/dist/indieauth/request.js.map +1 -0
- package/dist/indieauth/token.d.ts +19 -0
- package/dist/indieauth/token.d.ts.map +1 -0
- package/dist/indieauth/token.js +110 -0
- package/dist/indieauth/token.js.map +1 -0
- package/dist/indieauth/tokens.d.ts +131 -0
- package/dist/indieauth/tokens.d.ts.map +1 -0
- package/dist/indieauth/tokens.js +205 -0
- package/dist/indieauth/tokens.js.map +1 -0
- package/dist/web/admin-bar.d.ts +3 -1
- package/dist/web/admin-bar.d.ts.map +1 -1
- package/dist/web/admin-bar.js +5 -1
- package/dist/web/admin-bar.js.map +1 -1
- package/dist/web/archive.d.ts +1 -1
- package/dist/web/archive.d.ts.map +1 -1
- package/dist/web/archive.js +3 -2
- package/dist/web/archive.js.map +1 -1
- package/dist/web/authors.d.ts +19 -0
- package/dist/web/authors.d.ts.map +1 -1
- package/dist/web/authors.js +25 -0
- package/dist/web/authors.js.map +1 -1
- package/dist/web/context.d.ts +25 -0
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +18 -0
- package/dist/web/context.js.map +1 -1
- package/dist/web/feed-atom.d.ts.map +1 -1
- package/dist/web/feed-atom.js +5 -2
- package/dist/web/feed-atom.js.map +1 -1
- package/dist/web/feed-item.d.ts +24 -6
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +16 -8
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-json.d.ts +6 -2
- package/dist/web/feed-json.d.ts.map +1 -1
- package/dist/web/feed-json.js +6 -2
- package/dist/web/feed-json.js.map +1 -1
- package/dist/web/feed-rss.d.ts.map +1 -1
- package/dist/web/feed-rss.js +17 -4
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +31 -0
- package/dist/web/feed-source.d.ts.map +1 -1
- package/dist/web/feed-source.js +31 -0
- package/dist/web/feed-source.js.map +1 -1
- package/dist/web/feed-xml.d.ts +2 -0
- package/dist/web/feed-xml.d.ts.map +1 -1
- package/dist/web/feed-xml.js +2 -0
- package/dist/web/feed-xml.js.map +1 -1
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +15 -3
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/index.d.ts +4 -2
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +3 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/indexnow.d.ts +9 -0
- package/dist/web/indexnow.d.ts.map +1 -0
- package/dist/web/indexnow.js +27 -0
- package/dist/web/indexnow.js.map +1 -0
- package/dist/web/llms.d.ts +58 -0
- package/dist/web/llms.d.ts.map +1 -0
- package/dist/web/llms.js +91 -0
- package/dist/web/llms.js.map +1 -0
- package/dist/web/locale.d.ts +41 -0
- package/dist/web/locale.d.ts.map +1 -0
- package/dist/web/locale.js +56 -0
- package/dist/web/locale.js.map +1 -0
- package/dist/web/manifest.d.ts +50 -0
- package/dist/web/manifest.d.ts.map +1 -0
- package/dist/web/manifest.js +56 -0
- package/dist/web/manifest.js.map +1 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +6 -0
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/oembed.d.ts +104 -0
- package/dist/web/oembed.d.ts.map +1 -0
- package/dist/web/oembed.js +290 -0
- package/dist/web/oembed.js.map +1 -0
- package/dist/web/opensearch.d.ts +35 -0
- package/dist/web/opensearch.d.ts.map +1 -0
- package/dist/web/opensearch.js +65 -0
- package/dist/web/opensearch.js.map +1 -0
- package/dist/web/render.d.ts +6 -1
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +40 -10
- package/dist/web/render.js.map +1 -1
- package/dist/web/robots.d.ts +89 -0
- package/dist/web/robots.d.ts.map +1 -0
- package/dist/web/robots.js +259 -0
- package/dist/web/robots.js.map +1 -0
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +179 -8
- package/dist/web/routes.js.map +1 -1
- package/dist/web/share-image.d.ts +51 -0
- package/dist/web/share-image.d.ts.map +1 -0
- package/dist/web/share-image.js +43 -0
- package/dist/web/share-image.js.map +1 -0
- package/dist/web/sitemap.d.ts +0 -26
- package/dist/web/sitemap.d.ts.map +1 -1
- package/dist/web/sitemap.js +0 -44
- package/dist/web/sitemap.js.map +1 -1
- package/dist/web/templates.d.ts +16 -9
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +88 -55
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +33 -0
- package/dist/web/themes.d.ts.map +1 -1
- package/dist/web/themes.js +57 -0
- package/dist/web/themes.js.map +1 -1
- package/dist/webmention/discovery.d.ts +4 -2
- package/dist/webmention/discovery.d.ts.map +1 -1
- package/dist/webmention/discovery.js +14 -8
- package/dist/webmention/discovery.js.map +1 -1
- package/dist/webmention/fetch-public.d.ts +2 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -1
- package/dist/webmention/fetch-public.js +1 -1
- package/dist/webmention/fetch-public.js.map +1 -1
- package/package.json +2 -1
- package/templates/site/content/_data/site.json +0 -1
- package/themes/default/README.md +174 -43
- package/themes/default/layouts/base.njk +116 -17
- package/themes/default/layouts/front-page.njk +7 -1
- package/themes/default/layouts/home.njk +11 -0
- package/themes/default/layouts/page.njk +4 -3
- package/themes/default/layouts/post.njk +5 -4
- package/themes/default/layouts/search.njk +4 -5
- package/themes/default/partials/archive.njk +3 -1
- package/themes/default/partials/bio.njk +16 -2
- package/themes/default/partials/breadcrumbs.njk +20 -0
- package/themes/default/partials/conversation.njk +4 -1
- package/themes/default/partials/jsonld.njk +49 -9
- package/themes/default/partials/kicker.njk +6 -4
- package/themes/default/partials/lang.njk +7 -0
- package/themes/default/partials/post-list.njk +5 -3
- package/themes/default/partials/reply-context.njk +4 -2
- package/themes/default/src/style.css +14 -0
- package/themes/default/static/style.css +57 -0
- 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
|
|
1840
|
-
`homepage`, `postsPage`,
|
|
1841
|
-
`timezone`, `language`, `tagBase`,
|
|
1842
|
-
`categoryBase`, `notifyServer`, `
|
|
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": "
|
|
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`
|
|
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
|
|
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
|
-
{
|
|
20
|
-
|
|
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 }}" />
|