@geekity/cms 0.12.0 → 0.13.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 (232) hide show
  1. package/README.md +170 -4
  2. package/admin/pages/account/login.njk +3 -0
  3. package/admin/pages/documents/editor.njk +7 -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 +9 -0
  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 +2 -0
  16. package/dist/admin/documents.d.ts.map +1 -1
  17. package/dist/admin/documents.js +17 -1
  18. package/dist/admin/documents.js.map +1 -1
  19. package/dist/admin/headers.d.ts +5 -1
  20. package/dist/admin/headers.d.ts.map +1 -1
  21. package/dist/admin/headers.js +7 -3
  22. package/dist/admin/headers.js.map +1 -1
  23. package/dist/admin/menu.d.ts.map +1 -1
  24. package/dist/admin/menu.js +2 -1
  25. package/dist/admin/menu.js.map +1 -1
  26. package/dist/admin/routes.d.ts.map +1 -1
  27. package/dist/admin/routes.js +48 -5
  28. package/dist/admin/routes.js.map +1 -1
  29. package/dist/admin/settings-general.js +1 -1
  30. package/dist/admin/settings-general.js.map +1 -1
  31. package/dist/admin/settings-reading.d.ts.map +1 -1
  32. package/dist/admin/settings-reading.js +15 -1
  33. package/dist/admin/settings-reading.js.map +1 -1
  34. package/dist/admin/settings.d.ts +79 -2
  35. package/dist/admin/settings.d.ts.map +1 -1
  36. package/dist/admin/settings.js +168 -2
  37. package/dist/admin/settings.js.map +1 -1
  38. package/dist/admin/templates.d.ts +3 -0
  39. package/dist/admin/templates.d.ts.map +1 -1
  40. package/dist/admin/templates.js +3 -0
  41. package/dist/admin/templates.js.map +1 -1
  42. package/dist/admin/users.d.ts +8 -0
  43. package/dist/admin/users.d.ts.map +1 -1
  44. package/dist/admin/users.js +9 -0
  45. package/dist/admin/users.js.map +1 -1
  46. package/dist/config.d.ts +30 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +1 -0
  49. package/dist/config.js.map +1 -1
  50. package/dist/env.d.ts +14 -0
  51. package/dist/env.d.ts.map +1 -1
  52. package/dist/federation/article.d.ts.map +1 -1
  53. package/dist/federation/article.js +18 -5
  54. package/dist/federation/article.js.map +1 -1
  55. package/dist/images/icons.d.ts +44 -25
  56. package/dist/images/icons.d.ts.map +1 -1
  57. package/dist/images/icons.js +205 -60
  58. package/dist/images/icons.js.map +1 -1
  59. package/dist/images/index.d.ts +2 -2
  60. package/dist/images/index.d.ts.map +1 -1
  61. package/dist/images/index.js +1 -1
  62. package/dist/images/index.js.map +1 -1
  63. package/dist/images/markup.d.ts +14 -0
  64. package/dist/images/markup.d.ts.map +1 -1
  65. package/dist/images/markup.js +16 -11
  66. package/dist/images/markup.js.map +1 -1
  67. package/dist/images/variants.d.ts.map +1 -1
  68. package/dist/images/variants.js +3 -4
  69. package/dist/images/variants.js.map +1 -1
  70. package/dist/index.d.ts +14 -4
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +28 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/indexnow.d.ts +34 -0
  75. package/dist/indexnow.d.ts.map +1 -0
  76. package/dist/indexnow.js +141 -0
  77. package/dist/indexnow.js.map +1 -0
  78. package/dist/indieauth/bearer.d.ts +39 -0
  79. package/dist/indieauth/bearer.d.ts.map +1 -0
  80. package/dist/indieauth/bearer.js +63 -0
  81. package/dist/indieauth/bearer.js.map +1 -0
  82. package/dist/indieauth/client.d.ts +61 -0
  83. package/dist/indieauth/client.d.ts.map +1 -0
  84. package/dist/indieauth/client.js +139 -0
  85. package/dist/indieauth/client.js.map +1 -0
  86. package/dist/indieauth/connected-apps.d.ts +23 -0
  87. package/dist/indieauth/connected-apps.d.ts.map +1 -0
  88. package/dist/indieauth/connected-apps.js +73 -0
  89. package/dist/indieauth/connected-apps.js.map +1 -0
  90. package/dist/indieauth/consent.d.ts +28 -0
  91. package/dist/indieauth/consent.d.ts.map +1 -0
  92. package/dist/indieauth/consent.js +185 -0
  93. package/dist/indieauth/consent.js.map +1 -0
  94. package/dist/indieauth/discovery.d.ts +87 -0
  95. package/dist/indieauth/discovery.d.ts.map +1 -0
  96. package/dist/indieauth/discovery.js +127 -0
  97. package/dist/indieauth/discovery.js.map +1 -0
  98. package/dist/indieauth/grants.d.ts +57 -0
  99. package/dist/indieauth/grants.d.ts.map +1 -0
  100. package/dist/indieauth/grants.js +35 -0
  101. package/dist/indieauth/grants.js.map +1 -0
  102. package/dist/indieauth/identity.d.ts +36 -0
  103. package/dist/indieauth/identity.d.ts.map +1 -0
  104. package/dist/indieauth/identity.js +78 -0
  105. package/dist/indieauth/identity.js.map +1 -0
  106. package/dist/indieauth/redeem.d.ts +47 -0
  107. package/dist/indieauth/redeem.d.ts.map +1 -0
  108. package/dist/indieauth/redeem.js +69 -0
  109. package/dist/indieauth/redeem.js.map +1 -0
  110. package/dist/indieauth/request.d.ts +47 -0
  111. package/dist/indieauth/request.d.ts.map +1 -0
  112. package/dist/indieauth/request.js +104 -0
  113. package/dist/indieauth/request.js.map +1 -0
  114. package/dist/indieauth/token.d.ts +19 -0
  115. package/dist/indieauth/token.d.ts.map +1 -0
  116. package/dist/indieauth/token.js +110 -0
  117. package/dist/indieauth/token.js.map +1 -0
  118. package/dist/indieauth/tokens.d.ts +131 -0
  119. package/dist/indieauth/tokens.d.ts.map +1 -0
  120. package/dist/indieauth/tokens.js +205 -0
  121. package/dist/indieauth/tokens.js.map +1 -0
  122. package/dist/web/archive.d.ts +1 -1
  123. package/dist/web/archive.d.ts.map +1 -1
  124. package/dist/web/archive.js +3 -2
  125. package/dist/web/archive.js.map +1 -1
  126. package/dist/web/context.d.ts +25 -0
  127. package/dist/web/context.d.ts.map +1 -1
  128. package/dist/web/context.js +18 -0
  129. package/dist/web/context.js.map +1 -1
  130. package/dist/web/feed-atom.d.ts.map +1 -1
  131. package/dist/web/feed-atom.js +3 -1
  132. package/dist/web/feed-atom.js.map +1 -1
  133. package/dist/web/feed-item.d.ts +11 -2
  134. package/dist/web/feed-item.d.ts.map +1 -1
  135. package/dist/web/feed-item.js +9 -3
  136. package/dist/web/feed-item.js.map +1 -1
  137. package/dist/web/feed-json.d.ts +4 -0
  138. package/dist/web/feed-json.d.ts.map +1 -1
  139. package/dist/web/feed-json.js +4 -1
  140. package/dist/web/feed-json.js.map +1 -1
  141. package/dist/web/feed-rss.d.ts.map +1 -1
  142. package/dist/web/feed-rss.js +16 -3
  143. package/dist/web/feed-rss.js.map +1 -1
  144. package/dist/web/feed-source.d.ts +28 -0
  145. package/dist/web/feed-source.d.ts.map +1 -1
  146. package/dist/web/feed-source.js +31 -0
  147. package/dist/web/feed-source.js.map +1 -1
  148. package/dist/web/feed-xml.d.ts +2 -0
  149. package/dist/web/feed-xml.d.ts.map +1 -1
  150. package/dist/web/feed-xml.js +2 -0
  151. package/dist/web/feed-xml.js.map +1 -1
  152. package/dist/web/feeds.d.ts.map +1 -1
  153. package/dist/web/feeds.js +8 -1
  154. package/dist/web/feeds.js.map +1 -1
  155. package/dist/web/index.d.ts +3 -1
  156. package/dist/web/index.d.ts.map +1 -1
  157. package/dist/web/index.js +2 -1
  158. package/dist/web/index.js.map +1 -1
  159. package/dist/web/indexnow.d.ts +9 -0
  160. package/dist/web/indexnow.d.ts.map +1 -0
  161. package/dist/web/indexnow.js +27 -0
  162. package/dist/web/indexnow.js.map +1 -0
  163. package/dist/web/llms.d.ts +58 -0
  164. package/dist/web/llms.d.ts.map +1 -0
  165. package/dist/web/llms.js +91 -0
  166. package/dist/web/llms.js.map +1 -0
  167. package/dist/web/locale.d.ts +41 -0
  168. package/dist/web/locale.d.ts.map +1 -0
  169. package/dist/web/locale.js +56 -0
  170. package/dist/web/locale.js.map +1 -0
  171. package/dist/web/manifest.d.ts +50 -0
  172. package/dist/web/manifest.d.ts.map +1 -0
  173. package/dist/web/manifest.js +56 -0
  174. package/dist/web/manifest.js.map +1 -0
  175. package/dist/web/negotiate.d.ts.map +1 -1
  176. package/dist/web/negotiate.js +6 -0
  177. package/dist/web/negotiate.js.map +1 -1
  178. package/dist/web/render.d.ts +6 -1
  179. package/dist/web/render.d.ts.map +1 -1
  180. package/dist/web/render.js +41 -10
  181. package/dist/web/render.js.map +1 -1
  182. package/dist/web/robots.d.ts +89 -0
  183. package/dist/web/robots.d.ts.map +1 -0
  184. package/dist/web/robots.js +259 -0
  185. package/dist/web/robots.js.map +1 -0
  186. package/dist/web/routes.d.ts.map +1 -1
  187. package/dist/web/routes.js +105 -6
  188. package/dist/web/routes.js.map +1 -1
  189. package/dist/web/share-image.d.ts +51 -0
  190. package/dist/web/share-image.d.ts.map +1 -0
  191. package/dist/web/share-image.js +43 -0
  192. package/dist/web/share-image.js.map +1 -0
  193. package/dist/web/sitemap.d.ts +0 -26
  194. package/dist/web/sitemap.d.ts.map +1 -1
  195. package/dist/web/sitemap.js +0 -44
  196. package/dist/web/sitemap.js.map +1 -1
  197. package/dist/web/templates.d.ts +16 -9
  198. package/dist/web/templates.d.ts.map +1 -1
  199. package/dist/web/templates.js +83 -55
  200. package/dist/web/templates.js.map +1 -1
  201. package/dist/web/themes.d.ts +33 -0
  202. package/dist/web/themes.d.ts.map +1 -1
  203. package/dist/web/themes.js +57 -0
  204. package/dist/web/themes.js.map +1 -1
  205. package/dist/webmention/discovery.d.ts +4 -2
  206. package/dist/webmention/discovery.d.ts.map +1 -1
  207. package/dist/webmention/discovery.js +14 -8
  208. package/dist/webmention/discovery.js.map +1 -1
  209. package/dist/webmention/fetch-public.d.ts +2 -0
  210. package/dist/webmention/fetch-public.d.ts.map +1 -1
  211. package/dist/webmention/fetch-public.js +1 -1
  212. package/dist/webmention/fetch-public.js.map +1 -1
  213. package/package.json +1 -1
  214. package/themes/default/README.md +119 -29
  215. package/themes/default/layouts/base.njk +95 -16
  216. package/themes/default/layouts/front-page.njk +6 -1
  217. package/themes/default/layouts/home.njk +10 -0
  218. package/themes/default/layouts/page.njk +4 -3
  219. package/themes/default/layouts/post.njk +5 -4
  220. package/themes/default/layouts/search.njk +4 -5
  221. package/themes/default/partials/archive.njk +3 -1
  222. package/themes/default/partials/bio.njk +9 -2
  223. package/themes/default/partials/breadcrumbs.njk +20 -0
  224. package/themes/default/partials/conversation.njk +4 -1
  225. package/themes/default/partials/jsonld.njk +19 -4
  226. package/themes/default/partials/kicker.njk +6 -4
  227. package/themes/default/partials/lang.njk +7 -0
  228. package/themes/default/partials/post-list.njk +5 -3
  229. package/themes/default/partials/reply-context.njk +4 -2
  230. package/themes/default/src/style.css +14 -0
  231. package/themes/default/static/style.css +53 -0
  232. 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,23 @@
110
152
  {% if webmention %}
111
153
  <link rel="webmention" href="{{ webmention | url }}">
112
154
  {% endif %}
155
+ {#- The site's index for language models (TASK-149), from the front page,
156
+ which is where a tool that found the site looks for it. #}
157
+ {% if llmsTxt and atRoot %}
158
+ <link rel="describedby" type="text/markdown" href="{{ llmsTxt | url }}">
159
+ {% endif %}
113
160
  {% block head %}
114
161
  {#
115
162
  What a link to this page looks like when somebody posts it: the same
116
163
  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.
164
+ which is the pair every reader of this markup understands. The image
165
+ always carries its alt text, and its size when the CMS knows it. The
166
+ card is `summary_large_image` only for a picture wide enough to fill
167
+ one, and `summary` — a small picture beside the words — for the site's
168
+ avatar and anything else.
169
+
170
+ An entry is an article, with its times, its author and its tags: the
171
+ author is their profile here when they have one, else their name.
120
172
  #}
121
173
  <meta property="og:title" content="{{ metaTitle }}">
122
174
  {% if metaDescription %}
@@ -125,25 +177,49 @@
125
177
  <meta property="og:url" content="{{ metaUrl }}">
126
178
  <meta property="og:type" content="{% if isEntry %}article{% else %}website{% endif %}">
127
179
  <meta property="og:site_name" content="{{ site.title }}">
128
- {% if metaImage %}
129
- <meta property="og:image" content="{{ metaImage | absoluteUrl }}">
180
+ {% if shareImage %}
181
+ <meta property="og:image" content="{{ shareImage.url | absoluteUrl }}">
182
+ <meta property="og:image:alt" content="{{ shareImage.alt }}">
183
+ {% if shareImage.size %}
184
+ <meta property="og:image:width" content="{{ shareImage.size.width }}">
185
+ <meta property="og:image:height" content="{{ shareImage.size.height }}">
186
+ {% endif %}
187
+ {% endif %}
188
+ {% if isEntry %}
189
+ {% if date %}
190
+ <meta property="article:published_time" content="{{ date | date("iso") }}">
191
+ {% endif %}
192
+ {% if updated or date %}
193
+ <meta property="article:modified_time" content="{{ (updated or date) | date("iso") }}">
194
+ {% endif %}
195
+ {% if siteAuthor.url %}
196
+ <meta property="article:author" content="{{ siteAuthor.url | absoluteUrl }}">
197
+ {% elif siteAuthor.name or author.name or author or site.author %}
198
+ <meta property="article:author" content="{{ siteAuthor.name or author.name or author or site.author }}">
199
+ {% endif %}
200
+ {% for tag in tags %}
201
+ <meta property="article:tag" content="{{ tag }}">
202
+ {% endfor %}
130
203
  {% endif %}
131
- <meta name="twitter:card" content="summary">
204
+ <meta name="twitter:card" content="{{ shareImage.card if shareImage else "summary" }}">
132
205
  <meta name="twitter:title" content="{{ metaTitle }}">
133
206
  {% if metaDescription %}
134
207
  <meta name="twitter:description" content="{{ metaDescription }}">
135
208
  {% endif %}
136
- {% if metaImage %}
137
- <meta name="twitter:image" content="{{ metaImage | absoluteUrl }}">
209
+ {% if shareImage %}
210
+ <meta name="twitter:image" content="{{ shareImage.url | absoluteUrl }}">
211
+ <meta name="twitter:image:alt" content="{{ shareImage.alt }}">
138
212
  {% endif %}
139
213
  {#
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.
214
+ The site's icons, derived from its `icon` setting, else its avatar.
215
+ `icons` is empty — and these lines print nothing — until a site has one,
216
+ so a site with neither links no icon rather than links that answer 404.
217
+ An SVG icon has every size, so it carries no `sizes`.
143
218
  #}
144
219
  {% for icon in icons %}
145
- <link rel="{{ icon.rel }}" type="image/png" sizes="{{ icon.sizes }}" href="{{ icon.href | url }}">
220
+ <link rel="{{ icon.rel }}" type="{{ icon.type }}"{% if icon.sizes %} sizes="{{ icon.sizes }}"{% endif %} href="{{ icon.href | url }}">
146
221
  {% endfor %}
222
+ <link rel="manifest" href="{{ "/manifest.webmanifest" | url }}">
147
223
  {% include "partials/jsonld.njk" %}
148
224
  {% endblock %}
149
225
  </head>
@@ -170,13 +246,16 @@
170
246
  <p class="site-tagline">{{ site.tagline }}</p>
171
247
  {% endif %}
172
248
  {% else %}
173
- <a class="header-link-home" href="{{ "/" | url }}">{{ site.title }}</a>
249
+ {#- The solo author's own archive claims the homepage back, so the
250
+ two URLs are provably one person (TASK-180). -#}
251
+ <a class="header-link-home"{% if isAuthorArchive and soloAuthor.url == author.url %} rel="me"{% endif %} href="{{ "/" | url }}">{{ site.title }}</a>
174
252
  {% endif %}
175
253
  {{ nav.list(menus.primary, "Site") }}
176
254
  </header>
177
255
  {% endblock %}
178
256
 
179
257
  <main id="main">
258
+ {% block breadcrumbs %}{% include "partials/breadcrumbs.njk" %}{% endblock %}
180
259
  {% block content %}{% endblock %}
181
260
  </main>
182
261
 
@@ -10,13 +10,18 @@
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 %}
20
25
 
21
26
  {% block content %}
22
27
  <div class="page-body e-content">
@@ -22,4 +22,14 @@
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
+ <hr>
31
+ <footer>
32
+ {% include "partials/bio.njk" %}
33
+ </footer>
34
+ {% endif %}
25
35
  {% 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,11 @@
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
+
36
41
  The `p-note` and the `rel="me"` links are printed wherever the card is, which
37
42
  is what `bioProfile` used to ask for on an author archive alone. An entry
38
43
  left that switch unset because the page footer carried the site's identity
@@ -40,6 +45,7 @@
40
45
  wrote rather than one page further on.
41
46
  #}
42
47
  {% if bioAuthor %}
48
+ {%- set bioClaims = bioRelMe !== false %}
43
49
  <div class="bio p-author h-card">
44
50
  {%- if bioAuthor.avatar %}
45
51
  <div class="bio-avatar">
@@ -51,7 +57,7 @@
51
57
  <p>
52
58
  {{ bioLead | default("Written by") }}
53
59
  {%- if bioAuthor.url %}
54
- <a class="u-url p-name" rel="author me" href="{{ bioAuthor.url | url }}">{{ bioAuthor.name }}</a>
60
+ <a class="u-url p-name" rel="author{% if bioClaims %} me{% endif %}" href="{{ bioAuthor.url | url }}">{{ bioAuthor.name }}</a>
55
61
  {%- else %}
56
62
  <strong class="p-name">{{ bioAuthor.name }}</strong>
57
63
  {%- endif %}
@@ -66,7 +72,8 @@
66
72
  {%- if bioAuthor.links.length %}
67
73
  <ul class="hlist bio-links">
68
74
  {%- for link in bioAuthor.links %}
69
- <li><a class="u-url" rel="{{ link.rel }}" href="{{ link.href }}">{{ link.label }}</a></li>
75
+ {%- set linkRel = link.rel if bioClaims else (link.rel.split(" ") | reject("equalto", "me") | join(" ")) %}
76
+ <li><a class="u-url"{% if linkRel %} rel="{{ linkRel }}"{% endif %} href="{{ link.href }}">{{ link.label }}</a></li>
70
77
  {%- endfor %}
71
78
  </ul>
72
79
  {%- 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">
@@ -42,9 +42,6 @@
42
42
  {% for link in siteAuthor.links %}{% set _ = sameAs.push(link.href) %}{% endfor %}
43
43
  {% if personUrl %}{% set _ = sameAs.push(personUrl) %}{% endif %}
44
44
  {% 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
45
  {% set entryType = "BlogPosting" if type == "post" else "Article" %}
49
46
  {% set nodes = [] %}
50
47
  {% set website %}
@@ -67,6 +64,9 @@
67
64
  "query-input": "required name=search_term_string"
68
65
  }) }}
69
66
  {{- member("publisher", person) }}
67
+ {#- What the site is about, on the homepage of a solo author blog
68
+ (TASK-180): the site's author, and nobody on any other site. #}
69
+ {{- member("about", { "@id": (soloAuthor.url | absoluteUrl) + "#person" } if atRoot and soloAuthor.url else "") }}
70
70
  }{% endset %}
71
71
  {% set _ = nodes.push(website) %}
72
72
  {% if personId %}
@@ -84,7 +84,7 @@
84
84
  }{% endset %}
85
85
  {% set _ = nodes.push(profile) %}
86
86
  {% endif %}
87
- {% if isProfilePage %}
87
+ {% if isAuthorArchive %}
88
88
  {% set profilePage %}
89
89
  {
90
90
  "@type": "ProfilePage"
@@ -110,6 +110,21 @@
110
110
  }{% endset %}
111
111
  {% set _ = nodes.push(entry) %}
112
112
  {% endif %}
113
+ {#- Where the page sits, from the trail `layouts/base.njk` builds and the
114
+ visible breadcrumb prints (TASK-150). -#}
115
+ {% if breadcrumbs.length %}
116
+ {% set trail = [] %}
117
+ {% for crumb in breadcrumbs %}
118
+ {% set _ = trail.push({ "@type": "ListItem", "position": loop.index, "name": crumb.name, "item": crumb.url | absoluteUrl }) %}
119
+ {% endfor %}
120
+ {% set breadcrumbList %}
121
+ {
122
+ "@type": "BreadcrumbList"
123
+ {{- member("@id", metaUrl + "#breadcrumb") }}
124
+ {{- member("itemListElement", trail) }}
125
+ }{% endset %}
126
+ {% set _ = nodes.push(breadcrumbList) %}
127
+ {% endif %}
113
128
  <script type="application/ld+json">
114
129
  {
115
130
  "@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 %}
@@ -323,6 +323,20 @@
323
323
  @apply font-semibold text-accent;
324
324
  }
325
325
 
326
+ /* The breadcrumb: where the page sits, on one quiet line above it. The
327
+ crumbs are links like the kicker's; the last is the page itself. */
328
+ .breadcrumbs ol {
329
+ @apply mb-6 gap-x-2 font-sans text-sm text-muted;
330
+ }
331
+
332
+ .breadcrumbs a {
333
+ @apply target quiet-link text-muted;
334
+ }
335
+
336
+ .breadcrumbs-sep {
337
+ @apply me-2;
338
+ }
339
+
326
340
  /* Titles: what heads a listing, an archive, search and an error page. */
327
341
  .page-title {
328
342
  @apply mt-1 mb-8 text-4xl leading-[1.08] sm:text-[2.75rem];
@@ -687,6 +687,59 @@
687
687
  color: var(--color-primary);
688
688
  }
689
689
 
690
+ .breadcrumbs ol {
691
+ margin-bottom: calc(var(--spacing) * 6);
692
+ column-gap: calc(var(--spacing) * 2);
693
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
694
+ font-size: var(--text-sm);
695
+ line-height: var(--tw-leading, var(--text-sm--line-height));
696
+ color: var(--color-muted);
697
+ }
698
+
699
+ .breadcrumbs a {
700
+ align-items: center;
701
+ min-block-size: 24px;
702
+ min-inline-size: 24px;
703
+ text-decoration-line: underline;
704
+ text-decoration-color: currentColor;
705
+ display: inline-flex;
706
+ }
707
+
708
+ @supports (color: color-mix(in lab, red, red)) {
709
+ .breadcrumbs a {
710
+ -webkit-text-decoration-color: color-mix(in oklab, currentcolor 50%, transparent);
711
+ -webkit-text-decoration-color: color-mix(in oklab, currentcolor 50%, transparent);
712
+ text-decoration-color: color-mix(in oklab, currentcolor 50%, transparent);
713
+ }
714
+ }
715
+
716
+ .breadcrumbs a {
717
+ text-underline-offset: 4px;
718
+ text-decoration-thickness: 1px;
719
+ }
720
+
721
+ @media (hover: hover) {
722
+ .breadcrumbs a:hover {
723
+ color: var(--color-primary);
724
+ -webkit-text-decoration-color: var(--color-primary);
725
+ -webkit-text-decoration-color: var(--color-primary);
726
+ text-decoration-color: var(--color-primary);
727
+ text-decoration-thickness: 2px;
728
+ }
729
+ }
730
+
731
+ .breadcrumbs a:focus-visible {
732
+ text-decoration-thickness: 2px;
733
+ }
734
+
735
+ .breadcrumbs a {
736
+ color: var(--color-muted);
737
+ }
738
+
739
+ .breadcrumbs-sep {
740
+ margin-inline-end: calc(var(--spacing) * 2);
741
+ }
742
+
690
743
  .page-title {
691
744
  margin-top: var(--spacing);
692
745
  margin-bottom: calc(var(--spacing) * 8);
@@ -2,6 +2,8 @@
2
2
  "name": "Default",
3
3
  "kind": "site",
4
4
  "description": "The theme that ships with Geekity: Nunjucks and one stylesheet compiled from Tailwind, in the Paper design, and the layer every other theme is laid over.",
5
+ "colorScheme": "light dark",
6
+ "themeColor": { "light": "#faf7f2", "dark": "#171412" },
5
7
  "areas": [
6
8
  { "name": "primary", "label": "Site menu" },
7
9
  { "name": "footer", "label": "Footer links" }