@geekity/cms 0.14.0 → 0.16.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 (209) hide show
  1. package/README.md +270 -52
  2. package/admin/pages/documents/editor.njk +120 -0
  3. package/admin/pages/documents/syndication.njk +110 -0
  4. package/admin/pages/federation/followers.njk +7 -0
  5. package/admin/pages/media/library.njk +4 -3
  6. package/admin/pages/settings/general.njk +44 -0
  7. package/admin/static/admin.css +44 -0
  8. package/dist/admin/documents.d.ts +104 -0
  9. package/dist/admin/documents.d.ts.map +1 -1
  10. package/dist/admin/documents.js +220 -55
  11. package/dist/admin/documents.js.map +1 -1
  12. package/dist/admin/enclosure-field.d.ts +91 -0
  13. package/dist/admin/enclosure-field.d.ts.map +1 -0
  14. package/dist/admin/enclosure-field.js +282 -0
  15. package/dist/admin/enclosure-field.js.map +1 -0
  16. package/dist/admin/federation.d.ts +15 -0
  17. package/dist/admin/federation.d.ts.map +1 -1
  18. package/dist/admin/federation.js +19 -0
  19. package/dist/admin/federation.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/menu.d.ts.map +1 -1
  25. package/dist/admin/menu.js +3 -0
  26. package/dist/admin/menu.js.map +1 -1
  27. package/dist/admin/photo-field.d.ts +39 -0
  28. package/dist/admin/photo-field.d.ts.map +1 -0
  29. package/dist/admin/photo-field.js +91 -0
  30. package/dist/admin/photo-field.js.map +1 -0
  31. package/dist/admin/preview.d.ts.map +1 -1
  32. package/dist/admin/preview.js +6 -1
  33. package/dist/admin/preview.js.map +1 -1
  34. package/dist/admin/routes.d.ts.map +1 -1
  35. package/dist/admin/routes.js +4 -0
  36. package/dist/admin/routes.js.map +1 -1
  37. package/dist/admin/settings-general.d.ts.map +1 -1
  38. package/dist/admin/settings-general.js +36 -2
  39. package/dist/admin/settings-general.js.map +1 -1
  40. package/dist/admin/settings-page.d.ts.map +1 -1
  41. package/dist/admin/settings-page.js +1 -2
  42. package/dist/admin/settings-page.js.map +1 -1
  43. package/dist/admin/settings.d.ts +26 -0
  44. package/dist/admin/settings.d.ts.map +1 -1
  45. package/dist/admin/settings.js +88 -2
  46. package/dist/admin/settings.js.map +1 -1
  47. package/dist/admin/syndication-targets.d.ts +57 -0
  48. package/dist/admin/syndication-targets.d.ts.map +1 -0
  49. package/dist/admin/syndication-targets.js +266 -0
  50. package/dist/admin/syndication-targets.js.map +1 -0
  51. package/dist/admin/templates.d.ts +1 -0
  52. package/dist/admin/templates.d.ts.map +1 -1
  53. package/dist/admin/templates.js +1 -0
  54. package/dist/admin/templates.js.map +1 -1
  55. package/dist/admin/uploads.d.ts +13 -2
  56. package/dist/admin/uploads.d.ts.map +1 -1
  57. package/dist/admin/uploads.js +23 -5
  58. package/dist/admin/uploads.js.map +1 -1
  59. package/dist/config.d.ts +17 -3
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +11 -7
  62. package/dist/config.js.map +1 -1
  63. package/dist/content/citation.d.ts +36 -0
  64. package/dist/content/citation.d.ts.map +1 -0
  65. package/dist/content/citation.js +31 -0
  66. package/dist/content/citation.js.map +1 -0
  67. package/dist/content/enclosure.d.ts +69 -0
  68. package/dist/content/enclosure.d.ts.map +1 -0
  69. package/dist/content/enclosure.js +165 -0
  70. package/dist/content/enclosure.js.map +1 -0
  71. package/dist/content/index.d.ts +5 -1
  72. package/dist/content/index.d.ts.map +1 -1
  73. package/dist/content/index.js +2 -0
  74. package/dist/content/index.js.map +1 -1
  75. package/dist/content/media.d.ts +15 -2
  76. package/dist/content/media.d.ts.map +1 -1
  77. package/dist/content/media.js +92 -9
  78. package/dist/content/media.js.map +1 -1
  79. package/dist/content/photo.d.ts +45 -0
  80. package/dist/content/photo.d.ts.map +1 -0
  81. package/dist/content/photo.js +55 -0
  82. package/dist/content/photo.js.map +1 -0
  83. package/dist/content/post-type.d.ts +20 -8
  84. package/dist/content/post-type.d.ts.map +1 -1
  85. package/dist/content/post-type.js +17 -0
  86. package/dist/content/post-type.js.map +1 -1
  87. package/dist/federation/article.d.ts.map +1 -1
  88. package/dist/federation/article.js +93 -10
  89. package/dist/federation/article.js.map +1 -1
  90. package/dist/federation/citations.d.ts +38 -0
  91. package/dist/federation/citations.d.ts.map +1 -0
  92. package/dist/federation/citations.js +98 -0
  93. package/dist/federation/citations.js.map +1 -0
  94. package/dist/federation/delivery.d.ts.map +1 -1
  95. package/dist/federation/delivery.js +79 -31
  96. package/dist/federation/delivery.js.map +1 -1
  97. package/dist/images/alt-text.d.ts +6 -0
  98. package/dist/images/alt-text.d.ts.map +1 -1
  99. package/dist/images/alt-text.js +10 -0
  100. package/dist/images/alt-text.js.map +1 -1
  101. package/dist/images/icons.d.ts +5 -0
  102. package/dist/images/icons.d.ts.map +1 -1
  103. package/dist/images/icons.js +13 -8
  104. package/dist/images/icons.js.map +1 -1
  105. package/dist/images/paths.d.ts +1 -1
  106. package/dist/images/paths.d.ts.map +1 -1
  107. package/dist/images/paths.js.map +1 -1
  108. package/dist/index.d.ts +7 -7
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +25 -4
  111. package/dist/index.js.map +1 -1
  112. package/dist/indieauth/bearer.d.ts +6 -1
  113. package/dist/indieauth/bearer.d.ts.map +1 -1
  114. package/dist/indieauth/bearer.js +39 -12
  115. package/dist/indieauth/bearer.js.map +1 -1
  116. package/dist/indieauth/discovery.d.ts +9 -4
  117. package/dist/indieauth/discovery.d.ts.map +1 -1
  118. package/dist/indieauth/discovery.js +18 -7
  119. package/dist/indieauth/discovery.js.map +1 -1
  120. package/dist/micropub/create.d.ts +56 -0
  121. package/dist/micropub/create.d.ts.map +1 -0
  122. package/dist/micropub/create.js +200 -0
  123. package/dist/micropub/create.js.map +1 -0
  124. package/dist/micropub/endpoint.d.ts +21 -0
  125. package/dist/micropub/endpoint.d.ts.map +1 -0
  126. package/dist/micropub/endpoint.js +353 -0
  127. package/dist/micropub/endpoint.js.map +1 -0
  128. package/dist/micropub/media.d.ts +14 -0
  129. package/dist/micropub/media.d.ts.map +1 -0
  130. package/dist/micropub/media.js +93 -0
  131. package/dist/micropub/media.js.map +1 -0
  132. package/dist/micropub/update.d.ts +44 -0
  133. package/dist/micropub/update.d.ts.map +1 -0
  134. package/dist/micropub/update.js +133 -0
  135. package/dist/micropub/update.js.map +1 -0
  136. package/dist/web/assets.d.ts +14 -0
  137. package/dist/web/assets.d.ts.map +1 -1
  138. package/dist/web/assets.js +56 -1
  139. package/dist/web/assets.js.map +1 -1
  140. package/dist/web/context.d.ts +68 -3
  141. package/dist/web/context.d.ts.map +1 -1
  142. package/dist/web/context.js +56 -1
  143. package/dist/web/context.js.map +1 -1
  144. package/dist/web/feed-atom.d.ts.map +1 -1
  145. package/dist/web/feed-atom.js +20 -0
  146. package/dist/web/feed-atom.js.map +1 -1
  147. package/dist/web/feed-item.d.ts +19 -0
  148. package/dist/web/feed-item.d.ts.map +1 -1
  149. package/dist/web/feed-item.js +22 -0
  150. package/dist/web/feed-item.js.map +1 -1
  151. package/dist/web/feed-json.d.ts +13 -0
  152. package/dist/web/feed-json.d.ts.map +1 -1
  153. package/dist/web/feed-json.js +15 -0
  154. package/dist/web/feed-json.js.map +1 -1
  155. package/dist/web/feed-rss.d.ts.map +1 -1
  156. package/dist/web/feed-rss.js +59 -2
  157. package/dist/web/feed-rss.js.map +1 -1
  158. package/dist/web/feed-xml.d.ts +7 -1
  159. package/dist/web/feed-xml.d.ts.map +1 -1
  160. package/dist/web/feed-xml.js +8 -2
  161. package/dist/web/feed-xml.js.map +1 -1
  162. package/dist/web/feeds.d.ts +1 -1
  163. package/dist/web/feeds.d.ts.map +1 -1
  164. package/dist/web/feeds.js +6 -0
  165. package/dist/web/feeds.js.map +1 -1
  166. package/dist/web/index.d.ts +4 -1
  167. package/dist/web/index.d.ts.map +1 -1
  168. package/dist/web/index.js +2 -0
  169. package/dist/web/index.js.map +1 -1
  170. package/dist/web/license.d.ts +94 -0
  171. package/dist/web/license.d.ts.map +1 -0
  172. package/dist/web/license.js +116 -0
  173. package/dist/web/license.js.map +1 -0
  174. package/dist/web/oembed.d.ts.map +1 -1
  175. package/dist/web/oembed.js +1 -2
  176. package/dist/web/oembed.js.map +1 -1
  177. package/dist/web/render.d.ts +15 -0
  178. package/dist/web/render.d.ts.map +1 -1
  179. package/dist/web/render.js +27 -2
  180. package/dist/web/render.js.map +1 -1
  181. package/dist/web/routes.d.ts.map +1 -1
  182. package/dist/web/routes.js +10 -6
  183. package/dist/web/routes.js.map +1 -1
  184. package/dist/web/share-image.d.ts +7 -7
  185. package/dist/web/share-image.d.ts.map +1 -1
  186. package/dist/web/share-image.js +8 -7
  187. package/dist/web/share-image.js.map +1 -1
  188. package/dist/webmention/index.d.ts +2 -0
  189. package/dist/webmention/index.d.ts.map +1 -1
  190. package/dist/webmention/index.js +1 -0
  191. package/dist/webmention/index.js.map +1 -1
  192. package/dist/webmention/service.d.ts.map +1 -1
  193. package/dist/webmention/service.js +96 -18
  194. package/dist/webmention/service.js.map +1 -1
  195. package/dist/webmention/syndication.d.ts +135 -0
  196. package/dist/webmention/syndication.d.ts.map +1 -0
  197. package/dist/webmention/syndication.js +337 -0
  198. package/dist/webmention/syndication.js.map +1 -0
  199. package/package.json +1 -1
  200. package/themes/default/README.md +121 -37
  201. package/themes/default/layouts/base.njk +3 -4
  202. package/themes/default/layouts/post.njk +23 -6
  203. package/themes/default/partials/citations.njk +17 -0
  204. package/themes/default/partials/jsonld.njk +15 -6
  205. package/themes/default/partials/photos.njk +10 -0
  206. package/themes/default/partials/post-list.njk +7 -0
  207. package/themes/default/partials/recording.njk +41 -0
  208. package/themes/default/src/style.css +25 -0
  209. package/themes/default/static/style.css +41 -0
@@ -25,6 +25,7 @@ themes/default/
25
25
  tags.njk macros for tag and category links
26
26
  kicker.njk the line above an entry naming its kind and date
27
27
  reply-context.njk the citation of what a reply answers
28
+ citations.njk the citation of what a like, repost or bookmark cites
28
29
  bio.njk who an entry is by, as an h-card
29
30
  menu.njk one named menu, as a nav of links
30
31
  feeds.njk macros for the feed links in <head>
@@ -207,7 +208,8 @@ it sits beside the link home. See [Navigation](#navigation).
207
208
  **The footer** prints the copyright with the current year and the site
208
209
  author's display name, `soloAuthor.name`, or the site title on a site with
209
210
  several authors,
210
- `Published with Geekity`, and then `menus.footer` — the `footer` area this
211
+ then the page's license, when it has one, as `<a rel="license">` with its name,
212
+ then `Published with Geekity`, and then `menus.footer` — the `footer` area this
211
213
  theme declares, and the whole of what the footer links. A site that wants its
212
214
  feed there types an `RSS | /feed/` line into it, the way the starter site does.
213
215
  Nothing in the footer is read off an account: it used to hold one `rel="me"`
@@ -216,7 +218,9 @@ presented as the site's. An empty or missing footer menu prints no list at all,
216
218
  and the copyright line stands on its own. The year is
217
219
  `{{ "now" | date("year") }}` — `now` is the one word the `date` filter reads
218
220
  rather than parses — so it is the year at the moment the page is rendered, in
219
- the site's own timezone.
221
+ the site's own timezone. The license is `license`: a post's own from its
222
+ `license` front matter, else the site's from Settings > General, and nothing
223
+ on a site with none.
220
224
 
221
225
  **The breadcrumb** opens `<main>` on a page with a place in the site's
222
226
  hierarchy (TASK-150): a post filed under a category, and a category, tag or
@@ -230,8 +234,8 @@ two cannot disagree. Every other page has an empty trail and prints nothing. A
230
234
  site that wants no visible breadcrumb empties the `breadcrumbs` block, and the
231
235
  `BreadcrumbList` stays in the graph.
232
236
 
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`
237
+ Webrings, badges and anything else that is markup rather than a link are
238
+ deliberately not in the package. They go in a site theme's `footer`
235
239
  block:
236
240
 
237
241
  ```njk
@@ -331,8 +335,8 @@ is a microformats2 `h-entry`. An article, and a page:
331
335
  <section class="e-content">
332
336
  <p>The rendered body.</p>
333
337
  <p class="entry-meta">
334
- <a class="u-category small" href="https://news.indieweb.org/en"
335
- >#indienews</a
338
+ <a class="u-syndication small" href="https://news.indieweb.org/en"
339
+ >IndieNews</a
336
340
  >
337
341
  <a href="/2026/09/hello/" class="u-url"
338
342
  ><time class="small dt-published" datetime="2026-09-02T09:00:00.000Z"
@@ -343,6 +347,13 @@ is a microformats2 `h-entry`. An article, and a page:
343
347
  <time class="small dt-updated" datetime="2026-09-05T09:00:00.000Z"
344
348
  >Updated 5 September 2026</time
345
349
  >
350
+ <span aria-hidden="true">·</span>
351
+ <span class="small"
352
+ >Also on
353
+ <a class="u-syndication" href="https://news.indieweb.org/en/…"
354
+ >news.indieweb.org</a
355
+ ></span
356
+ >
346
357
  </p>
347
358
  </section>
348
359
  <p class="post-categories">…tags…</p>
@@ -366,10 +377,11 @@ permalink are part of the words, so a reader, or a fediverse peer reading the
366
377
  `e-content`, takes them with the post. The permalink wraps the `dt-published`
367
378
  time as a `u-url`, and a `dt-updated` follows it after a middle dot only when
368
379
  the update happened on a different day in the site's own timezone; a typo fixed
369
- an hour later is not news. A post tagged `indienews` opens the line with a
370
- `u-category` link to <https://news.indieweb.org/en>, which is how IndieNews is
371
- told the post is for it; a page never prints one, and its line is a
372
- `p.page-meta` rather than a `p.entry-meta`.
380
+ an hour later is not news. The line opens with a `u-syndication` link to each
381
+ syndication target the post selects (`syndicateTo`, TASK-155), which is what
382
+ IndieNews and Bridgy Publish look for on the page, and ends with the post's
383
+ copies elsewhere (`syndication`) after **Also on**. A page never prints either,
384
+ and its line is a `p.page-meta` rather than a `p.entry-meta`.
373
385
 
374
386
  **Every post's page has one `h1`.** A post with a name of its own is headed by
375
387
  it, as above. A note, or a reply without a title, opens on its words instead,
@@ -382,7 +394,7 @@ who wrote it and when, and then the kicker:
382
394
  <p class="kicker"><span class="kicker-kind">Note</span></p>
383
395
  ```
384
396
 
385
- It says `Reply` for a reply and leaves out the name or the date the post does
397
+ It says `Reply`, `Like`, `Repost` or `Bookmark` for those and leaves out the name or the date the post does
386
398
  not have. It is not a `p-name`: a parser that found one would take the post
387
399
  for an article. The headings under it step down one level at a time: the
388
400
  conversation and the comment form are `h2`, and a refused form's error summary
@@ -395,17 +407,70 @@ far as they are known, and its excerpt as a `blockquote.cite-quote.p-content`.
395
407
  An untitled reply prints it under the kicker; a titled reply prints it above
396
408
  its header. The same partial cites a reply in a feed.
397
409
 
410
+ **A like, a repost or a bookmark cites what it cites** (TASK-169) with
411
+ `partials/citations.njk`, beside the reply context and drawn the same way: one
412
+ `div.reply-context.cite.h-cite` per entry of `citations`, classed
413
+ `u-repost-of`, `u-like-of` or `u-bookmark-of`, whose `p.cite-line` says
414
+ Reposted, Liked or Bookmarked and links the target as its `u-url`. Its kicker
415
+ and its hidden `h1` say Repost, Like or Bookmark. A listing cites each entry's
416
+ from the same partial.
417
+
398
418
  The tags are printed under the words and inside the article, so that each link
399
419
  is a `p-category` of this entry, as one `p.post-categories` from
400
420
  `partials/tags.njk`. The stylesheet draws the `#` in front of each, so the
401
421
  category a parser reads is the tag and not the hash.
402
422
 
423
+ **A post with a recording plays it** (TASK-213), from
424
+ `partials/recording.njk`, between the header and the `e-content`. The main file
425
+ is the browser's own player, `<audio controls>` or `<video controls>` as
426
+ `enclosure.player` says, with `preload="metadata"` so a reader who never presses
427
+ play downloads nothing, and a download link inside it for a browser that plays
428
+ neither. The `<source>` carries `u-audio` or `u-video`, so a microformats parser
429
+ finds the file on the entry. A video whose transcript is WebVTT also gets it as
430
+ a `<track kind="captions">`. Under the player a `figcaption` links the
431
+ transcript, as Captions for VTT or SRT and Transcript otherwise, and lists the
432
+ other versions by their title, else their media type. A post without a
433
+ recording, or whose front matter is missing what a player needs, prints none of
434
+ it.
435
+
436
+ **A post with photos shows them** (TASK-166), from `partials/photos.njk`, after
437
+ the recording and before the `e-content`, each in a `figure.post-photo`. Each is
438
+ the context's `photo.html`: an `<img class="u-photo">` with its alt text, so a
439
+ microformats parser finds every photo on the entry, and a `<picture>` with the
440
+ upload's resizes when it has them. The alt text is the post's own, else the
441
+ media library's. A listing prints them the same way above each entry, and the
442
+ JSON-LD `BlogPosting` lists each as an `ImageObject` with its absolute `url`
443
+ and its alt text as `caption`. The first photo is the post's share image when
444
+ its front matter names no `image`.
445
+
446
+ ```html
447
+ <figure class="post-recording">
448
+ <audio controls preload="metadata" aria-label="Recording of Episode twelve">
449
+ <source
450
+ class="u-audio"
451
+ src="/uploads/2026/10/episode-12.mp3"
452
+ type="audio/mpeg"
453
+ />
454
+ <a href="/uploads/2026/10/episode-12.mp3">Download the recording</a>
455
+ </audio>
456
+ <figcaption class="post-recording-links">
457
+ <p><a href="/uploads/2026/10/episode-12.vtt">Captions</a></p>
458
+ <p id="post-recording-versions">Other versions</p>
459
+ <ul aria-labelledby="post-recording-versions">
460
+ <li>
461
+ <a href="/uploads/2026/10/episode-12.mp4" type="video/mp4">Video</a>
462
+ </li>
463
+ </ul>
464
+ </figcaption>
465
+ </figure>
466
+ ```
467
+
403
468
  After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
404
469
  posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
405
470
  `span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
406
471
  the conversation and the comment form. A page prints the contact form when its
407
- front matter asked for one. A page has no neighbours, no tags and no IndieNews
408
- link, because none of those are things a page has.
472
+ front matter asked for one. A page has no neighbours, no tags and no
473
+ syndication links, because none of those are things a page has.
409
474
 
410
475
  ### The bio
411
476
 
@@ -709,17 +774,28 @@ included. `og:url` is the canonical URL. An article also carries
709
774
  publish date), `article:author` (the author's profile URL here, else their
710
775
  name) and one `article:tag` per tag.
711
776
 
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.
777
+ **The picture** is the first of these that the page has, on the context as
778
+ `shareImage`:
779
+
780
+ 1. The `image` in the entry's front matter.
781
+ 2. `site.avatar`, which only a hand edit of `site.json` sets.
782
+ 3. The profile photo of the person the page is by: the writer of a post or page
783
+ that names an author, the user of an author archive, and the site's author
784
+ on a solo-author site's homepage and other pages about nobody in particular.
785
+ 4. The site icon, from `icon` in `site.json`.
786
+
787
+ A page with none of these prints no `og:image` and no `twitter:image` rather
788
+ than an empty one. The picture always carries `og:image:alt` and
789
+ `twitter:image:alt`. The alt text is what the media library says about the
790
+ upload, else words for what it shows. For the entry's own picture, its
791
+ `imageAlt` front matter comes first, and the entry's title last. For the
792
+ avatar, the last resort is the site's author; for a photo, that person's
793
+ display name; for the icon, the site's title. `og:image:width` and
794
+ `og:image:height` are printed when the image's variants have been derived,
795
+ because the size is read from their sidecar rather than from the file.
796
+ `twitter:card` is `summary_large_image` for a picture at least 1200 pixels wide
797
+ and wider than it is tall. Everything else, a square photo included, is
798
+ `summary`, the small picture beside the words.
723
799
 
724
800
  **The icons** come from the site's `icon` setting in `site.json`, else
725
801
  `site.avatar`, through the derived images (decision-10). `icon` exists for a
@@ -778,7 +854,7 @@ IndieWeb. The graph holds:
778
854
  `/search/?q={search_term_string}`. On a site whose author is a user, its
779
855
  `publisher` and `about` both point at that user's Person, on every page. On
780
856
  a site with several authors its `publisher` is the Organization below and it
781
- has no `about`.
857
+ has no `about`. With a site license, `license` is its URL.
782
858
  - `Organization`, on a site with several authors only: the site itself, named
783
859
  for its title, at `{baseUrl}/#organization`. It publishes the `WebSite` and
784
860
  every entry.
@@ -795,8 +871,9 @@ IndieWeb. The graph holds:
795
871
  `author`, and the site's `publisher`: the site author's Person, or the
796
872
  Organization. A post with no title is headed by the words its `<title>`
797
873
  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.
874
+ Its `image` is the picture the Open Graph tags print, so the two always
875
+ agree (see [The head](#the-head)). Its `license` is the page's license URL,
876
+ the front matter's over the site's, and absent with none.
800
877
  - `BreadcrumbList` wherever the page prints a breadcrumb (see
801
878
  [The page shell](#the-page-shell)): one `ListItem` per crumb with its
802
879
  `position`, `name` and absolute URL as `item`.
@@ -1055,16 +1132,18 @@ an Eleventy build needs few edits. It is part of the package's semver contract.
1055
1132
 
1056
1133
  Every template gets:
1057
1134
 
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. |
1135
+ | Key | What it holds |
1136
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1137
+ | `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. |
1138
+ | `menus` | Every menu the site stores, by name, marked for this page: `menus.primary`, `menus.footer`, and any other name. See [Navigation](#navigation). |
1139
+ | `siteAuthor` | Who the page is by, as a profile. **Absent** when nobody matches. See [Bylines and author archives](#bylines-and-author-archives). |
1140
+ | `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). |
1141
+ | `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). |
1142
+ | `theme` | The colours the theme declares, as `{ colorScheme, themeColor: { light, dark } }`, each absent when no theme declares it. See [The manifest](#the-manifest). |
1143
+ | `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). |
1144
+ | `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. |
1145
+ | `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. |
1146
+ | `siteLicense` | The site's own license as `{ name, url }`, whatever the page says. **Undefined** with none. |
1068
1147
 
1069
1148
  A document — one post, one page, or one entry of a listing — adds:
1070
1149
 
@@ -1085,6 +1164,11 @@ A document — one post, one page, or one entry of a listing — adds:
1085
1164
  | `type` | `post` or `page`. |
1086
1165
  | `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
1087
1166
  | `lang` | The `lang` front matter as a canonical tag, such as `fr-CA`. Absent when it names none or no tag. |
1167
+ | `enclosure` | The post's recording, parsed, with `player` (`audio` or `video`) and `transcript.captions`. Absent with none. |
1168
+ | `photos` | The post's photos, each `{ url, alt, html }`; `html` is the `img.u-photo`, responsive. Empty with none. |
1169
+ | `citations` | What it reposts, likes or bookmarks, each `{ property, url }` with `property` the mf2 name. Empty with none. |
1170
+ | `syndicateTo` | On a post's page, the syndication targets it selects, each `{ id, name, url }`. Empty with none. |
1171
+ | `syndication` | Its copies elsewhere, each `{ url, label }`: front matter `syndication`, and on its page the copies targets made. |
1088
1172
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1089
1173
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1090
1174
  | `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
@@ -39,6 +39,7 @@
39
39
  {#- A reply cites the post it answers, whose URL is also the link its
40
40
  webmention points a receiver at. -#}
41
41
  {% include "partials/reply-context.njk" %}
42
+ {% include "partials/citations.njk" %}
42
43
  <header class="post-header">
43
44
  {{ kicker.line(postType | capitalize, false, false, categories) }}
44
45
  <h1 class="p-name">{{ title }}</h1>
@@ -48,22 +49,33 @@
48
49
  </header>
49
50
  {% else %}
50
51
  <h1 class="screen-reader-text">
51
- {{- "Reply" if postType == "reply" else "Note" }}
52
+ {{- postType | capitalize if postType in ["reply", "like", "repost", "bookmark"] else "Note" }}
52
53
  {%- if bioAuthor.name %} by {{ bioAuthor.name }}{% endif %}
53
54
  {%- if date %}, {{ date | date("readable", none, lang) }}{% endif -%}
54
55
  </h1>
55
56
  {{ kicker.line(postType | capitalize, false, false, categories) }}
56
57
  {% include "partials/reply-context.njk" %}
58
+ {% include "partials/citations.njk" %}
59
+ {% endif %}
60
+
61
+ {% if enclosure %}
62
+ {% include "partials/recording.njk" %}
63
+ {% endif %}
64
+
65
+ {% if photos.length %}
66
+ {% include "partials/photos.njk" %}
57
67
  {% endif %}
58
68
 
59
69
  <section class="e-content">
60
70
  {{ content | safe }}
61
71
  <p class="entry-meta">
62
- {#- A post tagged `indienews` opens the line with the link that submits
63
- it there, which is how IndieNews is told the post is for it. -#}
64
- {%- if "indienews" in tags %}
65
- <a class="u-category small" href="https://news.indieweb.org/en">#indienews</a>
66
- {%- endif %}
72
+ {#- The line opens with a link to each syndication target the post
73
+ selects (TASK-155). It is inside the h-entry and marked
74
+ `u-syndication`, the markup IndieNews documents, and a target that
75
+ is sent a webmention checks the page for it. -#}
76
+ {%- for target in syndicateTo %}
77
+ <a class="u-syndication small" href="{{ target.url }}">{{ target.name }}</a>
78
+ {%- endfor %}
67
79
  {%- if date %}
68
80
  <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date("readable", none, lang) }}</time></a>
69
81
  {%- endif %}
@@ -71,6 +83,11 @@
71
83
  a typo fixed an hour after publishing is not news. -#}
72
84
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
73
85
  {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date("readable", none, lang) }}</time>
86
+ {%- endif %}
87
+ {#- The post's copies elsewhere: those its targets answered with and
88
+ those its front matter lists by hand. -#}
89
+ {%- if syndication.length %}
90
+ {% if date or updated %} <span aria-hidden="true">·</span> {% endif %}<span class="small">Also on {% for copy in syndication %}<a class="u-syndication" href="{{ copy.url }}">{{ copy.label }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</span>
74
91
  {%- endif -%}
75
92
  </p>
76
93
  </section>
@@ -0,0 +1,17 @@
1
+ {#
2
+ What a post reposts, likes or bookmarks (TASK-169), cited the way a reply
3
+ cites what it answers: each one an embedded `h-cite` under its own mf2
4
+ property, `u-repost-of`, `u-like-of` or `u-bookmark-of`, that always carries
5
+ the target's URL, so a parser reads what the post cites and the page its
6
+ webmention names links to it.
7
+
8
+ Drawn as the same citation block as `reply-context.njk` (doc-9). An entry
9
+ includes it with `citations` on the context, a list of `{ property, url }`;
10
+ a feed sets it from the entry it is printing.
11
+ #}
12
+ {% set citationVerbs = { "repost-of": "Reposted", "like-of": "Liked", "bookmark-of": "Bookmarked" } %}
13
+ {% for citation in citations %}
14
+ <div class="reply-context cite u-{{ citation.property }} h-cite">
15
+ <p class="cite-line small">{{ citationVerbs[citation.property] }} <a class="u-url" href="{{ citation.url }}">{{ citation.url }}</a></p>
16
+ </div>
17
+ {% endfor %}
@@ -54,12 +54,19 @@
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 "") %}
59
+ {#- A photo post's image is its photos, each an ImageObject captioned with
60
+ its alt text, in order (TASK-166); anything else's is the share image. -#}
61
+ {% set entryImage = metaImage | absoluteUrl if metaImage else "" %}
62
+ {% if photos and photos.length %}
63
+ {% set entryImage = [] %}
64
+ {% for photo in photos %}
65
+ {% set imageObject = { "@type": "ImageObject", "url": photo.url | absoluteUrl } %}
66
+ {% if photo.alt %}{% set imageObject = { "@type": "ImageObject", "url": photo.url | absoluteUrl, "caption": photo.alt } %}{% endif %}
67
+ {% set _ = entryImage.push(imageObject) %}
68
+ {% endfor %}
69
+ {% endif %}
63
70
  {% set nodes = [] %}
64
71
  {% set website %}
65
72
  {
@@ -82,6 +89,7 @@
82
89
  }) }}
83
90
  {{- member("publisher", publisher) }}
84
91
  {{- member("about", { "@id": soloId } if soloId else "") }}
92
+ {{- member("license", siteLicense.url if siteLicense else "") }}
85
93
  }{% endset %}
86
94
  {% set _ = nodes.push(website) %}
87
95
  {% if not soloId %}
@@ -129,9 +137,10 @@
129
137
  {{- member("datePublished", date | date("iso") if date else "") }}
130
138
  {{- member("dateModified", (updated or date) | date("iso") if (updated or date) else "") }}
131
139
  {{- member("description", metaDescription) }}
132
- {{- member("image", entryImage | absoluteUrl if entryImage else "") }}
140
+ {{- member("image", entryImage) }}
133
141
  {{- member("author", person) }}
134
142
  {{- member("publisher", publisher) }}
143
+ {{- member("license", license.url if license else "") }}
135
144
  }{% endset %}
136
145
  {% set _ = nodes.push(entry) %}
137
146
  {% endif %}
@@ -0,0 +1,10 @@
1
+ {#
2
+ A post's photos (TASK-166), each `photo.html`: an `img.u-photo` with its alt
3
+ text, so a microformats parser finds every photo on the h-entry, served as a
4
+ responsive picture when the upload has variants.
5
+ #}
6
+ <div class="post-photos">
7
+ {% for photo in photos %}
8
+ <figure class="post-photo">{{ photo.html | safe }}</figure>
9
+ {% endfor %}
10
+ </div>
@@ -39,6 +39,13 @@
39
39
  {% set lang = post.lang %}
40
40
  {% include "partials/reply-context.njk" %}
41
41
  {% endif %}
42
+ {#- What it reposts, likes or bookmarks, from the partial an entry uses. -#}
43
+ {% set citations = post.citations %}
44
+ {% include "partials/citations.njk" %}
45
+ {% if post.photos.length %}
46
+ {% set photos = post.photos %}
47
+ {% include "partials/photos.njk" %}
48
+ {% endif %}
42
49
  {% if not post.named %}
43
50
  <div class="feed-excerpt e-content">{{ post.content | safe }}</div>
44
51
  {% else %}
@@ -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,31 @@
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
+
679
+ /* A post's photos (TASK-166), one under another at the column's width. */
680
+ .post-photos {
681
+ @apply my-8 grid gap-4;
682
+ }
683
+
684
+ .post-photo img {
685
+ @apply block h-auto w-full;
686
+ }
687
+
663
688
  /* An untitled post is its words, set larger. It is the one entry whose
664
689
  heading is the hidden one TASK-144 gave it. */
665
690
  .blog-post > .screen-reader-text ~ .e-content {
@@ -1190,6 +1190,43 @@
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
+
1218
+ .post-photos {
1219
+ margin-block: calc(var(--spacing) * 8);
1220
+ gap: calc(var(--spacing) * 4);
1221
+ display: grid;
1222
+ }
1223
+
1224
+ .post-photo img {
1225
+ width: 100%;
1226
+ height: auto;
1227
+ display: block;
1228
+ }
1229
+
1193
1230
  .blog-post > .screen-reader-text ~ .e-content {
1194
1231
  --tw-leading: 1.55;
1195
1232
  font-size: 1.3rem;
@@ -2124,6 +2161,10 @@
2124
2161
  overflow: hidden;
2125
2162
  }
2126
2163
 
2164
+ .capitalize {
2165
+ text-transform: capitalize;
2166
+ }
2167
+
2127
2168
  .italic {
2128
2169
  font-style: italic;
2129
2170
  }