@geekity/cms 0.15.0 → 0.16.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.
- package/README.md +107 -0
- package/admin/pages/documents/editor.njk +51 -0
- package/admin/pages/documents/syndication.njk +110 -0
- package/admin/pages/federation/followers.njk +7 -0
- package/admin/static/admin.css +11 -0
- package/dist/admin/documents.d.ts +101 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +199 -67
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +15 -0
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +19 -0
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +3 -0
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/photo-field.d.ts +39 -0
- package/dist/admin/photo-field.d.ts.map +1 -0
- package/dist/admin/photo-field.js +91 -0
- package/dist/admin/photo-field.js.map +1 -0
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +6 -1
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +4 -0
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/syndication-targets.d.ts +57 -0
- package/dist/admin/syndication-targets.d.ts.map +1 -0
- package/dist/admin/syndication-targets.js +266 -0
- package/dist/admin/syndication-targets.js.map +1 -0
- package/dist/admin/templates.d.ts +1 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +1 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/uploads.d.ts +6 -0
- package/dist/admin/uploads.d.ts.map +1 -1
- package/dist/admin/uploads.js +1 -1
- package/dist/admin/uploads.js.map +1 -1
- package/dist/content/citation.d.ts +36 -0
- package/dist/content/citation.d.ts.map +1 -0
- package/dist/content/citation.js +31 -0
- package/dist/content/citation.js.map +1 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +1 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/photo.d.ts +45 -0
- package/dist/content/photo.d.ts.map +1 -0
- package/dist/content/photo.js +55 -0
- package/dist/content/photo.js.map +1 -0
- package/dist/content/post-type.d.ts +20 -8
- package/dist/content/post-type.d.ts.map +1 -1
- package/dist/content/post-type.js +17 -0
- package/dist/content/post-type.js.map +1 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +59 -5
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/citations.d.ts +38 -0
- package/dist/federation/citations.d.ts.map +1 -0
- package/dist/federation/citations.js +98 -0
- package/dist/federation/citations.js.map +1 -0
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +79 -31
- package/dist/federation/delivery.js.map +1 -1
- package/dist/images/alt-text.d.ts +6 -0
- package/dist/images/alt-text.d.ts.map +1 -1
- package/dist/images/alt-text.js +10 -0
- package/dist/images/alt-text.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -2
- package/dist/index.js.map +1 -1
- package/dist/indieauth/bearer.d.ts +6 -1
- package/dist/indieauth/bearer.d.ts.map +1 -1
- package/dist/indieauth/bearer.js +39 -12
- package/dist/indieauth/bearer.js.map +1 -1
- package/dist/indieauth/discovery.d.ts +9 -4
- package/dist/indieauth/discovery.d.ts.map +1 -1
- package/dist/indieauth/discovery.js +18 -7
- package/dist/indieauth/discovery.js.map +1 -1
- package/dist/micropub/create.d.ts +56 -0
- package/dist/micropub/create.d.ts.map +1 -0
- package/dist/micropub/create.js +200 -0
- package/dist/micropub/create.js.map +1 -0
- package/dist/micropub/endpoint.d.ts +21 -0
- package/dist/micropub/endpoint.d.ts.map +1 -0
- package/dist/micropub/endpoint.js +353 -0
- package/dist/micropub/endpoint.js.map +1 -0
- package/dist/micropub/media.d.ts +14 -0
- package/dist/micropub/media.d.ts.map +1 -0
- package/dist/micropub/media.js +93 -0
- package/dist/micropub/media.js.map +1 -0
- package/dist/micropub/update.d.ts +44 -0
- package/dist/micropub/update.d.ts.map +1 -0
- package/dist/micropub/update.js +133 -0
- package/dist/micropub/update.js.map +1 -0
- package/dist/web/context.d.ts +42 -3
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +41 -1
- package/dist/web/context.js.map +1 -1
- package/dist/web/render.d.ts +15 -0
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +16 -0
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.js +2 -2
- package/dist/webmention/index.d.ts +2 -0
- package/dist/webmention/index.d.ts.map +1 -1
- package/dist/webmention/index.js +1 -0
- package/dist/webmention/index.js.map +1 -1
- package/dist/webmention/service.d.ts.map +1 -1
- package/dist/webmention/service.js +96 -18
- package/dist/webmention/service.js.map +1 -1
- package/dist/webmention/syndication.d.ts +135 -0
- package/dist/webmention/syndication.d.ts.map +1 -0
- package/dist/webmention/syndication.js +337 -0
- package/dist/webmention/syndication.js.map +1 -0
- package/package.json +1 -1
- package/themes/default/README.md +40 -9
- package/themes/default/layouts/post.njk +19 -6
- package/themes/default/partials/citations.njk +17 -0
- package/themes/default/partials/jsonld.njk +12 -1
- package/themes/default/partials/photos.njk +10 -0
- package/themes/default/partials/post-list.njk +7 -0
- package/themes/default/src/style.css +9 -0
- 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
package/themes/default/README.md
CHANGED
|
@@ -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-
|
|
338
|
-
|
|
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.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`
|
|
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
|
|
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
|
|
446
|
-
|
|
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
|
-
{{-
|
|
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
|
-
{#-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{%-
|
|
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",
|
|
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
|
}
|