@geekity/cms 0.12.0 → 0.14.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 (278) hide show
  1. package/README.md +188 -9
  2. package/admin/pages/account/login.njk +3 -0
  3. package/admin/pages/documents/editor.njk +20 -0
  4. package/admin/pages/indieauth/consent.njk +51 -0
  5. package/admin/pages/indieauth/refused.njk +11 -0
  6. package/admin/pages/settings/general.njk +12 -2
  7. package/admin/pages/settings/reading.njk +93 -0
  8. package/admin/pages/users/apps.njk +74 -0
  9. package/admin/pages/users/edit.njk +12 -0
  10. package/admin/static/admin.css +27 -0
  11. package/dist/admin/accounts.d.ts +3 -0
  12. package/dist/admin/accounts.d.ts.map +1 -1
  13. package/dist/admin/accounts.js +5 -0
  14. package/dist/admin/accounts.js.map +1 -1
  15. package/dist/admin/documents.d.ts +4 -0
  16. package/dist/admin/documents.d.ts.map +1 -1
  17. package/dist/admin/documents.js +45 -5
  18. package/dist/admin/documents.js.map +1 -1
  19. package/dist/admin/federation.js +2 -2
  20. package/dist/admin/federation.js.map +1 -1
  21. package/dist/admin/headers.d.ts +9 -2
  22. package/dist/admin/headers.d.ts.map +1 -1
  23. package/dist/admin/headers.js +16 -7
  24. package/dist/admin/headers.js.map +1 -1
  25. package/dist/admin/menu.d.ts.map +1 -1
  26. package/dist/admin/menu.js +2 -1
  27. package/dist/admin/menu.js.map +1 -1
  28. package/dist/admin/routes.d.ts.map +1 -1
  29. package/dist/admin/routes.js +48 -5
  30. package/dist/admin/routes.js.map +1 -1
  31. package/dist/admin/settings-general.d.ts.map +1 -1
  32. package/dist/admin/settings-general.js +28 -3
  33. package/dist/admin/settings-general.js.map +1 -1
  34. package/dist/admin/settings-reading.d.ts.map +1 -1
  35. package/dist/admin/settings-reading.js +15 -1
  36. package/dist/admin/settings-reading.js.map +1 -1
  37. package/dist/admin/settings.d.ts +83 -4
  38. package/dist/admin/settings.d.ts.map +1 -1
  39. package/dist/admin/settings.js +166 -4
  40. package/dist/admin/settings.js.map +1 -1
  41. package/dist/admin/templates.d.ts +3 -0
  42. package/dist/admin/templates.d.ts.map +1 -1
  43. package/dist/admin/templates.js +3 -0
  44. package/dist/admin/templates.js.map +1 -1
  45. package/dist/admin/users.d.ts +8 -0
  46. package/dist/admin/users.d.ts.map +1 -1
  47. package/dist/admin/users.js +9 -0
  48. package/dist/admin/users.js.map +1 -1
  49. package/dist/config.d.ts +34 -0
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/content/pinned.d.ts +26 -0
  54. package/dist/content/pinned.d.ts.map +1 -0
  55. package/dist/content/pinned.js +42 -0
  56. package/dist/content/pinned.js.map +1 -0
  57. package/dist/content/store.d.ts +7 -0
  58. package/dist/content/store.d.ts.map +1 -1
  59. package/dist/content/store.js +20 -0
  60. package/dist/content/store.js.map +1 -1
  61. package/dist/env.d.ts +21 -0
  62. package/dist/env.d.ts.map +1 -1
  63. package/dist/federation/actor.d.ts +15 -0
  64. package/dist/federation/actor.d.ts.map +1 -1
  65. package/dist/federation/actor.js +94 -3
  66. package/dist/federation/actor.js.map +1 -1
  67. package/dist/federation/article.d.ts +13 -1
  68. package/dist/federation/article.d.ts.map +1 -1
  69. package/dist/federation/article.js +45 -6
  70. package/dist/federation/article.js.map +1 -1
  71. package/dist/federation/delivery.d.ts.map +1 -1
  72. package/dist/federation/delivery.js +36 -4
  73. package/dist/federation/delivery.js.map +1 -1
  74. package/dist/federation/federation.d.ts.map +1 -1
  75. package/dist/federation/federation.js +24 -2
  76. package/dist/federation/federation.js.map +1 -1
  77. package/dist/federation/mount.js +2 -2
  78. package/dist/federation/mount.js.map +1 -1
  79. package/dist/federation/paths.d.ts +30 -0
  80. package/dist/federation/paths.d.ts.map +1 -1
  81. package/dist/federation/paths.js +36 -0
  82. package/dist/federation/paths.js.map +1 -1
  83. package/dist/images/icons.d.ts +44 -25
  84. package/dist/images/icons.d.ts.map +1 -1
  85. package/dist/images/icons.js +205 -60
  86. package/dist/images/icons.js.map +1 -1
  87. package/dist/images/index.d.ts +2 -2
  88. package/dist/images/index.d.ts.map +1 -1
  89. package/dist/images/index.js +1 -1
  90. package/dist/images/index.js.map +1 -1
  91. package/dist/images/markup.d.ts +14 -0
  92. package/dist/images/markup.d.ts.map +1 -1
  93. package/dist/images/markup.js +16 -11
  94. package/dist/images/markup.js.map +1 -1
  95. package/dist/images/variants.d.ts +11 -0
  96. package/dist/images/variants.d.ts.map +1 -1
  97. package/dist/images/variants.js +24 -4
  98. package/dist/images/variants.js.map +1 -1
  99. package/dist/index.d.ts +15 -5
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +33 -2
  102. package/dist/index.js.map +1 -1
  103. package/dist/indexnow.d.ts +34 -0
  104. package/dist/indexnow.d.ts.map +1 -0
  105. package/dist/indexnow.js +141 -0
  106. package/dist/indexnow.js.map +1 -0
  107. package/dist/indieauth/bearer.d.ts +39 -0
  108. package/dist/indieauth/bearer.d.ts.map +1 -0
  109. package/dist/indieauth/bearer.js +63 -0
  110. package/dist/indieauth/bearer.js.map +1 -0
  111. package/dist/indieauth/client.d.ts +61 -0
  112. package/dist/indieauth/client.d.ts.map +1 -0
  113. package/dist/indieauth/client.js +139 -0
  114. package/dist/indieauth/client.js.map +1 -0
  115. package/dist/indieauth/connected-apps.d.ts +23 -0
  116. package/dist/indieauth/connected-apps.d.ts.map +1 -0
  117. package/dist/indieauth/connected-apps.js +73 -0
  118. package/dist/indieauth/connected-apps.js.map +1 -0
  119. package/dist/indieauth/consent.d.ts +28 -0
  120. package/dist/indieauth/consent.d.ts.map +1 -0
  121. package/dist/indieauth/consent.js +185 -0
  122. package/dist/indieauth/consent.js.map +1 -0
  123. package/dist/indieauth/discovery.d.ts +87 -0
  124. package/dist/indieauth/discovery.d.ts.map +1 -0
  125. package/dist/indieauth/discovery.js +127 -0
  126. package/dist/indieauth/discovery.js.map +1 -0
  127. package/dist/indieauth/grants.d.ts +57 -0
  128. package/dist/indieauth/grants.d.ts.map +1 -0
  129. package/dist/indieauth/grants.js +35 -0
  130. package/dist/indieauth/grants.js.map +1 -0
  131. package/dist/indieauth/identity.d.ts +36 -0
  132. package/dist/indieauth/identity.d.ts.map +1 -0
  133. package/dist/indieauth/identity.js +77 -0
  134. package/dist/indieauth/identity.js.map +1 -0
  135. package/dist/indieauth/redeem.d.ts +47 -0
  136. package/dist/indieauth/redeem.d.ts.map +1 -0
  137. package/dist/indieauth/redeem.js +69 -0
  138. package/dist/indieauth/redeem.js.map +1 -0
  139. package/dist/indieauth/request.d.ts +47 -0
  140. package/dist/indieauth/request.d.ts.map +1 -0
  141. package/dist/indieauth/request.js +104 -0
  142. package/dist/indieauth/request.js.map +1 -0
  143. package/dist/indieauth/token.d.ts +19 -0
  144. package/dist/indieauth/token.d.ts.map +1 -0
  145. package/dist/indieauth/token.js +110 -0
  146. package/dist/indieauth/token.js.map +1 -0
  147. package/dist/indieauth/tokens.d.ts +131 -0
  148. package/dist/indieauth/tokens.d.ts.map +1 -0
  149. package/dist/indieauth/tokens.js +205 -0
  150. package/dist/indieauth/tokens.js.map +1 -0
  151. package/dist/web/admin-bar.d.ts +3 -1
  152. package/dist/web/admin-bar.d.ts.map +1 -1
  153. package/dist/web/admin-bar.js +5 -1
  154. package/dist/web/admin-bar.js.map +1 -1
  155. package/dist/web/archive.d.ts +1 -1
  156. package/dist/web/archive.d.ts.map +1 -1
  157. package/dist/web/archive.js +3 -2
  158. package/dist/web/archive.js.map +1 -1
  159. package/dist/web/authors.d.ts +19 -0
  160. package/dist/web/authors.d.ts.map +1 -1
  161. package/dist/web/authors.js +25 -0
  162. package/dist/web/authors.js.map +1 -1
  163. package/dist/web/context.d.ts +25 -0
  164. package/dist/web/context.d.ts.map +1 -1
  165. package/dist/web/context.js +18 -0
  166. package/dist/web/context.js.map +1 -1
  167. package/dist/web/feed-atom.d.ts.map +1 -1
  168. package/dist/web/feed-atom.js +5 -2
  169. package/dist/web/feed-atom.js.map +1 -1
  170. package/dist/web/feed-item.d.ts +24 -6
  171. package/dist/web/feed-item.d.ts.map +1 -1
  172. package/dist/web/feed-item.js +16 -8
  173. package/dist/web/feed-item.js.map +1 -1
  174. package/dist/web/feed-json.d.ts +6 -2
  175. package/dist/web/feed-json.d.ts.map +1 -1
  176. package/dist/web/feed-json.js +6 -2
  177. package/dist/web/feed-json.js.map +1 -1
  178. package/dist/web/feed-rss.d.ts.map +1 -1
  179. package/dist/web/feed-rss.js +17 -4
  180. package/dist/web/feed-rss.js.map +1 -1
  181. package/dist/web/feed-source.d.ts +31 -0
  182. package/dist/web/feed-source.d.ts.map +1 -1
  183. package/dist/web/feed-source.js +31 -0
  184. package/dist/web/feed-source.js.map +1 -1
  185. package/dist/web/feed-xml.d.ts +2 -0
  186. package/dist/web/feed-xml.d.ts.map +1 -1
  187. package/dist/web/feed-xml.js +2 -0
  188. package/dist/web/feed-xml.js.map +1 -1
  189. package/dist/web/feeds.d.ts.map +1 -1
  190. package/dist/web/feeds.js +15 -3
  191. package/dist/web/feeds.js.map +1 -1
  192. package/dist/web/index.d.ts +4 -2
  193. package/dist/web/index.d.ts.map +1 -1
  194. package/dist/web/index.js +3 -2
  195. package/dist/web/index.js.map +1 -1
  196. package/dist/web/indexnow.d.ts +9 -0
  197. package/dist/web/indexnow.d.ts.map +1 -0
  198. package/dist/web/indexnow.js +27 -0
  199. package/dist/web/indexnow.js.map +1 -0
  200. package/dist/web/llms.d.ts +58 -0
  201. package/dist/web/llms.d.ts.map +1 -0
  202. package/dist/web/llms.js +91 -0
  203. package/dist/web/llms.js.map +1 -0
  204. package/dist/web/locale.d.ts +41 -0
  205. package/dist/web/locale.d.ts.map +1 -0
  206. package/dist/web/locale.js +56 -0
  207. package/dist/web/locale.js.map +1 -0
  208. package/dist/web/manifest.d.ts +50 -0
  209. package/dist/web/manifest.d.ts.map +1 -0
  210. package/dist/web/manifest.js +56 -0
  211. package/dist/web/manifest.js.map +1 -0
  212. package/dist/web/negotiate.d.ts.map +1 -1
  213. package/dist/web/negotiate.js +6 -0
  214. package/dist/web/negotiate.js.map +1 -1
  215. package/dist/web/oembed.d.ts +104 -0
  216. package/dist/web/oembed.d.ts.map +1 -0
  217. package/dist/web/oembed.js +290 -0
  218. package/dist/web/oembed.js.map +1 -0
  219. package/dist/web/opensearch.d.ts +35 -0
  220. package/dist/web/opensearch.d.ts.map +1 -0
  221. package/dist/web/opensearch.js +65 -0
  222. package/dist/web/opensearch.js.map +1 -0
  223. package/dist/web/render.d.ts +6 -1
  224. package/dist/web/render.d.ts.map +1 -1
  225. package/dist/web/render.js +40 -10
  226. package/dist/web/render.js.map +1 -1
  227. package/dist/web/robots.d.ts +89 -0
  228. package/dist/web/robots.d.ts.map +1 -0
  229. package/dist/web/robots.js +259 -0
  230. package/dist/web/robots.js.map +1 -0
  231. package/dist/web/routes.d.ts.map +1 -1
  232. package/dist/web/routes.js +179 -8
  233. package/dist/web/routes.js.map +1 -1
  234. package/dist/web/share-image.d.ts +51 -0
  235. package/dist/web/share-image.d.ts.map +1 -0
  236. package/dist/web/share-image.js +43 -0
  237. package/dist/web/share-image.js.map +1 -0
  238. package/dist/web/sitemap.d.ts +0 -26
  239. package/dist/web/sitemap.d.ts.map +1 -1
  240. package/dist/web/sitemap.js +0 -44
  241. package/dist/web/sitemap.js.map +1 -1
  242. package/dist/web/templates.d.ts +16 -9
  243. package/dist/web/templates.d.ts.map +1 -1
  244. package/dist/web/templates.js +88 -55
  245. package/dist/web/templates.js.map +1 -1
  246. package/dist/web/themes.d.ts +33 -0
  247. package/dist/web/themes.d.ts.map +1 -1
  248. package/dist/web/themes.js +57 -0
  249. package/dist/web/themes.js.map +1 -1
  250. package/dist/webmention/discovery.d.ts +4 -2
  251. package/dist/webmention/discovery.d.ts.map +1 -1
  252. package/dist/webmention/discovery.js +14 -8
  253. package/dist/webmention/discovery.js.map +1 -1
  254. package/dist/webmention/fetch-public.d.ts +2 -0
  255. package/dist/webmention/fetch-public.d.ts.map +1 -1
  256. package/dist/webmention/fetch-public.js +1 -1
  257. package/dist/webmention/fetch-public.js.map +1 -1
  258. package/package.json +2 -1
  259. package/templates/site/content/_data/site.json +0 -1
  260. package/themes/default/README.md +174 -43
  261. package/themes/default/layouts/base.njk +116 -17
  262. package/themes/default/layouts/front-page.njk +7 -1
  263. package/themes/default/layouts/home.njk +11 -0
  264. package/themes/default/layouts/page.njk +4 -3
  265. package/themes/default/layouts/post.njk +5 -4
  266. package/themes/default/layouts/search.njk +4 -5
  267. package/themes/default/partials/archive.njk +3 -1
  268. package/themes/default/partials/bio.njk +16 -2
  269. package/themes/default/partials/breadcrumbs.njk +20 -0
  270. package/themes/default/partials/conversation.njk +4 -1
  271. package/themes/default/partials/jsonld.njk +49 -9
  272. package/themes/default/partials/kicker.njk +6 -4
  273. package/themes/default/partials/lang.njk +7 -0
  274. package/themes/default/partials/post-list.njk +5 -3
  275. package/themes/default/partials/reply-context.njk +4 -2
  276. package/themes/default/src/style.css +14 -0
  277. package/themes/default/static/style.css +57 -0
  278. package/themes/default/theme.json +2 -0
@@ -23,6 +23,8 @@
23
23
  adds to it.
24
24
  alternates <link rel="alternate"> tags, where the feeds go
25
25
  header the site header, with the site menu
26
+ breadcrumbs the trail at the top of <main>; empty it to print none
27
+ (the JSON-LD keeps its BreadcrumbList)
26
28
  content the page body
27
29
  footer the site footer
28
30
  scripts scripts at the end of <body>
@@ -38,8 +40,10 @@
38
40
 
39
41
  `metaDescription` is the page's own description, else the entry's summary
40
42
  — the very line the feeds publish — else the site's tagline. `metaImage` is
41
- the picture the front matter names, else the site's avatar; with neither,
42
- nothing is printed rather than an empty tag. `isEntry` is a rendered post
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
46
+ template cannot read. `isEntry` is a rendered post
43
47
  or page, which is what Open Graph calls an article: a listing has
44
48
  `pagination`, and the posts page is a listing carrying a page's front
45
49
  matter, so it is a website like every other listing. -#}
@@ -48,13 +52,51 @@
48
52
  {% set pageTitle = title or label %}
49
53
  {% set metaTitle = pageTitle or site.title %}
50
54
  {% set metaUrl = (page.url or "/") | absoluteUrl %}
51
- {% set metaImage = image or site.avatar %}
55
+ {% set metaImage = shareImage.url if shareImage else "" %}
52
56
  {% set isEntry = (type == "post" or type == "page") and not pagination %}
57
+ {#- An author archive is the only page that is about a person: a listing whose
58
+ `author` is whose page this is, rather than the posts page's writer. -#}
59
+ {% set isAuthorArchive = pagination and author.url and url and (author.url in url) %}
60
+ {#- Where this page sits, as `{ name, url }` from the front page down to it
61
+ (TASK-150): a post under its first category, and an archive under the
62
+ front page, with a crumb of its own past page one. The visible breadcrumb
63
+ and the JSON-LD both print this list, so they cannot say different things.
64
+ Every other page has no hierarchy worth showing and an empty trail. -#}
65
+ {% set crumbs = [] %}
66
+ {% if pagination and category %}
67
+ {% set _ = crumbs.push({ "name": category, "url": "/" + (site.categoryBase or "category") + "/" + (category | urlencode) + "/" }) %}
68
+ {% elif pagination and tag %}
69
+ {% set _ = crumbs.push({ "name": tag, "url": "/" + (site.tagBase or "tag") + "/" + (tag | urlencode) + "/" }) %}
70
+ {% elif isAuthorArchive %}
71
+ {% set _ = crumbs.push({ "name": author.name, "url": author.url }) %}
72
+ {% elif type == "post" and isEntry and categories.length %}
73
+ {% set _ = crumbs.push({ "name": categories[0], "url": "/" + (site.categoryBase or "category") + "/" + (categories[0] | urlencode) + "/" }) %}
74
+ {% set _ = crumbs.push({ "name": pageTitle, "url": page.url or url }) %}
75
+ {% endif %}
76
+ {% if crumbs.length and pagination.pageNumber > 0 %}
77
+ {% set _ = crumbs.push({ "name": "Page " + (pagination.pageNumber + 1), "url": page.url or url }) %}
78
+ {% endif %}
79
+ {% set breadcrumbs = [{ "name": "Home", "url": "/" }].concat(crumbs) if crumbs.length else [] %}
53
80
  <!doctype html>
54
81
  <html lang="{{ site.language | default("en") }}">
55
82
  <head>
56
83
  <meta charset="utf-8">
57
84
  <meta name="viewport" content="width=device-width, initial-scale=1">
85
+ {#
86
+ The colours the theme's `theme.json` declares (TASK-146). The colour
87
+ scheme goes before the stylesheet, so a reader in dark mode gets a dark
88
+ canvas while the stylesheet is still on its way rather than a white
89
+ flash. The theme colour tints the browser around the page, one per
90
+ scheme, and is the page's own background so the two meet without a seam.
91
+ #}
92
+ {% if theme.colorScheme %}
93
+ <meta name="color-scheme" content="{{ theme.colorScheme }}">
94
+ {% endif %}
95
+ {% for scheme in ["light", "dark"] %}
96
+ {% if theme.themeColor[scheme] %}
97
+ <meta name="theme-color" content="{{ theme.themeColor[scheme] }}" media="(prefers-color-scheme: {{ scheme }})">
98
+ {% endif %}
99
+ {% endfor %}
58
100
  <title>{% block title %}{% if pageTitle %}{{ pageTitle }} &middot; {% endif %}{{ site.title }}{% endblock %}</title>
59
101
  {% if metaDescription %}
60
102
  <meta name="description" content="{{ metaDescription }}">
@@ -110,13 +152,33 @@
110
152
  {% if webmention %}
111
153
  <link rel="webmention" href="{{ webmention | url }}">
112
154
  {% endif %}
155
+ {#- The site's search, for a browser to offer from its address bar
156
+ (TASK-204). #}
157
+ <link rel="search" type="application/opensearchdescription+xml" title="{{ site.title }}" href="{{ "/opensearch.xml" | url }}">
158
+ {#- Where a tool that is pasted this post's URL asks for its card
159
+ (TASK-205), as JSON and as XML. #}
160
+ {% if isEntry %}
161
+ {% set oembed = "/_geekity/oembed?url=" + (metaUrl | urlencode) + "&format=" %}
162
+ <link rel="alternate" type="application/json+oembed" href="{{ (oembed + "json") | absoluteUrl }}" title="{{ metaTitle }}">
163
+ <link rel="alternate" type="text/xml+oembed" href="{{ (oembed + "xml") | absoluteUrl }}" title="{{ metaTitle }}">
164
+ {% endif %}
165
+ {#- The site's index for language models (TASK-149), from the front page,
166
+ which is where a tool that found the site looks for it. #}
167
+ {% if llmsTxt and atRoot %}
168
+ <link rel="describedby" type="text/markdown" href="{{ llmsTxt | url }}">
169
+ {% endif %}
113
170
  {% block head %}
114
171
  {#
115
172
  What a link to this page looks like when somebody posts it: the same
116
173
  title, description and picture to Open Graph and to a Twitter card,
117
- which is the pair every reader of this markup understands. The card is
118
- always `summary` — a small square picture beside the words — because the
119
- picture is usually the site's avatar rather than a wide photograph.
174
+ which is the pair every reader of this markup understands. The image
175
+ always carries its alt text, and its size when the CMS knows it. The
176
+ card is `summary_large_image` only for a picture wide enough to fill
177
+ one, and `summary` — a small picture beside the words — for the site's
178
+ avatar and anything else.
179
+
180
+ An entry is an article, with its times, its author and its tags: the
181
+ author is their profile here when they have one, else their name.
120
182
  #}
121
183
  <meta property="og:title" content="{{ metaTitle }}">
122
184
  {% if metaDescription %}
@@ -125,25 +187,59 @@
125
187
  <meta property="og:url" content="{{ metaUrl }}">
126
188
  <meta property="og:type" content="{% if isEntry %}article{% else %}website{% endif %}">
127
189
  <meta property="og:site_name" content="{{ site.title }}">
128
- {% if metaImage %}
129
- <meta property="og:image" content="{{ metaImage | absoluteUrl }}">
190
+ {% if shareImage %}
191
+ <meta property="og:image" content="{{ shareImage.url | absoluteUrl }}">
192
+ <meta property="og:image:alt" content="{{ shareImage.alt }}">
193
+ {% if shareImage.size %}
194
+ <meta property="og:image:width" content="{{ shareImage.size.width }}">
195
+ <meta property="og:image:height" content="{{ shareImage.size.height }}">
196
+ {% endif %}
197
+ {% endif %}
198
+ {% if isEntry %}
199
+ {% if date %}
200
+ <meta property="article:published_time" content="{{ date | date("iso") }}">
201
+ {% endif %}
202
+ {% if updated or date %}
203
+ <meta property="article:modified_time" content="{{ (updated or date) | date("iso") }}">
204
+ {% endif %}
205
+ {% if siteAuthor.url %}
206
+ <meta property="article:author" content="{{ siteAuthor.url | absoluteUrl }}">
207
+ {% elif siteAuthor.name or author.name or author %}
208
+ <meta property="article:author" content="{{ siteAuthor.name or author.name or author }}">
209
+ {% endif %}
210
+ {% for tag in tags %}
211
+ <meta property="article:tag" content="{{ tag }}">
212
+ {% endfor %}
213
+ {% endif %}
214
+ {#
215
+ Who a Mastodon link preview credits, with a follow button (TASK-202):
216
+ the user's own account here, on an entry by them and on the homepage of
217
+ a site one person writes. Mastodon shows it only when that account lists
218
+ this site's domain under author attribution.
219
+ #}
220
+ {% set creator = siteAuthor if isEntry else (soloAuthor if atRoot) %}
221
+ {% if creator.username %}
222
+ <meta name="fediverse:creator" content="{{ creator.username | fediverseHandle }}">
130
223
  {% endif %}
131
- <meta name="twitter:card" content="summary">
224
+ <meta name="twitter:card" content="{{ shareImage.card if shareImage else "summary" }}">
132
225
  <meta name="twitter:title" content="{{ metaTitle }}">
133
226
  {% if metaDescription %}
134
227
  <meta name="twitter:description" content="{{ metaDescription }}">
135
228
  {% endif %}
136
- {% if metaImage %}
137
- <meta name="twitter:image" content="{{ metaImage | absoluteUrl }}">
229
+ {% if shareImage %}
230
+ <meta name="twitter:image" content="{{ shareImage.url | absoluteUrl }}">
231
+ <meta name="twitter:image:alt" content="{{ shareImage.alt }}">
138
232
  {% endif %}
139
233
  {#
140
- The site's icons, derived from its avatar. `icons` is empty — and these
141
- lines print nothing — until a site has one, so a site with no avatar
142
- links no icon rather than three that answer 404.
234
+ The site's icons, derived from its `icon` setting, else its avatar.
235
+ `icons` is empty — and these lines print nothing — until a site has one,
236
+ so a site with neither links no icon rather than links that answer 404.
237
+ An SVG icon has every size, so it carries no `sizes`.
143
238
  #}
144
239
  {% for icon in icons %}
145
- <link rel="{{ icon.rel }}" type="image/png" sizes="{{ icon.sizes }}" href="{{ icon.href | url }}">
240
+ <link rel="{{ icon.rel }}" type="{{ icon.type }}"{% if icon.sizes %} sizes="{{ icon.sizes }}"{% endif %} href="{{ icon.href | url }}">
146
241
  {% endfor %}
242
+ <link rel="manifest" href="{{ "/manifest.webmanifest" | url }}">
147
243
  {% include "partials/jsonld.njk" %}
148
244
  {% endblock %}
149
245
  </head>
@@ -170,19 +266,22 @@
170
266
  <p class="site-tagline">{{ site.tagline }}</p>
171
267
  {% endif %}
172
268
  {% else %}
173
- <a class="header-link-home" href="{{ "/" | url }}">{{ site.title }}</a>
269
+ {#- The solo author's own archive claims the homepage back, so the
270
+ two URLs are provably one person (TASK-180). -#}
271
+ <a class="header-link-home"{% if isAuthorArchive and soloAuthor.url == author.url %} rel="me"{% endif %} href="{{ "/" | url }}">{{ site.title }}</a>
174
272
  {% endif %}
175
273
  {{ nav.list(menus.primary, "Site") }}
176
274
  </header>
177
275
  {% endblock %}
178
276
 
179
277
  <main id="main">
278
+ {% block breadcrumbs %}{% include "partials/breadcrumbs.njk" %}{% endblock %}
180
279
  {% block content %}{% endblock %}
181
280
  </main>
182
281
 
183
282
  {% block footer %}
184
283
  <footer>
185
- <p>&copy; {{ "now" | date("year") }}{% if site.author %}, {{ site.author }}{% endif %} &middot; Published with Geekity</p>
284
+ <p>&copy; {{ "now" | date("year") }}, {{ soloAuthor.name or site.title }} &middot; Published with Geekity</p>
186
285
  {#
187
286
  The footer menu: the `footer` area this theme declares in its
188
287
  `theme.json`, which a site fills in on the Navigation screen, and the
@@ -10,13 +10,19 @@
10
10
  over the same feed a listing is made of, headed at 3 so the outline never
11
11
  skips a level, then the line of links, then the bio.
12
12
 
13
+ The bio is the site's author's on a solo author blog, with the `rel="me"`
14
+ claims that make this page their identity (TASK-180). On any other site it is
15
+ whoever the page is by, and it claims nothing: the homepage speaks for nobody.
16
+
13
17
  A theme that wants a different front page replaces this file; a site that
14
18
  wants no front page of its own sets no homepage, and `/` is the listing.
15
19
  #}
16
20
  {% extends "layouts/base.njk" %}
17
21
 
18
22
  {#- Who the bio credits: the person the site's author setting names. -#}
19
- {% set bioAuthor = siteAuthor %}
23
+ {% set bioAuthor = soloAuthor or siteAuthor %}
24
+ {% set bioRelMe = true if soloAuthor else false %}
25
+ {% set bioHome = "/" | absoluteUrl if soloAuthor else "" %}
20
26
 
21
27
  {% block content %}
22
28
  <div class="page-body e-content">
@@ -22,4 +22,15 @@
22
22
  {% endif %}
23
23
  {% include "partials/post-list.njk" %}
24
24
  {% include "partials/pagination.njk" %}
25
+ {#- On a solo author blog the homepage speaks for the site's author, so it
26
+ ends with their card, as the front page does (TASK-180). Any other
27
+ listing, and the homepage of any other site, is nobody's. -#}
28
+ {% if atRoot and soloAuthor %}
29
+ {% set bioAuthor = soloAuthor %}
30
+ {% set bioHome = "/" | absoluteUrl %}
31
+ <hr>
32
+ <footer>
33
+ {% include "partials/bio.njk" %}
34
+ </footer>
35
+ {% endif %}
25
36
  {% endblock %}
@@ -14,6 +14,7 @@
14
14
  #}
15
15
  {% extends "layouts/base.njk" %}
16
16
  {% import "partials/kicker.njk" as kicker with context %}
17
+ {% import "partials/lang.njk" as language with context %}
17
18
 
18
19
  {#- Who the bio credits, and what tells `layouts/base.njk` the site menu is in
19
20
  the bio rather than in the page footer. -#}
@@ -21,7 +22,7 @@
21
22
 
22
23
  {% block content %}
23
24
  {% set entryUrl = page.url or url %}
24
- <article class="blog-post h-entry">
25
+ <article class="blog-post h-entry"{{ language.attr(lang) }}>
25
26
  <header class="post-header">
26
27
  {{ kicker.line("Page") }}
27
28
  <h1 class="p-name">{{ title }}</h1>
@@ -35,10 +36,10 @@
35
36
  deciding twice whether there is anything to say. -#}
36
37
  <p class="page-meta">
37
38
  {%- if date %}
38
- <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date }}</time></a>
39
+ <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date("readable", none, lang) }}</time></a>
39
40
  {%- endif %}
40
41
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
41
- {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date }}</time>
42
+ {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date("readable", none, lang) }}</time>
42
43
  {%- endif -%}
43
44
  </p>
44
45
  </section>
@@ -17,6 +17,7 @@
17
17
  {% extends "layouts/base.njk" %}
18
18
  {% import "partials/tags.njk" as taxonomy with context %}
19
19
  {% import "partials/kicker.njk" as kicker with context %}
20
+ {% import "partials/lang.njk" as language with context %}
20
21
 
21
22
  {#- Who the bio credits: the person the file names, and otherwise whoever the
22
23
  site is by, which is what the source theme's bio reads. A post naming
@@ -28,7 +29,7 @@
28
29
 
29
30
  {% block content %}
30
31
  {% set entryUrl = page.url or url %}
31
- <article class="blog-post h-entry">
32
+ <article class="blog-post h-entry"{{ language.attr(lang) }}>
32
33
  {#- A post with no name of its own (a note, or a reply without a title) is
33
34
  its words, so it opens on them in sight. Its page still needs one h1 to
34
35
  navigate by, so it gets one that says what it is, who wrote it and when,
@@ -49,7 +50,7 @@
49
50
  <h1 class="screen-reader-text">
50
51
  {{- "Reply" if postType == "reply" else "Note" }}
51
52
  {%- if bioAuthor.name %} by {{ bioAuthor.name }}{% endif %}
52
- {%- if date %}, {{ date | date }}{% endif -%}
53
+ {%- if date %}, {{ date | date("readable", none, lang) }}{% endif -%}
53
54
  </h1>
54
55
  {{ kicker.line(postType | capitalize, false, false, categories) }}
55
56
  {% include "partials/reply-context.njk" %}
@@ -64,12 +65,12 @@
64
65
  <a class="u-category small" href="https://news.indieweb.org/en">#indienews</a>
65
66
  {%- endif %}
66
67
  {%- if date %}
67
- <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date }}</time></a>
68
+ <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date("readable", none, lang) }}</time></a>
68
69
  {%- endif %}
69
70
  {#- Updated only when it is a different day in the site's own timezone:
70
71
  a typo fixed an hour after publishing is not news. -#}
71
72
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
72
- {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date }}</time>
73
+ {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date("readable", none, lang) }}</time>
73
74
  {%- endif -%}
74
75
  </p>
75
76
  </section>
@@ -21,6 +21,7 @@
21
21
  {% endblock %}
22
22
 
23
23
  {% import "partials/kicker.njk" as kicker with context %}
24
+ {% import "partials/lang.njk" as language with context %}
24
25
  {% block content %}
25
26
  <h1 class="page-title">Search</h1>
26
27
  {% include "partials/search-form.njk" %}
@@ -28,17 +29,15 @@
28
29
  <p class="search-summary" role="status">
29
30
  {%- if pagination.total == 0 -%}
30
31
  Nothing found for &ldquo;{{ query }}&rdquo;.
31
- {%- elif pagination.total == 1 -%}
32
- One result for &ldquo;{{ query }}&rdquo;.
33
32
  {%- else -%}
34
- {{ pagination.total }} results for &ldquo;{{ query }}&rdquo;.
33
+ {{ pagination.total | plural({ one: "One result", other: "# results" }, "en") }} for &ldquo;{{ query }}&rdquo;.
35
34
  {%- endif -%}
36
35
  </p>
37
36
  {% if posts.length %}
38
37
  <div class="search-results">
39
38
  {% for post in posts %}
40
- <article class="search-result h-entry">
41
- {{ kicker.line(("Page" if post.type == "page" else post.postType) | capitalize, post.date) }}
39
+ <article class="search-result h-entry"{{ language.attr(post.lang) }}>
40
+ {{ kicker.line(("Page" if post.type == "page" else post.postType) | capitalize, post.date, lang=post.lang) }}
42
41
  {#- A post with no name of its own has none to claim, so its first words
43
42
  link it without being marked up as one. -#}
44
43
  <h2 class="feed-title{% if post.named %} p-name{% endif %}">
@@ -14,7 +14,9 @@
14
14
  rather than an index.
15
15
  #}
16
16
  {% if archiveMonths.length %}
17
- <section class="archive">
17
+ {#- The month headings are in the site's locale, so a page in another
18
+ language hands the section back to the site's language. -#}
19
+ <section class="archive"{% if lang and lang != (site.language | default("en")) %} lang="{{ site.language | default("en") }}"{% endif %}>
18
20
  {% for group in archiveMonths %}
19
21
  <h2>{{ group.month }}</h2>
20
22
  <ol class="list-none">
@@ -33,6 +33,17 @@
33
33
  Links box typed it — followed by whatever else that line carried, so this
34
34
  prints one attribute rather than one of its own beside one of theirs.
35
35
 
36
+ `bioRelMe` set to `false` keeps the card and drops its claims: the name is
37
+ linked `rel="author"` alone and `me` comes off every profile link. The
38
+ homepage of a site that is not a solo author blog sets it (TASK-180), because
39
+ that page speaks for nobody even when it shows the site author's card.
40
+
41
+ `bioHome` set to the homepage's absolute URL makes the card the site's
42
+ representative h-card (TASK-193): its first `u-url` and its `u-uid` are that
43
+ URL, which is what a parser reading the homepage looks for to learn who the
44
+ site is. The name keeps its link to the archive, as a second `u-url`. Only
45
+ the homepage of a solo author blog sets it; every other card is unchanged.
46
+
36
47
  The `p-note` and the `rel="me"` links are printed wherever the card is, which
37
48
  is what `bioProfile` used to ask for on an author archive alone. An entry
38
49
  left that switch unset because the page footer carried the site's identity
@@ -40,6 +51,7 @@
40
51
  wrote rather than one page further on.
41
52
  #}
42
53
  {% if bioAuthor %}
54
+ {%- set bioClaims = bioRelMe !== false %}
43
55
  <div class="bio p-author h-card">
44
56
  {%- if bioAuthor.avatar %}
45
57
  <div class="bio-avatar">
@@ -49,9 +61,10 @@
49
61
 
50
62
  <div>
51
63
  <p>
64
+ {%- if bioHome %}<data class="u-url u-uid" value="{{ bioHome }}"></data>{% endif %}
52
65
  {{ bioLead | default("Written by") }}
53
66
  {%- if bioAuthor.url %}
54
- <a class="u-url p-name" rel="author me" href="{{ bioAuthor.url | url }}">{{ bioAuthor.name }}</a>
67
+ <a class="u-url p-name" rel="author{% if bioClaims %} me{% endif %}" href="{{ bioAuthor.url | url }}">{{ bioAuthor.name }}</a>
55
68
  {%- else %}
56
69
  <strong class="p-name">{{ bioAuthor.name }}</strong>
57
70
  {%- endif %}
@@ -66,7 +79,8 @@
66
79
  {%- if bioAuthor.links.length %}
67
80
  <ul class="hlist bio-links">
68
81
  {%- for link in bioAuthor.links %}
69
- <li><a class="u-url" rel="{{ link.rel }}" href="{{ link.href }}">{{ link.label }}</a></li>
82
+ {%- set linkRel = link.rel if bioClaims else (link.rel.split(" ") | reject("equalto", "me") | join(" ")) %}
83
+ <li><a class="u-url"{% if linkRel %} rel="{{ linkRel }}"{% endif %} href="{{ link.href }}">{{ link.label }}</a></li>
70
84
  {%- endfor %}
71
85
  </ul>
72
86
  {%- endif %}
@@ -0,0 +1,20 @@
1
+ {#
2
+ The trail from the front page down to this one (TASK-150), printed from
3
+ `breadcrumbs`, the list `layouts/base.njk` builds and `partials/jsonld.njk`
4
+ prints as a BreadcrumbList. Every crumb but the last is a link; the last is
5
+ the page the reader is on, so it is text marked as the current page. The
6
+ separators are decoration and a screen reader skips them: the list already
7
+ says there are steps.
8
+ #}
9
+ {% if breadcrumbs.length %}
10
+ <nav class="breadcrumbs" aria-label="Breadcrumb">
11
+ <ol class="hlist">
12
+ {% for crumb in breadcrumbs %}
13
+ <li>
14
+ {%- if not loop.first %}<span class="breadcrumbs-sep" aria-hidden="true">›</span>{% endif -%}
15
+ {%- if loop.last %}<span aria-current="page">{{ crumb.name }}</span>{% else %}<a href="{{ crumb.url | url }}">{{ crumb.name }}</a>{% endif -%}
16
+ </li>
17
+ {% endfor %}
18
+ </ol>
19
+ </nav>
20
+ {% endif %}
@@ -120,6 +120,9 @@
120
120
  And the thread. The title counts the whole of it rather than the top level,
121
121
  which is what `counts.replies` is. It does not quote the post's title: the
122
122
  post is the page it sits under, and a note has no title to quote (TASK-186).
123
+ The count goes through `plural` with "en" because the words are English: a
124
+ theme that translates them drops the "en" and gives the forms its language
125
+ has, so the site's locale picks between them.
123
126
 
124
127
  "Comments are closed." is printed under a post that has been answered and is
125
128
  no longer taking comments. That a post is closed is what the absence of
@@ -130,7 +133,7 @@
130
133
  {% if conversation.replies.length %}
131
134
  <div id="comments" class="comments-area">
132
135
  <h2 class="comments-title">
133
- {%- if conversation.counts.replies == 1 %}One reply{% else %}{{ conversation.counts.replies }} replies{% endif -%}
136
+ {{- conversation.counts.replies | plural({ one: "One reply", other: "# replies" }, "en") -}}
134
137
  </h2>
135
138
 
136
139
  <ol class="comment-list">
@@ -12,7 +12,12 @@
12
12
  `siteAuthor`: the site's own author on a page about nobody in particular, the
13
13
  writer on a post, the archive's user on an author archive. Identity comes
14
14
  from a user profile, so a name that answers to no account here has no Person
15
- at all, and the author and publisher references go with it.
15
+ at all, and the author reference goes with it.
16
+
17
+ Who publishes the site follows the Site author setting (TASK-192). A site
18
+ whose author is a user is published by that user's Person and is about
19
+ them, on every page. A site with several authors is published by an
20
+ Organization named for the site and is about nobody.
16
21
 
17
22
  How it is printed: every value goes through `dump`, which is JSON.stringify,
18
23
  with every `<` escaped to its `\u003C` form so nothing in a title or a
@@ -34,6 +39,10 @@
34
39
  {#- What the author, publisher and mainEntity members hold: a reference to the
35
40
  Person rather than a second copy of them. -#}
36
41
  {% set person = { "@id": personId } if personId else "" %}
42
+ {#- Who publishes the site and everything on it: the site author, else the
43
+ site itself. -#}
44
+ {% set soloId = (soloAuthor.url | absoluteUrl) + "#person" if soloAuthor.url else "" %}
45
+ {% set publisher = { "@id": soloId or home + "#organization" } %}
37
46
  {#- `sameAs` is everywhere else this person is: the profile links they listed,
38
47
  and their actor id, which asserts that the schema.org Person and the
39
48
  fediverse actor are one identity — decision-14 makes their archive URL the
@@ -42,10 +51,15 @@
42
51
  {% for link in siteAuthor.links %}{% set _ = sameAs.push(link.href) %}{% endfor %}
43
52
  {% if personUrl %}{% set _ = sameAs.push(personUrl) %}{% endif %}
44
53
  {% set address = { "@type": "PostalAddress", "addressLocality": siteAuthor.location } if siteAuthor.location else "" %}
45
- {#- An author archive is the only page that is about a person: a listing whose
46
- `author` is whose page this is. -#}
47
- {% set isProfilePage = pagination and author.url and url and (author.url in url) %}
48
54
  {% set entryType = "BlogPosting" if type == "post" else "Article" %}
55
+ {#- A post with no title is headed by the words its <title> names it by,
56
+ within the 110 characters Google reads, cut where a word ends (TASK-201).
57
+ 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. -#}
61
+ {% 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 "") %}
49
63
  {% set nodes = [] %}
50
64
  {% set website %}
51
65
  {
@@ -66,9 +80,20 @@
66
80
  },
67
81
  "query-input": "required name=search_term_string"
68
82
  }) }}
69
- {{- member("publisher", person) }}
83
+ {{- member("publisher", publisher) }}
84
+ {{- member("about", { "@id": soloId } if soloId else "") }}
70
85
  }{% endset %}
71
86
  {% set _ = nodes.push(website) %}
87
+ {% if not soloId %}
88
+ {% set organization %}
89
+ {
90
+ "@type": "Organization"
91
+ {{- member("@id", publisher["@id"]) }}
92
+ {{- member("name", site.title) }}
93
+ {{- member("url", home) }}
94
+ }{% endset %}
95
+ {% set _ = nodes.push(organization) %}
96
+ {% endif %}
72
97
  {% if personId %}
73
98
  {% set profile %}
74
99
  {
@@ -84,7 +109,7 @@
84
109
  }{% endset %}
85
110
  {% set _ = nodes.push(profile) %}
86
111
  {% endif %}
87
- {% if isProfilePage %}
112
+ {% if isAuthorArchive %}
88
113
  {% set profilePage %}
89
114
  {
90
115
  "@type": "ProfilePage"
@@ -98,18 +123,33 @@
98
123
  {% set entry %}
99
124
  {
100
125
  "@type": {{ entryType | dump | safe }}
101
- {{- member("headline", title) }}
126
+ {{- member("headline", headline) }}
102
127
  {{- member("url", metaUrl) }}
103
128
  {{- member("mainEntityOfPage", metaUrl) }}
104
129
  {{- member("datePublished", date | date("iso") if date else "") }}
105
130
  {{- member("dateModified", (updated or date) | date("iso") if (updated or date) else "") }}
106
131
  {{- member("description", metaDescription) }}
107
- {{- member("image", metaImage | absoluteUrl if metaImage else "") }}
132
+ {{- member("image", entryImage | absoluteUrl if entryImage else "") }}
108
133
  {{- member("author", person) }}
109
- {{- member("publisher", person) }}
134
+ {{- member("publisher", publisher) }}
110
135
  }{% endset %}
111
136
  {% set _ = nodes.push(entry) %}
112
137
  {% endif %}
138
+ {#- Where the page sits, from the trail `layouts/base.njk` builds and the
139
+ visible breadcrumb prints (TASK-150). -#}
140
+ {% if breadcrumbs.length %}
141
+ {% set trail = [] %}
142
+ {% for crumb in breadcrumbs %}
143
+ {% set _ = trail.push({ "@type": "ListItem", "position": loop.index, "name": crumb.name, "item": crumb.url | absoluteUrl }) %}
144
+ {% endfor %}
145
+ {% set breadcrumbList %}
146
+ {
147
+ "@type": "BreadcrumbList"
148
+ {{- member("@id", metaUrl + "#breadcrumb") }}
149
+ {{- member("itemListElement", trail) }}
150
+ }{% endset %}
151
+ {% set _ = nodes.push(breadcrumbList) %}
152
+ {% endif %}
113
153
  <script type="application/ld+json">
114
154
  {
115
155
  "@context": "https://schema.org",
@@ -14,17 +14,19 @@
14
14
  an error page. `date` is the entry's `dt-published`, printed long. With
15
15
  `href` the date is the entry's `u-url`, which is how a feed links an entry
16
16
  that has no title to link; an entry with no date links `label` instead.
17
- `categories` are printed as `p-category` links.
17
+ `categories` are printed as `p-category` links. `lang` is the entry's own
18
+ language when it names one, so its date is written in that language rather
19
+ than the site's (TASK-189); a listing passes `lang=post.lang`.
18
20
  #}
19
- {% macro line(kind, date=false, href=false, categories=[], label="") %}
21
+ {% macro line(kind, date=false, href=false, categories=[], label="", lang=false) %}
20
22
  <p class="kicker">
21
23
  <span class="kicker-kind">{{ kind }}</span>
22
24
  {%- if href %}
23
25
  <span class="kicker-dot" aria-hidden="true">·</span>
24
- <a href="{{ href | url }}" class="u-url">{% if date %}<time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date }}</time>{% else %}{{ label }}{% endif %}</a>
26
+ <a href="{{ href | url }}" class="u-url">{% if date %}<time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date("readable", none, lang) }}</time>{% else %}{{ label }}{% endif %}</a>
25
27
  {%- elif date %}
26
28
  <span class="kicker-dot" aria-hidden="true">·</span>
27
- <time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date }}</time>
29
+ <time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date("readable", none, lang) }}</time>
28
30
  {%- endif %}
29
31
  {%- for category in categories %}
30
32
  <span class="kicker-dot" aria-hidden="true">·</span>
@@ -0,0 +1,7 @@
1
+ {#- The `lang` attribute of an article written in a language other than the
2
+ site's (TASK-154), with its leading space, and nothing for one in the
3
+ site's own: the `<html lang>` already names that. Imported with context,
4
+ since the site's language is what it compares against. -#}
5
+ {% macro attr(tag) -%}
6
+ {%- if tag and tag != (site.language | default("en")) %} lang="{{ tag }}"{% endif -%}
7
+ {%- endmacro %}
@@ -18,6 +18,7 @@
18
18
  never skips a level.
19
19
  #}
20
20
  {% import "partials/kicker.njk" as kicker with context %}
21
+ {% import "partials/lang.njk" as language with context %}
21
22
  {% set feedTitleTag = "h" + (feedHeading | default(2)) %}
22
23
  {% if posts.length %}
23
24
  <div class="feed h-feed">
@@ -25,16 +26,17 @@
25
26
  {% if not loop.first %}
26
27
  <hr class="feed-separator">
27
28
  {% endif %}
28
- <article class="feed-item h-entry">
29
+ <article class="feed-item h-entry"{{ language.attr(post.lang) }}>
29
30
  {% if post.named %}
30
- {{ kicker.line(post.postType | capitalize, post.date, false, post.categories) }}
31
+ {{ kicker.line(post.postType | capitalize, post.date, false, post.categories, lang=post.lang) }}
31
32
  {% else %}
32
- {{ kicker.line(post.postType | capitalize, post.date, post.url, [], post.label) }}
33
+ {{ kicker.line(post.postType | capitalize, post.date, post.url, [], post.label, lang=post.lang) }}
33
34
  {% endif %}
34
35
  {#- What a reply answers, from the same partial an entry cites it with. -#}
35
36
  {% if post.inReplyTo %}
36
37
  {% set inReplyTo = post.inReplyTo %}
37
38
  {% set replyContext = post.replyContext %}
39
+ {% set lang = post.lang %}
38
40
  {% include "partials/reply-context.njk" %}
39
41
  {% endif %}
40
42
  {% if not post.named %}
@@ -12,7 +12,9 @@
12
12
  or synced: its name, a short excerpt, its author and its date, each only when
13
13
  it said so. It is somebody else's words, so every piece is printed through
14
14
  autoescaping and none of it with `safe`. With a name, the name is the link;
15
- with only an author, "a post" is; with neither, the bare URL.
15
+ with only an author, "a post" is; with neither, the bare URL. Its date is
16
+ written in `lang`, the language of the entry it sits in (TASK-189), so a
17
+ listing sets `lang` from each entry as it sets `replyContext`.
16
18
  #}
17
19
  {% if inReplyTo %}
18
20
  <div class="reply-context cite u-in-reply-to h-cite">
@@ -26,7 +28,7 @@
26
28
  {%- else %}<span class="p-name">{{ replyContext.author.name }}</span>{% endif -%}
27
29
  </span>
28
30
  {%- endif %}
29
- {%- if replyContext.published %}, <time class="dt-published" datetime="{{ replyContext.published | date("iso") }}">{{ replyContext.published | date }}</time>
31
+ {%- if replyContext.published %}, <time class="dt-published" datetime="{{ replyContext.published | date("iso") }}">{{ replyContext.published | date("readable", none, lang) }}</time>
30
32
  {%- endif -%}
31
33
  </p>
32
34
  {% if replyContext.text %}