@marktiderman/genesis-sites 0.1.0 → 0.3.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 +241 -13
- package/dist/assets/islands.77d11f5697.js +1 -0
- package/dist/assets/sites-forms.732cc7e0c1.css +10 -0
- package/dist/assets/sites-forms.css +1 -1
- package/dist/assets/sites.716b4ac1a5.css +2 -0
- package/dist/assets/sites.css +1 -1
- package/dist/assets.generated-QSB7EJWX.js +3 -0
- package/dist/assets.generated-QSB7EJWX.js.map +1 -0
- package/dist/blocks.d.ts +1 -1
- package/dist/blocks.js +5 -1
- package/dist/blocks.js.map +1 -1
- package/dist/chunk-3VKTBE7S.js +69 -0
- package/dist/chunk-3VKTBE7S.js.map +1 -0
- package/dist/chunk-5IXDFFEG.js +465 -0
- package/dist/chunk-5IXDFFEG.js.map +1 -0
- package/dist/chunk-C375ZPU2.js +2241 -0
- package/dist/chunk-C375ZPU2.js.map +1 -0
- package/dist/{chunk-GA7U3CIN.js → chunk-FBQF2L7O.js} +317 -97
- package/dist/chunk-FBQF2L7O.js.map +1 -0
- package/dist/chunk-H6ED3K4R.js +18 -0
- package/dist/chunk-H6ED3K4R.js.map +1 -0
- package/dist/chunk-KPOX7D4S.js +250 -0
- package/dist/chunk-KPOX7D4S.js.map +1 -0
- package/dist/chunk-VZUDRPFG.js +13 -0
- package/dist/chunk-VZUDRPFG.js.map +1 -0
- package/dist/{chunk-KEF3V5ZN.js → chunk-ZGWGQGT2.js} +169 -73
- package/dist/chunk-ZGWGQGT2.js.map +1 -0
- package/dist/editor.d.ts +526 -0
- package/dist/editor.js +2480 -0
- package/dist/editor.js.map +1 -0
- package/dist/handler-QHH453YG.js +9 -0
- package/dist/handler-QHH453YG.js.map +1 -0
- package/dist/import.d.ts +130 -0
- package/dist/import.js +330 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +47 -8
- package/dist/index.js +5 -250
- package/dist/index.js.map +1 -1
- package/dist/{registry-CdgVymsu.d.ts → registry-HBn1X60x.d.ts} +274 -8
- package/dist/sections.d.ts +422 -6
- package/dist/sections.js +4 -2
- package/dist/{store-BzGw2i5J.d.ts → store-BXiZeZzH.d.ts} +119 -53
- package/dist/supabase.d.ts +5 -5
- package/dist/supabase.js +115 -6
- package/dist/supabase.js.map +1 -1
- package/dist/web.d.ts +26 -8
- package/dist/web.js +7 -353
- package/dist/web.js.map +1 -1
- package/migrations/0001_genesis_sites.sql +64 -6
- package/migrations/0002_genesis_sites_posts.sql +404 -0
- package/migrations/tests/0001_genesis_sites.test.sql +30 -1
- package/migrations/tests/0002_genesis_sites_posts.test.sql +242 -0
- package/package.json +28 -10
- package/dist/assets/islands.230b20b9c4.js +0 -1
- package/dist/assets/sites-forms.5e6ebf31c3.css +0 -10
- package/dist/assets/sites.b28b796c6f.css +0 -2
- package/dist/chunk-43XUAO63.js +0 -1255
- package/dist/chunk-43XUAO63.js.map +0 -1
- package/dist/chunk-GA7U3CIN.js.map +0 -1
- package/dist/chunk-KEF3V5ZN.js.map +0 -1
|
@@ -1,93 +1,7 @@
|
|
|
1
|
-
import { getSection,
|
|
1
|
+
import { getSection, PostSlug, PostDoc, ReleaseSnapshot, validatePostSlug, MAX_IMPORT_POSTS, MAX_LIVE_POSTS, postSummary, redirectsAfterMove, PageDoc, SiteDoc } from './chunk-ZGWGQGT2.js';
|
|
2
|
+
import { pagePathProblems, isReservedPath, DEFAULT_RESERVED_PATHS, validatePagePath } from './chunk-3VKTBE7S.js';
|
|
2
3
|
import { genesisBrand, themeFromBrand } from '@marktiderman/genesis-design-system';
|
|
3
4
|
|
|
4
|
-
// src/core/migrate.ts
|
|
5
|
-
function migrateNode(node, lookup = getSection) {
|
|
6
|
-
const def = lookup(node.type);
|
|
7
|
-
const where = { sectionId: node.id, type: node.type };
|
|
8
|
-
if (!def) {
|
|
9
|
-
return {
|
|
10
|
-
node: null,
|
|
11
|
-
changed: false,
|
|
12
|
-
issues: [{ ...where, severity: "error", code: "unknown-section", message: `No section "${node.type}" is registered; section ${node.id} can't be shown.` }]
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
if (node.v > def.version) {
|
|
16
|
-
return {
|
|
17
|
-
node: null,
|
|
18
|
-
changed: false,
|
|
19
|
-
issues: [
|
|
20
|
-
{
|
|
21
|
-
...where,
|
|
22
|
-
severity: "error",
|
|
23
|
-
code: "newer-version",
|
|
24
|
-
message: `Section ${node.id} was saved by a newer "${node.type}" (v${node.v}) than this code knows (v${def.version}).`
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
let props = node.props;
|
|
30
|
-
for (let n = node.v + 1; n <= def.version; n++) {
|
|
31
|
-
const step = def.migrations?.[n];
|
|
32
|
-
try {
|
|
33
|
-
props = step ? step(structuredClone(props)) : props;
|
|
34
|
-
} catch (err) {
|
|
35
|
-
return {
|
|
36
|
-
node: null,
|
|
37
|
-
changed: false,
|
|
38
|
-
issues: [{ ...where, severity: "error", code: "migration-failed", message: `Migrating section ${node.id} to v${n} failed: ${err.message}` }]
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const parsed = def.schema.safeParse(props);
|
|
43
|
-
if (!parsed.success) {
|
|
44
|
-
return {
|
|
45
|
-
node: null,
|
|
46
|
-
changed: false,
|
|
47
|
-
issues: [
|
|
48
|
-
{
|
|
49
|
-
...where,
|
|
50
|
-
severity: "error",
|
|
51
|
-
code: "invalid-props",
|
|
52
|
-
message: `Section ${node.id} (${def.label}): ${parsed.error.issues.map((i) => `${i.path.join(".") || "props"}: ${i.message}`).join("; ")}`
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const issues = [];
|
|
58
|
-
let variant = node.variant ?? def.defaultVariant;
|
|
59
|
-
if (!def.variants.some((v) => v.id === variant)) {
|
|
60
|
-
issues.push({ ...where, severity: "warning", code: "unknown-variant", message: `Section ${node.id} asks for variant "${variant}", which "${node.type}" doesn't have; showing "${def.defaultVariant}".` });
|
|
61
|
-
variant = def.defaultVariant;
|
|
62
|
-
}
|
|
63
|
-
const next = { ...node, v: def.version, variant, props: parsed.data };
|
|
64
|
-
const changed = node.v !== def.version || variant !== node.variant || JSON.stringify(parsed.data) !== JSON.stringify(node.props);
|
|
65
|
-
return { node: next, changed, issues };
|
|
66
|
-
}
|
|
67
|
-
function migratePage(doc, lookup) {
|
|
68
|
-
const issues = [];
|
|
69
|
-
let changed = false;
|
|
70
|
-
const content = doc.content.map((node) => {
|
|
71
|
-
const m = migrateNode(node, lookup);
|
|
72
|
-
issues.push(...m.issues);
|
|
73
|
-
if (m.node && m.changed) changed = true;
|
|
74
|
-
return m.node ?? node;
|
|
75
|
-
});
|
|
76
|
-
return { doc: changed ? { ...doc, content } : doc, changed, issues };
|
|
77
|
-
}
|
|
78
|
-
function migrateSite(site, lookup) {
|
|
79
|
-
const issues = [];
|
|
80
|
-
let changed = false;
|
|
81
|
-
const run = (nodes) => nodes.map((node) => {
|
|
82
|
-
const m = migrateNode(node, lookup);
|
|
83
|
-
issues.push(...m.issues);
|
|
84
|
-
if (m.node && m.changed) changed = true;
|
|
85
|
-
return m.node ?? node;
|
|
86
|
-
});
|
|
87
|
-
const header = run(site.header);
|
|
88
|
-
const footer = run(site.footer);
|
|
89
|
-
return { site: changed ? { ...site, header, footer } : site, changed, issues };
|
|
90
|
-
}
|
|
91
5
|
var FONT_CHOICES = Object.freeze({
|
|
92
6
|
system: { label: "System", stack: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif' },
|
|
93
7
|
inter: { label: "Inter", stack: "Inter, ui-sans-serif, system-ui, sans-serif", google: "Inter:wght@400;500;600;700" },
|
|
@@ -217,7 +131,10 @@ function pairsFor(c, mode) {
|
|
|
217
131
|
{ name: "body text on a muted band", mode, foreground: c.foreground, background: c.muted, minimum: 4.5 },
|
|
218
132
|
{ name: "button and accent-band text on the primary colour", mode, foreground: c.primaryForeground, background: c.primary, minimum: 4.5 },
|
|
219
133
|
{ name: "text on an inverse band", mode, foreground: c.background, background: c.foreground, minimum: 4.5 },
|
|
220
|
-
{ name: "the focus ring on the page", mode, foreground: c.ring, background: c.background, minimum: 3 }
|
|
134
|
+
{ name: "the focus ring on the page", mode, foreground: c.ring, background: c.background, minimum: 3 },
|
|
135
|
+
// Non-text (1.4.11): the primary colour as a border or marker (a timetable chip, the picked day, a timeline's step).
|
|
136
|
+
// Every such mark also has text or a shape (a chip names its type, a marker shows a dot), so it warns.
|
|
137
|
+
{ name: "the primary colour as a border or marker on the page", mode, foreground: c.primary, background: c.background, minimum: 3, severity: "warning" }
|
|
221
138
|
];
|
|
222
139
|
}
|
|
223
140
|
function stableHash(value) {
|
|
@@ -277,13 +194,101 @@ function themeCss(theme) {
|
|
|
277
194
|
return block(":root", theme.light) + ":root{color-scheme:light}";
|
|
278
195
|
}
|
|
279
196
|
|
|
197
|
+
// src/core/migrate.ts
|
|
198
|
+
function migrateNode(node, lookup = getSection) {
|
|
199
|
+
const def = lookup(node.type);
|
|
200
|
+
const where = { sectionId: node.id, type: node.type };
|
|
201
|
+
if (!def) {
|
|
202
|
+
return {
|
|
203
|
+
node: null,
|
|
204
|
+
changed: false,
|
|
205
|
+
issues: [{ ...where, severity: "error", code: "unknown-section", message: `No section "${node.type}" is registered; section ${node.id} can't be shown.` }]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (node.v > def.version) {
|
|
209
|
+
return {
|
|
210
|
+
node: null,
|
|
211
|
+
changed: false,
|
|
212
|
+
issues: [
|
|
213
|
+
{
|
|
214
|
+
...where,
|
|
215
|
+
severity: "error",
|
|
216
|
+
code: "newer-version",
|
|
217
|
+
message: `Section ${node.id} was saved by a newer "${node.type}" (v${node.v}) than this code knows (v${def.version}).`
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
let props = node.props;
|
|
223
|
+
for (let n = node.v + 1; n <= def.version; n++) {
|
|
224
|
+
const step = def.migrations?.[n];
|
|
225
|
+
try {
|
|
226
|
+
props = step ? step(structuredClone(props)) : props;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return {
|
|
229
|
+
node: null,
|
|
230
|
+
changed: false,
|
|
231
|
+
issues: [{ ...where, severity: "error", code: "migration-failed", message: `Migrating section ${node.id} to v${n} failed: ${err.message}` }]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const parsed = def.schema.safeParse(props);
|
|
236
|
+
if (!parsed.success) {
|
|
237
|
+
return {
|
|
238
|
+
node: null,
|
|
239
|
+
changed: false,
|
|
240
|
+
issues: [
|
|
241
|
+
{
|
|
242
|
+
...where,
|
|
243
|
+
severity: "error",
|
|
244
|
+
code: "invalid-props",
|
|
245
|
+
message: `Section ${node.id} (${def.label}): ${parsed.error.issues.map((i) => `${i.path.join(".") || "props"}: ${i.message}`).join("; ")}`
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const issues = [];
|
|
251
|
+
let variant = node.variant ?? def.defaultVariant;
|
|
252
|
+
if (!def.variants.some((v) => v.id === variant)) {
|
|
253
|
+
issues.push({ ...where, severity: "warning", code: "unknown-variant", message: `Section ${node.id} asks for variant "${variant}", which "${node.type}" doesn't have; showing "${def.defaultVariant}".` });
|
|
254
|
+
variant = def.defaultVariant;
|
|
255
|
+
}
|
|
256
|
+
const next = { ...node, v: def.version, variant, props: parsed.data };
|
|
257
|
+
const changed = node.v !== def.version || variant !== node.variant || stableHash(props) !== stableHash(node.props);
|
|
258
|
+
return { node: next, changed, issues };
|
|
259
|
+
}
|
|
260
|
+
function migratePage(doc, lookup) {
|
|
261
|
+
const issues = [];
|
|
262
|
+
let changed = false;
|
|
263
|
+
const content = doc.content.map((node) => {
|
|
264
|
+
const m = migrateNode(node, lookup);
|
|
265
|
+
issues.push(...m.issues);
|
|
266
|
+
if (m.node && m.changed) changed = true;
|
|
267
|
+
return m.node ?? node;
|
|
268
|
+
});
|
|
269
|
+
return { doc: { ...doc, content }, changed, issues };
|
|
270
|
+
}
|
|
271
|
+
function migrateSite(site, lookup) {
|
|
272
|
+
const issues = [];
|
|
273
|
+
let changed = false;
|
|
274
|
+
const run = (nodes) => nodes.map((node) => {
|
|
275
|
+
const m = migrateNode(node, lookup);
|
|
276
|
+
issues.push(...m.issues);
|
|
277
|
+
if (m.node && m.changed) changed = true;
|
|
278
|
+
return m.node ?? node;
|
|
279
|
+
});
|
|
280
|
+
const header = run(site.header);
|
|
281
|
+
const footer = run(site.footer);
|
|
282
|
+
return { site: { ...site, header, footer }, changed, issues };
|
|
283
|
+
}
|
|
284
|
+
|
|
280
285
|
// src/core/a11y.ts
|
|
281
286
|
var isPublishable = (issues) => !issues.some((i) => i.severity === "error");
|
|
282
287
|
function images(value, path = []) {
|
|
283
288
|
if (!value || typeof value !== "object") return [];
|
|
284
289
|
if (Array.isArray(value)) return value.flatMap((v, i) => images(v, [...path, String(i)]));
|
|
285
290
|
const record = value;
|
|
286
|
-
const found = typeof record.src === "string" && ("alt" in record || "decorative" in record || /\.(png|jpe?g|webp|avif|gif|svg)(\?|$)/i.test(record.src)) ? [{ path: path.join("."), image: record }] : [];
|
|
291
|
+
const found = typeof record.src === "string" && !("mime" in record) && ("alt" in record || "decorative" in record || /\.(png|jpe?g|webp|avif|gif|svg)(\?|$)/i.test(record.src)) ? [{ path: path.join("."), image: record }] : [];
|
|
287
292
|
return [...found, ...Object.entries(record).flatMap(([k, v]) => k === "src" ? [] : images(v, [...path, k]))];
|
|
288
293
|
}
|
|
289
294
|
function richTextHeadings(value) {
|
|
@@ -368,7 +373,7 @@ function checkSiteDoc(site, lookup = getSection) {
|
|
|
368
373
|
}
|
|
369
374
|
function checkContrast(theme) {
|
|
370
375
|
return theme.pairs.map((pair) => ({ pair, ratio: contrastRatio(pair.foreground, pair.background) })).filter(({ pair, ratio }) => ratio < pair.minimum).map(({ pair, ratio }) => ({
|
|
371
|
-
severity: "error",
|
|
376
|
+
severity: pair.severity ?? "error",
|
|
372
377
|
code: "contrast",
|
|
373
378
|
message: `In ${pair.mode} mode, ${pair.name} is ${ratio.toFixed(2)}:1 (${pair.foreground} on ${pair.background}); it needs ${pair.minimum}:1. Adjust the brand colours.`
|
|
374
379
|
}));
|
|
@@ -384,6 +389,7 @@ function checkRelease(snapshot, options = {}) {
|
|
|
384
389
|
issues.push(...checkPage(page, lookup, options.reserved));
|
|
385
390
|
}
|
|
386
391
|
if (!paths.has("/")) issues.push({ severity: "error", code: "no-home", message: "The site has no home page (a page at /)." });
|
|
392
|
+
issues.push(...checkBlog(snapshot, lookup));
|
|
387
393
|
for (const r of snapshot.site.redirects) {
|
|
388
394
|
if (paths.has(r.from)) issues.push({ severity: "error", code: "redirect-shadows-page", path: r.from, message: `The redirect from ${r.from} hides the page there; remove the redirect.` });
|
|
389
395
|
const target = r.to.split(/[?#]/)[0];
|
|
@@ -397,6 +403,55 @@ function checkRelease(snapshot, options = {}) {
|
|
|
397
403
|
if (options.theme) issues.push(...checkContrast(options.theme));
|
|
398
404
|
return issues;
|
|
399
405
|
}
|
|
406
|
+
function checkBlog(snapshot, lookup = getSection) {
|
|
407
|
+
const issues = [];
|
|
408
|
+
const postPages = snapshot.pages.filter((p) => p.kind === "post");
|
|
409
|
+
if (postPages.length > 1) {
|
|
410
|
+
issues.push({ severity: "error", code: "post-page-count", message: `The site has ${postPages.length} post pages (${postPages.map((p) => p.path).join(", ")}); posts render in one.` });
|
|
411
|
+
}
|
|
412
|
+
for (const page of postPages) {
|
|
413
|
+
const base = { pageId: page.id, path: page.path };
|
|
414
|
+
if (page.path === "/") issues.push({ ...base, severity: "error", code: "post-page-home", message: "The home page can't be the post page: posts would take over the site's paths. Put it at /blog." });
|
|
415
|
+
if (page.path.split("/").filter(Boolean).length >= 8) issues.push({ ...base, severity: "error", code: "post-page-depth", message: `${page.path}: the post page is too deep for posts to go below it.` });
|
|
416
|
+
if (!page.doc.content.some((n) => !n.hidden && n.type === "post-body")) {
|
|
417
|
+
issues.push({ ...base, severity: "error", code: "post-page-body", message: `${page.path}: the post page needs a Post section, where each post's title and text go.` });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!postPages.length) {
|
|
421
|
+
for (const page of snapshot.pages) {
|
|
422
|
+
for (const node of page.doc.content) {
|
|
423
|
+
if (!node.hidden && node.type === "blog-list" && lookup(node.type)) {
|
|
424
|
+
issues.push({ pageId: page.id, path: page.path, sectionId: node.id, severity: "error", code: "blog-list-no-post-page", message: `${page.path}: a post list needs the site's post page (a page of kind "post", such as /blog) for its links to open.` });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return issues;
|
|
430
|
+
}
|
|
431
|
+
function checkPost(slug, doc) {
|
|
432
|
+
const issues = [];
|
|
433
|
+
const at = `The post ${JSON.stringify(slug)}`;
|
|
434
|
+
if (!PostSlug.safeParse(slug).success) issues.push({ severity: "error", code: "post-slug", message: `${at}: a slug is lowercase letters, digits and single hyphens.` });
|
|
435
|
+
const parsed = PostDoc.safeParse(doc);
|
|
436
|
+
if (!parsed.success) {
|
|
437
|
+
issues.push({ severity: "error", code: "post-invalid", message: `${at}: ${parsed.error.issues.map((i) => `${i.path.join(".") || "post"} ${i.message}`).join("; ")}` });
|
|
438
|
+
return issues;
|
|
439
|
+
}
|
|
440
|
+
for (const { path, image } of images(parsed.data)) {
|
|
441
|
+
if (path.startsWith("seo")) continue;
|
|
442
|
+
const alt = typeof image.alt === "string" ? image.alt.trim() : "";
|
|
443
|
+
if (!alt && image.decorative !== true) issues.push({ severity: "error", code: "image-alt", message: `${at}: an image (${path || "image"}) needs alt text, or to be marked decorative.` });
|
|
444
|
+
}
|
|
445
|
+
const levels = richTextHeadings(parsed.data.body);
|
|
446
|
+
for (let i = 0; i < levels.length; i++) {
|
|
447
|
+
const level = levels[i];
|
|
448
|
+
const previous = i === 0 ? 1 : levels[i - 1];
|
|
449
|
+
if (level === 1) issues.push({ severity: "error", code: "heading-h1", message: `${at}: the text can't hold a level 1 heading; the post's title is the page's.` });
|
|
450
|
+
else if (level > previous + 1) issues.push({ severity: "error", code: "heading-order", message: `${at}: a level ${level} heading follows a level ${previous}; don't skip a level.` });
|
|
451
|
+
}
|
|
452
|
+
if (!parsed.data.excerpt && !parsed.data.seo?.description) issues.push({ severity: "warning", code: "seo-description", message: `${at}: no excerpt for the list, search results and the feed.` });
|
|
453
|
+
return issues;
|
|
454
|
+
}
|
|
400
455
|
function checkHtmlOutline(html) {
|
|
401
456
|
const issues = [];
|
|
402
457
|
const body = html.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
|
|
@@ -415,6 +470,11 @@ function checkHtmlOutline(html) {
|
|
|
415
470
|
|
|
416
471
|
// src/core/store.ts
|
|
417
472
|
var isConflict = (result) => "conflict" in result;
|
|
473
|
+
var ASSET_TYPES = Object.freeze(["image/png", "image/jpeg", "image/webp", "image/avif", "image/gif", "application/pdf"]);
|
|
474
|
+
function assetPath(siteId, id, name, mime) {
|
|
475
|
+
const clean = (name ?? "file").toLowerCase().replace(/[^a-z0-9.]+/g, "-").replace(/\.{2,}/g, ".").replace(/^[.-]+/, "").slice(0, 80) || "file";
|
|
476
|
+
return `sites/${siteId}/${mime === "application/pdf" ? "files/" : ""}${id}-${clean}`;
|
|
477
|
+
}
|
|
418
478
|
var PublishBlockedError = class extends Error {
|
|
419
479
|
constructor(issues) {
|
|
420
480
|
const errors = issues.filter((i) => i.severity === "error");
|
|
@@ -479,6 +539,39 @@ function withCacheInvalidation(store, invalidate) {
|
|
|
479
539
|
deletePage: (id) => store.deletePage(id),
|
|
480
540
|
listReleases: (id) => store.listReleases(id),
|
|
481
541
|
...store.putAsset ? { putAsset: store.putAsset.bind(store) } : {},
|
|
542
|
+
...store.listLivePosts ? { listLivePosts: store.listLivePosts.bind(store) } : {},
|
|
543
|
+
...store.getLivePost ? { getLivePost: store.getLivePost.bind(store) } : {},
|
|
544
|
+
...store.listPosts ? { listPosts: store.listPosts.bind(store) } : {},
|
|
545
|
+
...store.getPost ? { getPost: store.getPost.bind(store) } : {},
|
|
546
|
+
...store.savePost ? { savePost: store.savePost.bind(store) } : {},
|
|
547
|
+
...store.publishPost ? {
|
|
548
|
+
async publishPost(postId) {
|
|
549
|
+
const meta = await store.publishPost(postId);
|
|
550
|
+
await purge(meta.siteId, "post");
|
|
551
|
+
return meta;
|
|
552
|
+
}
|
|
553
|
+
} : {},
|
|
554
|
+
...store.unpublishPost ? {
|
|
555
|
+
async unpublishPost(postId) {
|
|
556
|
+
const meta = await store.unpublishPost(postId);
|
|
557
|
+
await purge(meta.siteId, "post");
|
|
558
|
+
return meta;
|
|
559
|
+
}
|
|
560
|
+
} : {},
|
|
561
|
+
...store.deletePost ? {
|
|
562
|
+
async deletePost(postId) {
|
|
563
|
+
const deleted = await store.deletePost(postId);
|
|
564
|
+
if (deleted) await purge(deleted.siteId, "post");
|
|
565
|
+
return deleted;
|
|
566
|
+
}
|
|
567
|
+
} : {},
|
|
568
|
+
...store.importPosts ? {
|
|
569
|
+
async importPosts(siteId, list) {
|
|
570
|
+
const result = await store.importPosts(siteId, list);
|
|
571
|
+
await purge(siteId, "post");
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
} : {},
|
|
482
575
|
async publish(siteId, options) {
|
|
483
576
|
const release = await store.publish(siteId, options);
|
|
484
577
|
await purge(siteId, "publish");
|
|
@@ -491,6 +584,23 @@ function withCacheInvalidation(store, invalidate) {
|
|
|
491
584
|
}
|
|
492
585
|
};
|
|
493
586
|
}
|
|
587
|
+
function prepareImportPost(item) {
|
|
588
|
+
const slug = typeof item?.slug === "string" ? item.slug : String(item?.slug);
|
|
589
|
+
try {
|
|
590
|
+
validatePostSlug(slug);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return { slug, reason: err.message };
|
|
593
|
+
}
|
|
594
|
+
const parsed = PostDoc.safeParse(item.doc);
|
|
595
|
+
if (!parsed.success) return { slug, reason: `The post doesn't parse: ${parsed.error.issues.map((i) => `${i.path.join(".") || "post"} ${i.message}`).join("; ")}` };
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
const given = item.publishedAt ? new Date(item.publishedAt) : new Date(now);
|
|
598
|
+
if (Number.isNaN(given.getTime()) || given.getTime() < 0) return { slug, reason: `"${item.publishedAt}" isn't a usable publish date.` };
|
|
599
|
+
const date = new Date(Math.min(given.getTime(), now));
|
|
600
|
+
const errors = checkPost(slug, parsed.data).filter((i) => i.severity === "error").map((i) => i.message);
|
|
601
|
+
const status = item.status === "draft" || errors.length ? "draft" : "published";
|
|
602
|
+
return { doc: parsed.data, status, publishedAt: date.toISOString(), issues: errors };
|
|
603
|
+
}
|
|
494
604
|
var clone = (value) => structuredClone(value);
|
|
495
605
|
function createMemorySiteStore(options = {}) {
|
|
496
606
|
const reserved = options.reserved ?? DEFAULT_RESERVED_PATHS;
|
|
@@ -498,6 +608,7 @@ function createMemorySiteStore(options = {}) {
|
|
|
498
608
|
const sites = /* @__PURE__ */ new Map();
|
|
499
609
|
const pages = /* @__PURE__ */ new Map();
|
|
500
610
|
const releases = /* @__PURE__ */ new Map();
|
|
611
|
+
const posts = /* @__PURE__ */ new Map();
|
|
501
612
|
let seq = 0;
|
|
502
613
|
const id = (prefix) => `${prefix}_${(++seq).toString(36).padStart(4, "0")}`;
|
|
503
614
|
const editable = (siteId) => {
|
|
@@ -510,6 +621,20 @@ function createMemorySiteStore(options = {}) {
|
|
|
510
621
|
const meta = (p) => ({ id: p.id, siteId: p.siteId, path: p.path, kind: p.kind, title: p.doc.root.props.title, sort: p.sort, rev: p.rev });
|
|
511
622
|
const releaseMeta = ({ snapshot: _s, ...m }) => clone(m);
|
|
512
623
|
const pathTaken = (siteId, path, except) => livePages(siteId).some((p) => p.path === path && p.id !== except);
|
|
624
|
+
const sitePosts = (siteId) => [...posts.values()].filter((p) => p.siteId === siteId && !p.deleted);
|
|
625
|
+
const postMeta = (p) => ({ id: p.id, siteId: p.siteId, slug: p.slug, title: p.doc.title, status: p.status, rev: p.rev, publishedAt: p.publishedAt, updatedAt: p.updatedAt });
|
|
626
|
+
const byNewest = (a, b) => (b.publishedAt ?? b.updatedAt).localeCompare(a.publishedAt ?? a.updatedAt);
|
|
627
|
+
const livePost = (p) => p.status === "published" && p.published && p.publishedAt ? { slug: p.slug, publishedAt: p.publishedAt, updatedAt: p.published.at, doc: clone(p.published.doc) } : null;
|
|
628
|
+
const siteIsLive = (siteId) => {
|
|
629
|
+
const s = sites.get(siteId);
|
|
630
|
+
return Boolean(s && s.status === "live" && s.liveReleaseId);
|
|
631
|
+
};
|
|
632
|
+
const editablePost = (postId) => {
|
|
633
|
+
const p = posts.get(postId);
|
|
634
|
+
if (!p || p.deleted) throw new Error(`No post ${postId}.`);
|
|
635
|
+
editable(p.siteId);
|
|
636
|
+
return p;
|
|
637
|
+
};
|
|
513
638
|
const store = {
|
|
514
639
|
async createSite(input) {
|
|
515
640
|
const siteId = input.id ?? id("site");
|
|
@@ -644,15 +769,110 @@ function createMemorySiteStore(options = {}) {
|
|
|
644
769
|
},
|
|
645
770
|
async putAsset(siteId, file, assetMeta = {}) {
|
|
646
771
|
editable(siteId);
|
|
772
|
+
const mime = file.type || "application/octet-stream";
|
|
773
|
+
if (!ASSET_TYPES.includes(mime)) throw new Error(`${mime} isn't an image (PNG, JPEG, WebP, AVIF, GIF) or a PDF.`);
|
|
647
774
|
const assetId = id("asset");
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
775
|
+
const path = assetPath(siteId, assetId, assetMeta.name, mime);
|
|
776
|
+
return { id: assetId, siteId, path, url: `/${path}`, mime, alt: assetMeta.alt ?? null, width: assetMeta.width ?? null, height: assetMeta.height ?? null };
|
|
777
|
+
},
|
|
778
|
+
async listLivePosts(siteId) {
|
|
779
|
+
if (!siteIsLive(siteId)) return [];
|
|
780
|
+
return sitePosts(siteId).map(livePost).filter((p) => p !== null).sort(byNewest).slice(0, MAX_LIVE_POSTS).map(postSummary);
|
|
781
|
+
},
|
|
782
|
+
async getLivePost(siteId, slug) {
|
|
783
|
+
if (!siteIsLive(siteId)) return null;
|
|
784
|
+
const p = sitePosts(siteId).find((x) => x.slug === slug);
|
|
785
|
+
return p ? livePost(p) : null;
|
|
786
|
+
},
|
|
787
|
+
async listPosts(siteId) {
|
|
788
|
+
if (options.canEdit && !options.canEdit(siteId)) return [];
|
|
789
|
+
return sitePosts(siteId).map(postMeta).sort(byNewest);
|
|
790
|
+
},
|
|
791
|
+
async getPost(postId) {
|
|
792
|
+
const p = posts.get(postId);
|
|
793
|
+
if (!p || p.deleted || options.canEdit && !options.canEdit(p.siteId)) return null;
|
|
794
|
+
return { ...postMeta(p), doc: clone(p.doc) };
|
|
795
|
+
},
|
|
796
|
+
async savePost(siteId, input) {
|
|
797
|
+
editable(siteId);
|
|
798
|
+
validatePostSlug(input.slug);
|
|
799
|
+
const doc = PostDoc.parse(clone(input.doc));
|
|
800
|
+
const at = now().toISOString();
|
|
801
|
+
if (!input.id) {
|
|
802
|
+
if (sitePosts(siteId).some((p2) => p2.slug === input.slug)) throw new Error(`A post is already at ${input.slug}.`);
|
|
803
|
+
const postId = id("post");
|
|
804
|
+
posts.set(postId, { id: postId, siteId, slug: input.slug, doc, rev: 1, status: "draft", published: null, publishedAt: null, updatedAt: at, deleted: false });
|
|
805
|
+
return { id: postId, rev: 1 };
|
|
806
|
+
}
|
|
807
|
+
const p = editablePost(input.id);
|
|
808
|
+
if (p.siteId !== siteId) throw new Error(`Post ${input.id} isn't one of site ${siteId}'s.`);
|
|
809
|
+
if (p.rev !== input.rev) return { id: p.id, conflict: clone(p.doc), rev: p.rev };
|
|
810
|
+
if (input.slug !== p.slug) {
|
|
811
|
+
if (p.publishedAt) throw new Error("A post keeps its slug once it has been published: its address is out in the world.");
|
|
812
|
+
if (sitePosts(siteId).some((x) => x.slug === input.slug && x.id !== p.id)) throw new Error(`A post is already at ${input.slug}.`);
|
|
813
|
+
p.slug = input.slug;
|
|
814
|
+
}
|
|
815
|
+
p.doc = doc;
|
|
816
|
+
p.rev += 1;
|
|
817
|
+
p.updatedAt = at;
|
|
818
|
+
return { id: p.id, rev: p.rev };
|
|
819
|
+
},
|
|
820
|
+
async publishPost(postId) {
|
|
821
|
+
const p = editablePost(postId);
|
|
822
|
+
if (sites.get(p.siteId)?.status === "archived") throw new Error("An archived site can't publish posts.");
|
|
823
|
+
const issues = checkPost(p.slug, p.doc);
|
|
824
|
+
if (!isPublishable(issues)) throw new PublishBlockedError(issues);
|
|
825
|
+
const at = now().toISOString();
|
|
826
|
+
p.published = { doc: clone(p.doc), at };
|
|
827
|
+
p.status = "published";
|
|
828
|
+
p.publishedAt ??= at;
|
|
829
|
+
p.updatedAt = at;
|
|
830
|
+
return postMeta(p);
|
|
831
|
+
},
|
|
832
|
+
async unpublishPost(postId) {
|
|
833
|
+
const p = editablePost(postId);
|
|
834
|
+
p.status = "draft";
|
|
835
|
+
p.published = null;
|
|
836
|
+
p.updatedAt = now().toISOString();
|
|
837
|
+
return postMeta(p);
|
|
838
|
+
},
|
|
839
|
+
async deletePost(postId) {
|
|
840
|
+
const p = posts.get(postId);
|
|
841
|
+
if (!p || p.deleted) return null;
|
|
842
|
+
editable(p.siteId);
|
|
843
|
+
p.deleted = true;
|
|
844
|
+
p.status = "draft";
|
|
845
|
+
p.published = null;
|
|
846
|
+
return { siteId: p.siteId };
|
|
847
|
+
},
|
|
848
|
+
async importPosts(siteId, list) {
|
|
849
|
+
if (editable(siteId).status === "archived") throw new Error(`No site ${siteId} to import into: it is archived.`);
|
|
850
|
+
if (list.length > MAX_IMPORT_POSTS) throw new Error(`Import at most ${MAX_IMPORT_POSTS} posts at a time.`);
|
|
851
|
+
const result = { imported: [], skipped: [] };
|
|
852
|
+
const seen = new Set(sitePosts(siteId).map((p) => p.slug));
|
|
853
|
+
for (const item of list) {
|
|
854
|
+
const prepared = prepareImportPost(item);
|
|
855
|
+
if ("reason" in prepared) {
|
|
856
|
+
result.skipped.push(prepared);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (seen.has(item.slug)) {
|
|
860
|
+
result.skipped.push({ slug: item.slug, reason: "The site already has a post at this slug." });
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
seen.add(item.slug);
|
|
864
|
+
const postId = id("post");
|
|
865
|
+
const at = now().toISOString();
|
|
866
|
+
const published = prepared.status === "published" ? { doc: clone(prepared.doc), at: prepared.publishedAt } : null;
|
|
867
|
+
posts.set(postId, { id: postId, siteId, slug: item.slug, doc: prepared.doc, rev: 1, status: prepared.status, published, publishedAt: published ? prepared.publishedAt : null, updatedAt: at, deleted: false });
|
|
868
|
+
result.imported.push({ slug: item.slug, id: postId, status: prepared.status, ...prepared.issues.length ? { issues: prepared.issues } : {} });
|
|
869
|
+
}
|
|
870
|
+
return result;
|
|
651
871
|
}
|
|
652
872
|
};
|
|
653
873
|
return store;
|
|
654
874
|
}
|
|
655
875
|
|
|
656
|
-
export { FONT_CHOICES, PublishBlockedError, SiteConflictError, brandDisplayName, checkContrast, checkHtmlOutline, checkPage, checkRelease, checkSiteDoc, contrastRatio, createMemorySiteStore, formCacheTag, hexToHslTriplet, isConflict, isPublishable, isSiteBrand, luminance, migrateNode, migratePage, migrateSite, mix, parseHex, prepareRelease, siteCacheTag, siteTheme, stableHash, themeCss, toCanonicalBrand, withCacheInvalidation };
|
|
657
|
-
//# sourceMappingURL=chunk-
|
|
658
|
-
//# sourceMappingURL=chunk-
|
|
876
|
+
export { ASSET_TYPES, FONT_CHOICES, PublishBlockedError, SiteConflictError, assetPath, brandDisplayName, checkBlog, checkContrast, checkHtmlOutline, checkPage, checkPost, checkRelease, checkSiteDoc, contrastRatio, createMemorySiteStore, formCacheTag, hexToHslTriplet, isConflict, isPublishable, isSiteBrand, luminance, migrateNode, migratePage, migrateSite, mix, parseHex, prepareImportPost, prepareRelease, siteCacheTag, siteTheme, stableHash, themeCss, toCanonicalBrand, withCacheInvalidation };
|
|
877
|
+
//# sourceMappingURL=chunk-FBQF2L7O.js.map
|
|
878
|
+
//# sourceMappingURL=chunk-FBQF2L7O.js.map
|