bearminion 1.4.0 → 1.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # bearminion
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eec6f3d: New starter sections and editor features.
8
+
9
+ - **Gallery** section: grid, masonry and featured layouts, a keyboard-friendly lightbox, an optional box around each picture, and required alt text.
10
+ - **Text** section: long-form rich text for an About or policy page, with a narrow, readable column on by default. Rich-text fields can opt into **H2** and **H3** (`data-rich-headings`); `renderRichText`, `sanitizeRichHtml` and `stripPlainRich` take a `{ headings }` option.
11
+ - **Media** now covers video: the separate Video section is gone, and Media gains `playback` (`controls` or `loop`). Content `schemaVersion` 2 migrates existing Video sections to Media automatically.
12
+ - **Video uploads**: upload an mp4, webm, mov or ogv to `public/videos` from the editor (`videosDir`, `maxVideoBytes`, default 50 MB), pick an uploaded one, and see what a pasted link is or why it will not play. New API routes `GET`/`POST /videos` and `DELETE /videos/:filename`.
13
+ - `parseMediaSource` and `VIDEO_FILE` are exported from `bearminion`.
14
+
15
+ ### Patch Changes
16
+
17
+ - eec6f3d: Clearer package descriptions and READMEs: what BearMinion is, who it is for, and what `create-bearminion` refuses (a folder that is not new or empty).
18
+
19
+ ## 1.5.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 8a428e7: Sitewide JSON-LD. `site.seo.jsonLd` is optional (up to 10 blocks, 50 KB, so no migration) and the default template emits its blocks on every page. A page's own `seo.jsonLd` still replaces only the generated `WebPage` block, and a block with the same `@id` is emitted once, the page's first. With the conventional ids `<url>/#organization` and `<url>/#website` in the site's blocks, the generated `WebPage` gets `isPartOf` and `publisher` as `@id` references. The Site tab has a Structured data group with an Edit JSON-LD dialog and a Generate button that fills an `Organization` and a `WebSite` from the site name, address and language, replacing blocks with the same `@id`. The logo is added only for a full https address. A warning shows while the site address is still `example.com`. `siteJsonLdIds`, `mergeJsonLd`, `hasJsonLdId`, `generateSiteJsonLd`, `upsertJsonLd` and `isPlaceholderSiteUrl` are exported; themes that emit JSON-LD should use `mergeJsonLd` (see the theme docs).
24
+
3
25
  ## 1.4.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- A website you edit by clicking on it. No database, no admin panel, no server.<br />
6
+ The BearMinion for the bare minimum. A website you edit by clicking on it: no database to patch at 3 AM, no admin panel, no server.<br />
7
7
  Built on <a href="https://astro.build">Astro</a>.
8
8
  </p>
9
9
 
@@ -34,7 +34,7 @@ It asks for the site's name and web address. Open the address `npm run dev` prin
34
34
  ## What you can change
35
35
 
36
36
  - **Text** right on the page, with bold, italic, code, links and lists
37
- - **Sections**: thirteen ready-made types, each with three layouts, added from a visual picker
37
+ - **Sections**: fourteen ready-made types, each with two or three layouts, added from a visual picker
38
38
  - **Pages, menus and footer**, with titles and descriptions for search and sharing
39
39
  - **Images**: upload, choose how they fill their frame and which part stays in view
40
40
  - **Colors**: pick three, and the editor builds a full light and dark palette that stays readable
package/dist/astro.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as SiteDefinition, I as SiteOf, It as ContentMigrator, _ as SectionRegistry, d as SectionOf, g as ContentModel, t as Page } from "./page-C87bm-bf.js";
2
- import { a as StoreKeyError, c as createFileStore, i as FileStoreOptions, n as EditorContext, o as StoreOperation, r as ContentStore, s as UploadStore, t as BearminionOptions } from "./options-qFsKXOuE.js";
1
+ import { A as SectionRegistry, J as SiteDefinition, Y as SiteOf, Yt as ContentMigrator, k as ContentModel, t as Page, u as SectionOf } from "./page-BFuPEq6Q.js";
2
+ import { a as StoreKeyError, c as createFileStore, i as FileStoreOptions, n as EditorContext, o as StoreOperation, r as ContentStore, s as UploadStore, t as BearminionOptions } from "./options-DdCyRbPs.js";
3
3
  import { AstroIntegration } from "astro";
4
4
  //#region src/integration.d.ts
5
5
  interface BearminionIntegration extends AstroIntegration {
package/dist/astro.js CHANGED
@@ -1,15 +1,15 @@
1
- import { a as imageFields, c as ContentReadError, l as ContentValidationError, r as loadThemePreview, t as PreviewUnavailableError } from "./preview-CTVEBdEY.js";
2
- import { t as defaultTheme } from "./theme-DMpt3hcT.js";
3
- import { c as withoutOrphanSections, d as isOrphanSection, i as isSystemPage, n as createPageSchema$1, r as createTolerantPageSchema } from "./page-ON4Jkcm-.js";
4
- import { a as DEFAULT_UPLOADS_DIR, i as DEFAULT_CONTENT_DIR, n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-B56wxCiC.js";
5
- import { a as JSON_BODY_LIMIT, c as isInside, d as resolveDir, f as resolveInside, i as IMAGE_BYTE_LIMIT, l as isLocalRequest, n as contentReloadPlugin, o as MULTIPART_BODY_LIMIT, r as dropEarlyReloadAfterSave, s as asDirectory, u as readSlug } from "./reload-Cq434rtI.js";
1
+ import { a as imageFields, c as ContentReadError, l as ContentValidationError, r as loadThemePreview, t as PreviewUnavailableError } from "./preview--S1QUrMM.js";
2
+ import { t as defaultTheme } from "./theme-DFjHjgFO.js";
3
+ import { i as isSystemPage, n as createPageSchema$1, r as createTolerantPageSchema, s as withoutOrphanSections, u as isOrphanSection } from "./page-NOFVEqn1.js";
4
+ import { a as DEFAULT_UPLOADS_DIR, i as DEFAULT_CONTENT_DIR, n as createMigrator, o as DEFAULT_VIDEOS_DIR, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-b408EaLp.js";
5
+ import { a as JSON_BODY_LIMIT, c as isInside, d as resolveDir, f as resolveInside, i as IMAGE_BYTE_LIMIT, l as isLocalRequest, n as contentReloadPlugin, o as MULTIPART_BODY_LIMIT, r as dropEarlyReloadAfterSave, s as asDirectory, u as readSlug } from "./reload-D0vwI0OT.js";
6
6
  import { S as isAssetPath, b as describeSiteRef, c as projectId, v as assetRefs, y as describePageRef } from "./draft-store-1Y4-sn45.js";
7
7
  import { z } from "zod";
8
8
  import fs from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import path from "node:path";
11
- import { access, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
12
11
  import { randomBytes } from "node:crypto";
12
+ import { access, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
13
13
  import { execFile } from "node:child_process";
14
14
  import { promisify } from "node:util";
15
15
  //#region src/editor-client.ts
@@ -35,6 +35,112 @@ function editorClientPlugin() {
35
35
  };
36
36
  }
37
37
  //#endregion
38
+ //#region src/videos.ts
39
+ const EXTENSION$1 = {
40
+ mp4: ".mp4",
41
+ mov: ".mov",
42
+ webm: ".webm",
43
+ ogv: ".ogv"
44
+ };
45
+ const MIME_TO_KIND$1 = {
46
+ "video/mp4": "mp4",
47
+ "video/x-m4v": "mp4",
48
+ "video/quicktime": "mov",
49
+ "video/webm": "webm",
50
+ "video/ogg": "ogv"
51
+ };
52
+ /** Room for multipart boundaries around a max-size video. */
53
+ function videoBodyLimit(maxVideoBytes) {
54
+ return maxVideoBytes + 262144;
55
+ }
56
+ /** The container a file really is, from its first bytes. The file's name and declared type are not trusted. */
57
+ function sniffVideo(bytes) {
58
+ if (bytes.length >= 12 && ascii$1(bytes, 4, 8) === "ftyp") {
59
+ const brand = ascii$1(bytes, 8, 12);
60
+ if (brand === "avif" || brand === "avis") return null;
61
+ return brand === "qt " ? "mov" : "mp4";
62
+ }
63
+ if (bytes.length >= 4 && bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) return ascii$1(bytes, 0, Math.min(bytes.length, 64)).includes("webm") ? "webm" : null;
64
+ if (bytes.length >= 4 && ascii$1(bytes, 0, 4) === "OggS") return "ogv";
65
+ return null;
66
+ }
67
+ function videoKindFromMime(mime) {
68
+ const base = mime.split(";", 1)[0]?.trim().toLowerCase() ?? "";
69
+ return MIME_TO_KIND$1[base] ?? null;
70
+ }
71
+ /** The address a file in `public/` is served at. */
72
+ function publicUrl(root, filePath) {
73
+ return `/${path.relative(path.resolve(root, "public"), filePath).split(path.sep).join("/")}`;
74
+ }
75
+ async function saveVideo(options) {
76
+ await mkdir(options.videosDir, { recursive: true });
77
+ const extension = EXTENSION$1[options.kind];
78
+ const stem = safeStem$1(options.originalName);
79
+ for (let attempt = 0; attempt < 8; attempt += 1) {
80
+ const filename = `${stem}-${randomBytes(4).toString("hex")}${extension}`;
81
+ const filePath = resolveInside(options.videosDir, filename);
82
+ if (filePath === null) throw new Error("Resolved upload path escaped the videos directory.");
83
+ try {
84
+ await writeFile(filePath, options.bytes, { flag: "wx" });
85
+ } catch (error) {
86
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") continue;
87
+ throw error;
88
+ }
89
+ if (!isInside(options.root, filePath)) throw new Error("Upload path is outside the project root.");
90
+ return {
91
+ filename,
92
+ path: path.relative(options.root, filePath).split(path.sep).join("/"),
93
+ url: publicUrl(options.root, filePath),
94
+ bytes: options.bytes.byteLength
95
+ };
96
+ }
97
+ throw new Error("Unable to allocate a unique upload filename.");
98
+ }
99
+ async function listVideos(root, videosDir) {
100
+ let entries;
101
+ try {
102
+ entries = await readdir(videosDir, { withFileTypes: true });
103
+ } catch {
104
+ return [];
105
+ }
106
+ const videos = [];
107
+ for (const entry of entries) {
108
+ if (!entry.isFile() || entry.name.startsWith(".")) continue;
109
+ if (!Object.values(EXTENSION$1).includes(path.extname(entry.name).toLowerCase())) continue;
110
+ const filePath = resolveInside(videosDir, entry.name);
111
+ if (filePath === null) continue;
112
+ const info = await stat(filePath).catch(() => null);
113
+ videos.push({
114
+ filename: entry.name,
115
+ path: path.relative(root, filePath).split(path.sep).join("/"),
116
+ url: publicUrl(root, filePath),
117
+ ...info ? { bytes: info.size } : {}
118
+ });
119
+ }
120
+ videos.sort((a, b) => a.filename.localeCompare(b.filename));
121
+ return videos;
122
+ }
123
+ /** Delete one uploaded video by file name. False when there is no such file or the name is not a plain video file name. */
124
+ async function deleteVideo(videosDir, filename) {
125
+ if (filename !== path.basename(filename) || filename.startsWith(".")) return false;
126
+ if (!Object.values(EXTENSION$1).includes(path.extname(filename).toLowerCase())) return false;
127
+ const filePath = resolveInside(videosDir, filename);
128
+ if (filePath === null) return false;
129
+ try {
130
+ if (!(await stat(filePath)).isFile()) return false;
131
+ await unlink(filePath);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ function safeStem$1(originalName) {
138
+ return path.win32.basename(path.basename(originalName)).replace(/\.[^.]+$/, "").normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "video";
139
+ }
140
+ function ascii$1(bytes, from, to) {
141
+ return Buffer.from(bytes.subarray(from, to)).toString("latin1");
142
+ }
143
+ //#endregion
38
144
  //#region src/asset-refs.ts
39
145
  /**
40
146
  * Where an image path may live on disk: under `src/assets` or the uploads directory. The absolute path, or null
@@ -237,7 +343,7 @@ function pageKey(slug) {
237
343
  return `${PAGES_DIR}/${slug}.json`;
238
344
  }
239
345
  /** The content store for a local project: content as JSON files, uploads in a folder. */
240
- function createFileStore({ root, contentDir, uploadsDir }) {
346
+ function createFileStore({ root, contentDir, uploadsDir, videosDir }) {
241
347
  function resolve(key) {
242
348
  const parts = key.split("/");
243
349
  if (key === "" || parts.some((part) => part === "" || part === "." || part === "..")) throw new StoreKeyError(key);
@@ -282,6 +388,17 @@ function createFileStore({ root, contentDir, uploadsDir }) {
282
388
  if (filePath === null) return false;
283
389
  return stat(filePath).then((info) => info.isFile(), () => false);
284
390
  },
391
+ ...videosDir === void 0 ? {} : { videos: {
392
+ list: () => listVideos(root, videosDir),
393
+ save: ({ originalName, bytes, kind }) => saveVideo({
394
+ root,
395
+ videosDir,
396
+ originalName,
397
+ bytes,
398
+ kind
399
+ }),
400
+ remove: (filename) => deleteVideo(videosDir, filename)
401
+ } },
285
402
  uploads: {
286
403
  async list() {
287
404
  return listImages(root, uploadsDir);
@@ -312,17 +429,21 @@ function resolveEditorContext(options, rootInput) {
312
429
  const root = asDirectory(rootInput);
313
430
  const contentDir = resolveDir(root, options?.contentDir, DEFAULT_CONTENT_DIR);
314
431
  const uploadsDir = resolveDir(root, options?.uploadsDir, DEFAULT_UPLOADS_DIR);
432
+ const videosDir = resolveDir(root, options?.videosDir, DEFAULT_VIDEOS_DIR);
315
433
  return {
316
434
  root,
317
435
  contentDir,
318
436
  uploadsDir,
437
+ videosDir,
438
+ maxVideoBytes: options?.maxVideoBytes ?? 52428800,
319
439
  sections: options.sections,
320
440
  site: options.site,
321
441
  theme: options.theme ?? defaultTheme,
322
442
  store: options.store ?? createFileStore({
323
443
  root,
324
444
  contentDir,
325
- uploadsDir
445
+ uploadsDir,
446
+ videosDir
326
447
  }),
327
448
  migrator: createMigrator(options),
328
449
  git: options?.git === true
@@ -452,7 +573,11 @@ async function git(root, args) {
452
573
  }
453
574
  /** The folders a commit may touch: the content and the uploads. Relative to the project root. */
454
575
  function trackedPaths(context) {
455
- return [context.contentDir, context.uploadsDir].filter((dir) => isInside(context.root, dir)).map((dir) => path.relative(context.root, dir).split(path.sep).join("/") || ".");
576
+ return [
577
+ context.contentDir,
578
+ context.uploadsDir,
579
+ context.videosDir
580
+ ].filter((dir) => isInside(context.root, dir)).map((dir) => path.relative(context.root, dir).split(path.sep).join("/") || ".");
456
581
  }
457
582
  function statusOf(code) {
458
583
  if (code === "??") return "untracked";
@@ -606,6 +731,15 @@ async function handleEditorRequest(request, context, meta = {}) {
606
731
  if (method === "DELETE") return removeImage(context, decodeSegment(parts[1] ?? ""));
607
732
  return methodNotAllowed(["DELETE"]);
608
733
  }
734
+ if (parts.length === 1 && parts[0] === "videos") {
735
+ if (method === "GET") return getVideos(context);
736
+ if (method === "POST") return postVideo(request, context);
737
+ return methodNotAllowed(["GET", "POST"]);
738
+ }
739
+ if (parts.length === 2 && parts[0] === "videos") {
740
+ if (method === "DELETE") return removeVideo(context, decodeSegment(parts[1] ?? ""));
741
+ return methodNotAllowed(["DELETE"]);
742
+ }
609
743
  if (parts.length === 1 && parts[0] === "preview") {
610
744
  if (method === "POST") return postPreview(request, context, meta);
611
745
  return methodNotAllowed(["POST"]);
@@ -1149,6 +1283,87 @@ async function removeImage(context, filename) {
1149
1283
  filename
1150
1284
  });
1151
1285
  }
1286
+ const NO_VIDEOS = "This content store does not take video uploads.";
1287
+ async function getVideos(context) {
1288
+ if (!context.store.videos) return json(200, {
1289
+ videos: [],
1290
+ maxBytes: context.maxVideoBytes
1291
+ });
1292
+ return json(200, {
1293
+ videos: await context.store.videos.list(),
1294
+ maxBytes: context.maxVideoBytes
1295
+ });
1296
+ }
1297
+ /** Every string in a JSON value. */
1298
+ function* strings(value) {
1299
+ if (typeof value === "string") yield value;
1300
+ else if (Array.isArray(value)) for (const item of value) yield* strings(item);
1301
+ else if (typeof value === "object" && value !== null) for (const item of Object.values(value)) yield* strings(item);
1302
+ }
1303
+ /** Where saved content links the video at `url`, named like the image usages: "Home page › Media". */
1304
+ async function videoUsages(context, url) {
1305
+ const labels = Object.fromEntries(Object.values(context.sections).map((definition) => [definition.type, definition.label]));
1306
+ const usages = [];
1307
+ if ([...strings(await readStoredJson(context, "site.json", "site"))].includes(url)) usages.push("Site settings");
1308
+ for (const name of await context.store.list("pages") ?? []) {
1309
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
1310
+ const page = await readStoredJson(context, `${PAGES_DIR}/${name}`, "page");
1311
+ if (typeof page !== "object" || page === null) continue;
1312
+ const title = "title" in page && typeof page.title === "string" ? page.title : name.replace(/\.json$/, "");
1313
+ const sections = "sections" in page && Array.isArray(page.sections) ? page.sections : [];
1314
+ for (const section of sections) if ([...strings(section)].includes(url)) usages.push(`${title} › ${labels[section.type ?? ""] ?? section.type ?? "section"}`);
1315
+ }
1316
+ return usages;
1317
+ }
1318
+ async function removeVideo(context, filename) {
1319
+ const videos = context.store.videos;
1320
+ if (!videos) return problem(404, "not_found", NO_VIDEOS);
1321
+ const video = (await videos.list()).find((item) => item.filename === filename);
1322
+ if (!video) return problem(404, "not_found", "No uploaded video has that name.");
1323
+ const usages = await videoUsages(context, video.url);
1324
+ if (usages.length > 0) return json(409, {
1325
+ error: "in_use",
1326
+ message: `${filename} is still used: ${usages.join("; ")}. Choose another video there and save, then delete it.`,
1327
+ usages
1328
+ });
1329
+ if (!await videos.remove(filename)) return problem(404, "not_found", "No uploaded video has that name.");
1330
+ return json(200, {
1331
+ deleted: true,
1332
+ filename
1333
+ });
1334
+ }
1335
+ async function postVideo(request, context) {
1336
+ const videos = context.store.videos;
1337
+ if (!videos) return problem(404, "not_found", NO_VIDEOS);
1338
+ const limitMb = Math.round(context.maxVideoBytes / 1048576);
1339
+ const tooLarge = () => problem(413, "too_large", `Videos must be ${limitMb}MB or smaller. Link a hosted video instead, or compress this one.`);
1340
+ const length = contentLength(request);
1341
+ if (length !== null && length > videoBodyLimit(context.maxVideoBytes)) return tooLarge();
1342
+ if (!(request.headers.get("content-type") ?? "").toLowerCase().includes("multipart/form-data")) return problem(415, "unsupported_media_type", "Upload the video as multipart/form-data.");
1343
+ let form;
1344
+ try {
1345
+ form = await request.formData();
1346
+ } catch {
1347
+ return problem(400, "invalid_upload", "The upload body could not be read.");
1348
+ }
1349
+ const entry = form.get("file");
1350
+ if (!(entry instanceof File)) return problem(400, "invalid_upload", "Expected a multipart file field named \"file\".");
1351
+ if (entry.size > context.maxVideoBytes) return tooLarge();
1352
+ const bytes = new Uint8Array(await entry.arrayBuffer());
1353
+ if (bytes.byteLength > context.maxVideoBytes) return tooLarge();
1354
+ const declared = videoKindFromMime(entry.type);
1355
+ const sniffed = sniffVideo(bytes);
1356
+ if (declared === null || sniffed === null || declared !== sniffed) return problem(415, "unsupported_media_type", "Upload an mp4, webm, mov or ogv video whose contents match the declared type.");
1357
+ try {
1358
+ return json(201, await videos.save({
1359
+ originalName: entry.name,
1360
+ bytes,
1361
+ kind: sniffed
1362
+ }));
1363
+ } catch (error) {
1364
+ return problem(500, "write_failed", error instanceof Error ? error.message : "Upload failed.");
1365
+ }
1366
+ }
1152
1367
  async function postImage(request, context) {
1153
1368
  const length = contentLength(request);
1154
1369
  if (length !== null && length > 5505024) return problem(413, "too_large", "Upload exceeds the 5MB image limit.");
@@ -1450,10 +1665,12 @@ async function dispatch(req, res, context, rawUrl, hooks) {
1450
1665
  const headers = headersFrom(req);
1451
1666
  const method = (req.method ?? "GET").toUpperCase();
1452
1667
  const multipart = (headers.get("content-type") ?? "").toLowerCase().includes("multipart/form-data");
1453
- const limit = multipart ? MULTIPART_BODY_LIMIT : JSON_BODY_LIMIT;
1668
+ const videoUpload = multipart && stripTrailingSlash(rawUrl.split("?")[0] ?? "") === `/__bearminion/api/videos`;
1669
+ const limit = videoUpload ? videoBodyLimit(context.maxVideoBytes) : multipart ? MULTIPART_BODY_LIMIT : JSON_BODY_LIMIT;
1670
+ const tooLargeMessage = videoUpload ? "Upload exceeds the video size limit." : multipart ? "Upload exceeds the 5MB image limit." : "JSON body exceeds 1MB.";
1454
1671
  const advertised = Number(headers.get("content-length") ?? "");
1455
1672
  if (Number.isFinite(advertised) && advertised > limit) {
1456
- await send(res, jsonProblem(413, "too_large", multipart ? "Upload exceeds the 5MB image limit." : "JSON body exceeds 1MB."));
1673
+ await send(res, jsonProblem(413, "too_large", tooLargeMessage));
1457
1674
  return;
1458
1675
  }
1459
1676
  let body;
@@ -1461,7 +1678,7 @@ async function dispatch(req, res, context, rawUrl, hooks) {
1461
1678
  body = await readBody(req, limit);
1462
1679
  } catch (error) {
1463
1680
  if (error instanceof PayloadTooLargeError) {
1464
- await send(res, jsonProblem(413, "too_large", multipart ? "Upload exceeds the 5MB image limit." : "JSON body exceeds 1MB."));
1681
+ await send(res, jsonProblem(413, "too_large", tooLargeMessage));
1465
1682
  return;
1466
1683
  }
1467
1684
  throw error;
@@ -1627,7 +1844,11 @@ function bearminion(options) {
1627
1844
  if (command !== "dev") return;
1628
1845
  const context = resolveEditorContext(options, rootInput(config.root));
1629
1846
  updateConfig({ vite: {
1630
- server: { watch: { ignored: [`${context.uploadsDir.replaceAll("\\", "/")}/**`, `${context.root.replaceAll("\\", "/")}/.editor/**`] } },
1847
+ server: { watch: { ignored: [
1848
+ `${context.uploadsDir.replaceAll("\\", "/")}/**`,
1849
+ `${context.videosDir.replaceAll("\\", "/")}/**`,
1850
+ `${context.root.replaceAll("\\", "/")}/.editor/**`
1851
+ ] } },
1631
1852
  plugins: [contentReloadPlugin([context.contentDir]), editorClientPlugin()]
1632
1853
  } });
1633
1854
  injectScript("page", "import \"virtual:bearminion-editor\"");
package/dist/content.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { d as SectionOf, g as ContentModel, t as Page } from "./page-C87bm-bf.js";
1
+ import { k as ContentModel, t as Page, u as SectionOf } from "./page-BFuPEq6Q.js";
2
2
  import { z } from "zod";
3
3
  //#region src/content.d.ts
4
4
  export interface ContentCollectionsOptions {
package/dist/content.js CHANGED
@@ -1,6 +1,6 @@
1
- import { n as createPageSchema, r as createTolerantPageSchema } from "./page-ON4Jkcm-.js";
2
- import { n as createMigrator } from "./migrate-B56wxCiC.js";
3
- import { t as CONTENT_COLLECTIONS_MARKER } from "./reload-Cq434rtI.js";
1
+ import { n as createPageSchema, r as createTolerantPageSchema } from "./page-NOFVEqn1.js";
2
+ import { n as createMigrator } from "./migrate-b408EaLp.js";
3
+ import { t as CONTENT_COLLECTIONS_MARKER } from "./reload-D0vwI0OT.js";
4
4
  import { z } from "zod";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import path from "node:path";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as Tone, A as HeaderConfig, At as variantOption, B as brandLogoSchema, Bt as createMigrator, C as BRAND_LOGO_HEIGHT, Ct as Option, D as FooterColumn, Dt as themeModes, E as Footer, Et as motionOption, F as SiteDefinition, Ft as ContentMigration, G as themeColorsSchema, H as siteAnalyticsSchema, I as SiteOf, It as ContentMigrator, J as Image, K as themeSchema, L as SiteSeo, Lt as ContentVersioning, M as SiteAnalytics, Mt as FieldIcons, N as SiteConfig, Nt as ShowCondition, O as FooterConfig, Ot as toneOption, P as SiteConsent, Pt as ShowWhen, Q as Text, R as SiteWith, Rt as DocumentKind, S as defineTheme, St as NumberOption, T as DEFAULT_MAX_HEADER_ACTIONS, Tt as buttonStyles, U as siteConsentSchema, V as defineSite, W as siteSeoSchema, X as MAX_NAV_DEPTH, Y as Link, Z as RichText, _ as SectionRegistry, _t as wireframeIssues, a as isSystemPage, at as textSchema, b as ThemeFont, bt as CountRule, c as withoutOrphanSections, ct as withEmphasis, d as SectionOf, dt as defineSection, et as htmlIdSchema, f as anchorSchema, ft as WireKind, g as ContentModel, gt as wireKinds, h as API_PREFIX, ht as defineWireframe, i as createPageSchema, it as textMarkSchema, j as Site, jt as FieldChoices, k as Header, kt as tones, l as BaseSection, lt as SectionConfig, m as sectionIdSchema, mt as Wireframe, n as SYSTEM_PAGES, nt as linkSchema, o as jsonLdSchema, ot as toneSchema, p as createSectionSchema, pt as WireRect, q as ButtonStyle, r as Seo, rt as richTextSchema, s as seoSchema, st as trackingIdSchema, t as Page, tt as imageSchema, u as PropsOf, ut as SectionDefinition, v as ThemeConfig, vt as OptionalContent, w as DEFAULT_MAX_FOOTER_COLUMNS, wt as StringOption, x as defaultTheme, xt as EnumOption, y as ThemeDefinition, yt as BooleanOption, z as Theme, zt as SCHEMA_VERSION_KEY } from "./page-C87bm-bf.js";
1
+ import { $ as brandLogoSchema, $t as createMigrator, A as SectionRegistry, At as wireframeIssues, B as FooterColumn, Bt as themeModes, C as jsonLdWarnings, Ct as SectionDefinition, D as upsertJsonLd, Dt as Wireframe, E as siteJsonLdIds, Et as WireRect, F as defineTheme, Ft as NumberOption, G as SiteAnalytics, Gt as FieldIcons, H as Header, Ht as tones, I as BRAND_LOGO_HEIGHT, It as Option, J as SiteDefinition, Jt as ContentMigration, K as SiteConfig, Kt as ShowCondition, L as DEFAULT_MAX_FOOTER_COLUMNS, Lt as StringOption, M as ThemeDefinition, Mt as BooleanOption, N as ThemeFont, Nt as CountRule, O as API_PREFIX, Ot as defineWireframe, P as defaultTheme, Pt as EnumOption, Q as Theme, Qt as SCHEMA_VERSION_KEY, R as DEFAULT_MAX_HEADER_ACTIONS, Rt as buttonStyles, S as jsonLdSchema, St as SectionConfig, T as parseJsonLdInput, Tt as WireKind, U as HeaderConfig, Ut as variantOption, V as FooterConfig, Vt as toneOption, W as Site, Wt as FieldChoices, X as SiteSeo, Xt as ContentVersioning, Y as SiteOf, Yt as ContentMigrator, Z as SiteWith, Zt as DocumentKind, _ as SiteJsonLdSource, _t as textMarkSchema, a as isSystemPage, at as themeSchema, b as hasJsonLdId, bt as trackingIdSchema, c as BaseSection, ct as Link, d as anchorSchema, dt as Text, et as defineSite, f as createSectionSchema, ft as Tone, g as SiteJsonLdIds, gt as richTextSchema, h as JsonLdResult, ht as linkSchema, i as createPageSchema, it as themeColorsSchema, j as ThemeConfig, jt as OptionalContent, k as ContentModel, kt as wireKinds, l as PropsOf, lt as MAX_NAV_DEPTH, m as JsonLdBlock, mt as imageSchema, n as SYSTEM_PAGES, nt as siteConsentSchema, o as seoSchema, ot as ButtonStyle, p as sectionIdSchema, pt as htmlIdSchema, q as SiteConsent, qt as ShowWhen, r as Seo, rt as siteSeoSchema, s as withoutOrphanSections, st as Image, t as Page, tt as siteAnalyticsSchema, u as SectionOf, ut as RichText, v as formatJsonLd, vt as textSchema, w as mergeJsonLd, wt as defineSection, x as isPlaceholderSiteUrl, xt as withEmphasis, y as generateSiteJsonLd, yt as toneSchema, z as Footer, zt as motionOption } from "./page-BFuPEq6Q.js";
2
2
  import { _ as renderMarkedHtml, a as WeightMark, f as markClass, g as reconcileMarks, i as TextRun, l as fontMarks, m as marksFromEmphasis, o as applyTextMark, r as TextMark, t as FontMark, v as splitMarkedText, y as weightMarks } from "./marks-BzLa48Y9.js";
3
- import "./options-qFsKXOuE.js";
3
+ import "./options-DdCyRbPs.js";
4
4
  //#region src/errors.d.ts
5
5
  /** One schema problem, addressed by a dot path such as `sections.0.props.heading`. */
6
6
  interface ContentIssue {
@@ -70,6 +70,29 @@ interface ThemePreview<TModel extends ContentModel = ContentModel> {
70
70
  */
71
71
  export declare function defineThemePreview<TModel extends ContentModel = ContentModel>(preview: ThemePreview<TModel>): ThemePreview<TModel>;
72
72
  //#endregion
73
+ //#region src/media-source.d.ts
74
+ /**
75
+ * What a Media section's video link points at. YouTube and Vimeo links become privacy-friendly embeds
76
+ * (youtube-nocookie.com, Vimeo with Do Not Track) that load only after the visitor presses play. A link to a video file
77
+ * plays in a `<video>` element.
78
+ */
79
+ type MediaSource = {
80
+ kind: 'youtube';
81
+ id: string;
82
+ embed: string;
83
+ thumbnail: string;
84
+ } | {
85
+ kind: 'vimeo';
86
+ id: string;
87
+ embed: string;
88
+ } | {
89
+ kind: 'file';
90
+ src: string;
91
+ };
92
+ /** A path in `public/` (`/videos/demo.webm`) or an `https://` link to a video file. */
93
+ export declare const VIDEO_FILE: RegExp;
94
+ export declare function parseMediaSource(value: string): MediaSource | null;
95
+ //#endregion
73
96
  //#region src/git.d.ts
74
97
  type ChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked';
75
98
  interface ChangedFile {
@@ -120,6 +143,15 @@ interface UploadedImage {
120
143
  path: string;
121
144
  bytes?: number;
122
145
  }
146
+ /** `GET /videos` entries and the body of a successful `POST /videos`. */
147
+ interface UploadedVideo {
148
+ filename: string;
149
+ /** Project-relative path of the file. */
150
+ path: string;
151
+ /** What a Media section's `video` field holds, such as `/videos/demo-1a2b3c4d.mp4`. */
152
+ url: string;
153
+ bytes?: number;
154
+ }
123
155
  /** `GET /history/site` and `GET /history/pages/:slug` entries. */
124
156
  interface HistoryEntry {
125
157
  id: string;
@@ -162,26 +194,6 @@ interface SitePreview {
162
194
  footer: string;
163
195
  }
164
196
  //#endregion
165
- //#region src/json-ld.d.ts
166
- /** JSON-LD blocks as a page stores them: parsed, so diffs stay readable and output can be escaped safely. */
167
- type JsonLdBlock = Record<string, unknown>;
168
- type JsonLdResult = {
169
- ok: true;
170
- value: JsonLdBlock[];
171
- } | {
172
- ok: false;
173
- error: string;
174
- };
175
- /**
176
- * Parses what a person pastes into the JSON-LD box: an object, an array of objects, or either one inside a
177
- * `<script type="application/ld+json">` tag. Empty input is valid and means "no custom JSON-LD".
178
- */
179
- export declare function parseJsonLdInput(raw: string): JsonLdResult;
180
- /** The text shown in the editor box: one block as an object, several as an array, none as empty. */
181
- export declare function formatJsonLd(blocks: readonly JsonLdBlock[] | undefined): string;
182
- /** Advice, not errors: a block without `@type` or `@context` is usually a paste mistake, but Google decides what it accepts. */
183
- export declare function jsonLdWarnings(blocks: readonly JsonLdBlock[]): string[];
184
- //#endregion
185
197
  //#region src/consent.d.ts
186
198
  /** Longest copy the consent banner takes, in characters. The editor shows the same limits. */
187
199
  export declare const CONSENT_LIMITS: {
@@ -292,4 +304,4 @@ interface ContrastCheck {
292
304
  /** Text pairs a visitor reads: text on its background, label on the accent fill, link on its background. */
293
305
  export declare function contrastChecks(colors: ThemeColors | undefined): ContrastCheck[];
294
306
  //#endregion
295
- export { API_PREFIX, type ApiIssue, type ApiProblem, BRAND_LOGO_HEIGHT, type BaseSection, type BooleanOption, type ButtonStyle, type ChangedFile, type CommitRequest, type CommitResult, type ConsentCopy, type ConsentReopen, type ConsentText, type ConsentTextKey, type ContentIssue, type ContentMigration, type ContentMigrator, type ContentModel, type ContentVersioning, type ContrastCheck, type CountRule, type CreatePageRequest, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, type DocumentKind, type EnumOption, type FieldChoices, type FieldIcons, type FontMark, type Footer, type FooterColumn, type FooterConfig, type GitInfo, type Header, type HeaderConfig, type HistoryEntry, type Image, type ImageFit, type ImagePosition, type JsonLdBlock, type JsonLdResult, type Link, MAX_NAV_DEPTH, type NumberOption, type Option, type OptionalContent, type Page, type PageListItem, type PropsOf, type RichText, SCHEMA_VERSION_KEY, SYSTEM_PAGES, type SaveRequest, type SaveResponse, type SectionConfig, type SectionDefinition, type SectionOf, type SectionRegistry, type Seo, type ShowCondition, type ShowWhen, type Site, type SiteAnalytics, type SiteConfig, type SiteConsent, type SiteDefinition, type SiteOf, type SitePreview, type SitePreviewHtml, type SiteSeo, type SiteWith, type StringOption, type Text, type TextMark, type TextRun, type Theme, type ThemeColors, type ThemeConfig, type ThemeDefinition, type ThemeFont, type ThemePreview, type Tone, type UploadedImage, type WeightMark, type WireKind, type WireRect, type Wireframe, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineWireframe, fontMarks, htmlIdSchema, imageSchema, isSystemPage, jsonLdSchema, linkSchema, markClass, marksFromEmphasis, motionOption, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeModes, themeSchema, toneOption, toneSchema, tones, trackingIdSchema, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
307
+ export { API_PREFIX, type ApiIssue, type ApiProblem, BRAND_LOGO_HEIGHT, type BaseSection, type BooleanOption, type ButtonStyle, type ChangedFile, type CommitRequest, type CommitResult, type ConsentCopy, type ConsentReopen, type ConsentText, type ConsentTextKey, type ContentIssue, type ContentMigration, type ContentMigrator, type ContentModel, type ContentVersioning, type ContrastCheck, type CountRule, type CreatePageRequest, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, type DocumentKind, type EnumOption, type FieldChoices, type FieldIcons, type FontMark, type Footer, type FooterColumn, type FooterConfig, type GitInfo, type Header, type HeaderConfig, type HistoryEntry, type Image, type ImageFit, type ImagePosition, type JsonLdBlock, type JsonLdResult, type Link, MAX_NAV_DEPTH, type MediaSource, type NumberOption, type Option, type OptionalContent, type Page, type PageListItem, type PropsOf, type RichText, SCHEMA_VERSION_KEY, SYSTEM_PAGES, type SaveRequest, type SaveResponse, type SectionConfig, type SectionDefinition, type SectionOf, type SectionRegistry, type Seo, type ShowCondition, type ShowWhen, type Site, type SiteAnalytics, type SiteConfig, type SiteConsent, type SiteDefinition, type SiteJsonLdIds, type SiteJsonLdSource, type SiteOf, type SitePreview, type SitePreviewHtml, type SiteSeo, type SiteWith, type StringOption, type Text, type TextMark, type TextRun, type Theme, type ThemeColors, type ThemeConfig, type ThemeDefinition, type ThemeFont, type ThemePreview, type Tone, type UploadedImage, type UploadedVideo, type WeightMark, type WireKind, type WireRect, type Wireframe, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineWireframe, fontMarks, formatJsonLd, generateSiteJsonLd, hasJsonLdId, htmlIdSchema, imageSchema, isPlaceholderSiteUrl, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, mergeJsonLd, motionOption, parseJsonLdInput, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteJsonLdIds, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeModes, themeSchema, toneOption, toneSchema, tones, trackingIdSchema, upsertJsonLd, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { c as ContentReadError, i as enumFields, l as ContentValidationError, n as defineThemePreview, o as schemaProps, s as ContentError, u as formatContentIssues } from "./preview-CTVEBdEY.js";
2
- import { n as defineTheme, t as defaultTheme } from "./theme-DMpt3hcT.js";
3
- import { A as isImagePosition, C as trackingIdSchema, D as IMAGE_FITS, E as DEFAULT_IMAGE_POSITION, F as toneOption, I as tones, L as variantOption, M as buttonStyles, N as motionOption, O as IMAGE_POSITIONS, P as themeModes, S as toneSchema, T as DEFAULT_IMAGE_FIT, _ as imageSchema, a as jsonLdSchema, b as textMarkSchema, c as withoutOrphanSections, f as sectionIdSchema, g as htmlIdSchema, h as addMarkIssues, i as isSystemPage, k as isImageFit, l as anchorSchema, m as MAX_NAV_DEPTH, n as createPageSchema, o as seoSchema, p as walkLinkIds, t as SYSTEM_PAGES, u as createSectionSchema, v as linkSchema, w as withEmphasis, x as textSchema, y as richTextSchema } from "./page-ON4Jkcm-.js";
1
+ import { c as ContentReadError, i as enumFields, l as ContentValidationError, n as defineThemePreview, o as schemaProps, s as ContentError, u as formatContentIssues } from "./preview--S1QUrMM.js";
2
+ import { n as defineTheme, t as defaultTheme } from "./theme-DFjHjgFO.js";
3
+ import { A as textSchema, C as MAX_NAV_DEPTH, D as linkSchema, E as imageSchema, F as DEFAULT_IMAGE_POSITION, G as tones, H as motionOption, I as IMAGE_FITS, K as variantOption, L as IMAGE_POSITIONS, M as trackingIdSchema, N as withEmphasis, O as richTextSchema, P as DEFAULT_IMAGE_FIT, R as isImageFit, S as upsertJsonLd, T as htmlIdSchema, U as themeModes, V as buttonStyles, W as toneOption, _ as jsonLdSchema, a as seoSchema, b as parseJsonLdInput, c as anchorSchema, d as sectionIdSchema, f as walkLinkIds, g as isPlaceholderSiteUrl, h as hasJsonLdId, i as isSystemPage, j as toneSchema, k as textMarkSchema, l as createSectionSchema, m as generateSiteJsonLd, n as createPageSchema, p as formatJsonLd, s as withoutOrphanSections, t as SYSTEM_PAGES, v as jsonLdWarnings, w as addMarkIssues, x as siteJsonLdIds, y as mergeJsonLd, z as isImagePosition } from "./page-NOFVEqn1.js";
4
4
  import { applyTextMark, fontMarks, markClass, marksFromEmphasis, reconcileMarks, renderMarkedHtml, splitMarkedText, weightMarks } from "./marks.js";
5
- import { n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-B56wxCiC.js";
6
- import { _ as consentCopy, a as contrastRatio, b as jsonLdWarnings, c as hasThemeColors, d as tokenToHex, f as CONSENT_COPY, g as CONSENT_REOPEN, h as CONSENT_LIMITS, i as contrastChecks, l as hexToToken, m as CONSENT_HREF_MESSAGE, n as HEX_COLOR, o as derivePalette, p as CONSENT_HREF, s as hasOverrides, u as themeCss, v as consentTextIssues, x as parseJsonLdInput, y as formatJsonLd } from "./theme-colors-zICpze42.js";
5
+ import { n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-b408EaLp.js";
6
+ import { _ as consentCopy, a as contrastRatio, b as parseMediaSource, c as hasThemeColors, d as tokenToHex, f as CONSENT_COPY, g as CONSENT_REOPEN, h as CONSENT_LIMITS, i as contrastChecks, l as hexToToken, m as CONSENT_HREF_MESSAGE, n as HEX_COLOR, o as derivePalette, p as CONSENT_HREF, s as hasOverrides, u as themeCss, v as consentTextIssues, y as VIDEO_FILE } from "./theme-colors-CHROGZkI.js";
7
7
  import { z } from "zod";
8
8
  //#region src/wireframe.ts
9
9
  /**
@@ -239,7 +239,9 @@ const siteSeoSchema = z.object({
239
239
  /** `%s` is replaced with the page SEO title. */
240
240
  titleTemplate: z.string().min(1).optional(),
241
241
  description: z.string().optional(),
242
- ogImage: z.string().min(1).optional()
242
+ ogImage: z.string().min(1).optional(),
243
+ /** Structured data emitted on every page, such as `Organization` and `WebSite`. A page's own blocks add to it. */
244
+ jsonLd: jsonLdSchema.optional()
243
245
  }).strict();
244
246
  /**
245
247
  * Raw HTML snippets for analytics and similar tags. A theme renders `head` at the end of `<head>` and
@@ -439,4 +441,4 @@ function defineSite(config) {
439
441
  };
440
442
  }
441
443
  //#endregion
442
- export { API_PREFIX, BRAND_LOGO_HEIGHT, CONSENT_COPY, CONSENT_HREF, CONSENT_LIMITS, CONSENT_REOPEN, ContentError, ContentReadError, ContentValidationError, DEFAULT_IMAGE_FIT, DEFAULT_IMAGE_POSITION, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, HEX_COLOR, IMAGE_FITS, IMAGE_POSITIONS, MAX_NAV_DEPTH, SCHEMA_VERSION_KEY, SYSTEM_PAGES, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, consentCopy, consentTextIssues, contrastChecks, contrastRatio, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineThemePreview, defineWireframe, derivePalette, fontMarks, formatJsonLd, hasOverrides, hasThemeColors, hexToToken, htmlIdSchema, imageSchema, isImageFit, isImagePosition, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, motionOption, parseJsonLdInput, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeCss, themeModes, themeSchema, tokenToHex, toneOption, toneSchema, tones, trackingIdSchema, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
444
+ export { API_PREFIX, BRAND_LOGO_HEIGHT, CONSENT_COPY, CONSENT_HREF, CONSENT_LIMITS, CONSENT_REOPEN, ContentError, ContentReadError, ContentValidationError, DEFAULT_IMAGE_FIT, DEFAULT_IMAGE_POSITION, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, HEX_COLOR, IMAGE_FITS, IMAGE_POSITIONS, MAX_NAV_DEPTH, SCHEMA_VERSION_KEY, SYSTEM_PAGES, VIDEO_FILE, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, consentCopy, consentTextIssues, contrastChecks, contrastRatio, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineThemePreview, defineWireframe, derivePalette, fontMarks, formatJsonLd, generateSiteJsonLd, hasJsonLdId, hasOverrides, hasThemeColors, hexToToken, htmlIdSchema, imageSchema, isImageFit, isImagePosition, isPlaceholderSiteUrl, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, mergeJsonLd, motionOption, parseJsonLdInput, parseMediaSource, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteJsonLdIds, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeCss, themeModes, themeSchema, tokenToHex, toneOption, toneSchema, tones, trackingIdSchema, upsertJsonLd, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
@@ -3,6 +3,8 @@
3
3
  const API_PREFIX = "/__bearminion/api";
4
4
  const DEFAULT_CONTENT_DIR = "content";
5
5
  const DEFAULT_UPLOADS_DIR = "src/assets/uploads";
6
+ /** Videos are served as they are, so they live in `public/`. */
7
+ const DEFAULT_VIDEOS_DIR = "public/videos";
6
8
  //#endregion
7
9
  //#region src/migrate.ts
8
10
  /** The key that stores a document's version. It is stripped on read and stamped on write; schemas never see it. */
@@ -56,4 +58,4 @@ function createMigrator(versioning = {}) {
56
58
  };
57
59
  }
58
60
  //#endregion
59
- export { DEFAULT_UPLOADS_DIR as a, DEFAULT_CONTENT_DIR as i, createMigrator as n, API_PREFIX as r, SCHEMA_VERSION_KEY as t };
61
+ export { DEFAULT_UPLOADS_DIR as a, DEFAULT_CONTENT_DIR as i, createMigrator as n, DEFAULT_VIDEOS_DIR as o, API_PREFIX as r, SCHEMA_VERSION_KEY as t };