@geekity/cms 0.15.0 → 0.17.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 (147) hide show
  1. package/README.md +108 -0
  2. package/admin/pages/documents/editor.njk +51 -0
  3. package/admin/pages/documents/syndication.njk +110 -0
  4. package/admin/pages/federation/followers.njk +7 -0
  5. package/admin/pages/users/activity-entry.njk +58 -0
  6. package/admin/pages/users/activity.njk +66 -0
  7. package/admin/static/admin.css +11 -0
  8. package/dist/admin/documents.d.ts +101 -0
  9. package/dist/admin/documents.d.ts.map +1 -1
  10. package/dist/admin/documents.js +199 -67
  11. package/dist/admin/documents.js.map +1 -1
  12. package/dist/admin/federation.d.ts +15 -0
  13. package/dist/admin/federation.d.ts.map +1 -1
  14. package/dist/admin/federation.js +19 -0
  15. package/dist/admin/federation.js.map +1 -1
  16. package/dist/admin/menu.d.ts.map +1 -1
  17. package/dist/admin/menu.js +7 -1
  18. package/dist/admin/menu.js.map +1 -1
  19. package/dist/admin/photo-field.d.ts +39 -0
  20. package/dist/admin/photo-field.d.ts.map +1 -0
  21. package/dist/admin/photo-field.js +91 -0
  22. package/dist/admin/photo-field.js.map +1 -0
  23. package/dist/admin/preview.d.ts.map +1 -1
  24. package/dist/admin/preview.js +6 -1
  25. package/dist/admin/preview.js.map +1 -1
  26. package/dist/admin/routes.d.ts.map +1 -1
  27. package/dist/admin/routes.js +8 -0
  28. package/dist/admin/routes.js.map +1 -1
  29. package/dist/admin/syndication-targets.d.ts +57 -0
  30. package/dist/admin/syndication-targets.d.ts.map +1 -0
  31. package/dist/admin/syndication-targets.js +266 -0
  32. package/dist/admin/syndication-targets.js.map +1 -0
  33. package/dist/admin/templates.d.ts +3 -0
  34. package/dist/admin/templates.d.ts.map +1 -1
  35. package/dist/admin/templates.js +3 -0
  36. package/dist/admin/templates.js.map +1 -1
  37. package/dist/admin/uploads.d.ts +6 -0
  38. package/dist/admin/uploads.d.ts.map +1 -1
  39. package/dist/admin/uploads.js +1 -1
  40. package/dist/admin/uploads.js.map +1 -1
  41. package/dist/admin/users.d.ts +8 -0
  42. package/dist/admin/users.d.ts.map +1 -1
  43. package/dist/admin/users.js +8 -0
  44. package/dist/admin/users.js.map +1 -1
  45. package/dist/content/citation.d.ts +36 -0
  46. package/dist/content/citation.d.ts.map +1 -0
  47. package/dist/content/citation.js +31 -0
  48. package/dist/content/citation.js.map +1 -0
  49. package/dist/content/index.d.ts +2 -0
  50. package/dist/content/index.d.ts.map +1 -1
  51. package/dist/content/index.js +1 -0
  52. package/dist/content/index.js.map +1 -1
  53. package/dist/content/photo.d.ts +45 -0
  54. package/dist/content/photo.d.ts.map +1 -0
  55. package/dist/content/photo.js +55 -0
  56. package/dist/content/photo.js.map +1 -0
  57. package/dist/content/post-type.d.ts +20 -8
  58. package/dist/content/post-type.d.ts.map +1 -1
  59. package/dist/content/post-type.js +17 -0
  60. package/dist/content/post-type.js.map +1 -1
  61. package/dist/env.d.ts +7 -0
  62. package/dist/env.d.ts.map +1 -1
  63. package/dist/federation/article.d.ts.map +1 -1
  64. package/dist/federation/article.js +59 -5
  65. package/dist/federation/article.js.map +1 -1
  66. package/dist/federation/citations.d.ts +38 -0
  67. package/dist/federation/citations.d.ts.map +1 -0
  68. package/dist/federation/citations.js +98 -0
  69. package/dist/federation/citations.js.map +1 -0
  70. package/dist/federation/delivery.d.ts.map +1 -1
  71. package/dist/federation/delivery.js +79 -31
  72. package/dist/federation/delivery.js.map +1 -1
  73. package/dist/images/alt-text.d.ts +6 -0
  74. package/dist/images/alt-text.d.ts.map +1 -1
  75. package/dist/images/alt-text.js +10 -0
  76. package/dist/images/alt-text.js.map +1 -1
  77. package/dist/index.d.ts +4 -4
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +25 -2
  80. package/dist/index.js.map +1 -1
  81. package/dist/indieauth/activity-log.d.ts +112 -0
  82. package/dist/indieauth/activity-log.d.ts.map +1 -0
  83. package/dist/indieauth/activity-log.js +325 -0
  84. package/dist/indieauth/activity-log.js.map +1 -0
  85. package/dist/indieauth/app-activity.d.ts +14 -0
  86. package/dist/indieauth/app-activity.d.ts.map +1 -0
  87. package/dist/indieauth/app-activity.js +106 -0
  88. package/dist/indieauth/app-activity.js.map +1 -0
  89. package/dist/indieauth/bearer.d.ts +6 -1
  90. package/dist/indieauth/bearer.d.ts.map +1 -1
  91. package/dist/indieauth/bearer.js +40 -12
  92. package/dist/indieauth/bearer.js.map +1 -1
  93. package/dist/indieauth/consent.d.ts.map +1 -1
  94. package/dist/indieauth/consent.js +5 -2
  95. package/dist/indieauth/consent.js.map +1 -1
  96. package/dist/indieauth/discovery.d.ts +10 -4
  97. package/dist/indieauth/discovery.d.ts.map +1 -1
  98. package/dist/indieauth/discovery.js +22 -7
  99. package/dist/indieauth/discovery.js.map +1 -1
  100. package/dist/indieauth/token.d.ts.map +1 -1
  101. package/dist/indieauth/token.js +3 -1
  102. package/dist/indieauth/token.js.map +1 -1
  103. package/dist/micropub/create.d.ts +56 -0
  104. package/dist/micropub/create.d.ts.map +1 -0
  105. package/dist/micropub/create.js +200 -0
  106. package/dist/micropub/create.js.map +1 -0
  107. package/dist/micropub/endpoint.d.ts +21 -0
  108. package/dist/micropub/endpoint.d.ts.map +1 -0
  109. package/dist/micropub/endpoint.js +354 -0
  110. package/dist/micropub/endpoint.js.map +1 -0
  111. package/dist/micropub/media.d.ts +14 -0
  112. package/dist/micropub/media.d.ts.map +1 -0
  113. package/dist/micropub/media.js +94 -0
  114. package/dist/micropub/media.js.map +1 -0
  115. package/dist/micropub/update.d.ts +44 -0
  116. package/dist/micropub/update.d.ts.map +1 -0
  117. package/dist/micropub/update.js +133 -0
  118. package/dist/micropub/update.js.map +1 -0
  119. package/dist/web/context.d.ts +42 -3
  120. package/dist/web/context.d.ts.map +1 -1
  121. package/dist/web/context.js +41 -1
  122. package/dist/web/context.js.map +1 -1
  123. package/dist/web/render.d.ts +15 -0
  124. package/dist/web/render.d.ts.map +1 -1
  125. package/dist/web/render.js +16 -0
  126. package/dist/web/render.js.map +1 -1
  127. package/dist/web/routes.js +2 -2
  128. package/dist/webmention/index.d.ts +2 -0
  129. package/dist/webmention/index.d.ts.map +1 -1
  130. package/dist/webmention/index.js +1 -0
  131. package/dist/webmention/index.js.map +1 -1
  132. package/dist/webmention/service.d.ts.map +1 -1
  133. package/dist/webmention/service.js +96 -18
  134. package/dist/webmention/service.js.map +1 -1
  135. package/dist/webmention/syndication.d.ts +135 -0
  136. package/dist/webmention/syndication.d.ts.map +1 -0
  137. package/dist/webmention/syndication.js +337 -0
  138. package/dist/webmention/syndication.js.map +1 -0
  139. package/package.json +1 -1
  140. package/themes/default/README.md +40 -9
  141. package/themes/default/layouts/post.njk +19 -6
  142. package/themes/default/partials/citations.njk +17 -0
  143. package/themes/default/partials/jsonld.njk +12 -1
  144. package/themes/default/partials/photos.njk +10 -0
  145. package/themes/default/partials/post-list.njk +7 -0
  146. package/themes/default/src/style.css +9 -0
  147. package/themes/default/static/style.css +16 -0
@@ -0,0 +1,337 @@
1
+ import path from 'node:path';
2
+ import { readFileIfPresentSync, withFileLock, writeFileAtomicallySync } from '../files/atomic.js';
3
+ import { canonicalLocale, DEFAULT_LOCALE, documentLanguage } from '../web/locale.js';
4
+ /**
5
+ * Syndication targets (TASK-155): services such as IndieNews and Bridgy
6
+ * Publish that take a copy of a post when it links to them and sends them a
7
+ * webmention, and answer with the URL of the copy they made.
8
+ *
9
+ * Nothing here names a service. A site declares its targets in
10
+ * {@link SYNDICATION_TARGETS_FILE}; a post selects some with its
11
+ * `syndicate-to` list or by carrying a target's tag; the theme links to each
12
+ * one inside the post's h-entry; the sender notifies them as it notifies any
13
+ * linked page; and the URL each one answers with is kept in
14
+ * {@link SYNDICATION_FILE} (decision-26) and printed as `u-syndication`.
15
+ */
16
+ /** Where a site declares its targets, relative to the content directory. */
17
+ export const SYNDICATION_TARGETS_FILE = '_data/syndicationTargets.json';
18
+ /**
19
+ * Where the copies targets answered with are kept, relative to the content
20
+ * directory: one object keyed by a post's permalink, each mapping a target's
21
+ * URL to the copy it made (decision-26). Eleventy reads it as `syndication`.
22
+ */
23
+ export const SYNDICATION_FILE = '_data/syndication.json';
24
+ /** The front matter key a post lists the ids of its targets under. */
25
+ export const SYNDICATE_TO_FRONT_MATTER_KEY = 'syndicate-to';
26
+ /** The front matter key, and mf2 property, for the URLs of a post's copies. */
27
+ export const SYNDICATION_FRONT_MATTER_KEY = 'syndication';
28
+ /** What an id may be made of, so it can name a form field and a Micropub uid alike. */
29
+ const ID_PATTERN = /^[A-Za-z0-9._-]+$/;
30
+ /** What a target's `url` may hold to follow the post's language (TASK-156). */
31
+ export const LANGUAGE_PLACEHOLDER = '{lang}';
32
+ /** Read the targets file's text. An entry that is not a target is reported and left out. */
33
+ export function parseSyndicationTargets(text) {
34
+ const read = syndicationEntries(text);
35
+ if ('problem' in read)
36
+ return { targets: [], problems: [read.problem] };
37
+ const targets = [];
38
+ const problems = [];
39
+ read.entries.forEach((entry, index) => {
40
+ if ('target' in entry) {
41
+ targets.push(entry.target);
42
+ return;
43
+ }
44
+ const id = isRecord(entry.raw) && typeof entry.raw['id'] === 'string' ? entry.raw['id'] : '';
45
+ const named = id === '' ? '' : ` ("${id}")`;
46
+ problems.push(`${SYNDICATION_TARGETS_FILE} entry ${String(index + 1)}${named} was ignored. ${entry.problems.join(' ')}`);
47
+ });
48
+ return { targets, problems };
49
+ }
50
+ /**
51
+ * Every entry of the targets file's text, each with the target it declares or
52
+ * its problems, in file order. A later entry repeating an earlier one's id
53
+ * declares nothing, so the first one wins.
54
+ */
55
+ export function syndicationEntries(text) {
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(text);
59
+ }
60
+ catch {
61
+ return { problem: `${SYNDICATION_TARGETS_FILE} is not valid JSON.` };
62
+ }
63
+ if (!Array.isArray(parsed)) {
64
+ return { problem: `${SYNDICATION_TARGETS_FILE} is not a list of targets.` };
65
+ }
66
+ const seen = new Set();
67
+ const entries = parsed.map((raw) => {
68
+ if (!isRecord(raw)) {
69
+ return { raw, problems: ['It is not an object with an id, a name and a url.'] };
70
+ }
71
+ const checked = checkSyndicationTarget(raw);
72
+ if ('problems' in checked)
73
+ return { raw, problems: Object.values(checked.problems) };
74
+ if (seen.has(checked.target.id)) {
75
+ return { raw, problems: [repeatedIdProblem(checked.target.id)] };
76
+ }
77
+ seen.add(checked.target.id);
78
+ return { raw, target: checked.target };
79
+ });
80
+ return { entries };
81
+ }
82
+ /** What an id another target already has is told. */
83
+ export function repeatedIdProblem(id) {
84
+ return `Another target already has the id "${id}".`;
85
+ }
86
+ /**
87
+ * One entry as a target, or a sentence for each key that is wrong. The one
88
+ * rule a target is read by, whether it comes from the file or from the admin's
89
+ * form; whether its id is unique depends on the other entries, so that is the
90
+ * caller's.
91
+ */
92
+ export function checkSyndicationTarget(entry) {
93
+ const { id, name, url, tag, languages } = entry;
94
+ const problems = {};
95
+ if (typeof id !== 'string' || !ID_PATTERN.test(id)) {
96
+ problems.id = 'An id is letters, digits, dots, dashes and underscores, and nothing else.';
97
+ }
98
+ if (typeof name !== 'string' || name.trim() === '')
99
+ problems.name = 'A target needs a name.';
100
+ const href = urlTemplate(url);
101
+ if (href === undefined) {
102
+ problems.url = `The url must be an http or https address. It may hold ${LANGUAGE_PLACEHOLDER} where the language goes.`;
103
+ }
104
+ if (tag !== undefined && (typeof tag !== 'string' || tag.trim() === '')) {
105
+ problems.tag = 'A tag must be a word, or left out.';
106
+ }
107
+ const tags = languages === undefined ? undefined : languageList(languages);
108
+ if (tags === null) {
109
+ problems.languages = 'Languages must be a list of language tags, such as en or de-AT.';
110
+ }
111
+ if (typeof id !== 'string' ||
112
+ typeof name !== 'string' ||
113
+ href === undefined ||
114
+ tags === null ||
115
+ Object.keys(problems).length > 0) {
116
+ return { problems };
117
+ }
118
+ return {
119
+ target: {
120
+ id,
121
+ name: name.trim(),
122
+ url: href,
123
+ ...(typeof tag === 'string' ? { tag: tag.trim() } : {}),
124
+ ...(tags === undefined ? {} : { languages: tags }),
125
+ },
126
+ };
127
+ }
128
+ /**
129
+ * A declared `url` as kept: an http or https URL, which may hold
130
+ * {@link LANGUAGE_PLACEHOLDER}. One with the placeholder is checked with a
131
+ * language filled in and kept as written, since a URL parser would escape the
132
+ * braces.
133
+ */
134
+ function urlTemplate(value) {
135
+ if (typeof value !== 'string')
136
+ return undefined;
137
+ if (!value.includes(LANGUAGE_PLACEHOLDER))
138
+ return webUrl(value);
139
+ const template = value.trim();
140
+ return webUrl(fillLanguage(template, DEFAULT_LOCALE)) === undefined ? undefined : template;
141
+ }
142
+ /** A non-empty list of language tags, canonical, or `null` when it is not one. */
143
+ function languageList(value) {
144
+ if (!Array.isArray(value) || value.length === 0)
145
+ return null;
146
+ const tags = [];
147
+ for (const item of value) {
148
+ const tag = typeof item === 'string' ? canonicalLocale(item) : undefined;
149
+ if (tag === undefined)
150
+ return null;
151
+ tags.push(tag);
152
+ }
153
+ return tags;
154
+ }
155
+ /** A reader over one content directory's targets file. A missing file declares none. */
156
+ export function syndicationTargetsReader(contentDir) {
157
+ const file = path.join(contentDir, ...SYNDICATION_TARGETS_FILE.split('/'));
158
+ let cachedText;
159
+ let cached = [];
160
+ return () => {
161
+ const text = readFileIfPresentSync(file);
162
+ if (text === undefined) {
163
+ cachedText = undefined;
164
+ cached = [];
165
+ }
166
+ else if (text !== cachedText) {
167
+ cachedText = text;
168
+ cached = parseSyndicationTargets(text).targets;
169
+ }
170
+ return cached;
171
+ };
172
+ }
173
+ /** What is wrong with a content directory's targets file, for the boot log. */
174
+ export function syndicationTargetProblems(contentDir) {
175
+ const text = readFileIfPresentSync(path.join(contentDir, ...SYNDICATION_TARGETS_FILE.split('/')));
176
+ return text === undefined ? [] : parseSyndicationTargets(text).problems;
177
+ }
178
+ /**
179
+ * The declared targets a post selects, in the order the site declares them,
180
+ * each with its `url` filled for the post: those its `syndicate-to` names, and
181
+ * those whose tag it carries. An id nobody declared selects nothing.
182
+ *
183
+ * The post's language is its own `lang`, else `siteLanguage` (TASK-156). A
184
+ * target that lists `languages` is selected only for a post in one of them or
185
+ * in a regional form of one, so `de-AT` goes to a target that lists `de`, and
186
+ * {@link LANGUAGE_PLACEHOLDER} takes the listed tag; without a list it takes
187
+ * the post's. The one place the URL is decided, so the theme's link, the
188
+ * webmention and the stored copy's key cannot disagree.
189
+ */
190
+ export function selectedTargets(document, targets, siteLanguage) {
191
+ const listed = new Set(stringsOf(document.extra[SYNDICATE_TO_FRONT_MATTER_KEY]));
192
+ const tags = new Set(document.tags.map((tag) => tag.toLowerCase()));
193
+ const language = documentLanguage(document) ?? canonicalLocale(siteLanguage) ?? DEFAULT_LOCALE;
194
+ return targets.flatMap((target) => {
195
+ const selected = listed.has(target.id) || (target.tag !== undefined && tags.has(target.tag.toLowerCase()));
196
+ if (!selected)
197
+ return [];
198
+ const filled = target.languages === undefined ? language : matchingLanguage(language, target.languages);
199
+ if (filled === undefined)
200
+ return [];
201
+ if (!target.url.includes(LANGUAGE_PLACEHOLDER))
202
+ return [target];
203
+ const url = webUrl(fillLanguage(target.url, filled));
204
+ return url === undefined ? [] : [{ ...target, url }];
205
+ });
206
+ }
207
+ /** The first of `languages` that `language` is, or is a regional form of. */
208
+ function matchingLanguage(language, languages) {
209
+ const wanted = language.toLowerCase();
210
+ return languages.find((tag) => {
211
+ const listed = tag.toLowerCase();
212
+ return wanted === listed || wanted.startsWith(`${listed}-`);
213
+ });
214
+ }
215
+ function fillLanguage(template, language) {
216
+ return template.replaceAll(LANGUAGE_PLACEHOLDER, encodeURIComponent(language));
217
+ }
218
+ /** The ids a post's `syndicate-to` lists, as written. */
219
+ export function syndicateToOf(extra) {
220
+ return stringsOf(extra[SYNDICATE_TO_FRONT_MATTER_KEY]);
221
+ }
222
+ /** The copies an author listed by hand under `syndication`, each an http or https URL. */
223
+ export function handSyndicationOf(extra) {
224
+ return stringsOf(extra[SYNDICATION_FRONT_MATTER_KEY]).flatMap((value) => webUrl(value) ?? []);
225
+ }
226
+ /** The copies file of one content directory. */
227
+ export function syndicationCopies(contentDir) {
228
+ const file = path.join(contentDir, ...SYNDICATION_FILE.split('/'));
229
+ let cachedText;
230
+ let cached = {};
231
+ function readAll() {
232
+ const text = readFileIfPresentSync(file);
233
+ if (text === undefined) {
234
+ cachedText = undefined;
235
+ cached = {};
236
+ }
237
+ else if (text !== cachedText) {
238
+ cachedText = text;
239
+ cached = parseCopies(text);
240
+ }
241
+ return cached;
242
+ }
243
+ /**
244
+ * Change the file under its lock, and write it only when something changed:
245
+ * a resend that gets the same copy back must not churn a file that is in the
246
+ * site's git history.
247
+ */
248
+ function update(change) {
249
+ return withFileLock(file, () => {
250
+ const current = readFileIfPresentSync(file);
251
+ const all = current === undefined ? {} : parseCopies(current);
252
+ if (change(all))
253
+ writeFileAtomicallySync(file, `${JSON.stringify(all, null, 2)}\n`);
254
+ });
255
+ }
256
+ return {
257
+ read(permalink) {
258
+ return readAll()[permalink] ?? {};
259
+ },
260
+ write(permalink, target, copy) {
261
+ return update((all) => {
262
+ const copies = { ...all[permalink] };
263
+ if (copies[target] === copy)
264
+ return false;
265
+ if (copy === undefined)
266
+ delete copies[target];
267
+ else
268
+ copies[target] = copy;
269
+ if (Object.keys(copies).length === 0)
270
+ delete all[permalink];
271
+ else
272
+ all[permalink] = copies;
273
+ return true;
274
+ });
275
+ },
276
+ forget(permalink) {
277
+ return update((all) => {
278
+ if (!(permalink in all))
279
+ return false;
280
+ delete all[permalink];
281
+ return true;
282
+ });
283
+ },
284
+ };
285
+ }
286
+ /**
287
+ * The copies file's entries, each checked, since a person may edit it by hand.
288
+ * An entry that is not a URL is dropped rather than failing a render.
289
+ */
290
+ function parseCopies(text) {
291
+ let parsed;
292
+ try {
293
+ parsed = JSON.parse(text);
294
+ }
295
+ catch {
296
+ return {};
297
+ }
298
+ if (!isRecord(parsed))
299
+ return {};
300
+ const all = {};
301
+ for (const [permalink, value] of Object.entries(parsed)) {
302
+ if (!isRecord(value))
303
+ continue;
304
+ const copies = {};
305
+ for (const [target, copy] of Object.entries(value)) {
306
+ const href = webUrl(copy);
307
+ if (href !== undefined)
308
+ copies[target] = href;
309
+ }
310
+ if (Object.keys(copies).length > 0)
311
+ all[permalink] = copies;
312
+ }
313
+ return all;
314
+ }
315
+ /** A front matter value as a list of strings: a lone string is a list of one. */
316
+ function stringsOf(value) {
317
+ if (typeof value === 'string')
318
+ return value === '' ? [] : [value];
319
+ if (!Array.isArray(value))
320
+ return [];
321
+ return value.filter((item) => typeof item === 'string' && item !== '');
322
+ }
323
+ function webUrl(value) {
324
+ if (typeof value !== 'string')
325
+ return undefined;
326
+ try {
327
+ const url = new URL(value);
328
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : undefined;
329
+ }
330
+ catch {
331
+ return undefined;
332
+ }
333
+ }
334
+ function isRecord(value) {
335
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
336
+ }
337
+ //# sourceMappingURL=syndication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syndication.js","sourceRoot":"","sources":["../../src/webmention/syndication.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekity/cms",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "A file-first CMS: Markdown content, a SQLite index, a Hono server that is both the editor and the public site.",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@ themes/default/
25
25
  tags.njk macros for tag and category links
26
26
  kicker.njk the line above an entry naming its kind and date
27
27
  reply-context.njk the citation of what a reply answers
28
+ citations.njk the citation of what a like, repost or bookmark cites
28
29
  bio.njk who an entry is by, as an h-card
29
30
  menu.njk one named menu, as a nav of links
30
31
  feeds.njk macros for the feed links in <head>
@@ -334,8 +335,8 @@ is a microformats2 `h-entry`. An article, and a page:
334
335
  <section class="e-content">
335
336
  <p>The rendered body.</p>
336
337
  <p class="entry-meta">
337
- <a class="u-category small" href="https://news.indieweb.org/en"
338
- >#indienews</a
338
+ <a class="u-syndication small" href="https://news.indieweb.org/en"
339
+ >IndieNews</a
339
340
  >
340
341
  <a href="/2026/09/hello/" class="u-url"
341
342
  ><time class="small dt-published" datetime="2026-09-02T09:00:00.000Z"
@@ -346,6 +347,13 @@ is a microformats2 `h-entry`. An article, and a page:
346
347
  <time class="small dt-updated" datetime="2026-09-05T09:00:00.000Z"
347
348
  >Updated 5 September 2026</time
348
349
  >
350
+ <span aria-hidden="true">·</span>
351
+ <span class="small"
352
+ >Also on
353
+ <a class="u-syndication" href="https://news.indieweb.org/en/…"
354
+ >news.indieweb.org</a
355
+ ></span
356
+ >
349
357
  </p>
350
358
  </section>
351
359
  <p class="post-categories">…tags…</p>
@@ -369,10 +377,11 @@ permalink are part of the words, so a reader, or a fediverse peer reading the
369
377
  `e-content`, takes them with the post. The permalink wraps the `dt-published`
370
378
  time as a `u-url`, and a `dt-updated` follows it after a middle dot only when
371
379
  the update happened on a different day in the site's own timezone; a typo fixed
372
- an hour later is not news. A post tagged `indienews` opens the line with a
373
- `u-category` link to <https://news.indieweb.org/en>, which is how IndieNews is
374
- told the post is for it; a page never prints one, and its line is a
375
- `p.page-meta` rather than a `p.entry-meta`.
380
+ an hour later is not news. The line opens with a `u-syndication` link to each
381
+ syndication target the post selects (`syndicateTo`, TASK-155), which is what
382
+ IndieNews and Bridgy Publish look for on the page, and ends with the post's
383
+ copies elsewhere (`syndication`) after **Also on**. A page never prints either,
384
+ and its line is a `p.page-meta` rather than a `p.entry-meta`.
376
385
 
377
386
  **Every post's page has one `h1`.** A post with a name of its own is headed by
378
387
  it, as above. A note, or a reply without a title, opens on its words instead,
@@ -385,7 +394,7 @@ who wrote it and when, and then the kicker:
385
394
  <p class="kicker"><span class="kicker-kind">Note</span></p>
386
395
  ```
387
396
 
388
- It says `Reply` for a reply and leaves out the name or the date the post does
397
+ It says `Reply`, `Like`, `Repost` or `Bookmark` for those and leaves out the name or the date the post does
389
398
  not have. It is not a `p-name`: a parser that found one would take the post
390
399
  for an article. The headings under it step down one level at a time: the
391
400
  conversation and the comment form are `h2`, and a refused form's error summary
@@ -398,6 +407,14 @@ far as they are known, and its excerpt as a `blockquote.cite-quote.p-content`.
398
407
  An untitled reply prints it under the kicker; a titled reply prints it above
399
408
  its header. The same partial cites a reply in a feed.
400
409
 
410
+ **A like, a repost or a bookmark cites what it cites** (TASK-169) with
411
+ `partials/citations.njk`, beside the reply context and drawn the same way: one
412
+ `div.reply-context.cite.h-cite` per entry of `citations`, classed
413
+ `u-repost-of`, `u-like-of` or `u-bookmark-of`, whose `p.cite-line` says
414
+ Reposted, Liked or Bookmarked and links the target as its `u-url`. Its kicker
415
+ and its hidden `h1` say Repost, Like or Bookmark. A listing cites each entry's
416
+ from the same partial.
417
+
401
418
  The tags are printed under the words and inside the article, so that each link
402
419
  is a `p-category` of this entry, as one `p.post-categories` from
403
420
  `partials/tags.njk`. The stylesheet draws the `#` in front of each, so the
@@ -416,6 +433,16 @@ other versions by their title, else their media type. A post without a
416
433
  recording, or whose front matter is missing what a player needs, prints none of
417
434
  it.
418
435
 
436
+ **A post with photos shows them** (TASK-166), from `partials/photos.njk`, after
437
+ the recording and before the `e-content`, each in a `figure.post-photo`. Each is
438
+ the context's `photo.html`: an `<img class="u-photo">` with its alt text, so a
439
+ microformats parser finds every photo on the entry, and a `<picture>` with the
440
+ upload's resizes when it has them. The alt text is the post's own, else the
441
+ media library's. A listing prints them the same way above each entry, and the
442
+ JSON-LD `BlogPosting` lists each as an `ImageObject` with its absolute `url`
443
+ and its alt text as `caption`. The first photo is the post's share image when
444
+ its front matter names no `image`.
445
+
419
446
  ```html
420
447
  <figure class="post-recording">
421
448
  <audio controls preload="metadata" aria-label="Recording of Episode twelve">
@@ -442,8 +469,8 @@ After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
442
469
  posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
443
470
  `span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
444
471
  the conversation and the comment form. A page prints the contact form when its
445
- front matter asked for one. A page has no neighbours, no tags and no IndieNews
446
- link, because none of those are things a page has.
472
+ front matter asked for one. A page has no neighbours, no tags and no
473
+ syndication links, because none of those are things a page has.
447
474
 
448
475
  ### The bio
449
476
 
@@ -1138,6 +1165,10 @@ A document — one post, one page, or one entry of a listing — adds:
1138
1165
  | `permalink`, `slug`, `draft`, `description` | Straight from the front matter. |
1139
1166
  | `lang` | The `lang` front matter as a canonical tag, such as `fr-CA`. Absent when it names none or no tag. |
1140
1167
  | `enclosure` | The post's recording, parsed, with `player` (`audio` or `video`) and `transcript.captions`. Absent with none. |
1168
+ | `photos` | The post's photos, each `{ url, alt, html }`; `html` is the `img.u-photo`, responsive. Empty with none. |
1169
+ | `citations` | What it reposts, likes or bookmarks, each `{ property, url }` with `property` the mf2 name. Empty with none. |
1170
+ | `syndicateTo` | On a post's page, the syndication targets it selects, each `{ id, name, url }`. Empty with none. |
1171
+ | `syndication` | Its copies elsewhere, each `{ url, label }`: front matter `syndication`, and on its page the copies targets made. |
1141
1172
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1142
1173
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1143
1174
  | `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
@@ -39,6 +39,7 @@
39
39
  {#- A reply cites the post it answers, whose URL is also the link its
40
40
  webmention points a receiver at. -#}
41
41
  {% include "partials/reply-context.njk" %}
42
+ {% include "partials/citations.njk" %}
42
43
  <header class="post-header">
43
44
  {{ kicker.line(postType | capitalize, false, false, categories) }}
44
45
  <h1 class="p-name">{{ title }}</h1>
@@ -48,26 +49,33 @@
48
49
  </header>
49
50
  {% else %}
50
51
  <h1 class="screen-reader-text">
51
- {{- "Reply" if postType == "reply" else "Note" }}
52
+ {{- postType | capitalize if postType in ["reply", "like", "repost", "bookmark"] else "Note" }}
52
53
  {%- if bioAuthor.name %} by {{ bioAuthor.name }}{% endif %}
53
54
  {%- if date %}, {{ date | date("readable", none, lang) }}{% endif -%}
54
55
  </h1>
55
56
  {{ kicker.line(postType | capitalize, false, false, categories) }}
56
57
  {% include "partials/reply-context.njk" %}
58
+ {% include "partials/citations.njk" %}
57
59
  {% endif %}
58
60
 
59
61
  {% if enclosure %}
60
62
  {% include "partials/recording.njk" %}
61
63
  {% endif %}
62
64
 
65
+ {% if photos.length %}
66
+ {% include "partials/photos.njk" %}
67
+ {% endif %}
68
+
63
69
  <section class="e-content">
64
70
  {{ content | safe }}
65
71
  <p class="entry-meta">
66
- {#- A post tagged `indienews` opens the line with the link that submits
67
- it there, which is how IndieNews is told the post is for it. -#}
68
- {%- if "indienews" in tags %}
69
- <a class="u-category small" href="https://news.indieweb.org/en">#indienews</a>
70
- {%- endif %}
72
+ {#- The line opens with a link to each syndication target the post
73
+ selects (TASK-155). It is inside the h-entry and marked
74
+ `u-syndication`, the markup IndieNews documents, and a target that
75
+ is sent a webmention checks the page for it. -#}
76
+ {%- for target in syndicateTo %}
77
+ <a class="u-syndication small" href="{{ target.url }}">{{ target.name }}</a>
78
+ {%- endfor %}
71
79
  {%- if date %}
72
80
  <a href="{{ entryUrl | url }}" class="u-url"><time class="small dt-published" datetime="{{ date | date("iso") }}">Published {{ date | date("readable", none, lang) }}</time></a>
73
81
  {%- endif %}
@@ -75,6 +83,11 @@
75
83
  a typo fixed an hour after publishing is not news. -#}
76
84
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
77
85
  {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date("readable", none, lang) }}</time>
86
+ {%- endif %}
87
+ {#- The post's copies elsewhere: those its targets answered with and
88
+ those its front matter lists by hand. -#}
89
+ {%- if syndication.length %}
90
+ {% if date or updated %} <span aria-hidden="true">·</span> {% endif %}<span class="small">Also on {% for copy in syndication %}<a class="u-syndication" href="{{ copy.url }}">{{ copy.label }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</span>
78
91
  {%- endif -%}
79
92
  </p>
80
93
  </section>
@@ -0,0 +1,17 @@
1
+ {#
2
+ What a post reposts, likes or bookmarks (TASK-169), cited the way a reply
3
+ cites what it answers: each one an embedded `h-cite` under its own mf2
4
+ property, `u-repost-of`, `u-like-of` or `u-bookmark-of`, that always carries
5
+ the target's URL, so a parser reads what the post cites and the page its
6
+ webmention names links to it.
7
+
8
+ Drawn as the same citation block as `reply-context.njk` (doc-9). An entry
9
+ includes it with `citations` on the context, a list of `{ property, url }`;
10
+ a feed sets it from the entry it is printing.
11
+ #}
12
+ {% set citationVerbs = { "repost-of": "Reposted", "like-of": "Liked", "bookmark-of": "Bookmarked" } %}
13
+ {% for citation in citations %}
14
+ <div class="reply-context cite u-{{ citation.property }} h-cite">
15
+ <p class="cite-line small">{{ citationVerbs[citation.property] }} <a class="u-url" href="{{ citation.url }}">{{ citation.url }}</a></p>
16
+ </div>
17
+ {% endfor %}
@@ -56,6 +56,17 @@
56
56
  within the 110 characters Google reads, cut where a word ends (TASK-201).
57
57
  A page keeps its title. -#}
58
58
  {% set headline = title or (label | truncate(109, false, "…") if type == "post" else "") %}
59
+ {#- A photo post's image is its photos, each an ImageObject captioned with
60
+ its alt text, in order (TASK-166); anything else's is the share image. -#}
61
+ {% set entryImage = metaImage | absoluteUrl if metaImage else "" %}
62
+ {% if photos and photos.length %}
63
+ {% set entryImage = [] %}
64
+ {% for photo in photos %}
65
+ {% set imageObject = { "@type": "ImageObject", "url": photo.url | absoluteUrl } %}
66
+ {% if photo.alt %}{% set imageObject = { "@type": "ImageObject", "url": photo.url | absoluteUrl, "caption": photo.alt } %}{% endif %}
67
+ {% set _ = entryImage.push(imageObject) %}
68
+ {% endfor %}
69
+ {% endif %}
59
70
  {% set nodes = [] %}
60
71
  {% set website %}
61
72
  {
@@ -126,7 +137,7 @@
126
137
  {{- member("datePublished", date | date("iso") if date else "") }}
127
138
  {{- member("dateModified", (updated or date) | date("iso") if (updated or date) else "") }}
128
139
  {{- member("description", metaDescription) }}
129
- {{- member("image", metaImage | absoluteUrl if metaImage else "") }}
140
+ {{- member("image", entryImage) }}
130
141
  {{- member("author", person) }}
131
142
  {{- member("publisher", publisher) }}
132
143
  {{- member("license", license.url if license else "") }}
@@ -0,0 +1,10 @@
1
+ {#
2
+ A post's photos (TASK-166), each `photo.html`: an `img.u-photo` with its alt
3
+ text, so a microformats parser finds every photo on the h-entry, served as a
4
+ responsive picture when the upload has variants.
5
+ #}
6
+ <div class="post-photos">
7
+ {% for photo in photos %}
8
+ <figure class="post-photo">{{ photo.html | safe }}</figure>
9
+ {% endfor %}
10
+ </div>
@@ -39,6 +39,13 @@
39
39
  {% set lang = post.lang %}
40
40
  {% include "partials/reply-context.njk" %}
41
41
  {% endif %}
42
+ {#- What it reposts, likes or bookmarks, from the partial an entry uses. -#}
43
+ {% set citations = post.citations %}
44
+ {% include "partials/citations.njk" %}
45
+ {% if post.photos.length %}
46
+ {% set photos = post.photos %}
47
+ {% include "partials/photos.njk" %}
48
+ {% endif %}
42
49
  {% if not post.named %}
43
50
  <div class="feed-excerpt e-content">{{ post.content | safe }}</div>
44
51
  {% else %}
@@ -676,6 +676,15 @@
676
676
  @apply mt-1 flex flex-wrap gap-x-4 gap-y-1;
677
677
  }
678
678
 
679
+ /* A post's photos (TASK-166), one under another at the column's width. */
680
+ .post-photos {
681
+ @apply my-8 grid gap-4;
682
+ }
683
+
684
+ .post-photo img {
685
+ @apply block h-auto w-full;
686
+ }
687
+
679
688
  /* An untitled post is its words, set larger. It is the one entry whose
680
689
  heading is the hidden one TASK-144 gave it. */
681
690
  .blog-post > .screen-reader-text ~ .e-content {
@@ -1215,6 +1215,18 @@
1215
1215
  display: flex;
1216
1216
  }
1217
1217
 
1218
+ .post-photos {
1219
+ margin-block: calc(var(--spacing) * 8);
1220
+ gap: calc(var(--spacing) * 4);
1221
+ display: grid;
1222
+ }
1223
+
1224
+ .post-photo img {
1225
+ width: 100%;
1226
+ height: auto;
1227
+ display: block;
1228
+ }
1229
+
1218
1230
  .blog-post > .screen-reader-text ~ .e-content {
1219
1231
  --tw-leading: 1.55;
1220
1232
  font-size: 1.3rem;
@@ -2149,6 +2161,10 @@
2149
2161
  overflow: hidden;
2150
2162
  }
2151
2163
 
2164
+ .capitalize {
2165
+ text-transform: capitalize;
2166
+ }
2167
+
2152
2168
  .italic {
2153
2169
  font-style: italic;
2154
2170
  }