@geekity/cms 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +181 -57
  2. package/admin/pages/documents/editor.njk +82 -0
  3. package/admin/pages/media/library.njk +4 -3
  4. package/admin/pages/settings/general.njk +51 -6
  5. package/admin/static/admin.css +33 -0
  6. package/dist/admin/documents.d.ts +5 -0
  7. package/dist/admin/documents.d.ts.map +1 -1
  8. package/dist/admin/documents.js +63 -6
  9. package/dist/admin/documents.js.map +1 -1
  10. package/dist/admin/enclosure-field.d.ts +91 -0
  11. package/dist/admin/enclosure-field.d.ts.map +1 -0
  12. package/dist/admin/enclosure-field.js +282 -0
  13. package/dist/admin/enclosure-field.js.map +1 -0
  14. package/dist/admin/federation.js +2 -2
  15. package/dist/admin/federation.js.map +1 -1
  16. package/dist/admin/headers.d.ts +4 -1
  17. package/dist/admin/headers.d.ts.map +1 -1
  18. package/dist/admin/headers.js +9 -4
  19. package/dist/admin/headers.js.map +1 -1
  20. package/dist/admin/media.d.ts +7 -2
  21. package/dist/admin/media.d.ts.map +1 -1
  22. package/dist/admin/media.js +4 -2
  23. package/dist/admin/media.js.map +1 -1
  24. package/dist/admin/settings-general.d.ts.map +1 -1
  25. package/dist/admin/settings-general.js +63 -4
  26. package/dist/admin/settings-general.js.map +1 -1
  27. package/dist/admin/settings-page.d.ts.map +1 -1
  28. package/dist/admin/settings-page.js +1 -2
  29. package/dist/admin/settings-page.js.map +1 -1
  30. package/dist/admin/settings.d.ts +37 -9
  31. package/dist/admin/settings.d.ts.map +1 -1
  32. package/dist/admin/settings.js +93 -11
  33. package/dist/admin/settings.js.map +1 -1
  34. package/dist/admin/uploads.d.ts +7 -2
  35. package/dist/admin/uploads.d.ts.map +1 -1
  36. package/dist/admin/uploads.js +23 -5
  37. package/dist/admin/uploads.js.map +1 -1
  38. package/dist/config.d.ts +21 -3
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +11 -7
  41. package/dist/config.js.map +1 -1
  42. package/dist/content/enclosure.d.ts +69 -0
  43. package/dist/content/enclosure.d.ts.map +1 -0
  44. package/dist/content/enclosure.js +165 -0
  45. package/dist/content/enclosure.js.map +1 -0
  46. package/dist/content/index.d.ts +3 -1
  47. package/dist/content/index.d.ts.map +1 -1
  48. package/dist/content/index.js +1 -0
  49. package/dist/content/index.js.map +1 -1
  50. package/dist/content/media.d.ts +15 -2
  51. package/dist/content/media.d.ts.map +1 -1
  52. package/dist/content/media.js +92 -9
  53. package/dist/content/media.js.map +1 -1
  54. package/dist/content/pinned.d.ts +26 -0
  55. package/dist/content/pinned.d.ts.map +1 -0
  56. package/dist/content/pinned.js +42 -0
  57. package/dist/content/pinned.js.map +1 -0
  58. package/dist/content/store.d.ts +7 -0
  59. package/dist/content/store.d.ts.map +1 -1
  60. package/dist/content/store.js +20 -0
  61. package/dist/content/store.js.map +1 -1
  62. package/dist/env.d.ts +7 -0
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/federation/actor.d.ts +15 -0
  65. package/dist/federation/actor.d.ts.map +1 -1
  66. package/dist/federation/actor.js +94 -3
  67. package/dist/federation/actor.js.map +1 -1
  68. package/dist/federation/article.d.ts +13 -1
  69. package/dist/federation/article.d.ts.map +1 -1
  70. package/dist/federation/article.js +62 -7
  71. package/dist/federation/article.js.map +1 -1
  72. package/dist/federation/delivery.d.ts.map +1 -1
  73. package/dist/federation/delivery.js +36 -4
  74. package/dist/federation/delivery.js.map +1 -1
  75. package/dist/federation/federation.d.ts.map +1 -1
  76. package/dist/federation/federation.js +24 -2
  77. package/dist/federation/federation.js.map +1 -1
  78. package/dist/federation/mount.js +2 -2
  79. package/dist/federation/mount.js.map +1 -1
  80. package/dist/federation/paths.d.ts +30 -0
  81. package/dist/federation/paths.d.ts.map +1 -1
  82. package/dist/federation/paths.js +36 -0
  83. package/dist/federation/paths.js.map +1 -1
  84. package/dist/images/icons.d.ts +5 -0
  85. package/dist/images/icons.d.ts.map +1 -1
  86. package/dist/images/icons.js +13 -8
  87. package/dist/images/icons.js.map +1 -1
  88. package/dist/images/paths.d.ts +1 -1
  89. package/dist/images/paths.d.ts.map +1 -1
  90. package/dist/images/paths.js.map +1 -1
  91. package/dist/images/variants.d.ts +11 -0
  92. package/dist/images/variants.d.ts.map +1 -1
  93. package/dist/images/variants.js +21 -0
  94. package/dist/images/variants.js.map +1 -1
  95. package/dist/index.d.ts +5 -5
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +7 -3
  98. package/dist/index.js.map +1 -1
  99. package/dist/indieauth/identity.d.ts +3 -3
  100. package/dist/indieauth/identity.d.ts.map +1 -1
  101. package/dist/indieauth/identity.js +6 -7
  102. package/dist/indieauth/identity.js.map +1 -1
  103. package/dist/web/admin-bar.d.ts +3 -1
  104. package/dist/web/admin-bar.d.ts.map +1 -1
  105. package/dist/web/admin-bar.js +5 -1
  106. package/dist/web/admin-bar.js.map +1 -1
  107. package/dist/web/assets.d.ts +14 -0
  108. package/dist/web/assets.d.ts.map +1 -1
  109. package/dist/web/assets.js +56 -1
  110. package/dist/web/assets.js.map +1 -1
  111. package/dist/web/authors.d.ts +19 -0
  112. package/dist/web/authors.d.ts.map +1 -1
  113. package/dist/web/authors.js +25 -0
  114. package/dist/web/authors.js.map +1 -1
  115. package/dist/web/context.d.ts +26 -0
  116. package/dist/web/context.d.ts.map +1 -1
  117. package/dist/web/context.js +15 -0
  118. package/dist/web/context.js.map +1 -1
  119. package/dist/web/feed-atom.d.ts.map +1 -1
  120. package/dist/web/feed-atom.js +22 -1
  121. package/dist/web/feed-atom.js.map +1 -1
  122. package/dist/web/feed-item.d.ts +34 -6
  123. package/dist/web/feed-item.d.ts.map +1 -1
  124. package/dist/web/feed-item.js +31 -7
  125. package/dist/web/feed-item.js.map +1 -1
  126. package/dist/web/feed-json.d.ts +15 -2
  127. package/dist/web/feed-json.d.ts.map +1 -1
  128. package/dist/web/feed-json.js +17 -1
  129. package/dist/web/feed-json.js.map +1 -1
  130. package/dist/web/feed-rss.d.ts.map +1 -1
  131. package/dist/web/feed-rss.js +60 -3
  132. package/dist/web/feed-rss.js.map +1 -1
  133. package/dist/web/feed-source.d.ts +3 -0
  134. package/dist/web/feed-source.d.ts.map +1 -1
  135. package/dist/web/feed-source.js.map +1 -1
  136. package/dist/web/feed-xml.d.ts +7 -1
  137. package/dist/web/feed-xml.d.ts.map +1 -1
  138. package/dist/web/feed-xml.js +8 -2
  139. package/dist/web/feed-xml.js.map +1 -1
  140. package/dist/web/feeds.d.ts +1 -1
  141. package/dist/web/feeds.d.ts.map +1 -1
  142. package/dist/web/feeds.js +13 -2
  143. package/dist/web/feeds.js.map +1 -1
  144. package/dist/web/index.d.ts +5 -2
  145. package/dist/web/index.d.ts.map +1 -1
  146. package/dist/web/index.js +3 -1
  147. package/dist/web/index.js.map +1 -1
  148. package/dist/web/license.d.ts +94 -0
  149. package/dist/web/license.d.ts.map +1 -0
  150. package/dist/web/license.js +116 -0
  151. package/dist/web/license.js.map +1 -0
  152. package/dist/web/oembed.d.ts +104 -0
  153. package/dist/web/oembed.d.ts.map +1 -0
  154. package/dist/web/oembed.js +289 -0
  155. package/dist/web/oembed.js.map +1 -0
  156. package/dist/web/opensearch.d.ts +35 -0
  157. package/dist/web/opensearch.d.ts.map +1 -0
  158. package/dist/web/opensearch.js +65 -0
  159. package/dist/web/opensearch.js.map +1 -0
  160. package/dist/web/render.d.ts.map +1 -1
  161. package/dist/web/render.js +16 -8
  162. package/dist/web/render.js.map +1 -1
  163. package/dist/web/routes.d.ts.map +1 -1
  164. package/dist/web/routes.js +82 -6
  165. package/dist/web/routes.js.map +1 -1
  166. package/dist/web/share-image.d.ts +7 -7
  167. package/dist/web/share-image.d.ts.map +1 -1
  168. package/dist/web/share-image.js +8 -7
  169. package/dist/web/share-image.js.map +1 -1
  170. package/dist/web/templates.d.ts.map +1 -1
  171. package/dist/web/templates.js +5 -0
  172. package/dist/web/templates.js.map +1 -1
  173. package/package.json +2 -1
  174. package/templates/site/content/_data/site.json +0 -1
  175. package/themes/default/README.md +142 -48
  176. package/themes/default/layouts/base.njk +25 -6
  177. package/themes/default/layouts/front-page.njk +1 -0
  178. package/themes/default/layouts/home.njk +1 -0
  179. package/themes/default/layouts/post.njk +4 -0
  180. package/themes/default/partials/bio.njk +7 -0
  181. package/themes/default/partials/jsonld.njk +30 -7
  182. package/themes/default/partials/recording.njk +41 -0
  183. package/themes/default/src/style.css +16 -0
  184. package/themes/default/static/style.css +29 -0
@@ -1,5 +1,6 @@
1
1
  import { Environment, FileSystemLoader } from 'nunjucks';
2
2
  import { calendarDayIn, DEFAULT_TIMEZONE } from '../content/time.js';
3
+ import { accountOf } from '../federation/paths.js';
3
4
  import { themeAssetUrl } from './assets.js';
4
5
  import { siteLocale, siteTimezone } from './context.js';
5
6
  import { canonicalLocale, DEFAULT_LOCALE, pluralForm } from './locale.js';
@@ -113,6 +114,10 @@ function addFilters(environment, baseUrl, loader) {
113
114
  return pathname;
114
115
  }
115
116
  });
117
+ // `{{ siteAuthor.username | fediverseHandle }}`: a user's own account on
118
+ // this site, `@ada@example.com`, exactly as WebFinger answers for it
119
+ // (TASK-202), so a theme never spells the host.
120
+ environment.addFilter('fediverseHandle', (value) => typeof value === 'string' && value !== '' ? `@${accountOf(value, baseUrl)}` : '');
116
121
  return environment;
117
122
  }
118
123
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/web/templates.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/web/templates.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekity/cms",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "A file-first CMS: Markdown content, a SQLite index, a Hono server that is both the editor and the public site.",
6
6
  "license": "MIT",
@@ -55,6 +55,7 @@
55
55
  "codemirror": "^6.0.2",
56
56
  "esbuild": "^0.28.2",
57
57
  "highlight.js": "11.12.0",
58
+ "microformats-parser": "^2.0.6",
58
59
  "tailwindcss": "^4.3.3",
59
60
  "tsx": "^4.23.15",
60
61
  "typescript": "^7.0.2"
@@ -2,7 +2,6 @@
2
2
  "title": "A Geekity site",
3
3
  "tagline": "Markdown on disk, served straight",
4
4
  "url": "http://localhost:3000",
5
- "author": "You",
6
5
  "language": "en",
7
6
  "menus": {
8
7
  "primary": [
@@ -204,8 +204,11 @@ stylesheet tells the two apart. `menus.primary` is printed inside the
204
204
  tagline, and everywhere else the stylesheet lays the header out as one line so
205
205
  it sits beside the link home. See [Navigation](#navigation).
206
206
 
207
- **The footer** prints the copyright with the current year and `site.author`,
208
- `Published with Geekity`, and then `menus.footer` — the `footer` area this
207
+ **The footer** prints the copyright with the current year and the site
208
+ author's display name, `soloAuthor.name`, or the site title on a site with
209
+ several authors,
210
+ then the page's license, when it has one, as `<a rel="license">` with its name,
211
+ then `Published with Geekity`, and then `menus.footer` — the `footer` area this
209
212
  theme declares, and the whole of what the footer links. A site that wants its
210
213
  feed there types an `RSS | /feed/` line into it, the way the starter site does.
211
214
  Nothing in the footer is read off an account: it used to hold one `rel="me"`
@@ -214,7 +217,9 @@ presented as the site's. An empty or missing footer menu prints no list at all,
214
217
  and the copyright line stands on its own. The year is
215
218
  `{{ "now" | date("year") }}` — `now` is the one word the `date` filter reads
216
219
  rather than parses — so it is the year at the moment the page is rendered, in
217
- the site's own timezone.
220
+ the site's own timezone. The license is `license`: a post's own from its
221
+ `license` front matter, else the site's from Settings > General, and nothing
222
+ on a site with none.
218
223
 
219
224
  **The breadcrumb** opens `<main>` on a page with a place in the site's
220
225
  hierarchy (TASK-150): a post filed under a category, and a category, tag or
@@ -228,8 +233,8 @@ two cannot disagree. Every other page has an empty trail and prints nothing. A
228
233
  site that wants no visible breadcrumb empties the `breadcrumbs` block, and the
229
234
  `BreadcrumbList` stays in the graph.
230
235
 
231
- Webrings, badges, a licence notice and anything else that is markup rather than
232
- a link are deliberately not in the package. They go in a site theme's `footer`
236
+ Webrings, badges and anything else that is markup rather than a link are
237
+ deliberately not in the package. They go in a site theme's `footer`
233
238
  block:
234
239
 
235
240
  ```njk
@@ -398,6 +403,41 @@ is a `p-category` of this entry, as one `p.post-categories` from
398
403
  `partials/tags.njk`. The stylesheet draws the `#` in front of each, so the
399
404
  category a parser reads is the tag and not the hash.
400
405
 
406
+ **A post with a recording plays it** (TASK-213), from
407
+ `partials/recording.njk`, between the header and the `e-content`. The main file
408
+ is the browser's own player, `<audio controls>` or `<video controls>` as
409
+ `enclosure.player` says, with `preload="metadata"` so a reader who never presses
410
+ play downloads nothing, and a download link inside it for a browser that plays
411
+ neither. The `<source>` carries `u-audio` or `u-video`, so a microformats parser
412
+ finds the file on the entry. A video whose transcript is WebVTT also gets it as
413
+ a `<track kind="captions">`. Under the player a `figcaption` links the
414
+ transcript, as Captions for VTT or SRT and Transcript otherwise, and lists the
415
+ other versions by their title, else their media type. A post without a
416
+ recording, or whose front matter is missing what a player needs, prints none of
417
+ it.
418
+
419
+ ```html
420
+ <figure class="post-recording">
421
+ <audio controls preload="metadata" aria-label="Recording of Episode twelve">
422
+ <source
423
+ class="u-audio"
424
+ src="/uploads/2026/10/episode-12.mp3"
425
+ type="audio/mpeg"
426
+ />
427
+ <a href="/uploads/2026/10/episode-12.mp3">Download the recording</a>
428
+ </audio>
429
+ <figcaption class="post-recording-links">
430
+ <p><a href="/uploads/2026/10/episode-12.vtt">Captions</a></p>
431
+ <p id="post-recording-versions">Other versions</p>
432
+ <ul aria-labelledby="post-recording-versions">
433
+ <li>
434
+ <a href="/uploads/2026/10/episode-12.mp4" type="video/mp4">Video</a>
435
+ </li>
436
+ </ul>
437
+ </figcaption>
438
+ </figure>
439
+ ```
440
+
401
441
  After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
402
442
  posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
403
443
  `span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
@@ -431,12 +471,17 @@ Links box typed. Set `bioRelMe` to `false` to keep the card and drop its
431
471
  claims: the name is linked `rel="author"` alone and `me` comes off every
432
472
  link.
433
473
 
434
- **The homepage** speaks for the site's author only on a solo author blog,
435
- which `soloAuthor` on the context says. Then both `layouts/home.njk` at `/` and
436
- `layouts/front-page.njk` end with that person's card, with its `rel="me"`
437
- claims, and their author archive's link home carries `rel="me"` back, so the
438
- homepage and the archive name each other. On any other site a listing homepage
439
- prints no card, and the front page prints its card with `bioRelMe` false. Each of
474
+ **The homepage** speaks for the site's author only on a site whose Site author
475
+ setting names a user, which `soloAuthor` on the context says. Then both
476
+ `layouts/home.njk` at `/` and `layouts/front-page.njk` end with that person's
477
+ card, with its `rel="me"` claims, and their author archive's link home carries
478
+ `rel="me"` back, so the homepage and the archive name each other. Those two
479
+ layouts also set `bioHome` to the homepage's absolute URL, which puts a
480
+ `data.u-url.u-uid` holding it first in the card, so a parser running the
481
+ [representative h-card algorithm](https://microformats.org/wiki/representative-h-card-parsing)
482
+ on the homepage finds this card as the site's (TASK-193). The archive stays a
483
+ second `u-url`. No other page sets `bioHome`. On a site
484
+ with several authors neither homepage prints a card. Each of
440
485
  those is printed only when the profile says it, so a profile holding a name
441
486
  alone prints a name alone. A name this site has no account for is printed
442
487
  unlinked, because the file still said somebody wrote this.
@@ -459,7 +504,7 @@ an overridden layout — either includes this partial instead or writes the line
459
504
  itself. It was four lines of markup, and the object behind it has not changed:
460
505
 
461
506
  ```njk
462
- {% set writer = author or { name: site.author } %}
507
+ {% set writer = author or { name: site.title } %}
463
508
  <span class="p-author h-card">
464
509
  {%- if writer.url %}<a class="u-url" href="{{ writer.url | url }}">{{ writer.name }}</a>
465
510
  {%- else %}{{ writer.name }}{% endif %}
@@ -702,17 +747,28 @@ included. `og:url` is the canonical URL. An article also carries
702
747
  publish date), `article:author` (the author's profile URL here, else their
703
748
  name) and one `article:tag` per tag.
704
749
 
705
- **The picture** is the `image` in the entry's front matter, else `site.avatar`,
706
- on the context as `shareImage`. A site with neither prints no `og:image` and no
707
- `twitter:image` rather than an empty one. The picture always carries
708
- `og:image:alt` and `twitter:image:alt`. For the entry's own picture, the alt
709
- text is its `imageAlt` front matter, else what the media library says about the
710
- upload, else the entry's title. For the avatar, it is the media library's text,
711
- else the site's author. `og:image:width` and `og:image:height` are printed when
712
- the image's variants have been derived, because the size is read from their
713
- sidecar rather than from the file. `twitter:card` is `summary_large_image` for a
714
- picture at least 1200 pixels wide and wider than it is tall. Everything else,
715
- the avatar included, is `summary`, the small picture beside the words.
750
+ **The picture** is the first of these that the page has, on the context as
751
+ `shareImage`:
752
+
753
+ 1. The `image` in the entry's front matter.
754
+ 2. `site.avatar`, which only a hand edit of `site.json` sets.
755
+ 3. The profile photo of the person the page is by: the writer of a post or page
756
+ that names an author, the user of an author archive, and the site's author
757
+ on a solo-author site's homepage and other pages about nobody in particular.
758
+ 4. The site icon, from `icon` in `site.json`.
759
+
760
+ A page with none of these prints no `og:image` and no `twitter:image` rather
761
+ than an empty one. The picture always carries `og:image:alt` and
762
+ `twitter:image:alt`. The alt text is what the media library says about the
763
+ upload, else words for what it shows. For the entry's own picture, its
764
+ `imageAlt` front matter comes first, and the entry's title last. For the
765
+ avatar, the last resort is the site's author; for a photo, that person's
766
+ display name; for the icon, the site's title. `og:image:width` and
767
+ `og:image:height` are printed when the image's variants have been derived,
768
+ because the size is read from their sidecar rather than from the file.
769
+ `twitter:card` is `summary_large_image` for a picture at least 1200 pixels wide
770
+ and wider than it is tall. Everything else, a square photo included, is
771
+ `summary`, the small picture beside the words.
716
772
 
717
773
  **The icons** come from the site's `icon` setting in `site.json`, else
718
774
  `site.avatar`, through the derived images (decision-10). `icon` exists for a
@@ -734,26 +790,63 @@ site with no icon. `/manifest.webmanifest` gives the site's title as `name` and
734
790
  pixels plus a maskable 512 whose picture sits inside the middle 80%, padded
735
791
  with its own dominant colour. The head links it on every page.
736
792
 
793
+ **The search description** is a route too. `/opensearch.xml` is an OpenSearch
794
+ 1.1 description that names the site by its title (cut to the 16 characters
795
+ `ShortName` allows), describes it by its tagline, else its title, shows
796
+ `/favicon.ico` as its `Image` when the site has an icon, and searches
797
+ `/search/?q={searchTerms}`. Every URL in it is absolute and carries the base
798
+ path. The head links it on every page with `<link rel="search"
799
+ type="application/opensearchdescription+xml">`, outside the `head` block, so a
800
+ browser offers the site as a search engine. A theme that replaces `base.njk`
801
+ copies that line to keep the offer.
802
+
803
+ **The embed card** is a route too. `/_geekity/oembed?url=…` is an oEmbed
804
+ provider: for the URL of a published post or page it answers a `rich` embed
805
+ with the title, the author and their archive, the site as provider, a
806
+ thumbnail when the post's own `image` has a recorded size, and an `html`
807
+ card that is a blockquote of escaped text and links with no script, followed
808
+ by a hidden, sandboxed `<iframe>` of `/_geekity/embed?url=…`. That route is
809
+ the same card as a page of its own, and it is the one response another site may
810
+ frame. WordPress keeps a card from a provider it does not know only when the
811
+ card has an iframe, and it shows the frame once the page inside reports its
812
+ height. A consumer that strips iframes still has the blockquote. `maxwidth`
813
+ and `maxheight` are honoured, `format=xml` gives the same fields as XML, and
814
+ any other URL is a 404. The head of each post and page links it with
815
+ `<link rel="alternate" type="application/json+oembed">` and its `text/xml+oembed`
816
+ twin, outside the `head` block. A theme that replaces `base.njk` copies those
817
+ lines to keep the card.
818
+
737
819
  **The structured data** is `partials/jsonld.njk`, one `<script
738
820
  type="application/ld+json">` holding one `@graph` per page, and it is the only
739
821
  structured data the theme emits — there is no Microdata anywhere, by
740
822
  decision-16, because the visible markup already carries microformats2 for the
741
823
  IndieWeb. The graph holds:
742
824
 
743
- - `WebSite`, always, with the site's title, tagline and URL, and a `publisher`
744
- pointing at the Person, and a `potentialAction` that is a `SearchAction` on
745
- `/search/?q={search_term_string}`. On the homepage of a solo author blog it
746
- also has an `about` pointing at the site author's Person.
825
+ - `WebSite`, always, with the site's title, tagline and URL, and a
826
+ `potentialAction` that is a `SearchAction` on
827
+ `/search/?q={search_term_string}`. On a site whose author is a user, its
828
+ `publisher` and `about` both point at that user's Person, on every page. On
829
+ a site with several authors its `publisher` is the Organization below and it
830
+ has no `about`. With a site license, `license` is its URL.
831
+ - `Organization`, on a site with several authors only: the site itself, named
832
+ for its title, at `{baseUrl}/#organization`. It publishes the `WebSite` and
833
+ every entry.
747
834
  - `Person`, from `siteAuthor`: their name, archive URL, avatar, bio, job title
748
835
  and location, and a `sameAs` of their profile links and their actor id, which
749
836
  is what asserts that the schema.org Person and the fediverse actor are one
750
837
  identity. Identity comes from a user profile, so a site — or a byline —
751
- naming nobody with an account here prints no Person, and the `author` and
752
- `publisher` references go with it.
838
+ naming nobody with an account here prints no Person, and the `author`
839
+ reference goes with it. A site with several authors prints no Person on a
840
+ page about nobody in particular.
753
841
  - `ProfilePage` on an author archive, whose `mainEntity` is that Person.
754
842
  - `BlogPosting` on a post and `Article` on a page, with the headline, URL,
755
843
  `mainEntityOfPage`, `datePublished`, `dateModified`, description, image,
756
- `author` and `publisher`.
844
+ `author`, and the site's `publisher`: the site author's Person, or the
845
+ Organization. A post with no title is headed by the words its `<title>`
846
+ names it by, its `label`, cut on a word boundary to at most 110 characters.
847
+ Its `image` is the picture the Open Graph tags print, so the two always
848
+ agree (see [The head](#the-head)). Its `license` is the page's license URL,
849
+ the front matter's over the site's, and absent with none.
757
850
  - `BreadcrumbList` wherever the page prints a breadcrumb (see
758
851
  [The page shell](#the-page-shell)): one `ListItem` per crumb with its
759
852
  `position`, `name` and absolute URL as `item`.
@@ -1012,16 +1105,18 @@ an Eleventy build needs few edits. It is part of the package's semver contract.
1012
1105
 
1013
1106
  Every template gets:
1014
1107
 
1015
- | Key | What it holds |
1016
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1017
- | `site` | `content/_data/site.json`, if the site has one, over the defaults `title` and `url`. Any key in the file is readable, so `site.tagline`, `site.author` and anything else a site adds are all available. |
1018
- | `menus` | Every menu the site stores, by name, marked for this page: `menus.primary`, `menus.footer`, and any other name. See [Navigation](#navigation). |
1019
- | `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
1020
- | `soloAuthor` | The site author's profile, on a site whose Solo author blog setting is on. **Absent** when it is off or the author setting names nobody. See [The bio](#the-bio). |
1021
- | `icons` | The site's icons, as `{ rel, sizes, type, href }`. Empty until the site has an icon or an avatar to derive them from. See [The head](#the-head). |
1022
- | `theme` | The colours the theme declares, as `{ colorScheme, themeColor: { light, dark } }`, each absent when no theme declares it. See [The manifest](#the-manifest). |
1023
- | `shareImage` | The picture a shared link shows, as `{ url, alt, size: { width, height }, card }`. `size` is absent when it is not known. **Absent** when there is no picture. See [The head](#the-head). |
1024
- | `llmsTxt` | The path of `/llms.txt`, while the site serves it. **Absent** when the site has turned it off. The default theme links it from the front page only. |
1108
+ | Key | What it holds |
1109
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1110
+ | `site` | `content/_data/site.json`, if the site has one, over the defaults `title` and `url`. Any key in the file is readable, so `site.tagline`, `site.author` and anything else a site adds are all available. |
1111
+ | `menus` | Every menu the site stores, by name, marked for this page: `menus.primary`, `menus.footer`, and any other name. See [Navigation](#navigation). |
1112
+ | `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
1113
+ | `soloAuthor` | The site author's profile, on every page of a site whose Site author setting names a user. **Absent** on a site with several authors. See [The bio](#the-bio). |
1114
+ | `icons` | The site's icons, as `{ rel, sizes, type, href }`. Empty until the site has an icon or an avatar to derive them from. See [The head](#the-head). |
1115
+ | `theme` | The colours the theme declares, as `{ colorScheme, themeColor: { light, dark } }`, each absent when no theme declares it. See [The manifest](#the-manifest). |
1116
+ | `shareImage` | The picture a shared link shows, as `{ url, alt, size: { width, height }, card }`. `size` is absent when it is not known. **Absent** when there is no picture. See [The head](#the-head). |
1117
+ | `llmsTxt` | The path of `/llms.txt`, while the site serves it. **Absent** when the site has turned it off. The default theme links it from the front page only. |
1118
+ | `license` | The page's license as `{ name, url }`: the page's `license` front matter, else the site's. **Undefined** with none. It replaces the raw front matter value on the context. |
1119
+ | `siteLicense` | The site's own license as `{ name, url }`, whatever the page says. **Undefined** with none. |
1025
1120
 
1026
1121
  A document — one post, one page, or one entry of a listing — adds:
1027
1122
 
@@ -1042,6 +1137,7 @@ A document — one post, one page, or one entry of a listing — adds:
1042
1137
  | `type` | `post` or `page`. |
1043
1138
  | `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
1044
1139
  | `lang` | The `lang` front matter as a canonical tag, such as `fr-CA`. Absent when it names none or no tag. |
1140
+ | `enclosure` | The post's recording, parsed, with `player` (`audio` or `video`) and `transcript.captions`. Absent with none. |
1045
1141
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1046
1142
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1047
1143
  | `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
@@ -1121,14 +1217,12 @@ account. It resolves in this order:
1121
1217
 
1122
1218
  - the document's own `author`, on a post or a page that names one;
1123
1219
  - the person whose archive it is, on an author archive;
1124
- - the profile behind the site's `author` setting, everywhere else — the home
1125
- page, a taxonomy archive, a page that names nobody, the 404.
1126
-
1127
- It is **absent** when none of those name anybody this site has. The site
1128
- setting is read more strictly than a byline is: a byline prints the name a file
1129
- gives whether or not somebody answers to it, but a site author with no profile
1130
- behind it has no picture, no bio and nowhere to link, so there is nothing to
1131
- print and the key is not there. Write `{% if siteAuthor %}` around the bio.
1220
+ - the site author, `soloAuthor`, everywhere else — the home page, a taxonomy
1221
+ archive, a page that names nobody, the 404.
1222
+
1223
+ It is **absent** when none of those name anybody this site has, which on a
1224
+ site with several authors is every page about nobody in particular. Write
1225
+ `{% if siteAuthor %}` around the bio.
1132
1226
 
1133
1227
  The URL is not only a page. Each user is an ActivityPub actor at that address,
1134
1228
  so it is the page a follower lands on when they click through from the
@@ -40,9 +40,8 @@
40
40
 
41
41
  `metaDescription` is the page's own description, else the entry's summary
42
42
  — the very line the feeds publish — else the site's tagline. `metaImage` is
43
- `shareImage.url`: the picture the front matter names, else the site's
44
- avatar; with neither, nothing is printed rather than an empty tag. The CMS
45
- works out `shareImage` because its alt text and its size live in files a
43
+ `shareImage.url`; with none, nothing is printed rather than an empty tag.
44
+ The CMS works out `shareImage` because its alt text and its size live in files a
46
45
  template cannot read. `isEntry` is a rendered post
47
46
  or page, which is what Open Graph calls an article: a listing has
48
47
  `pagination`, and the posts page is a listing carrying a page's front
@@ -152,6 +151,16 @@
152
151
  {% if webmention %}
153
152
  <link rel="webmention" href="{{ webmention | url }}">
154
153
  {% endif %}
154
+ {#- The site's search, for a browser to offer from its address bar
155
+ (TASK-204). #}
156
+ <link rel="search" type="application/opensearchdescription+xml" title="{{ site.title }}" href="{{ "/opensearch.xml" | url }}">
157
+ {#- Where a tool that is pasted this post's URL asks for its card
158
+ (TASK-205), as JSON and as XML. #}
159
+ {% if isEntry %}
160
+ {% set oembed = "/_geekity/oembed?url=" + (metaUrl | urlencode) + "&format=" %}
161
+ <link rel="alternate" type="application/json+oembed" href="{{ (oembed + "json") | absoluteUrl }}" title="{{ metaTitle }}">
162
+ <link rel="alternate" type="text/xml+oembed" href="{{ (oembed + "xml") | absoluteUrl }}" title="{{ metaTitle }}">
163
+ {% endif %}
155
164
  {#- The site's index for language models (TASK-149), from the front page,
156
165
  which is where a tool that found the site looks for it. #}
157
166
  {% if llmsTxt and atRoot %}
@@ -194,13 +203,23 @@
194
203
  {% endif %}
195
204
  {% if siteAuthor.url %}
196
205
  <meta property="article:author" content="{{ siteAuthor.url | absoluteUrl }}">
197
- {% elif siteAuthor.name or author.name or author or site.author %}
198
- <meta property="article:author" content="{{ siteAuthor.name or author.name or author or site.author }}">
206
+ {% elif siteAuthor.name or author.name or author %}
207
+ <meta property="article:author" content="{{ siteAuthor.name or author.name or author }}">
199
208
  {% endif %}
200
209
  {% for tag in tags %}
201
210
  <meta property="article:tag" content="{{ tag }}">
202
211
  {% endfor %}
203
212
  {% endif %}
213
+ {#
214
+ Who a Mastodon link preview credits, with a follow button (TASK-202):
215
+ the user's own account here, on an entry by them and on the homepage of
216
+ a site one person writes. Mastodon shows it only when that account lists
217
+ this site's domain under author attribution.
218
+ #}
219
+ {% set creator = siteAuthor if isEntry else (soloAuthor if atRoot) %}
220
+ {% if creator.username %}
221
+ <meta name="fediverse:creator" content="{{ creator.username | fediverseHandle }}">
222
+ {% endif %}
204
223
  <meta name="twitter:card" content="{{ shareImage.card if shareImage else "summary" }}">
205
224
  <meta name="twitter:title" content="{{ metaTitle }}">
206
225
  {% if metaDescription %}
@@ -261,7 +280,7 @@
261
280
 
262
281
  {% block footer %}
263
282
  <footer>
264
- <p>&copy; {{ "now" | date("year") }}{% if site.author %}, {{ site.author }}{% endif %} &middot; Published with Geekity</p>
283
+ <p>&copy; {{ "now" | date("year") }}, {{ soloAuthor.name or site.title }}{% if license %} &middot; <a rel="license" href="{{ license.url }}">{{ license.name }}</a>{% endif %} &middot; Published with Geekity</p>
265
284
  {#
266
285
  The footer menu: the `footer` area this theme declares in its
267
286
  `theme.json`, which a site fills in on the Navigation screen, and the
@@ -22,6 +22,7 @@
22
22
  {#- Who the bio credits: the person the site's author setting names. -#}
23
23
  {% set bioAuthor = soloAuthor or siteAuthor %}
24
24
  {% set bioRelMe = true if soloAuthor else false %}
25
+ {% set bioHome = "/" | absoluteUrl if soloAuthor else "" %}
25
26
 
26
27
  {% block content %}
27
28
  <div class="page-body e-content">
@@ -27,6 +27,7 @@
27
27
  listing, and the homepage of any other site, is nobody's. -#}
28
28
  {% if atRoot and soloAuthor %}
29
29
  {% set bioAuthor = soloAuthor %}
30
+ {% set bioHome = "/" | absoluteUrl %}
30
31
  <hr>
31
32
  <footer>
32
33
  {% include "partials/bio.njk" %}
@@ -56,6 +56,10 @@
56
56
  {% include "partials/reply-context.njk" %}
57
57
  {% endif %}
58
58
 
59
+ {% if enclosure %}
60
+ {% include "partials/recording.njk" %}
61
+ {% endif %}
62
+
59
63
  <section class="e-content">
60
64
  {{ content | safe }}
61
65
  <p class="entry-meta">
@@ -38,6 +38,12 @@
38
38
  homepage of a site that is not a solo author blog sets it (TASK-180), because
39
39
  that page speaks for nobody even when it shows the site author's card.
40
40
 
41
+ `bioHome` set to the homepage's absolute URL makes the card the site's
42
+ representative h-card (TASK-193): its first `u-url` and its `u-uid` are that
43
+ URL, which is what a parser reading the homepage looks for to learn who the
44
+ site is. The name keeps its link to the archive, as a second `u-url`. Only
45
+ the homepage of a solo author blog sets it; every other card is unchanged.
46
+
41
47
  The `p-note` and the `rel="me"` links are printed wherever the card is, which
42
48
  is what `bioProfile` used to ask for on an author archive alone. An entry
43
49
  left that switch unset because the page footer carried the site's identity
@@ -55,6 +61,7 @@
55
61
 
56
62
  <div>
57
63
  <p>
64
+ {%- if bioHome %}<data class="u-url u-uid" value="{{ bioHome }}"></data>{% endif %}
58
65
  {{ bioLead | default("Written by") }}
59
66
  {%- if bioAuthor.url %}
60
67
  <a class="u-url p-name" rel="author{% if bioClaims %} me{% endif %}" href="{{ bioAuthor.url | url }}">{{ bioAuthor.name }}</a>
@@ -12,7 +12,12 @@
12
12
  `siteAuthor`: the site's own author on a page about nobody in particular, the
13
13
  writer on a post, the archive's user on an author archive. Identity comes
14
14
  from a user profile, so a name that answers to no account here has no Person
15
- at all, and the author and publisher references go with it.
15
+ at all, and the author reference goes with it.
16
+
17
+ Who publishes the site follows the Site author setting (TASK-192). A site
18
+ whose author is a user is published by that user's Person and is about
19
+ them, on every page. A site with several authors is published by an
20
+ Organization named for the site and is about nobody.
16
21
 
17
22
  How it is printed: every value goes through `dump`, which is JSON.stringify,
18
23
  with every `<` escaped to its `\u003C` form so nothing in a title or a
@@ -34,6 +39,10 @@
34
39
  {#- What the author, publisher and mainEntity members hold: a reference to the
35
40
  Person rather than a second copy of them. -#}
36
41
  {% set person = { "@id": personId } if personId else "" %}
42
+ {#- Who publishes the site and everything on it: the site author, else the
43
+ site itself. -#}
44
+ {% set soloId = (soloAuthor.url | absoluteUrl) + "#person" if soloAuthor.url else "" %}
45
+ {% set publisher = { "@id": soloId or home + "#organization" } %}
37
46
  {#- `sameAs` is everywhere else this person is: the profile links they listed,
38
47
  and their actor id, which asserts that the schema.org Person and the
39
48
  fediverse actor are one identity — decision-14 makes their archive URL the
@@ -43,6 +52,10 @@
43
52
  {% if personUrl %}{% set _ = sameAs.push(personUrl) %}{% endif %}
44
53
  {% set address = { "@type": "PostalAddress", "addressLocality": siteAuthor.location } if siteAuthor.location else "" %}
45
54
  {% set entryType = "BlogPosting" if type == "post" else "Article" %}
55
+ {#- A post with no title is headed by the words its <title> names it by,
56
+ within the 110 characters Google reads, cut where a word ends (TASK-201).
57
+ A page keeps its title. -#}
58
+ {% set headline = title or (label | truncate(109, false, "…") if type == "post" else "") %}
46
59
  {% set nodes = [] %}
47
60
  {% set website %}
48
61
  {
@@ -63,12 +76,21 @@
63
76
  },
64
77
  "query-input": "required name=search_term_string"
65
78
  }) }}
66
- {{- member("publisher", person) }}
67
- {#- What the site is about, on the homepage of a solo author blog
68
- (TASK-180): the site's author, and nobody on any other site. #}
69
- {{- member("about", { "@id": (soloAuthor.url | absoluteUrl) + "#person" } if atRoot and soloAuthor.url else "") }}
79
+ {{- member("publisher", publisher) }}
80
+ {{- member("about", { "@id": soloId } if soloId else "") }}
81
+ {{- member("license", siteLicense.url if siteLicense else "") }}
70
82
  }{% endset %}
71
83
  {% set _ = nodes.push(website) %}
84
+ {% if not soloId %}
85
+ {% set organization %}
86
+ {
87
+ "@type": "Organization"
88
+ {{- member("@id", publisher["@id"]) }}
89
+ {{- member("name", site.title) }}
90
+ {{- member("url", home) }}
91
+ }{% endset %}
92
+ {% set _ = nodes.push(organization) %}
93
+ {% endif %}
72
94
  {% if personId %}
73
95
  {% set profile %}
74
96
  {
@@ -98,7 +120,7 @@
98
120
  {% set entry %}
99
121
  {
100
122
  "@type": {{ entryType | dump | safe }}
101
- {{- member("headline", title) }}
123
+ {{- member("headline", headline) }}
102
124
  {{- member("url", metaUrl) }}
103
125
  {{- member("mainEntityOfPage", metaUrl) }}
104
126
  {{- member("datePublished", date | date("iso") if date else "") }}
@@ -106,7 +128,8 @@
106
128
  {{- member("description", metaDescription) }}
107
129
  {{- member("image", metaImage | absoluteUrl if metaImage else "") }}
108
130
  {{- member("author", person) }}
109
- {{- member("publisher", person) }}
131
+ {{- member("publisher", publisher) }}
132
+ {{- member("license", license.url if license else "") }}
110
133
  }{% endset %}
111
134
  {% set _ = nodes.push(entry) %}
112
135
  {% endif %}
@@ -0,0 +1,41 @@
1
+ {#
2
+ Timed captions in WebVTT go on a video as a track as well as in the links,
3
+ since that is the one format a browser shows over the picture. The source
4
+ carries `u-audio` or `u-video`, so a microformats parser finds the file on
5
+ the entry.
6
+
7
+ The `preload="metadata"` asks for the length and the first frame and no
8
+ more: a reader who never presses play does not download the episode.
9
+ #}
10
+ {% set recordingLabel = ("Recording of " ~ title) if named else "Recording" %}
11
+ <figure class="post-recording">
12
+ {% if enclosure.player == "video" %}
13
+ <video controls preload="metadata" aria-label="{{ recordingLabel }}">
14
+ <source class="u-video" src="{{ enclosure.url | url }}" type="{{ enclosure.type }}">
15
+ {% if enclosure.transcript and enclosure.transcript.type == "text/vtt" %}
16
+ <track kind="captions" src="{{ enclosure.transcript.url | url }}" srclang="{{ lang or (site.language | default("en")) }}" label="Captions" default>
17
+ {% endif %}
18
+ <a href="{{ enclosure.url | url }}">Download the video</a>
19
+ </video>
20
+ {% else %}
21
+ <audio controls preload="metadata" aria-label="{{ recordingLabel }}">
22
+ <source class="u-audio" src="{{ enclosure.url | url }}" type="{{ enclosure.type }}">
23
+ <a href="{{ enclosure.url | url }}">Download the recording</a>
24
+ </audio>
25
+ {% endif %}
26
+ {% if enclosure.alternates.length or enclosure.transcript %}
27
+ <figcaption class="post-recording-links">
28
+ {% if enclosure.transcript %}
29
+ <p><a href="{{ enclosure.transcript.url | url }}">{{ "Captions" if enclosure.transcript.captions else "Transcript" }}</a></p>
30
+ {% endif %}
31
+ {% if enclosure.alternates.length %}
32
+ <p id="post-recording-versions">Other versions</p>
33
+ <ul aria-labelledby="post-recording-versions">
34
+ {% for alternate in enclosure.alternates %}
35
+ <li><a href="{{ alternate.url | url }}" type="{{ alternate.type }}"{% if alternate.lang %} hreflang="{{ alternate.lang }}"{% endif %}>{{ alternate.title or alternate.type }}</a></li>
36
+ {% endfor %}
37
+ </ul>
38
+ {% endif %}
39
+ </figcaption>
40
+ {% endif %}
41
+ </figure>
@@ -660,6 +660,22 @@
660
660
  @apply text-[1.08rem] leading-[1.65];
661
661
  }
662
662
 
663
+ .post-recording {
664
+ @apply my-8;
665
+ }
666
+
667
+ .post-recording :is(audio, video) {
668
+ @apply block w-full;
669
+ }
670
+
671
+ .post-recording-links {
672
+ @apply mt-3 font-sans text-sm text-muted;
673
+ }
674
+
675
+ .post-recording-links ul {
676
+ @apply mt-1 flex flex-wrap gap-x-4 gap-y-1;
677
+ }
678
+
663
679
  /* An untitled post is its words, set larger. It is the one entry whose
664
680
  heading is the hidden one TASK-144 gave it. */
665
681
  .blog-post > .screen-reader-text ~ .e-content {