@geekity/cms 0.11.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 (240) hide show
  1. package/README.md +177 -6
  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 +68 -22
  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 +7 -2
  214. package/themes/default/README.md +320 -139
  215. package/themes/default/layouts/404.njk +2 -0
  216. package/themes/default/layouts/500.njk +2 -0
  217. package/themes/default/layouts/503.njk +2 -0
  218. package/themes/default/layouts/author.njk +3 -1
  219. package/themes/default/layouts/base.njk +96 -17
  220. package/themes/default/layouts/category.njk +4 -2
  221. package/themes/default/layouts/front-page.njk +7 -2
  222. package/themes/default/layouts/home.njk +10 -0
  223. package/themes/default/layouts/page.njk +7 -4
  224. package/themes/default/layouts/post.njk +35 -25
  225. package/themes/default/layouts/search.njk +5 -9
  226. package/themes/default/layouts/tag.njk +7 -1
  227. package/themes/default/partials/archive.njk +3 -1
  228. package/themes/default/partials/bio.njk +9 -2
  229. package/themes/default/partials/breadcrumbs.njk +20 -0
  230. package/themes/default/partials/comment-form.njk +8 -0
  231. package/themes/default/partials/contact-form.njk +3 -0
  232. package/themes/default/partials/conversation.njk +14 -4
  233. package/themes/default/partials/jsonld.njk +19 -4
  234. package/themes/default/partials/kicker.njk +36 -0
  235. package/themes/default/partials/lang.njk +7 -0
  236. package/themes/default/partials/post-list.njk +43 -47
  237. package/themes/default/partials/reply-context.njk +14 -5
  238. package/themes/default/src/style.css +1109 -0
  239. package/themes/default/static/style.css +2124 -1160
  240. package/themes/default/theme.json +3 -1
@@ -1,7 +1,9 @@
1
1
  {% extends "layouts/base.njk" %}
2
+ {% import "partials/kicker.njk" as kicker with context %}
2
3
 
3
4
  {% block content %}
4
5
  <article class="not-found">
6
+ {{ kicker.line("Not found") }}
5
7
  <h1 class="page-title">Content not found.</h1>
6
8
  <p>Go <a href="{{ "/" | url }}">home</a>, or <a href="{{ "/search/" | url }}">search the site</a>.</p>
7
9
  </article>
@@ -1,7 +1,9 @@
1
1
  {% extends "layouts/base.njk" %}
2
+ {% import "partials/kicker.njk" as kicker with context %}
2
3
 
3
4
  {% block content %}
4
5
  <article class="server-error">
6
+ {{ kicker.line("Server error") }}
5
7
  <h1 class="page-title">Something went wrong.</h1>
6
8
  <p>This page could not be shown because of a problem on the server. Try again in a moment, or go <a href="{{ "/" | url }}">home</a>.</p>
7
9
  </article>
@@ -1,7 +1,9 @@
1
1
  {% extends "layouts/base.njk" %}
2
+ {% import "partials/kicker.njk" as kicker with context %}
2
3
 
3
4
  {% block content %}
4
5
  <article class="maintenance">
6
+ {{ kicker.line("Maintenance") }}
5
7
  <h1 class="page-title">Down for maintenance.</h1>
6
8
  <p>This site is down for maintenance and will be back shortly.{% if until %} It should be back by <time datetime="{{ until }}">{{ untilText }}</time>.{% endif %} Please try again then.</p>
7
9
  </article>
@@ -26,8 +26,10 @@
26
26
  {{ feeds.feedLinks(author.url, site.title + ": " + author.name) }}
27
27
  {% endblock %}
28
28
 
29
+ {% import "partials/kicker.njk" as kicker with context %}
29
30
  {% block content %}
30
- <header class="author-header">
31
+ <header class="author-header archive-header">
32
+ {{ kicker.line("Author") }}
31
33
  <h1 class="page-title">{{ author.name }}</h1>
32
34
  </header>
33
35
  {% include "partials/bio.njk" %}
@@ -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>
@@ -167,16 +243,19 @@
167
243
  <a href="{{ "/" | url }}">{{ site.title }}</a>
168
244
  </h1>
169
245
  {% if site.tagline %}
170
- <p>{{ site.tagline }}</p>
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
 
@@ -9,6 +9,7 @@
9
9
  {{ feeds.feedLinks(categoryRoot, site.title + ": " + category) }}
10
10
  {% endblock %}
11
11
 
12
+ {% import "partials/kicker.njk" as kicker with context %}
12
13
  {% block content %}
13
14
  {#
14
15
  The archive's header. `categoryDescription` is what the design prints under
@@ -16,8 +17,9 @@
16
17
  alone until something does — a site whose own context carries one gets it
17
18
  printed without overriding this layout.
18
19
  #}
19
- <header class="category-header">
20
- <h1 class="page-title">Filed under &ldquo;{{ category }}&rdquo;</h1>
20
+ <header class="category-header archive-header">
21
+ {{ kicker.line("Category") }}
22
+ <h1 class="page-title">{{ category }}</h1>
21
23
  {% if categoryDescription %}
22
24
  <div class="category-description">{{ categoryDescription }}</div>
23
25
  {% endif %}
@@ -10,20 +10,25 @@
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">
23
28
  {{ content | safe }}
24
29
  </div>
25
30
 
26
- <h2>Recent Posts</h2>
31
+ <h2 class="section-title">Recent Posts</h2>
27
32
  {#- The feed a listing prints, over the posts the CMS decided were recent:
28
33
  this month's writing when there is enough of it and the five newest
29
34
  otherwise. `feedHeading` is 3 because the `h2` above is what they sit
@@ -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 %}
@@ -13,6 +13,8 @@
13
13
  whoever the site is by, not by the person a front matter line names.
14
14
  #}
15
15
  {% extends "layouts/base.njk" %}
16
+ {% import "partials/kicker.njk" as kicker with context %}
17
+ {% import "partials/lang.njk" as language with context %}
16
18
 
17
19
  {#- Who the bio credits, and what tells `layouts/base.njk` the site menu is in
18
20
  the bio rather than in the page footer. -#}
@@ -20,8 +22,9 @@
20
22
 
21
23
  {% block content %}
22
24
  {% set entryUrl = page.url or url %}
23
- <article class="blog-post h-entry">
24
- <header>
25
+ <article class="blog-post h-entry"{{ language.attr(lang) }}>
26
+ <header class="post-header">
27
+ {{ kicker.line("Page") }}
25
28
  <h1 class="p-name">{{ title }}</h1>
26
29
  </header>
27
30
 
@@ -33,10 +36,10 @@
33
36
  deciding twice whether there is anything to say. -#}
34
37
  <p class="page-meta">
35
38
  {%- if date %}
36
- <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>
37
40
  {%- endif %}
38
41
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
39
- {% if date %}<br>{% 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>
40
43
  {%- endif -%}
41
44
  </p>
42
45
  </section>
@@ -1,16 +1,23 @@
1
1
  {#
2
- One post, as the source design's `content-essay.php` draws it (decision-16,
3
- TASK-83): `article.blog-post.h-entry`, the title as its `p-name`, the body
4
- and the Published line together in one `section.e-content`, then a rule and a
5
- footer holding the bio. The neighbouring posts, the conversation and the
6
- comment form follow the entry.
2
+ One post, in the Paper design (doc-9): `article.blog-post.h-entry` opening on
3
+ a kicker that names its kind (Article, Note, Reply) and the categories it is
4
+ filed under.
5
+
6
+ An article, and a reply with a title, has the only headline: the title as its
7
+ `p-name` and the author's `description` under it as the deck, a
8
+ `p-summary`. A post with no title opens on its words, set larger. A reply
9
+ cites what it answers above its words, or above its header when it has one.
10
+ Then the Published line, the tags, a rule and the bio. The neighbouring posts,
11
+ the conversation and the comment form follow the entry.
7
12
 
8
13
  The Published line is inside the `e-content` on purpose. It is what the
9
- source theme does, and it is what makes a reader who copies the words out
14
+ source design does, and it is what makes a reader who copies the words out
10
15
  take the date and the permalink with them.
11
16
  #}
12
17
  {% extends "layouts/base.njk" %}
13
18
  {% import "partials/tags.njk" as taxonomy with context %}
19
+ {% import "partials/kicker.njk" as kicker with context %}
20
+ {% import "partials/lang.njk" as language with context %}
14
21
 
15
22
  {#- Who the bio credits: the person the file names, and otherwise whoever the
16
23
  site is by, which is what the source theme's bio reads. A post naming
@@ -22,27 +29,32 @@
22
29
 
23
30
  {% block content %}
24
31
  {% set entryUrl = page.url or url %}
25
- <article class="blog-post h-entry">
32
+ <article class="blog-post h-entry"{{ language.attr(lang) }}>
26
33
  {#- A post with no name of its own (a note, or a reply without a title) is
27
34
  its words, so it opens on them in sight. Its page still needs one h1 to
28
35
  navigate by, so it gets one that says what it is, who wrote it and when,
29
36
  hidden from sight. It is no `p-name`: a parser would take that as a
30
37
  title and read the note as an article. -#}
31
38
  {% if named %}
32
- <header>
39
+ {#- A reply cites the post it answers, whose URL is also the link its
40
+ webmention points a receiver at. -#}
41
+ {% include "partials/reply-context.njk" %}
42
+ <header class="post-header">
43
+ {{ kicker.line(postType | capitalize, false, false, categories) }}
33
44
  <h1 class="p-name">{{ title }}</h1>
45
+ {% if description %}
46
+ <p class="post-deck p-summary">{{ description }}</p>
47
+ {% endif %}
34
48
  </header>
35
49
  {% else %}
36
50
  <h1 class="screen-reader-text">
37
51
  {{- "Reply" if postType == "reply" else "Note" }}
38
52
  {%- if bioAuthor.name %} by {{ bioAuthor.name }}{% endif %}
39
- {%- if date %}, {{ date | date }}{% endif -%}
53
+ {%- if date %}, {{ date | date("readable", none, lang) }}{% endif -%}
40
54
  </h1>
41
- {% endif %}
42
-
43
- {#- A reply cites the post it answers, whose URL is also the link its
44
- webmention points a receiver at. -#}
55
+ {{ kicker.line(postType | capitalize, false, false, categories) }}
45
56
  {% include "partials/reply-context.njk" %}
57
+ {% endif %}
46
58
 
47
59
  <section class="e-content">
48
60
  {{ content | safe }}
@@ -53,21 +65,19 @@
53
65
  <a class="u-category small" href="https://news.indieweb.org/en">#indienews</a>
54
66
  {%- endif %}
55
67
  {%- if date %}
56
- <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>
57
69
  {%- endif %}
58
70
  {#- Updated only when it is a different day in the site's own timezone:
59
71
  a typo fixed an hour after publishing is not news. -#}
60
72
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
61
- {% if date %}<br>{% 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>
62
74
  {%- endif -%}
63
75
  </p>
64
76
  </section>
65
77
 
66
- {# What it is filed under, inside the entry so that each link is a
67
- `p-category` of it: the categories first, then the tags. #}
68
- {{ taxonomy.categories(categories) }}
78
+ {# The tags, inside the entry so that each link is a `p-category` of it. The
79
+ categories are in the kicker. #}
69
80
  {{ taxonomy.list(tags) }}
70
-
71
81
  {#- The rule and the footer are the bio's own: a post nobody is credited with
72
82
  ends at its words rather than at an empty box. -#}
73
83
  {% if bioAuthor %}
@@ -78,17 +88,17 @@
78
88
  {% endif %}
79
89
  </article>
80
90
 
81
- {# The post either side of this one. Each is on the context only when there is
82
- one, so the ends of the archive draw one arrow and a site of a single post
83
- draws none. #}
91
+ {# The post either side of this one, as two cards. Each is on the context only
92
+ when there is one, so the ends of the archive draw one card and a site of a
93
+ single post draws none. #}
84
94
  {% if previous or next %}
85
- <nav class="blog-post-nav">
95
+ <nav class="blog-post-nav" aria-label="Neighbouring posts">
86
96
  <ul>
87
97
  {% if previous %}
88
- <li><a rel="prev" href="{{ previous.url | url }}">&larr; {{ previous.title }}</a></li>
98
+ <li><a rel="prev" href="{{ previous.url | url }}"><span class="blog-post-nav-label">Previous</span> {{ previous.title }}</a></li>
89
99
  {% endif %}
90
100
  {% if next %}
91
- <li><a rel="next" href="{{ next.url | url }}">{{ next.title }} &rarr;</a></li>
101
+ <li><a rel="next" href="{{ next.url | url }}"><span class="blog-post-nav-label">Next</span> {{ next.title }}</a></li>
92
102
  {% endif %}
93
103
  </ul>
94
104
  </nav>
@@ -20,6 +20,8 @@
20
20
  <meta name="robots" content="noindex">
21
21
  {% endblock %}
22
22
 
23
+ {% import "partials/kicker.njk" as kicker with context %}
24
+ {% import "partials/lang.njk" as language with context %}
23
25
  {% block content %}
24
26
  <h1 class="page-title">Search</h1>
25
27
  {% include "partials/search-form.njk" %}
@@ -27,16 +29,15 @@
27
29
  <p class="search-summary" role="status">
28
30
  {%- if pagination.total == 0 -%}
29
31
  Nothing found for &ldquo;{{ query }}&rdquo;.
30
- {%- elif pagination.total == 1 -%}
31
- One result for &ldquo;{{ query }}&rdquo;.
32
32
  {%- else -%}
33
- {{ pagination.total }} results for &ldquo;{{ query }}&rdquo;.
33
+ {{ pagination.total | plural({ one: "One result", other: "# results" }, "en") }} for &ldquo;{{ query }}&rdquo;.
34
34
  {%- endif -%}
35
35
  </p>
36
36
  {% if posts.length %}
37
37
  <div class="search-results">
38
38
  {% for post in posts %}
39
- <article class="search-result h-entry">
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) }}
40
41
  {#- A post with no name of its own has none to claim, so its first words
41
42
  link it without being marked up as one. -#}
42
43
  <h2 class="feed-title{% if post.named %} p-name{% endif %}">
@@ -45,11 +46,6 @@
45
46
  {% if post.snippet %}
46
47
  <p class="search-snippet">{{ post.snippet | safe }}</p>
47
48
  {% endif %}
48
- {% if post.date %}
49
- <p class="feed-meta">
50
- <time class="feed-date dt-published" datetime="{{ post.date | date("iso") }}">{{ post.date | date }}</time>
51
- </p>
52
- {% endif %}
53
49
  </article>
54
50
  {% endfor %}
55
51
  </div>
@@ -9,8 +9,14 @@
9
9
  {{ feeds.feedLinks(tagRoot, site.title + ": " + tag) }}
10
10
  {% endblock %}
11
11
 
12
+ {% import "partials/kicker.njk" as kicker with context %}
12
13
  {% block content %}
13
- <h1 class="page-title">Tagged &ldquo;{{ tag }}&rdquo;</h1>
14
+ {# The archive's header: the kicker says what kind of archive, the heading
15
+ whose. #}
16
+ <header class="archive-header">
17
+ {{ kicker.line("Tag") }}
18
+ <h1 class="page-title">{{ tag }}</h1>
19
+ </header>
14
20
  {% include "partials/post-list.njk" %}
15
21
  {% include "partials/pagination.njk" %}
16
22
  {% endblock %}
@@ -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 %}
@@ -24,6 +24,11 @@
24
24
  #}
25
25
  <section class="comment-respond" id="respond">
26
26
  <h2 class="comment-reply-title">Leave a comment</h2>
27
+ {#- A site that takes webmentions says so here, where somebody deciding how
28
+ to answer is looking. -#}
29
+ {% if webmention %}
30
+ <p class="comment-help">Or reply from your own site and send a <a href="https://indieweb.org/Webmention">webmention</a>.</p>
31
+ {% endif %}
27
32
 
28
33
  {% if commentNotice %}
29
34
  <p class="comment-notice" role="status">{{ commentNotice }}</p>
@@ -100,6 +105,8 @@
100
105
  <input id="comment-website" name="{{ commentForm.fields.trap }}" type="text" tabindex="-1" autocomplete="off" value="" />
101
106
  </div>
102
107
 
108
+ {#- Name and email side by side on a wide screen. -#}
109
+ <div class="field-row">
103
110
  <p class="comment-field">
104
111
  <label for="comment-name">Name</label>
105
112
  <input id="comment-name" name="{{ commentForm.fields.name }}" type="text" value="{{ commentForm.values.name }}" maxlength="{{ commentForm.nameLength }}" autocomplete="name" required{% if problems.name %} aria-invalid="true" aria-describedby="comment-name-error"{% endif %} />
@@ -115,6 +122,7 @@
115
122
  <span class="comment-field-error" id="comment-email-error">{{ problems.email }}</span>
116
123
  {% endif %}
117
124
  </p>
125
+ </div>
118
126
 
119
127
  <p class="comment-field">
120
128
  <label for="comment-url">Your site <span class="comment-optional">(optional)</span></label>
@@ -65,6 +65,8 @@
65
65
  <input id="contact-website" name="{{ contactForm.fields.trap }}" type="text" tabindex="-1" autocomplete="off" value="" />
66
66
  </div>
67
67
 
68
+ {#- Name and email side by side on a wide screen. -#}
69
+ <div class="field-row">
68
70
  <p class="contact-field">
69
71
  <label for="contact-name">Name</label>
70
72
  <input id="contact-name" name="{{ contactForm.fields.name }}" type="text" value="{{ contactForm.values.name }}" maxlength="{{ contactForm.nameLength }}" autocomplete="name" required{% if problems.name %} aria-invalid="true" aria-describedby="contact-name-error"{% endif %} />
@@ -81,6 +83,7 @@
81
83
  {% endif %}
82
84
  <span class="contact-hint">So there is somewhere to reply to. It is never shown on the site.</span>
83
85
  </p>
86
+ </div>
84
87
 
85
88
  <p class="contact-field">
86
89
  <label for="contact-subject">Subject <span class="contact-optional">(optional)</span></label>