@geekity/cms 0.11.0 → 0.13.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 +177 -6
- package/admin/pages/account/login.njk +3 -0
- package/admin/pages/documents/editor.njk +7 -0
- package/admin/pages/indieauth/consent.njk +51 -0
- package/admin/pages/indieauth/refused.njk +11 -0
- package/admin/pages/settings/general.njk +9 -0
- 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 +2 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +17 -1
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/headers.d.ts +5 -1
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +7 -3
- 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.js +1 -1
- 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 +79 -2
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +168 -2
- 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 +30 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/env.d.ts +14 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +18 -5
- package/dist/federation/article.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.map +1 -1
- package/dist/images/variants.js +3 -4
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- 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 +78 -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/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/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 +3 -1
- package/dist/web/feed-atom.js.map +1 -1
- package/dist/web/feed-item.d.ts +11 -2
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +9 -3
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-json.d.ts +4 -0
- package/dist/web/feed-json.d.ts.map +1 -1
- package/dist/web/feed-json.js +4 -1
- 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 +16 -3
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +28 -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 +8 -1
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/index.d.ts +3 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +2 -1
- 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/render.d.ts +6 -1
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +68 -22
- 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 +105 -6
- 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 +83 -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 +7 -2
- package/themes/default/README.md +320 -139
- package/themes/default/layouts/404.njk +2 -0
- package/themes/default/layouts/500.njk +2 -0
- package/themes/default/layouts/503.njk +2 -0
- package/themes/default/layouts/author.njk +3 -1
- package/themes/default/layouts/base.njk +96 -17
- package/themes/default/layouts/category.njk +4 -2
- package/themes/default/layouts/front-page.njk +7 -2
- package/themes/default/layouts/home.njk +10 -0
- package/themes/default/layouts/page.njk +7 -4
- package/themes/default/layouts/post.njk +35 -25
- package/themes/default/layouts/search.njk +5 -9
- package/themes/default/layouts/tag.njk +7 -1
- package/themes/default/partials/archive.njk +3 -1
- package/themes/default/partials/bio.njk +9 -2
- package/themes/default/partials/breadcrumbs.njk +20 -0
- package/themes/default/partials/comment-form.njk +8 -0
- package/themes/default/partials/contact-form.njk +3 -0
- package/themes/default/partials/conversation.njk +14 -4
- package/themes/default/partials/jsonld.njk +19 -4
- package/themes/default/partials/kicker.njk +36 -0
- package/themes/default/partials/lang.njk +7 -0
- package/themes/default/partials/post-list.njk +43 -47
- package/themes/default/partials/reply-context.njk +14 -5
- package/themes/default/src/style.css +1109 -0
- package/themes/default/static/style.css +2124 -1160
- package/themes/default/theme.json +3 -1
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. |
|
|
@@ -1838,8 +1846,9 @@ at once cannot each keep half of what the other kept.
|
|
|
1838
1846
|
|
|
1839
1847
|
The file carries `title`, `tagline`, `url`, `author`, `postsPerPage`,
|
|
1840
1848
|
`homepage`, `postsPage`,
|
|
1841
|
-
`timezone`, `language`, `tagBase`,
|
|
1842
|
-
`categoryBase`, `notifyServer`, `
|
|
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`,
|
|
@@ -2354,6 +2363,8 @@ Booting mounts the public site on the app. The routes are:
|
|
|
2354
2363
|
| `/sitemap.xml` | Every public URL, for a search engine. |
|
|
2355
2364
|
| `/sitemap-{n}.xml` | One file of a sitemap too big to be a single one. |
|
|
2356
2365
|
| `/robots.txt` | What a crawler may have, and where the sitemap is. |
|
|
2366
|
+
| `/llms.txt` | The site's pages and recent posts for a language model, each linked to its Markdown. See [below](#llmstxt). |
|
|
2367
|
+
| `/{key}.txt` | The IndexNow key, while the site has IndexNow on. See [below](#indexnow). |
|
|
2357
2368
|
| `/.well-known/security.txt` | Where to report a vulnerability; a 404 until a security contact is set. See [above](#securitytxt-and-change-password). |
|
|
2358
2369
|
| `/.well-known/change-password` | A redirect to the signed-in user's change-password form, or to the login form. |
|
|
2359
2370
|
| `/_geekity/comments` | `POST` only. Where the comment form under a post submits. |
|
|
@@ -2705,6 +2716,10 @@ without changing the document, so a development server would otherwise answer
|
|
|
2705
2716
|
`304` with a page that had already moved on. The `.md` and `.json`
|
|
2706
2717
|
representations are validated either way.
|
|
2707
2718
|
|
|
2719
|
+
Every representation but the HTML carries `X-Robots-Tag: noindex`, whether it
|
|
2720
|
+
was asked for by extension or by `Accept`. They say what the page says, and a
|
|
2721
|
+
search engine that indexed them would list one page two or three times.
|
|
2722
|
+
|
|
2708
2723
|
Adding a representation — an ActivityStreams object, say — means adding it to
|
|
2709
2724
|
`Representation` in `src/web/negotiate.ts` with its media type and, if it wants
|
|
2710
2725
|
one, its extension. The selection, the `Link` alternates, the `ETag` and the
|
|
@@ -2748,6 +2763,16 @@ post on a site that publishes several.
|
|
|
2748
2763
|
{ "title": "My Site", "tagline": "Notes", "author": "Me", "feedSize": 20 }
|
|
2749
2764
|
```
|
|
2750
2765
|
|
|
2766
|
+
Every RSS feed, the comments feeds included, tells a reader how often to poll
|
|
2767
|
+
it with the Syndication module's `sy:updatePeriod` and `sy:updateFrequency`.
|
|
2768
|
+
Settings → Reading sets both, as `feedUpdatePeriod` (`hourly`, `daily`,
|
|
2769
|
+
`weekly`, `monthly` or `yearly`) and `feedUpdateFrequency` (a whole number, 1 or
|
|
2770
|
+
more) in `site.json`. The default is `hourly` and `1`, which is what WordPress
|
|
2771
|
+
declares, so the subscribers of a migrated site keep polling as they did. A
|
|
2772
|
+
reader may back off from a feed that stays quiet much longer than it says, so
|
|
2773
|
+
a site that posts weekly does better to say `weekly`. Atom and JSON Feed have no
|
|
2774
|
+
element for it.
|
|
2775
|
+
|
|
2751
2776
|
### What every format says about a post
|
|
2752
2777
|
|
|
2753
2778
|
The three formats render one **feed item**, derived once per post, rather than
|
|
@@ -2802,6 +2827,13 @@ post that is not a reply, or whose `in-reply-to` is not such a URL, carries
|
|
|
2802
2827
|
neither. RSS 2.0 has no equivalent element, so an RSS item for a reply is
|
|
2803
2828
|
written the same as any other.
|
|
2804
2829
|
|
|
2830
|
+
**Languages.** Every feed declares the site's `language` once: RSS as the
|
|
2831
|
+
channel's `language`, Atom as the feed's `xml:lang` and JSON Feed as its
|
|
2832
|
+
`language`. A post whose `lang` front matter names another language says so on
|
|
2833
|
+
its own item: RSS as `dc:language`, Atom as `xml:lang` on the `entry` and JSON
|
|
2834
|
+
Feed as the item's `language`. A post in the site's language, or whose `lang`
|
|
2835
|
+
is not a language tag, adds nothing.
|
|
2836
|
+
|
|
2805
2837
|
### RSS 2.0
|
|
2806
2838
|
|
|
2807
2839
|
The channel carries `title`, `link`, `description` (the tagline), `language`
|
|
@@ -3096,7 +3128,7 @@ holding its own slice and dated by the newest URL in it. The address a search
|
|
|
3096
3128
|
engine holds does not change, which is the point of the index living there.
|
|
3097
3129
|
While the whole sitemap fits in one file the children name nothing and 404.
|
|
3098
3130
|
|
|
3099
|
-
`/robots.txt`
|
|
3131
|
+
`/robots.txt` starts short:
|
|
3100
3132
|
|
|
3101
3133
|
```
|
|
3102
3134
|
User-agent: *
|
|
@@ -3111,6 +3143,44 @@ fetched, they carry the same
|
|
|
3111
3143
|
content as the pages that link to them, and a crawler that follows one gets
|
|
3112
3144
|
JSON it will ignore.
|
|
3113
3145
|
|
|
3146
|
+
The Crawlers section of Settings → Reading adds to it, and writes what it sets
|
|
3147
|
+
to `site.json`:
|
|
3148
|
+
|
|
3149
|
+
| Setting | `site.json` key | What the file gains |
|
|
3150
|
+
| --------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3151
|
+
| 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. |
|
|
3152
|
+
| 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`. |
|
|
3153
|
+
| 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. |
|
|
3154
|
+
|
|
3155
|
+
A crawler obeys only the one group that names it (RFC 9309), so the CMS puts
|
|
3156
|
+
`Disallow: /admin/` and the content signal into every group, its own and the
|
|
3157
|
+
site's, unless the group already disallows everything or carries a signal of
|
|
3158
|
+
its own. The form refuses a line that is not a robots.txt field, a rule with no
|
|
3159
|
+
`User-agent` above it, and an `Allow` under `/admin/`; a hand edit of
|
|
3160
|
+
`site.json` that does any of these loses that line. The site's own `Sitemap:`
|
|
3161
|
+
line is always there.
|
|
3162
|
+
|
|
3163
|
+
For example, `block-training`, `ai-train` set to `no` and a rule of
|
|
3164
|
+
`User-agent: SlowBot` / `Crawl-delay: 10` serve:
|
|
3165
|
+
|
|
3166
|
+
```
|
|
3167
|
+
User-agent: *
|
|
3168
|
+
Disallow: /admin/
|
|
3169
|
+
Content-Signal: ai-train=no
|
|
3170
|
+
|
|
3171
|
+
User-agent: GPTBot
|
|
3172
|
+
Disallow: /
|
|
3173
|
+
|
|
3174
|
+
…one group per training crawler…
|
|
3175
|
+
|
|
3176
|
+
User-agent: SlowBot
|
|
3177
|
+
Disallow: /admin/
|
|
3178
|
+
Crawl-delay: 10
|
|
3179
|
+
Content-Signal: ai-train=no
|
|
3180
|
+
|
|
3181
|
+
Sitemap: https://example.com/sitemap.xml
|
|
3182
|
+
```
|
|
3183
|
+
|
|
3114
3184
|
Both are registered routes rather than anything resolved from the content
|
|
3115
3185
|
index, so a document permalinked at `/sitemap.xml` cannot take the URL a
|
|
3116
3186
|
search engine polls, and both carry an `ETag` and answer a conditional request
|
|
@@ -3120,6 +3190,102 @@ robots without one, because nothing dates it.
|
|
|
3120
3190
|
Nothing links the sitemap from a page: `robots.txt` names it, which is where a
|
|
3121
3191
|
crawler looks.
|
|
3122
3192
|
|
|
3193
|
+
## llms.txt
|
|
3194
|
+
|
|
3195
|
+
`/llms.txt` describes the site for a language model, in the
|
|
3196
|
+
[llms.txt](https://llmstxt.org/) format. It holds the site's title as a heading
|
|
3197
|
+
and its tagline as a quote. Then it lists every public page by title and the
|
|
3198
|
+
posts the feeds carry, newest first. Each entry links to the document's
|
|
3199
|
+
Markdown, which is what a model reads best:
|
|
3200
|
+
|
|
3201
|
+
```markdown
|
|
3202
|
+
# Field Notes
|
|
3203
|
+
|
|
3204
|
+
> Notes from the field.
|
|
3205
|
+
|
|
3206
|
+
## Pages
|
|
3207
|
+
|
|
3208
|
+
- [About](https://example.com/about/index.md): Who writes this.
|
|
3209
|
+
|
|
3210
|
+
## Recent posts
|
|
3211
|
+
|
|
3212
|
+
- [Third post](https://example.com/third/index.md)
|
|
3213
|
+
```
|
|
3214
|
+
|
|
3215
|
+
A document's `description` follows its link. A static homepage is listed at
|
|
3216
|
+
`/index.md`, because `/` is where it is published. The file is served as
|
|
3217
|
+
`text/markdown` and carries an `ETag` and a `Last-Modified`, the newest date
|
|
3218
|
+
among its entries. It answers a conditional request with 304, as the sitemap
|
|
3219
|
+
does.
|
|
3220
|
+
|
|
3221
|
+
The home page advertises the file twice. The response carries
|
|
3222
|
+
`Link: </llms.txt>; rel="describedby"; type="text/markdown"`, for a tool that
|
|
3223
|
+
reads no HTML. The default theme's head carries
|
|
3224
|
+
`<link rel="describedby" type="text/markdown" href="/llms.txt">`. A theme
|
|
3225
|
+
gets the path as `llmsTxt` while the site serves the file.
|
|
3226
|
+
|
|
3227
|
+
To write the file yourself, put it at `content/llms.txt`. The CMS serves it
|
|
3228
|
+
exactly as written in place of the generated one, dated by the file. To serve
|
|
3229
|
+
no file at all, clear **Serve /llms.txt** under Settings → Reading. This
|
|
3230
|
+
writes `"llmsTxt": false` to `site.json`. `/llms.txt` then answers 404, even
|
|
3231
|
+
when `content/llms.txt` exists, and the home page stops advertising it.
|
|
3232
|
+
|
|
3233
|
+
## IndexNow
|
|
3234
|
+
|
|
3235
|
+
[IndexNow](https://www.indexnow.org/) gets a changed URL recrawled by Bing,
|
|
3236
|
+
Yandex, Naver, Seznam and the other engines that share it within minutes,
|
|
3237
|
+
instead of on their next visit. It is off by default, because it sends the
|
|
3238
|
+
site's URLs to a third party. To turn it on, select **Submit changes to
|
|
3239
|
+
IndexNow** under Settings → Reading.
|
|
3240
|
+
|
|
3241
|
+
The first time you turn it on, the CMS generates a key of 32 hex digits and
|
|
3242
|
+
writes it to `site.json` as `indexNowKey`, beside `"indexNow": true`. The site
|
|
3243
|
+
then serves the key at `/{key}.txt` as plain text, which is how an engine
|
|
3244
|
+
checks that the submissions are yours. Turning IndexNow off takes the file away
|
|
3245
|
+
and keeps the key, so turning it back on reuses the key the engines already
|
|
3246
|
+
checked. To change the key, write a new one of 8 to 128 letters, digits or
|
|
3247
|
+
dashes to `indexNowKey`. A key that does not fit that pattern counts as none,
|
|
3248
|
+
and the site sends nothing.
|
|
3249
|
+
|
|
3250
|
+
When a post or a page is published, changed, moved or deleted, the CMS submits
|
|
3251
|
+
its URL. A move submits the old URL and the new one. Unpublishing, trashing and
|
|
3252
|
+
deleting submit the URL that went away. The changes of the next ten seconds go
|
|
3253
|
+
out together as one `POST` to `https://api.indexnow.org/indexnow`:
|
|
3254
|
+
|
|
3255
|
+
```json
|
|
3256
|
+
{
|
|
3257
|
+
"host": "example.com",
|
|
3258
|
+
"key": "0123456789abcdef0123456789abcdef",
|
|
3259
|
+
"keyLocation": "https://example.com/0123456789abcdef0123456789abcdef.txt",
|
|
3260
|
+
"urlList": ["https://example.com/hello/"]
|
|
3261
|
+
}
|
|
3262
|
+
```
|
|
3263
|
+
|
|
3264
|
+
A network failure, a 429 or a 5xx answer is tried again after ten seconds, then
|
|
3265
|
+
after forty. Any other refusal, such as a 403 for a key the engine could not
|
|
3266
|
+
fetch, is not. A batch that still fails is logged as a warning, and the post
|
|
3267
|
+
stays published.
|
|
3268
|
+
|
|
3269
|
+
Nothing is sent for a draft or a scheduled post, for the boot scan, or while
|
|
3270
|
+
the base URL's host is local: `localhost`, a `.local` name, or a private IP
|
|
3271
|
+
address.
|
|
3272
|
+
|
|
3273
|
+
A site can change how the CMS sends with `indexNow` in its config. Every field
|
|
3274
|
+
is optional:
|
|
3275
|
+
|
|
3276
|
+
```ts
|
|
3277
|
+
export default defineConfig({
|
|
3278
|
+
indexNow: {
|
|
3279
|
+
fetch, // what submits the URLs; a test names one
|
|
3280
|
+
endpoint: 'https://www.bing.com/indexnow',
|
|
3281
|
+
batchMs: 10_000,
|
|
3282
|
+
attempts: 3,
|
|
3283
|
+
backoffMs: (attempt) => attempt * attempt * 10_000,
|
|
3284
|
+
logger: console,
|
|
3285
|
+
},
|
|
3286
|
+
});
|
|
3287
|
+
```
|
|
3288
|
+
|
|
3123
3289
|
## Theme overrides
|
|
3124
3290
|
|
|
3125
3291
|
A site's themes live one directory per theme under `themesDir` (`themes/` by
|
|
@@ -3190,8 +3356,13 @@ The admin is not themed. Its templates and its static files live in a tree of
|
|
|
3190
3356
|
their own with a loader of their own, deliberately off this search path, so no
|
|
3191
3357
|
theme can shadow the login form or the CSRF field inside it.
|
|
3192
3358
|
|
|
3193
|
-
The packaged theme is the
|
|
3194
|
-
|
|
3359
|
+
The packaged theme is the Paper design (doc-9) on the andrewshell.org shell
|
|
3360
|
+
(decision-16). Its stylesheet is written in Tailwind v4 in
|
|
3361
|
+
`themes/default/src/style.css` and compiled to `themes/default/static/style.css`
|
|
3362
|
+
when the package is built (decision-22), so what ships is one plain CSS file; a
|
|
3363
|
+
site theme's own `static/style.css` replaces it wholesale and needs no
|
|
3364
|
+
Tailwind. Every colour is a `--color-*` custom property a site can override.
|
|
3365
|
+
`base.njk` is its shell: a skip link, a `.global-wrapper` carrying `data-is-root-path="true"`
|
|
3195
3366
|
at `/` only, a header that is the site title and tagline on the front page and a
|
|
3196
3367
|
small link home on every other — with `menus.primary` inside it on every page
|
|
3197
3368
|
— `<main id="main">`, and a footer with the copyright year, the site author,
|
|
@@ -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
|
|
@@ -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 %}
|
|
@@ -19,6 +20,10 @@
|
|
|
19
20
|
{{ field.text('settings-author', fields.author, 'Author', form.author,
|
|
20
21
|
hint='The name on the feeds.') }}
|
|
21
22
|
|
|
23
|
+
{% set soloAuthorHint %}The homepage speaks for the author: it shows their bio and links, and claims their author archive and profiles with <code>rel="me"</code>. Leave it off for a site with more than one author.{% endset %}
|
|
24
|
+
{{ field.checkbox('settings-solo-author', fields.soloAuthor, 'Solo author blog',
|
|
25
|
+
checked=form.soloAuthor, hint=soloAuthorHint) }}
|
|
26
|
+
|
|
22
27
|
{% set baseUrlHint %}In effect: <code>{{ baseUrlInEffect }}</code>. {{ baseUrlNote }}{% endset %}
|
|
23
28
|
{{ field.text('settings-base-url', fields.baseUrl, 'Base URL', form.baseUrl,
|
|
24
29
|
error=problems.baseUrl, hint=baseUrlHint, inputmode='url',
|
|
@@ -31,6 +36,10 @@
|
|
|
31
36
|
{% 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
37
|
{{ field.text('settings-language', fields.language, 'Language', form.language,
|
|
33
38
|
error=problems.language, hint=languageHint, required=true) }}
|
|
39
|
+
|
|
40
|
+
{% 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 %}
|
|
41
|
+
{{ field.text('settings-locale', fields.locale, 'Locale', form.locale,
|
|
42
|
+
error=problems.locale, hint=localeHint) }}
|
|
34
43
|
{% endblock %}
|
|
35
44
|
|
|
36
45
|
{% 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 }}" />
|
package/admin/static/admin.css
CHANGED
|
@@ -1347,3 +1347,30 @@ button[type='submit']:hover,
|
|
|
1347
1347
|
background: var(--admin-surface);
|
|
1348
1348
|
border: 1px solid var(--admin-border);
|
|
1349
1349
|
}
|
|
1350
|
+
|
|
1351
|
+
/* The IndieAuth consent screen (TASK-158): who is asking, where the answer
|
|
1352
|
+
goes and who it names, as label and value pairs a person reads down. */
|
|
1353
|
+
|
|
1354
|
+
.admin-consent {
|
|
1355
|
+
display: grid;
|
|
1356
|
+
margin: 0 0 1.5rem;
|
|
1357
|
+
gap: 0.4rem 1.5rem;
|
|
1358
|
+
grid-template-columns: max-content 1fr;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.admin-consent dt {
|
|
1362
|
+
color: var(--admin-muted);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.admin-consent dd {
|
|
1366
|
+
margin: 0;
|
|
1367
|
+
overflow-wrap: anywhere;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.admin-consent-logo {
|
|
1371
|
+
display: block;
|
|
1372
|
+
width: 4rem;
|
|
1373
|
+
height: 4rem;
|
|
1374
|
+
margin: 0 0 1rem;
|
|
1375
|
+
object-fit: contain;
|
|
1376
|
+
}
|
package/dist/admin/accounts.d.ts
CHANGED
|
@@ -382,6 +382,9 @@ export declare class UnknownUserError extends Error {
|
|
|
382
382
|
/**
|
|
383
383
|
* Take a user out of the file. Returns `false` when there was nothing to take.
|
|
384
384
|
*
|
|
385
|
+
* Their IndieAuth tokens go with them (TASK-160): a token is a file in
|
|
386
|
+
* `data/` too, so it is revoked here, where every caller deletes a user.
|
|
387
|
+
*
|
|
385
388
|
* Their sessions are not ended here: sessions are in the database, and the
|
|
386
389
|
* screen that deletes a user ends them itself. A session that outlives this —
|
|
387
390
|
* one in a database restored from a backup, say — names a user the file no
|