@rigel-cms/rigel 0.1.3-beta.1 → 0.2.0-beta.1

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.
@@ -1,4 +1,7 @@
1
1
  .rigel, .rigel *, .rigel *::before, .rigel *::after { box-sizing: border-box; }
2
+ .rigel .rigel-tooltip { position: fixed; pointer-events: auto; inset: auto; margin: 0; padding: 0.375rem 0.625rem; max-width: min(20rem, calc(100vw - 16px)); max-height: calc(100dvh - 16px); overflow: auto; border: 1px solid var(--border-control); border-radius: var(--radius); background: var(--text); color: var(--panel); box-shadow: var(--shadow-md); font: 500 0.75rem/1.5 -apple-system, BlinkMacSystemFont, system-ui, sans-serif; overflow-wrap: anywhere; text-align: start; white-space: normal; }
3
+ .rigel .rigel-tooltip::backdrop { background: transparent; pointer-events: none; }
4
+ @media (forced-colors: active) { .rigel .rigel-tooltip { background: Canvas; color: CanvasText; border-color: CanvasText; } }
2
5
  .rigel {
3
6
  --rigel-50: #EFFBFF;
4
7
  --rigel-100: #D9F4FF;
@@ -26,6 +29,9 @@
26
29
 
27
30
  .rigel.connection-shell { align-items: center; justify-content: center; padding: 1.5rem; min-height: 100dvh; }
28
31
  .rigel .connection-card { flex: none; width: 100%; max-width: 560px; margin: 0; padding: 2rem; background: var(--panel); border: 1px solid var(--border); overflow-wrap: anywhere; }
32
+ .rigel .connection-brand { display: flex; align-items: center; gap: 0.75rem; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.04em; }
33
+ .rigel .brand-suffix { font-weight: 450; }
34
+ .rigel .connection-brand img { flex-shrink: 0; }
29
35
  .rigel .connection-card h1 { margin: 1rem 0; font-size: 1.75rem; line-height: 1.2; }
30
36
  .rigel .connection-card p { margin: 1rem 0; }
31
37
  .rigel .connection-card :is(button, a.primary) { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.625rem 1rem; font: inherit; text-decoration: none; text-align: center; }
@@ -169,6 +175,8 @@
169
175
  .rigel .rail-scroll > button:hover { background: var(--surface-raised); color: var(--text); }
170
176
  .rigel .sidebar-asset-icon { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; background-color: currentColor; mask-image: var(--sidebar-icon); mask-position: center; mask-size: contain; mask-repeat: no-repeat; }
171
177
  .rigel .rail-scroll > button { gap: 0.625rem; }
178
+ .rigel aside > button.sidebar-publish { gap: 0.625rem; width: calc(100% - 1.5rem); min-height: 44px; margin: 0 0.75rem 0.5rem; padding: 0.625rem; border: 1px solid var(--border-control); color: var(--text); }
179
+ .rigel aside > button.sidebar-publish:disabled { opacity: 0.5; cursor: not-allowed; }
172
180
  .rigel aside > button.sidebar-toggle { gap: 0.625rem; width: calc(100% - 1.5rem); margin: 0 0.75rem 0.5rem; padding: 0.625rem; font-size: 0.75rem; }
173
181
  @media (min-width: 768px) {
174
182
  .rigel.sidebar-collapsed { --sidebar-w: 64px; }
@@ -180,6 +188,7 @@
180
188
  .rigel.sidebar-collapsed .user-btn > .user-menu-icon { display: none; }
181
189
  .rigel.sidebar-collapsed .rail-scroll > button,
182
190
  .rigel.sidebar-collapsed .search-trigger,
191
+ .rigel.sidebar-collapsed .sidebar-publish,
183
192
  .rigel.sidebar-collapsed .sidebar-toggle { justify-content: center; }
184
193
  .rigel.sidebar-collapsed .sidebar-heading { height: 1px; font-size: 0; padding: 0; margin: 0.75rem 0.625rem; border-top: 1px solid var(--border-subtle); }
185
194
  .rigel.sidebar-collapsed .aside-foot { padding: 0.75rem 0.5rem; }
@@ -359,7 +368,18 @@
359
368
  .rigel .preview-heading span, .rigel .site-preview small { color: var(--text-muted); font-size: 0.75rem; }
360
369
  .rigel .site-preview p { overflow-wrap: anywhere; font-size: 0.8125rem; }
361
370
  .rigel .site-preview .preview-error { color: var(--danger); }
362
- .rigel .site-preview iframe { flex: 1; width: 100%; min-height: 300px; border: 1px solid var(--border); background: white; }
371
+ .rigel .preview-frame { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; background: var(--panel); }
372
+ .rigel .preview-frame iframe { opacity: 0; visibility: hidden; transition: none; }
373
+ .rigel .preview-frame iframe.revealed { opacity: 1; visibility: visible; transition: opacity 200ms ease-out; }
374
+ .rigel .preview-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 0.625rem; color: var(--text); background: var(--panel); font-size: 0.8125rem; }
375
+ .rigel .preview-spinner { width: 1rem; height: 1rem; border: 2px solid var(--border); border-top-color: currentColor; border-radius: 50%; animation: preview-spin 800ms linear infinite; }
376
+ @keyframes preview-spin { to { transform: rotate(360deg); } }
377
+ @media (prefers-reduced-motion: reduce) {
378
+ .rigel .preview-spinner { animation: none; }
379
+ .rigel .preview-frame iframe.revealed { transition: none; }
380
+ }
381
+ .rigel .site-preview .preview-frame { min-height: 300px; border: 1px solid var(--border); }
382
+ .rigel .site-preview iframe { flex: 1; width: 100%; min-height: 0; border: 0; background: white; }
363
383
  .rigel .site-preview small { padding-top: 0.75rem; }
364
384
  .rigel .preview-retry { align-self: start; }
365
385
  @media (max-width: 1050px) {
@@ -959,6 +979,10 @@
959
979
  font-size: 0.875rem; font-weight: 600; text-transform: none;
960
980
  letter-spacing: 0; color: var(--text); margin: 0 0 0.75rem;
961
981
  }
982
+ .rigel .connection-details { margin: 0; }
983
+ .rigel .connection-details dt { margin-top: 1rem; font-size: 0.75rem; font-weight: 600; color: var(--text-2); }
984
+ .rigel .connection-details dd { margin: 0.375rem 0 0; font-size: 0.875rem; overflow-wrap: anywhere; }
985
+ .rigel .connection-details small { display: block; margin-top: 0.125rem; color: var(--text-muted); }
962
986
  .rigel .settings-pane fieldset { border: none; padding: 0; margin: 0 0 1rem; }
963
987
  .rigel .settings-pane fieldset.accessibility-options { margin-top: 1.5rem; }
964
988
  .rigel .accessibility-options label.inline { margin-top: 0; }
package/src/env.d.ts CHANGED
@@ -17,6 +17,6 @@ declare module "virtual:rigel-content" {
17
17
 
18
18
  declare module "virtual:rigel-config" {
19
19
  import type { RigelConfig } from "@rigel-cms/rigel";
20
- const config: Pick<RigelConfig, "copyDir" | "backend" | "demo" | "collections" | "menus" | "mediaDir" | "schema"> & { publicDir?: string; root?: string };
20
+ const config: Pick<RigelConfig, "copyDir" | "backend" | "demo" | "collections" | "menus" | "mediaDir" | "schema" | "preview"> & { publicDir?: string; root?: string };
21
21
  export default config;
22
22
  }
package/src/preview.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import MarkdownIt from "markdown-it";
2
2
  import DOMPurify from "dompurify";
3
3
  import { pageRoute } from "./schema/paths";
4
+ import { slugify } from "./slugs";
4
5
  import { renderArticle } from "./article-render";
5
6
  import { safePreviewUrl, previewBaseUrl } from "./preview-urls";
6
7
  export { safePreviewUrl } from "./preview-urls";
@@ -20,6 +21,7 @@ export interface PreviewConfig {
20
21
  export interface PreviewTarget {
21
22
  kind: "page" | "post" | "menu" | "document";
22
23
  slug: string;
24
+ urlSlug?: string;
23
25
  collection?: string;
24
26
  type?: string;
25
27
  }
@@ -30,8 +32,13 @@ export function previewPath(config: PreviewConfig, target: PreviewTarget, isNew
30
32
  return typeof path === "string" && /^\/(?!\/)/.test(path) && !/[\\\s?#]/.test(path) && !/%(?:2e|2f|5c)/i.test(path) && !path.split("/").some(part => part === "." || part === "..") && !/^\/rigel(?:\/|$)/.test(path) ? path : null;
31
33
  }
32
34
 
33
- export function previewUrl(config: PreviewConfig, target: PreviewTarget, mode: "live" | "draft" = "draft", isNew = false): string | null {
34
- const path = previewPath(config, target, mode === "draft" && isNew);
35
+ export function previewUrl(config: PreviewConfig, target: PreviewTarget, mode: "live" | "draft" | "website" = "draft", isNew = false): string | null {
36
+ if (mode !== "draft" && target.kind !== "page" && target.urlSlug !== undefined) {
37
+ const slug = slugify(target.urlSlug);
38
+ if (!slug) return null;
39
+ target = { ...target, slug };
40
+ }
41
+ const path = previewPath(config, target, mode === "draft" && (target.kind === "post" || isNew));
35
42
  const base = mode === "live" ? config.liveUrl : config.draftUrl;
36
43
  if (!path || base === undefined) return path;
37
44
  try { return new URL(path.slice(1), previewBaseUrl(base)).href; } catch { return null; }
@@ -46,7 +46,7 @@ export const GET: APIRoute = async ({ params }) => {
46
46
  const target = validateDocumentId(guarded.schema, id);
47
47
  if (!target) return new Response("Unknown document", { status: 404 });
48
48
  try {
49
- const documents = await readDocuments(guarded.schema, guarded.root);
49
+ const documents = await readDocuments(guarded.schema, guarded.root, config.preview);
50
50
  const document = documents.find((entry) => entry.id === id);
51
51
  if (!document) return new Response("Unknown document", { status: 404 });
52
52
  return Response.json(document);
@@ -66,10 +66,10 @@ const handle: APIRoute = async ({ request, params }) => {
66
66
  if (!target) return new Response("Unknown document type or slug", { status: 400 });
67
67
  try {
68
68
  const data = await readBody(request);
69
- const documents = await readDocuments(guarded.schema, guarded.root);
70
- const next = { ...data, _id: id, _type: target.type.name, slug: target.slug };
69
+ const documents = await readDocuments(guarded.schema, guarded.root, config.preview);
70
+ const next = { ...data, _id: id, _type: target.type.name };
71
71
  validateDocument(guarded.schema, target.type.name, target.slug, next, documents);
72
- await writeDocument(guarded.schema, guarded.root, target.type.name, target.slug, next, request.method === "POST" ? "create" : "update");
72
+ await writeDocument(guarded.schema, guarded.root, target.type.name, target.slug, next, request.method === "POST" ? "create" : "update", config.preview);
73
73
  return new Response(null, { status: 204 });
74
74
  } catch (error) {
75
75
  if (error instanceof DocumentStoreError) return new Response(error.message, { status: error.status });
@@ -1,5 +1,6 @@
1
1
  import type { APIRoute } from "astro";
2
- import { writeFile, mkdir, rm, readFile, rename } from "node:fs/promises";
2
+ import { writeFile, mkdir, rm, readFile, readdir, rename, link } from "node:fs/promises";
3
+ import { assertUniqueUrlSlugs, normalizeUrlSlug } from "../slugs";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import path from "node:path";
5
6
 
@@ -23,7 +24,14 @@ function safeSlug(slug: string): string | null {
23
24
  return /^[a-z0-9][a-z0-9_-]*$/.test(slug) ? slug : null;
24
25
  }
25
26
 
26
- export const POST: APIRoute = async ({ request, params }) => {
27
+ let writes: Promise<unknown> = Promise.resolve();
28
+ export const POST: APIRoute = (context) => {
29
+ const result = writes.then(() => savePost(context));
30
+ writes = result.catch(() => {});
31
+ return result;
32
+ };
33
+
34
+ const savePost: APIRoute = async ({ request, params }) => {
27
35
  if (import.meta.env.PROD) {
28
36
  return new Response("Local backend is dev-only", { status: 404 });
29
37
  }
@@ -45,17 +53,27 @@ export const POST: APIRoute = async ({ request, params }) => {
45
53
  try {
46
54
  validateBody(body.body);
47
55
  const fields = config.collections?.find((collection: CollectionConfig) => collection.name === params.collection)?.fields ?? [];
48
- const documents = config.schema && config.root ? await readDocuments(config.schema, fileURLToPath(config.root)) : [];
56
+ const documents = config.schema && config.root ? await readDocuments(config.schema, fileURLToPath(config.root), config.preview) : [];
49
57
  validateCollectionValues(fields, body.frontmatter, documents);
58
+ if (Object.hasOwn(body.frontmatter, "slug")) body.frontmatter.slug = normalizeUrlSlug(body.frontmatter.slug);
59
+ const names = await readdir(folder).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return []; throw error; });
60
+ const entries = await Promise.all(names.filter(name => name.endsWith(".json") && name !== `${slug}.json`).map(async name => ({ slug: name.slice(0, -5), values: JSON.parse(await readFile(path.join(folder, name), "utf8")).frontmatter })));
61
+ assertUniqueUrlSlugs([...entries, { slug, values: body.frontmatter }]);
50
62
  } catch (error) {
51
63
  return new Response(error instanceof Error ? error.message : "Invalid article", { status: 400 });
52
64
  }
53
65
  await mkdir(path.dirname(file), { recursive: true });
54
- const fm = { ...body.frontmatter, slug };
66
+ const fm = { ...body.frontmatter };
55
67
  const temporary = `${file}.${randomUUID()}.tmp`;
56
68
  try {
57
69
  await writeFile(temporary, JSON.stringify({ frontmatter: fm, body: body.body }, null, 2) + "\n", { flag: "wx" });
58
- await rename(temporary, file);
70
+ if (request.headers.get("If-None-Match") === "*") {
71
+ try { await link(temporary, file); }
72
+ catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") return new Response("A post with that slug already exists. Reload the collection and create the draft again.", { status: 409 });
74
+ throw error;
75
+ }
76
+ } else await rename(temporary, file);
59
77
  } finally { await rm(temporary, { force: true }); }
60
78
  return new Response(null, { status: 204 });
61
79
  };
@@ -1,4 +1,5 @@
1
1
  import { documentDef, isPlainObject, objectDef } from "./validate";
2
+ import { effectiveSlug } from "../slugs";
2
3
  import {
3
4
  SLUG_PATTERN,
4
5
  type ContentDocument,
@@ -128,8 +129,9 @@ export function assertUniqueSlug(documents: ContentDocument[], typeName: string,
128
129
  if (legacySlugs.includes(slug)) throw new Error(`The slug "${slug}" is already used by a copy page.`);
129
130
  }
130
131
 
131
- export function pageRoute(slug: string, mapping: { url: string; index?: string } | undefined): string | null {
132
+ export function pageRoute(target: string | Pick<ContentDocument, "slug" | "data">, mapping: { url: string; index?: string } | undefined): string | null {
132
133
  if (!mapping) return null;
134
+ const slug = typeof target === "string" ? target : effectiveSlug(target.data, target.slug);
133
135
  if (slug === "home" && mapping.index) return mapping.index;
134
136
  const path = mapping.url.replaceAll("{slug}", slug);
135
137
  return /^\/(?!\/)/.test(path) && !/[\\\s?#]/.test(path) && !path.split("/").includes("..") && !/^\/rigel(?:\/|$)/.test(path) ? path : null;
@@ -4,6 +4,8 @@ import path from "node:path";
4
4
  import { constants } from "node:fs";
5
5
  import { isPlainObject } from "./validate";
6
6
  import { SLUG_PATTERN, type ContentDocument, type Schema } from "./types";
7
+ import { normalizeUrlSlug, validateDocumentSlugs } from "../slugs";
8
+ import type { PreviewConfig } from "../preview";
7
9
 
8
10
  const MAX_BYTES = 1024 * 1024;
9
11
  const FOLDER_PART = /^[a-zA-Z0-9_-][a-zA-Z0-9_. -]*$/;
@@ -68,7 +70,7 @@ async function ensureFolder(root: string, folder: string): Promise<string> {
68
70
  return directory;
69
71
  }
70
72
 
71
- export async function readDocuments(schema: Schema, root: string): Promise<ContentDocument[]> {
73
+ export async function readDocuments(schema: Schema, root: string, preview?: PreviewConfig): Promise<ContentDocument[]> {
72
74
  const documents: ContentDocument[] = [];
73
75
  for (const type of schema.documents) {
74
76
  let directory: string;
@@ -88,6 +90,8 @@ export async function readDocuments(schema: Schema, root: string): Promise<Conte
88
90
  documents.push({ id: `${type.name}:${slug}`, type: type.name, slug, data: data as Record<string, unknown> });
89
91
  }
90
92
  }
93
+ try { validateDocumentSlugs(schema, documents, preview); }
94
+ catch (error) { throw new DocumentStoreError((error as Error).message); }
91
95
  return documents;
92
96
  }
93
97
 
@@ -96,13 +100,21 @@ export async function readDocument(schema: Schema, root: string, typeName: strin
96
100
  return documents.find((doc) => doc.type === typeName && doc.slug === slug) ?? null;
97
101
  }
98
102
 
99
- export async function writeDocument(
103
+ let writes: Promise<void> = Promise.resolve();
104
+ export function writeDocument(...args: Parameters<typeof writeDocumentFile>): Promise<void> {
105
+ const result = writes.then(() => writeDocumentFile(...args));
106
+ writes = result.catch(() => {});
107
+ return result;
108
+ }
109
+
110
+ async function writeDocumentFile(
100
111
  schema: Schema,
101
112
  root: string,
102
113
  typeName: string,
103
114
  slug: string,
104
115
  data: Record<string, unknown>,
105
116
  mode: "create" | "update",
117
+ preview?: PreviewConfig,
106
118
  ): Promise<void> {
107
119
  const type = schema.documents.find((entry) => entry.name === typeName);
108
120
  if (!type) throw new DocumentStoreError(`Unknown document type ${typeName}`, 404);
@@ -118,6 +130,13 @@ export async function writeDocument(
118
130
  if (existing && (existing.isSymbolicLink() || !existing.isFile())) throw new DocumentStoreError("Document path is unsafe", 409);
119
131
  if (mode === "create" && existing) throw new DocumentStoreError("A document with that slug already exists", 409);
120
132
  if (mode === "update" && !existing) throw new DocumentStoreError("No such document", 404);
133
+ if (Buffer.byteLength(JSON.stringify(data)) > MAX_BYTES) throw new DocumentStoreError("Document exceeds the 1 MiB limit", 413);
134
+ data = { ...data };
135
+ try {
136
+ if (!type.shared && Object.hasOwn(data, "slug")) data.slug = normalizeUrlSlug(data.slug);
137
+ const documents = await readDocuments(schema, root, preview);
138
+ validateDocumentSlugs(schema, [...documents.filter(doc => doc.id !== `${typeName}:${slug}`), { id: `${typeName}:${slug}`, type: typeName, slug, data }], preview);
139
+ } catch (error) { throw new DocumentStoreError((error as Error).message); }
121
140
  const serialized = JSON.stringify(data, null, 2) + "\n";
122
141
  if (Buffer.byteLength(serialized) > MAX_BYTES) throw new DocumentStoreError("Document exceeds the 1 MiB limit", 413);
123
142
  const temporary = path.join(directory, `.${slug}-${randomUUID()}.tmp`);
package/src/slugs.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { ContentDocument, Schema } from "./schema/types";
2
+ import type { PreviewConfig } from "./preview";
3
+
4
+ export function slugify(input: string, existingSlugs: Iterable<string> = []): string {
5
+ const base = input.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
6
+ if (!base) return "";
7
+ const existing = new Set(existingSlugs);
8
+ let slug = base;
9
+ for (let suffix = 2; existing.has(slug); suffix++) slug = `${base}-${suffix}`;
10
+ return slug;
11
+ }
12
+
13
+ export function normalizeUrlSlug(value: unknown): string {
14
+ if (typeof value !== "string" || !slugify(value)) throw new Error("URL slug must contain letters or numbers.");
15
+ return slugify(value);
16
+ }
17
+
18
+ export function effectiveSlug(values: Record<string, unknown>, storageSlug: string): string {
19
+ if (!Object.hasOwn(values, "slug")) return storageSlug;
20
+ if (typeof values.slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(values.slug)) throw new Error("Invalid URL slug; use lowercase letters, numbers and single dashes.");
21
+ return values.slug;
22
+ }
23
+
24
+ export function assertUniqueUrlSlugs(entries: { slug: string; values: Record<string, unknown> }[]): void {
25
+ const seen = new Set<string>();
26
+ for (const entry of entries) {
27
+ const slug = effectiveSlug(entry.values, entry.slug);
28
+ if (seen.has(slug)) throw new Error(`URL slug "${slug}" is already used.`);
29
+ seen.add(slug);
30
+ }
31
+ }
32
+
33
+ export function validateDocumentSlugs(schema: Schema, documents: ContentDocument[], preview?: PreviewConfig): void {
34
+ const seen = new Set<string>();
35
+ const pages = documents.filter(doc => schema.documents.some(type => type.name === doc.type && !type.shared));
36
+ const fixedRoutes = Object.entries(preview?.pages ?? {}).filter(([key]) => !pages.some(doc => doc.slug === key)).map(([, route]) => route);
37
+ for (const doc of documents) {
38
+ const type = schema.documents.find(type => type.name === doc.type);
39
+ if (!type || type.shared) continue;
40
+ const slug = effectiveSlug(doc.data, doc.slug);
41
+ const mapping = preview?.documents?.[doc.type];
42
+ const route = mapping ? slug === "home" && mapping.index ? mapping.index : mapping.url.replaceAll("{slug}", slug) : `/${slug}/`;
43
+ if (seen.has(route) || fixedRoutes.includes(route)) throw new Error(`URL slug "${slug}" is already used.`);
44
+ seen.add(route);
45
+ }
46
+ }
@@ -22,7 +22,8 @@ export function rigelManifestPlugin(cfg: RigelConfig, publicDir?: URL, root?: UR
22
22
  ...(cfg.menus ? { menus: { folder: cfg.menus.folder, maxDepth: cfg.menus.maxDepth! } } : {}),
23
23
  } : undefined;
24
24
 
25
- const documentsSrc = documentTypes.length
25
+ const slugHelpers = `import { assertUniqueUrlSlugs as __rigelAssertUniqueUrlSlugs, validateDocumentSlugs as __rigelValidateDocumentSlugs } from ${JSON.stringify(fileURLToPath(new URL("../slugs.ts", import.meta.url)))};`;
26
+ const documentsSrc = slugHelpers + (documentTypes.length
26
27
  ? `
27
28
  ${documentTypes.map((type, index) => `const __rigelDoc${index} = import.meta.glob(${JSON.stringify(`/${type.folder}/*.json`)}, { eager: true, import: "default" });`).join("\n")}
28
29
  export const documentTypes = ${JSON.stringify(documentTypes.map((type) => ({ name: type.name, shared: Boolean(type.shared) })))};
@@ -33,9 +34,10 @@ export const documents = Object.entries(__rigelRawDocuments)
33
34
  return { id: type + ":" + slug, type, slug, data: { ...data, _id: type + ":" + slug, _type: type } };
34
35
  }))
35
36
  .sort((a, b) => a.id.localeCompare(b.id));
37
+ __rigelValidateDocumentSlugs(${JSON.stringify(cfg.schema)}, documents, ${JSON.stringify(cfg.preview)});
36
38
  const __rigelReservedSlugs = new Set(documents.filter((doc) => !documentTypes.find((type) => type.name === doc.type).shared).map((doc) => doc.slug));
37
39
  `
38
- : `export const documentTypes = [];\nexport const documents = [];\nconst __rigelReservedSlugs = new Set();\n`;
40
+ : `export const documentTypes = [];\nexport const documents = [];\nconst __rigelReservedSlugs = new Set();\n`);
39
41
 
40
42
  const collectionCode = collections
41
43
  .map(
@@ -137,6 +139,7 @@ export const menus = {
137
139
  menus: cfg.menus,
138
140
  collections: cfg.collections ?? [],
139
141
  schema: cfg.schema,
142
+ preview: cfg.preview,
140
143
  })};`;
141
144
  }
142
145
  if (id === "\0" + CONTENT_ID) {
@@ -174,6 +177,7 @@ export const pages = loaded.filter((page) => !__rigelReservedSlugs.has(page.slug
174
177
  export const collections = [
175
178
  ${collectionCode}
176
179
  ];
180
+ for (const collection of collections) __rigelAssertUniqueUrlSlugs(collection.posts.map(post => ({ slug: post.slug, values: post.frontmatter })));
177
181
  ${menuCode}
178
182
  export default { pages, collections, documents${github ? ', backend: "github", github: ' + JSON.stringify(github) : ""}${cfg.menus ? ", menus" : ""}${cfg.schema ? ", schema: " + JSON.stringify(cfg.schema) : ""}${cfg.preview ? ", preview: " + JSON.stringify(cfg.preview) : ""}${cfg.sidebar ? ", sidebar: " + JSON.stringify(cfg.sidebar) : ""} };
179
183
  `;