@geekity/cms 0.27.0 → 0.28.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 (187) hide show
  1. package/README.md +168 -33
  2. package/admin/layouts/shell.njk +6 -0
  3. package/admin/pages/documents/editor.njk +4 -0
  4. package/admin/pages/plugins/installed.njk +1 -1
  5. package/admin/pages/tools/dev-mode.njk +47 -0
  6. package/admin/static/admin.css +8 -0
  7. package/admin/static/editor-actions.js +13 -9
  8. package/admin/static/editor.js +31 -17
  9. package/dist/admin/dev-mode.d.ts +9 -0
  10. package/dist/admin/dev-mode.d.ts.map +1 -0
  11. package/dist/admin/dev-mode.js +42 -0
  12. package/dist/admin/dev-mode.js.map +1 -0
  13. package/dist/admin/editor-actions.js +3 -1
  14. package/dist/admin/editor-actions.js.map +1 -1
  15. package/dist/admin/federation.d.ts.map +1 -1
  16. package/dist/admin/federation.js +2 -0
  17. package/dist/admin/federation.js.map +1 -1
  18. package/dist/admin/menu.d.ts.map +1 -1
  19. package/dist/admin/menu.js +2 -0
  20. package/dist/admin/menu.js.map +1 -1
  21. package/dist/admin/own-paragraph.d.ts +7 -0
  22. package/dist/admin/own-paragraph.d.ts.map +1 -0
  23. package/dist/admin/own-paragraph.js +9 -0
  24. package/dist/admin/own-paragraph.js.map +1 -0
  25. package/dist/admin/plugins.d.ts.map +1 -1
  26. package/dist/admin/plugins.js +19 -8
  27. package/dist/admin/plugins.js.map +1 -1
  28. package/dist/admin/preview.d.ts.map +1 -1
  29. package/dist/admin/preview.js +4 -1
  30. package/dist/admin/preview.js.map +1 -1
  31. package/dist/admin/routes.d.ts.map +1 -1
  32. package/dist/admin/routes.js +4 -0
  33. package/dist/admin/routes.js.map +1 -1
  34. package/dist/admin/settings-email.d.ts.map +1 -1
  35. package/dist/admin/settings-email.js +4 -0
  36. package/dist/admin/settings-email.js.map +1 -1
  37. package/dist/admin/store.d.ts +4 -2
  38. package/dist/admin/store.d.ts.map +1 -1
  39. package/dist/admin/store.js +1 -1
  40. package/dist/admin/store.js.map +1 -1
  41. package/dist/admin/templates.d.ts +1 -0
  42. package/dist/admin/templates.d.ts.map +1 -1
  43. package/dist/admin/templates.js +1 -0
  44. package/dist/admin/templates.js.map +1 -1
  45. package/dist/admin/uploads.d.ts +7 -1
  46. package/dist/admin/uploads.d.ts.map +1 -1
  47. package/dist/admin/uploads.js +33 -30
  48. package/dist/admin/uploads.js.map +1 -1
  49. package/dist/cli.d.ts +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +76 -10
  52. package/dist/cli.js.map +1 -1
  53. package/dist/comments/index.d.ts +1 -1
  54. package/dist/comments/index.d.ts.map +1 -1
  55. package/dist/comments/index.js +1 -1
  56. package/dist/comments/index.js.map +1 -1
  57. package/dist/comments/records.d.ts +1 -0
  58. package/dist/comments/records.d.ts.map +1 -1
  59. package/dist/comments/records.js +39 -10
  60. package/dist/comments/records.js.map +1 -1
  61. package/dist/config.d.ts +21 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +2 -0
  64. package/dist/config.js.map +1 -1
  65. package/dist/content/markdown.d.ts.map +1 -1
  66. package/dist/content/markdown.js +33 -1
  67. package/dist/content/markdown.js.map +1 -1
  68. package/dist/content/migrated.d.ts +6 -0
  69. package/dist/content/migrated.d.ts.map +1 -0
  70. package/dist/content/migrated.js +12 -0
  71. package/dist/content/migrated.js.map +1 -0
  72. package/dist/content/original.d.ts +3 -0
  73. package/dist/content/original.d.ts.map +1 -0
  74. package/dist/content/original.js +12 -0
  75. package/dist/content/original.js.map +1 -0
  76. package/dist/content/parser.d.ts +5 -0
  77. package/dist/content/parser.d.ts.map +1 -1
  78. package/dist/content/parser.js +1 -1
  79. package/dist/content/parser.js.map +1 -1
  80. package/dist/content/schedule.d.ts +7 -0
  81. package/dist/content/schedule.d.ts.map +1 -1
  82. package/dist/content/schedule.js +11 -2
  83. package/dist/content/schedule.js.map +1 -1
  84. package/dist/content/sync.d.ts.map +1 -1
  85. package/dist/content/sync.js +27 -5
  86. package/dist/content/sync.js.map +1 -1
  87. package/dist/content/video.d.ts +12 -0
  88. package/dist/content/video.d.ts.map +1 -0
  89. package/dist/content/video.js +91 -0
  90. package/dist/content/video.js.map +1 -0
  91. package/dist/dev-mode.d.ts +28 -0
  92. package/dist/dev-mode.d.ts.map +1 -0
  93. package/dist/dev-mode.js +64 -0
  94. package/dist/dev-mode.js.map +1 -0
  95. package/dist/drain.d.ts +9 -3
  96. package/dist/drain.d.ts.map +1 -1
  97. package/dist/drain.js +24 -3
  98. package/dist/drain.js.map +1 -1
  99. package/dist/federation/delivery.d.ts +2 -0
  100. package/dist/federation/delivery.d.ts.map +1 -1
  101. package/dist/federation/delivery.js +46 -16
  102. package/dist/federation/delivery.js.map +1 -1
  103. package/dist/federation/inbox.d.ts.map +1 -1
  104. package/dist/federation/inbox.js +13 -3
  105. package/dist/federation/inbox.js.map +1 -1
  106. package/dist/federation/mount.d.ts.map +1 -1
  107. package/dist/federation/mount.js +8 -4
  108. package/dist/federation/mount.js.map +1 -1
  109. package/dist/federation/relays.d.ts.map +1 -1
  110. package/dist/federation/relays.js +16 -1
  111. package/dist/federation/relays.js.map +1 -1
  112. package/dist/index.d.ts +2 -0
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +16 -4
  115. package/dist/index.js.map +1 -1
  116. package/dist/indexnow.d.ts +1 -1
  117. package/dist/indexnow.d.ts.map +1 -1
  118. package/dist/indexnow.js +7 -0
  119. package/dist/indexnow.js.map +1 -1
  120. package/dist/mail/service.d.ts +3 -1
  121. package/dist/mail/service.d.ts.map +1 -1
  122. package/dist/mail/service.js +17 -0
  123. package/dist/mail/service.js.map +1 -1
  124. package/dist/notify.d.ts +1 -1
  125. package/dist/notify.d.ts.map +1 -1
  126. package/dist/notify.js +7 -0
  127. package/dist/notify.js.map +1 -1
  128. package/dist/plugin.d.ts +67 -4
  129. package/dist/plugin.d.ts.map +1 -1
  130. package/dist/plugin.js.map +1 -1
  131. package/dist/plugins/folder.d.ts +2 -0
  132. package/dist/plugins/folder.d.ts.map +1 -1
  133. package/dist/plugins/folder.js +26 -5
  134. package/dist/plugins/folder.js.map +1 -1
  135. package/dist/plugins/manage.d.ts +8 -6
  136. package/dist/plugins/manage.d.ts.map +1 -1
  137. package/dist/plugins/manage.js +1 -4
  138. package/dist/plugins/manage.js.map +1 -1
  139. package/dist/plugins/site.d.ts.map +1 -1
  140. package/dist/plugins/site.js +56 -0
  141. package/dist/plugins/site.js.map +1 -1
  142. package/dist/supervisor/control.d.ts +15 -0
  143. package/dist/supervisor/control.d.ts.map +1 -0
  144. package/dist/supervisor/control.js +89 -0
  145. package/dist/supervisor/control.js.map +1 -0
  146. package/dist/supervisor/primary.d.ts +18 -4
  147. package/dist/supervisor/primary.d.ts.map +1 -1
  148. package/dist/supervisor/primary.js +128 -16
  149. package/dist/supervisor/primary.js.map +1 -1
  150. package/dist/supervisor/protocol.d.ts +12 -0
  151. package/dist/supervisor/protocol.d.ts.map +1 -1
  152. package/dist/supervisor/protocol.js +21 -2
  153. package/dist/supervisor/protocol.js.map +1 -1
  154. package/dist/supervisor/supervision.d.ts +1 -0
  155. package/dist/supervisor/supervision.d.ts.map +1 -1
  156. package/dist/supervisor/supervision.js +14 -1
  157. package/dist/supervisor/supervision.js.map +1 -1
  158. package/dist/supervisor/worker.d.ts.map +1 -1
  159. package/dist/supervisor/worker.js +31 -6
  160. package/dist/supervisor/worker.js.map +1 -1
  161. package/dist/web/context.d.ts +7 -0
  162. package/dist/web/context.d.ts.map +1 -1
  163. package/dist/web/context.js +7 -4
  164. package/dist/web/context.js.map +1 -1
  165. package/dist/web/feed-item.d.ts +7 -6
  166. package/dist/web/feed-item.d.ts.map +1 -1
  167. package/dist/web/feed-item.js +5 -4
  168. package/dist/web/feed-item.js.map +1 -1
  169. package/dist/web/feed-rss.d.ts +2 -2
  170. package/dist/web/feed-rss.js +2 -2
  171. package/dist/web/redirects.d.ts +17 -10
  172. package/dist/web/redirects.d.ts.map +1 -1
  173. package/dist/web/redirects.js +83 -49
  174. package/dist/web/redirects.js.map +1 -1
  175. package/dist/webmention/service.d.ts +1 -1
  176. package/dist/webmention/service.d.ts.map +1 -1
  177. package/dist/webmention/service.js +12 -2
  178. package/dist/webmention/service.js.map +1 -1
  179. package/package.json +1 -1
  180. package/themes/default/README.md +152 -1
  181. package/themes/default/layouts/base.njk +11 -2
  182. package/themes/default/layouts/post.njk +8 -2
  183. package/themes/default/partials/body-end.njk +1 -0
  184. package/themes/default/partials/conversation.njk +1 -1
  185. package/themes/default/partials/head-end.njk +1 -0
  186. package/themes/default/src/style.css +4 -0
  187. package/themes/default/static/style.css +8 -0
@@ -38,6 +38,8 @@ themes/default/
38
38
  contact-form.njk the form on a page whose front matter says contact: true
39
39
  archive.njk every post by month, on a page that says archive: true
40
40
  search-form.njk the search box, on the search page
41
+ head-end.njk empty: what a site adds to the end of every <head>
42
+ body-end.njk empty: what a site adds to the end of every <body>
41
43
  mail/
42
44
  test.*.njk the Send test email message
43
45
  password-reset.*.njk the forgot-password link
@@ -187,6 +189,94 @@ until it writes one. On the posts page the page's own front matter and rendered
187
189
  body are on the context beside the listing, so `{{ content | safe }}` prints its
188
190
  words above the posts; `layouts/home.njk` already does.
189
191
 
192
+ ## Adding to every page
193
+
194
+ Analytics, a site verification tag or a chat widget goes on every page, and
195
+ none of them needs a layout copied. `layouts/base.njk` includes two partials
196
+ that this theme ships empty:
197
+
198
+ | Partial | Printed |
199
+ | ----------------------- | -------------------------------------------------- |
200
+ | `partials/head-end.njk` | last in `<head>`, after the cards and the JSON-LD |
201
+ | `partials/body-end.njk` | last in `<body>`, after the footer and the scripts |
202
+
203
+ A site theme that holds one of them, and nothing else but its `theme.json`,
204
+ adds that markup to every public HTML page: posts, pages, listings, search, the
205
+ front page, and the 404, 410, 500 and 503 pages. Every other template still
206
+ comes from this directory, so the site keeps receiving updates to them, and
207
+ the two partial names stay as they are. The includes sit outside every block,
208
+ so a layout that overrides `head` or `scripts` without `super()` keeps them.
209
+
210
+ They are part of the HTML page and nothing else. The Markdown, plain text and
211
+ JSON representations of a page, the feeds, `llms.txt`, mail and the admin do
212
+ not print them. The editor's preview renders the post or page layout in the
213
+ admin, so the context there carries `editorPreview: true` and `layouts/base.njk`
214
+ leaves both partials out. A theme that replaces `layouts/base.njk` and includes
215
+ them itself should test the same key.
216
+
217
+ Umami and Google Analytics 4, in `themes/analytics/`:
218
+
219
+ ```
220
+ themes/analytics/
221
+ theme.json
222
+ partials/head-end.njk
223
+ ```
224
+
225
+ ```json
226
+ { "name": "Analytics", "kind": "site" }
227
+ ```
228
+
229
+ ```njk
230
+ <script defer src="https://cloud.umami.is/script.js" data-website-id="your-website-id"></script>
231
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
232
+ <script>
233
+ window.dataLayer = window.dataLayer || [];
234
+ function gtag(){dataLayer.push(arguments);}
235
+ gtag('js', new Date());
236
+ gtag('config', 'G-XXXXXXXXXX');
237
+ </script>
238
+ ```
239
+
240
+ Then wear the theme, either with Activate on **Appearance > Themes** or with
241
+ `"theme": "analytics"` in `content/_data/site.json`. A site that already wears
242
+ a theme of its own puts the partial in that theme instead.
243
+
244
+ The partial is a template, rendered with the page's context, so it can read a
245
+ value out of `site.json`, as in `data-website-id="{{ site.umamiId }}"`.
246
+
247
+ **In Docker.** The image reads themes from `/site/themes`. Mount the site's
248
+ `themes/` directory there, read-only, which is the commented line in
249
+ `deploy/compose.yaml`:
250
+
251
+ ```yaml
252
+ volumes:
253
+ - ./themes:/site/themes:ro
254
+ ```
255
+
256
+ No custom image is needed. A change to the partial shows on the next request.
257
+
258
+ **Under a Content-Security-Policy.** The default headers set only
259
+ `frame-ancestors`, so they restrict nothing a page loads, and the inline gtag
260
+ configuration runs as it is. A site that sets a policy of its own with
261
+ `securityHeaders` must allow what the snippets load and where they send data:
262
+
263
+ | Directive | Umami | Google Analytics 4 |
264
+ | ------------- | -------------------------- | ---------------------------------------------------------------------------------------------- |
265
+ | `script-src` | `https://cloud.umami.is` | `https://*.googletagmanager.com`, and the inline configuration (see below) |
266
+ | `connect-src` | `https://gateway.umami.is` | `https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com` |
267
+ | `img-src` | none | `https://*.google-analytics.com https://*.googletagmanager.com` |
268
+
269
+ The inline `<script>` that configures gtag needs one of these:
270
+
271
+ - Move it to a file in the theme, `themes/analytics/static/gtag.js`, and load
272
+ it with `<script src="{{ "gtag.js" | asset }}"></script>`. It is then served
273
+ from the site, and `script-src 'self'` covers it.
274
+ - Allow its hash, `'sha256-…'`, in `script-src`. The hash covers every byte
275
+ between the tags, so it changes when the measurement ID or the whitespace
276
+ changes.
277
+ - Allow `'unsafe-inline'` in `script-src`. This also allows any injected
278
+ inline script, so prefer one of the other two.
279
+
190
280
  ## The page shell
191
281
 
192
282
  `layouts/base.njk` is the page shell: the skip link, one `.global-wrapper` at the
@@ -387,6 +477,17 @@ IndieNews and Bridgy Publish look for on the page, and ends with the post's
387
477
  copies elsewhere (`syndication`) after **Also on**. A page never prints either,
388
478
  and its line is a `p.page-meta` rather than a `p.entry-meta`.
389
479
 
480
+ **A post first published elsewhere says so.** A post whose front matter names
481
+ its original with `canonical_href`, an absolute http or https URL such as a
482
+ Substack essay's, has it on the context as `original` (TASK-293). The line
483
+ then reads **Originally published at** and links the original's host as a
484
+ second `u-url` of the h-entry, after the permalink's. It is no
485
+ `u-syndication`, because the original is not a copy of this post, and
486
+ original-post-discovery reads a copy's off-site `u-url` as its original. The
487
+ same URL is the page's `rel="canonical"` and its `og:url`, so search engines
488
+ credit the original. A value that is not an absolute http or https URL is
489
+ ignored, and `geekity sync` names the file.
490
+
390
491
  **Every post's page has one `h1`.** A post with a name of its own is headed by
391
492
  it, as above. So is a post with a title and no words, such as a titled like,
392
493
  repost or bookmark (TASK-256): its header comes first and its citation after
@@ -919,7 +1020,8 @@ mode gets a dark canvas rather than a white flash while the stylesheet loads.
919
1020
  **The card.** `og:title` is the page's title, or the site's on the front page;
920
1021
  `og:site_name` is always the site's. `og:type` is `article` on a rendered post
921
1022
  or page and `website` everywhere else, a listing carrying a page's front matter
922
- included. `og:url` is the canonical URL. An article also carries
1023
+ included. `og:url` is the canonical URL: the page's own, or the `original` a
1024
+ post first published elsewhere names. An article also carries
923
1025
  `article:published_time`, `article:modified_time` (the `updated` date, else the
924
1026
  publish date), `article:author` (the author's profile URL here, else their
925
1027
  name) and one `article:tag` per tag.
@@ -1220,6 +1322,54 @@ the `+` and the `-` in the text carry the meaning as well as the colour does.
1220
1322
  {% block scripts %}{% endblock %}
1221
1323
  ```
1222
1324
 
1325
+ ### Videos
1326
+
1327
+ A YouTube or Vimeo URL on a line of its own in a post plays as the video
1328
+ (decision-34; doc-2 lists the addresses it knows). The core renders it into the
1329
+ body, so it reaches a theme inside `content` as:
1330
+
1331
+ ```html
1332
+ <figure class="video-embed video-embed-youtube">
1333
+ <iframe
1334
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
1335
+ width="560"
1336
+ height="315"
1337
+ title="YouTube video"
1338
+ loading="lazy"
1339
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
1340
+ referrerpolicy="strict-origin-when-cross-origin"
1341
+ allowfullscreen
1342
+ ></iframe>
1343
+ <figcaption>
1344
+ <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
1345
+ >https://www.youtube.com/watch?v=dQw4w9WgXcQ</a
1346
+ >
1347
+ </figcaption>
1348
+ </figure>
1349
+ ```
1350
+
1351
+ It is one line in the real output. A Vimeo video is the same with
1352
+ `video-embed-vimeo`, a `https://player.vimeo.com/video/ID?dnt=1` player and the
1353
+ title `Vimeo video`. The core sets no style on it: with nothing from the theme
1354
+ the player is the providers' standard 560 by 315.
1355
+
1356
+ **Paper** makes it responsive. `.video-embed iframe` takes the full width of
1357
+ the text column at 16:9, with the rounded corners an image has, and the
1358
+ figcaption takes the muted caption style every figure in a post has. A theme
1359
+ that ships its own `static/style.css` writes that rule itself:
1360
+
1361
+ ```css
1362
+ .video-embed iframe {
1363
+ width: 100%;
1364
+ height: auto;
1365
+ aspect-ratio: 16 / 9;
1366
+ }
1367
+ ```
1368
+
1369
+ The figcaption is the link a feed reader, a fediverse server or the embed view
1370
+ keeps when it drops the iframe. A theme that would rather not show it on the
1371
+ page can hide `.video-embed figcaption`; the feeds carry it either way.
1372
+
1223
1373
  ## Mail templates
1224
1374
 
1225
1375
  The messages the CMS sends live under `mail/` and resolve the same way, so a
@@ -1328,6 +1478,7 @@ A document — one post, one page, or one entry of a listing — adds:
1328
1478
  | `event` | An event's `{ start, end, location }`: `Date`s, and `{ kind: "place", name }` or `{ kind: "virtual", url }`. |
1329
1479
  | `syndicateTo` | On a post's page, the syndication targets it selects, each `{ id, name, url }`. Empty with none. |
1330
1480
  | `syndication` | Its copies elsewhere, each `{ url, label }`: front matter `syndication`, and on its page the copies targets made. |
1481
+ | `original` | Where it was first published, as `{ url, label }`: front matter `canonical_href`. Absent unless http or https. |
1331
1482
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1332
1483
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1333
1484
  | `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
@@ -28,6 +28,12 @@
28
28
  content the page body
29
29
  footer the site footer
30
30
  scripts scripts at the end of <body>
31
+
32
+ Two partials, empty here, are included outside every block so that no layout
33
+ can drop them: `partials/head-end.njk` last in <head> and
34
+ `partials/body-end.njk` last in <body>. A site theme fills either one to add
35
+ analytics or a widget to every page without overriding a layout. The
36
+ editor's preview in the admin sets `editorPreview` and leaves both out.
31
37
  #}{% import "partials/feeds.njk" as feeds %}
32
38
  {%- import "partials/menu.njk" as nav %}
33
39
  {#- Whether this is the front page, which is the one page the header and the
@@ -53,6 +59,7 @@
53
59
  {% set metaUrl = (page.url or "/") | absoluteUrl %}
54
60
  {% set metaImage = shareImage.url if shareImage else "" %}
55
61
  {% set isEntry = (type == "post" or type == "page") and not pagination %}
62
+ {% set canonicalUrl = original.url if (isEntry and original) else metaUrl %}
56
63
  {#- An author archive is the only page that is about a person: a listing whose
57
64
  `author` is whose page this is, rather than the posts page's writer. -#}
58
65
  {% set isAuthorArchive = pagination and author.url and url and (author.url in url) %}
@@ -102,7 +109,7 @@
102
109
  {% endif %}{% if noindex %}
103
110
  <meta name="robots" content="noindex">
104
111
  {% endif %}
105
- <link rel="canonical" href="{{ (page.url or "/") | absoluteUrl }}">
112
+ <link rel="canonical" href="{{ canonicalUrl }}">
106
113
  <link rel="stylesheet" href="{{ "style.css" | asset }}">
107
114
  {% block speculationRules %}
108
115
  {#
@@ -188,7 +195,7 @@
188
195
  {% if metaDescription %}
189
196
  <meta property="og:description" content="{{ metaDescription }}">
190
197
  {% endif %}
191
- <meta property="og:url" content="{{ metaUrl }}">
198
+ <meta property="og:url" content="{{ canonicalUrl }}">
192
199
  <meta property="og:type" content="{% if isEntry %}article{% else %}website{% endif %}">
193
200
  <meta property="og:site_name" content="{{ site.title }}">
194
201
  {% if shareImage %}
@@ -246,6 +253,7 @@
246
253
  <link rel="manifest" href="{{ "/manifest.webmanifest" | url }}">
247
254
  {% include "partials/jsonld.njk" %}
248
255
  {% endblock %}
256
+ {% if not editorPreview %}{% include "partials/head-end.njk" %}{% endif %}
249
257
  </head>
250
258
  <body>
251
259
  <a class="screen-reader-text" href="#main">Skip to content</a>
@@ -331,5 +339,6 @@
331
339
  <script src="{{ "highlight.js" | asset }}" defer></script>
332
340
  {% endif %}
333
341
  {% endblock %}
342
+ {% if not editorPreview %}{% include "partials/body-end.njk" %}{% endif %}
334
343
  </body>
335
344
  </html>
@@ -90,13 +90,19 @@
90
90
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
91
91
  {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date("readable", none, lang) }}</time>
92
92
  {%- endif %}
93
+ {#- A second `u-url` rather than a `u-syndication`: the original is not
94
+ a copy of this post, and original-post-discovery reads a copy's
95
+ off-site `u-url` as the original. -#}
96
+ {%- if original %}
97
+ {% if date or updated %} <span aria-hidden="true">·</span> {% endif %}<span class="small">Originally published at <a class="u-url" href="{{ original.url }}">{{ original.label }}</a></span>
98
+ {%- endif %}
93
99
  {#- The post's copies elsewhere: those its targets answered with and
94
100
  those its front matter lists by hand. -#}
95
101
  {%- if syndication.length %}
96
- {% 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>
102
+ {% if date or updated or original %} <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>
97
103
  {%- endif -%}
98
104
  {%- if location %}
99
- {% if date or updated or syndication.length %} <span aria-hidden="true">·</span> {% endif %}<span class="small">{% include "partials/location.njk" %}</span>
105
+ {% if date or updated or original or syndication.length %} <span aria-hidden="true">·</span> {% endif %}<span class="small">{% include "partials/location.njk" %}</span>
100
106
  {%- endif -%}
101
107
  </p>
102
108
  </section>
@@ -0,0 +1 @@
1
+ {#- Printed last in <body> by layouts/base.njk. Empty here; a site theme fills it. -#}
@@ -94,7 +94,7 @@
94
94
  is answered on the page that sent it, so neither gets such a link.
95
95
  #}
96
96
  {% if commentForm and reply.source == "comment" %}
97
- <div class="reply"><a class="comment-reply-link" href="{{ page.url }}?reply_to={{ reply.id }}#respond">Reply</a></div>
97
+ <div class="reply"><a class="comment-reply-link" href="{{ page.url }}?reply_to={{ reply.id | urlencode }}#respond">Reply</a></div>
98
98
  {% endif %}
99
99
  </article>
100
100
  {% if reply.replies.length %}
@@ -0,0 +1 @@
1
+ {#- Printed last in <head> by layouts/base.njk. Empty here; a site theme fills it. -#}
@@ -404,6 +404,10 @@
404
404
  @apply h-auto max-w-full rounded-md;
405
405
  }
406
406
 
407
+ :is(.e-content, .page-body) .video-embed iframe {
408
+ @apply aspect-video h-auto w-full rounded-md;
409
+ }
410
+
407
411
  :is(.e-content, .page-body) figcaption {
408
412
  @apply mt-2 font-sans text-sm text-muted;
409
413
  }
@@ -66,6 +66,7 @@
66
66
  --radius-xs: .125rem;
67
67
  --radius-sm: .25rem;
68
68
  --radius-md: .375rem;
69
+ --aspect-video: 16 / 9;
69
70
  --default-transition-duration: .15s;
70
71
  --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);
71
72
  --default-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
@@ -840,6 +841,13 @@
840
841
  height: auto;
841
842
  }
842
843
 
844
+ :is(.e-content, .page-body) .video-embed iframe {
845
+ aspect-ratio: var(--aspect-video);
846
+ border-radius: var(--radius-md);
847
+ width: 100%;
848
+ height: auto;
849
+ }
850
+
843
851
  :is(.e-content, .page-body) figcaption {
844
852
  margin-top: calc(var(--spacing) * 2);
845
853
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;