@geekity/cms 0.14.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 (120) hide show
  1. package/README.md +163 -52
  2. package/admin/pages/documents/editor.njk +69 -0
  3. package/admin/pages/media/library.njk +4 -3
  4. package/admin/pages/settings/general.njk +44 -0
  5. package/admin/static/admin.css +33 -0
  6. package/dist/admin/documents.d.ts +3 -0
  7. package/dist/admin/documents.d.ts.map +1 -1
  8. package/dist/admin/documents.js +38 -5
  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/media.d.ts +7 -2
  15. package/dist/admin/media.d.ts.map +1 -1
  16. package/dist/admin/media.js +4 -2
  17. package/dist/admin/media.js.map +1 -1
  18. package/dist/admin/settings-general.d.ts.map +1 -1
  19. package/dist/admin/settings-general.js +36 -2
  20. package/dist/admin/settings-general.js.map +1 -1
  21. package/dist/admin/settings-page.d.ts.map +1 -1
  22. package/dist/admin/settings-page.js +1 -2
  23. package/dist/admin/settings-page.js.map +1 -1
  24. package/dist/admin/settings.d.ts +26 -0
  25. package/dist/admin/settings.d.ts.map +1 -1
  26. package/dist/admin/settings.js +88 -2
  27. package/dist/admin/settings.js.map +1 -1
  28. package/dist/admin/uploads.d.ts +7 -2
  29. package/dist/admin/uploads.d.ts.map +1 -1
  30. package/dist/admin/uploads.js +23 -5
  31. package/dist/admin/uploads.js.map +1 -1
  32. package/dist/config.d.ts +17 -3
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +11 -7
  35. package/dist/config.js.map +1 -1
  36. package/dist/content/enclosure.d.ts +69 -0
  37. package/dist/content/enclosure.d.ts.map +1 -0
  38. package/dist/content/enclosure.js +165 -0
  39. package/dist/content/enclosure.js.map +1 -0
  40. package/dist/content/index.d.ts +3 -1
  41. package/dist/content/index.d.ts.map +1 -1
  42. package/dist/content/index.js +1 -0
  43. package/dist/content/index.js.map +1 -1
  44. package/dist/content/media.d.ts +15 -2
  45. package/dist/content/media.d.ts.map +1 -1
  46. package/dist/content/media.js +92 -9
  47. package/dist/content/media.js.map +1 -1
  48. package/dist/federation/article.d.ts.map +1 -1
  49. package/dist/federation/article.js +35 -6
  50. package/dist/federation/article.js.map +1 -1
  51. package/dist/images/icons.d.ts +5 -0
  52. package/dist/images/icons.d.ts.map +1 -1
  53. package/dist/images/icons.js +13 -8
  54. package/dist/images/icons.js.map +1 -1
  55. package/dist/images/paths.d.ts +1 -1
  56. package/dist/images/paths.d.ts.map +1 -1
  57. package/dist/images/paths.js.map +1 -1
  58. package/dist/index.d.ts +5 -5
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/web/assets.d.ts +14 -0
  63. package/dist/web/assets.d.ts.map +1 -1
  64. package/dist/web/assets.js +56 -1
  65. package/dist/web/assets.js.map +1 -1
  66. package/dist/web/context.d.ts +26 -0
  67. package/dist/web/context.d.ts.map +1 -1
  68. package/dist/web/context.js +15 -0
  69. package/dist/web/context.js.map +1 -1
  70. package/dist/web/feed-atom.d.ts.map +1 -1
  71. package/dist/web/feed-atom.js +20 -0
  72. package/dist/web/feed-atom.js.map +1 -1
  73. package/dist/web/feed-item.d.ts +19 -0
  74. package/dist/web/feed-item.d.ts.map +1 -1
  75. package/dist/web/feed-item.js +22 -0
  76. package/dist/web/feed-item.js.map +1 -1
  77. package/dist/web/feed-json.d.ts +13 -0
  78. package/dist/web/feed-json.d.ts.map +1 -1
  79. package/dist/web/feed-json.js +15 -0
  80. package/dist/web/feed-json.js.map +1 -1
  81. package/dist/web/feed-rss.d.ts.map +1 -1
  82. package/dist/web/feed-rss.js +59 -2
  83. package/dist/web/feed-rss.js.map +1 -1
  84. package/dist/web/feed-xml.d.ts +7 -1
  85. package/dist/web/feed-xml.d.ts.map +1 -1
  86. package/dist/web/feed-xml.js +8 -2
  87. package/dist/web/feed-xml.js.map +1 -1
  88. package/dist/web/feeds.d.ts +1 -1
  89. package/dist/web/feeds.d.ts.map +1 -1
  90. package/dist/web/feeds.js +6 -0
  91. package/dist/web/feeds.js.map +1 -1
  92. package/dist/web/index.d.ts +4 -1
  93. package/dist/web/index.d.ts.map +1 -1
  94. package/dist/web/index.js +2 -0
  95. package/dist/web/index.js.map +1 -1
  96. package/dist/web/license.d.ts +94 -0
  97. package/dist/web/license.d.ts.map +1 -0
  98. package/dist/web/license.js +116 -0
  99. package/dist/web/license.js.map +1 -0
  100. package/dist/web/oembed.d.ts.map +1 -1
  101. package/dist/web/oembed.js +1 -2
  102. package/dist/web/oembed.js.map +1 -1
  103. package/dist/web/render.d.ts.map +1 -1
  104. package/dist/web/render.js +11 -2
  105. package/dist/web/render.js.map +1 -1
  106. package/dist/web/routes.d.ts.map +1 -1
  107. package/dist/web/routes.js +8 -4
  108. package/dist/web/routes.js.map +1 -1
  109. package/dist/web/share-image.d.ts +7 -7
  110. package/dist/web/share-image.d.ts.map +1 -1
  111. package/dist/web/share-image.js +8 -7
  112. package/dist/web/share-image.js.map +1 -1
  113. package/package.json +1 -1
  114. package/themes/default/README.md +81 -28
  115. package/themes/default/layouts/base.njk +3 -4
  116. package/themes/default/layouts/post.njk +4 -0
  117. package/themes/default/partials/jsonld.njk +4 -6
  118. package/themes/default/partials/recording.njk +41 -0
  119. package/themes/default/src/style.css +16 -0
  120. package/themes/default/static/style.css +25 -0
@@ -1 +1 @@
1
- {"version":3,"file":"share-image.d.ts","sourceRoot":"","sources":["../../src/web/share-image.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAChF,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,qBAAqB,CAAC;CAClD;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,UAAU,GAAG,SAAS,CAmBzB"}
1
+ {"version":3,"file":"share-image.d.ts","sourceRoot":"","sources":["../../src/web/share-image.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAChF,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,qBAAqB,CAAC;CAClD;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC1C,GAAG,UAAU,GAAG,SAAS,CAuBzB"}
@@ -7,13 +7,11 @@ import { siteImageRecord, uploadPath } from '../images/markup.js';
7
7
  */
8
8
  export const LARGE_CARD_MIN_WIDTH = 1200;
9
9
  /**
10
- * The page's share image, or `undefined` when it has none: the entry's own
11
- * `image`, else the site's avatar.
10
+ * The page's share image, or `undefined` when it has none.
12
11
  *
13
12
  * The alt text is the front matter's `imageAlt` for the entry's own picture,
14
13
  * because one picture can need describing differently in two posts; else what
15
- * the media library says about the upload; else the entry's title for its own
16
- * picture and the site's author for the avatar, which is a picture of them.
14
+ * the media library says about the upload.
17
15
  *
18
16
  * The size is read off the variant sidecar, never off the file, so a render
19
17
  * never opens an image (decision-10). A picture with no sidecar has no size
@@ -21,14 +19,17 @@ export const LARGE_CARD_MIN_WIDTH = 1200;
21
19
  */
22
20
  export function shareImage(input) {
23
21
  const own = nonEmpty(input.image);
24
- const url = own ?? nonEmpty(input.avatar);
25
- if (url === undefined)
22
+ const chosen = own === undefined
23
+ ? input.fallbacks.find((fallback) => nonEmpty(fallback.url) !== undefined)
24
+ : { url: own, describedAs: input.title };
25
+ const url = nonEmpty(chosen?.url);
26
+ if (chosen === undefined || url === undefined)
26
27
  return undefined;
27
28
  const source = uploadPath(url);
28
29
  const described = source === undefined ? undefined : readAltTexts(input.config.contentDir).get(source);
29
30
  const alt = (own === undefined ? undefined : nonEmpty(input.imageAlt)) ??
30
31
  (described?.kind === 'described' ? nonEmpty(described.text) : undefined) ??
31
- (own === undefined ? input.owner : input.title);
32
+ chosen.describedAs;
32
33
  const record = source === undefined ? undefined : siteImageRecord(input.config, source);
33
34
  const size = record === undefined ? undefined : { width: record.width, height: record.height };
34
35
  const large = size !== undefined && size.width >= LARGE_CARD_MIN_WIDTH && size.width > size.height;
@@ -1 +1 @@
1
- {"version":3,"file":"share-image.js","sourceRoot":"","sources":["../../src/web/share-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAmBlE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,KAW1B;IACC,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,SAAS,GACb,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvF,MAAM,GAAG,GACP,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1D,CAAC,SAAS,EAAE,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAC/F,MAAM,KAAK,GACT,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,IAAI,oBAAoB,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;IAEvF,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC"}
1
+ {"version":3,"file":"share-image.js","sourceRoot":"","sources":["../../src/web/share-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAmBlE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAOzC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,KAS1B;IACC,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,MAAM,GACV,GAAG,KAAK,SAAS;QACf,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;QAC1E,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhE,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,SAAS,GACb,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvF,MAAM,GAAG,GACP,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1D,CAAC,SAAS,EAAE,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,MAAM,CAAC,WAAW,CAAC;IAErB,MAAM,MAAM,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAC/F,MAAM,KAAK,GACT,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,IAAI,oBAAoB,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;IAEvF,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekity/cms",
3
- "version": "0.14.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",
@@ -207,7 +207,8 @@ it sits beside the link home. See [Navigation](#navigation).
207
207
  **The footer** prints the copyright with the current year and the site
208
208
  author's display name, `soloAuthor.name`, or the site title on a site with
209
209
  several authors,
210
- `Published with Geekity`, and then `menus.footer` — the `footer` area this
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
211
212
  theme declares, and the whole of what the footer links. A site that wants its
212
213
  feed there types an `RSS | /feed/` line into it, the way the starter site does.
213
214
  Nothing in the footer is read off an account: it used to hold one `rel="me"`
@@ -216,7 +217,9 @@ presented as the site's. An empty or missing footer menu prints no list at all,
216
217
  and the copyright line stands on its own. The year is
217
218
  `{{ "now" | date("year") }}` — `now` is the one word the `date` filter reads
218
219
  rather than parses — so it is the year at the moment the page is rendered, in
219
- 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.
220
223
 
221
224
  **The breadcrumb** opens `<main>` on a page with a place in the site's
222
225
  hierarchy (TASK-150): a post filed under a category, and a category, tag or
@@ -230,8 +233,8 @@ two cannot disagree. Every other page has an empty trail and prints nothing. A
230
233
  site that wants no visible breadcrumb empties the `breadcrumbs` block, and the
231
234
  `BreadcrumbList` stays in the graph.
232
235
 
233
- Webrings, badges, a licence notice and anything else that is markup rather than
234
- 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`
235
238
  block:
236
239
 
237
240
  ```njk
@@ -400,6 +403,41 @@ is a `p-category` of this entry, as one `p.post-categories` from
400
403
  `partials/tags.njk`. The stylesheet draws the `#` in front of each, so the
401
404
  category a parser reads is the tag and not the hash.
402
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
+
403
441
  After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
404
442
  posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
405
443
  `span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
@@ -709,17 +747,28 @@ included. `og:url` is the canonical URL. An article also carries
709
747
  publish date), `article:author` (the author's profile URL here, else their
710
748
  name) and one `article:tag` per tag.
711
749
 
712
- **The picture** is the `image` in the entry's front matter, else `site.avatar`,
713
- on the context as `shareImage`. A site with neither prints no `og:image` and no
714
- `twitter:image` rather than an empty one. The picture always carries
715
- `og:image:alt` and `twitter:image:alt`. For the entry's own picture, the alt
716
- text is its `imageAlt` front matter, else what the media library says about the
717
- upload, else the entry's title. For the avatar, it is the media library's text,
718
- else the site's author. `og:image:width` and `og:image:height` are printed when
719
- the image's variants have been derived, because the size is read from their
720
- sidecar rather than from the file. `twitter:card` is `summary_large_image` for a
721
- picture at least 1200 pixels wide and wider than it is tall. Everything else,
722
- 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.
723
772
 
724
773
  **The icons** come from the site's `icon` setting in `site.json`, else
725
774
  `site.avatar`, through the derived images (decision-10). `icon` exists for a
@@ -778,7 +827,7 @@ IndieWeb. The graph holds:
778
827
  `/search/?q={search_term_string}`. On a site whose author is a user, its
779
828
  `publisher` and `about` both point at that user's Person, on every page. On
780
829
  a site with several authors its `publisher` is the Organization below and it
781
- has no `about`.
830
+ has no `about`. With a site license, `license` is its URL.
782
831
  - `Organization`, on a site with several authors only: the site itself, named
783
832
  for its title, at `{baseUrl}/#organization`. It publishes the `WebSite` and
784
833
  every entry.
@@ -795,8 +844,9 @@ IndieWeb. The graph holds:
795
844
  `author`, and the site's `publisher`: the site author's Person, or the
796
845
  Organization. A post with no title is headed by the words its `<title>`
797
846
  names it by, its `label`, cut on a word boundary to at most 110 characters.
798
- A post with no picture of its own, on a site with no avatar, takes its
799
- author's photo, else the site's `icon` upload.
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.
800
850
  - `BreadcrumbList` wherever the page prints a breadcrumb (see
801
851
  [The page shell](#the-page-shell)): one `ListItem` per crumb with its
802
852
  `position`, `name` and absolute URL as `item`.
@@ -1055,16 +1105,18 @@ an Eleventy build needs few edits. It is part of the package's semver contract.
1055
1105
 
1056
1106
  Every template gets:
1057
1107
 
1058
- | Key | What it holds |
1059
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1060
- | `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. |
1061
- | `menus` | Every menu the site stores, by name, marked for this page: `menus.primary`, `menus.footer`, and any other name. See [Navigation](#navigation). |
1062
- | `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
1063
- | `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). |
1064
- | `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). |
1065
- | `theme` | The colours the theme declares, as `{ colorScheme, themeColor: { light, dark } }`, each absent when no theme declares it. See [The manifest](#the-manifest). |
1066
- | `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). |
1067
- | `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. |
1068
1120
 
1069
1121
  A document — one post, one page, or one entry of a listing — adds:
1070
1122
 
@@ -1085,6 +1137,7 @@ A document — one post, one page, or one entry of a listing — adds:
1085
1137
  | `type` | `post` or `page`. |
1086
1138
  | `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
1087
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. |
1088
1141
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1089
1142
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1090
1143
  | `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
@@ -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
@@ -281,7 +280,7 @@
281
280
 
282
281
  {% block footer %}
283
282
  <footer>
284
- <p>&copy; {{ "now" | date("year") }}, {{ soloAuthor.name or site.title }} &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>
285
284
  {#
286
285
  The footer menu: the `footer` area this theme declares in its
287
286
  `theme.json`, which a site fills in on the Navigation screen, and the
@@ -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">
@@ -54,12 +54,8 @@
54
54
  {% set entryType = "BlogPosting" if type == "post" else "Article" %}
55
55
  {#- A post with no title is headed by the words its <title> names it by,
56
56
  within the 110 characters Google reads, cut where a word ends (TASK-201).
57
- Its picture is its own, else the site's avatar, else its author's photo,
58
- else the site icon: the original upload rather than a derived icon, which
59
- at 180 pixels is too small for an article image. A page keeps its title
60
- and its own picture. -#}
57
+ A page keeps its title. -#}
61
58
  {% set headline = title or (label | truncate(109, false, "…") if type == "post" else "") %}
62
- {% set entryImage = metaImage or (siteAuthor.avatar or site.icon if type == "post" else "") %}
63
59
  {% set nodes = [] %}
64
60
  {% set website %}
65
61
  {
@@ -82,6 +78,7 @@
82
78
  }) }}
83
79
  {{- member("publisher", publisher) }}
84
80
  {{- member("about", { "@id": soloId } if soloId else "") }}
81
+ {{- member("license", siteLicense.url if siteLicense else "") }}
85
82
  }{% endset %}
86
83
  {% set _ = nodes.push(website) %}
87
84
  {% if not soloId %}
@@ -129,9 +126,10 @@
129
126
  {{- member("datePublished", date | date("iso") if date else "") }}
130
127
  {{- member("dateModified", (updated or date) | date("iso") if (updated or date) else "") }}
131
128
  {{- member("description", metaDescription) }}
132
- {{- member("image", entryImage | absoluteUrl if entryImage else "") }}
129
+ {{- member("image", metaImage | absoluteUrl if metaImage else "") }}
133
130
  {{- member("author", person) }}
134
131
  {{- member("publisher", publisher) }}
132
+ {{- member("license", license.url if license else "") }}
135
133
  }{% endset %}
136
134
  {% set _ = nodes.push(entry) %}
137
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 {
@@ -1190,6 +1190,31 @@
1190
1190
  line-height: 1.65;
1191
1191
  }
1192
1192
 
1193
+ .post-recording {
1194
+ margin-block: calc(var(--spacing) * 8);
1195
+ }
1196
+
1197
+ .post-recording :is(audio, video) {
1198
+ width: 100%;
1199
+ display: block;
1200
+ }
1201
+
1202
+ .post-recording-links {
1203
+ margin-top: calc(var(--spacing) * 3);
1204
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
1205
+ font-size: var(--text-sm);
1206
+ line-height: var(--tw-leading, var(--text-sm--line-height));
1207
+ color: var(--color-muted);
1208
+ }
1209
+
1210
+ .post-recording-links ul {
1211
+ margin-top: var(--spacing);
1212
+ column-gap: calc(var(--spacing) * 4);
1213
+ row-gap: var(--spacing);
1214
+ flex-wrap: wrap;
1215
+ display: flex;
1216
+ }
1217
+
1193
1218
  .blog-post > .screen-reader-text ~ .e-content {
1194
1219
  --tw-leading: 1.55;
1195
1220
  font-size: 1.3rem;