@micacms/astro 0.6.0 → 0.7.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import { mkdir, readFile, writeFile } from "node:fs/promises";
19
19
  import { dirname, join, relative, sep } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
- import { collectionEntryPath, createReferenceCheck, entryLayout, fieldsSchema, INDEX_FILE, singletonPath, isFieldKind, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/core";
21
+ import { collectionEntryPath, createReferenceCheck, entryLayout, fieldsSchema, INDEX_FILE, singletonPath, isFieldKind, resolveSlugField, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/core";
22
22
  import { fileSystemSource } from "@micacms/core/node";
23
23
  import { z } from "zod";
24
24
  import { imagePathsFromEntry } from "./images.js";
@@ -32,6 +32,7 @@ const metaSchema = z.object({
32
32
  });
33
33
  export function micaCollections(site, options = {}) {
34
34
  assertKnownKinds(site);
35
+ assertOneUrl(site);
35
36
  /**
36
37
  * Everything below is resolved on first load rather than here, because the
37
38
  * Astro project root is something only a loader is told — and it is what
@@ -132,11 +133,13 @@ export function micaCollections(site, options = {}) {
132
133
  position: entry.position,
133
134
  };
134
135
  const file = entryFile(context, def.fields, name, entry.slug);
135
- const data = await context.parseData({
136
+ const data = await draftTolerant(context, `${name}/${entry.slug}`, () => context.parseData({
136
137
  id: entry.slug,
137
138
  data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },
138
139
  ...(file.inProject ? { filePath: file.inProject } : {}),
139
- });
140
+ }));
141
+ if (data === undefined)
142
+ continue;
140
143
  context.store.set({
141
144
  id: entry.slug,
142
145
  data,
@@ -176,11 +179,13 @@ export function micaCollections(site, options = {}) {
176
179
  }
177
180
  await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);
178
181
  const file = entryFile(context, def.fields, name);
179
- const parsed = await context.parseData({
182
+ const parsed = await draftTolerant(context, name, () => context.parseData({
180
183
  id: name,
181
184
  data: imagePathsFromEntry(def.fields, data, file.inContent),
182
185
  ...(file.inProject ? { filePath: file.inProject } : {}),
183
- });
186
+ }));
187
+ if (parsed === undefined)
188
+ return;
184
189
  context.store.set({
185
190
  id: name,
186
191
  data: parsed,
@@ -197,6 +202,22 @@ export function micaCollections(site, options = {}) {
197
202
  }
198
203
  return collections;
199
204
  }
205
+ /**
206
+ * In dev, an entry that fails its schema is left out with a warning: the editor
207
+ * saves a draft before every required field is filled in, and Publish refuses it
208
+ * until then. A build has no watcher, and fails as before.
209
+ */
210
+ async function draftTolerant(context, label, parse) {
211
+ try {
212
+ return await parse();
213
+ }
214
+ catch (error) {
215
+ if (!context.watcher)
216
+ throw error;
217
+ context.logger.warn(`Left out "${label}" until it is valid: ${describe(error)}`);
218
+ return undefined;
219
+ }
220
+ }
200
221
  /**
201
222
  * The site's own model must use only kinds this package knows. Mica reads an
202
223
  * unknown kind as one from a newer version and makes its collection read-only,
@@ -217,6 +238,23 @@ function assertKnownKinds(site) {
217
238
  for (const [name, def] of Object.entries(site.singletons))
218
239
  check(def.fields, `singletons.${name}`);
219
240
  }
241
+ /**
242
+ * A collection's own slug fields are typed as never null, because Mica fills
243
+ * the URL field from the file name. A second one would be typed the same and
244
+ * could still be null, so the site's model refuses it. (A slug in an object,
245
+ * such as one per language, is nullable and fine.) Mica itself still reads a
246
+ * manifest with two, so an older site keeps working with a newer Mica.
247
+ */
248
+ function assertOneUrl(site) {
249
+ for (const [name, def] of Object.entries(site.collections)) {
250
+ const url = resolveSlugField(def);
251
+ const others = Object.entries(def.fields).filter(([key, field]) => field.kind === "slug" && key !== url).map(([key]) => key);
252
+ if (others.length > 0) {
253
+ throw new Error(`Collection "${name}" has slug fields that are not its URL (${others.join(", ")}); its URL is "${url}". ` +
254
+ "An entry has one URL: use text() for the others, or put them in an object, such as one per language.");
255
+ }
256
+ }
257
+ }
220
258
  /**
221
259
  * Writes the model into the content root, for Mica to read back.
222
260
  *
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @micacms/astro — a Git repository as an Astro content source.\n *\n * // src/content.config.ts — the model is in src/mica.ts\n * import { micaCollections } from \"@micacms/astro\";\n * import { site } from \"./mica\";\n *\n * export const collections = micaCollections(site);\n *\n * From there the website uses ordinary Astro APIs — `getCollection(\"projects\")`\n * — and knows nothing about how the content arrived.\n *\n * There is no CMS in this path. The loaders read files that are already in the\n * working tree, so a build needs a checkout and nothing else: no running\n * server, no token, no network. That is the point of the Git-native design.\n * Publishing is a commit, and CI builds the commit.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n collectionEntryPath,\n createReferenceCheck,\n entryLayout,\n fieldsSchema,\n INDEX_FILE,\n singletonPath,\n isFieldKind,\n MANIFEST_PATH,\n readCollection,\n readSingleton,\n type Collection,\n type ContentSource,\n type FieldMap,\n type InferCollection,\n type InferSingleton,\n type Singleton,\n type Site,\n} from \"@micacms/core\";\nimport { fileSystemSource } from \"@micacms/core/node\";\nimport type { ImageMetadata } from \"astro\";\nimport { z } from \"zod\";\n\nimport { imagePathsFromEntry } from \"./images.js\";\nimport { manifestFor } from \"./manifest.js\";\nimport { DEFAULT_CONTENT_ROOT, resolveContentRoot } from \"./root.js\";\n\nexport interface MicaCollectionsOptions {\n /**\n * Overrides `defineSite({ root })`, which is where the content root belongs.\n *\n * Relative paths resolve against the Astro project root, like Astro's own\n * `glob({ base })`. An absolute path is used as given — which is what a\n * project whose content is a separate repository, checked out by CI\n * somewhere else, wants.\n */\n root?: string;\n /** Overrides `root` entirely. Used by tests, and by a non-filesystem source. */\n source?: ContentSource;\n /**\n * Turns off publishing the model to `<root>/.mica/schema.json`.\n *\n * Only for a project that is not edited through Mica at all: without that\n * file in the repository, Mica has no model and refuses to start.\n */\n publishModel?: false;\n /**\n * How long to wait after a content change before reloading, in dev.\n *\n * Saving one entry touches several files — `index.json` and every `.mdoc`\n * beside it — and a Git checkout touches all of them at once. One reload\n * after the burst is both faster and less confusing than one per file.\n */\n debounceMs?: number;\n /**\n * Fails the build on a relation to a missing entry or a missing media file.\n * Without it, the build logs a warning for each and goes on; a missing image\n * file fails the build either way, because Astro cannot import it.\n */\n strict?: boolean;\n}\n\n/**\n * Entry metadata attached to every collection document.\n *\n * No timestamps: in Git they are commit metadata, which a static build cannot\n * read cheaply. A date an editor cares about belongs in the model as a `date()` field.\n *\n * Namespaced because these keys share a namespace with the customer's own\n * fields, and `position` is a plausible field name.\n */\nexport interface MicaMeta {\n /** The entry slug, which is its identity in the repository. */\n id: string;\n slug: string;\n collection: string;\n /** Index in the collection's order, manual when `_order.json` says so. */\n position: number;\n}\n\nconst metaSchema = z.object({\n id: z.string(),\n slug: z.string(),\n collection: z.string(),\n position: z.number(),\n});\n\n/** What a document looks like inside Astro: the payload plus `_mica`. */\nexport type AstroDocument<T> = T & { _mica: MicaMeta };\n\n/**\n * The shape Astro's config loader expects.\n *\n * Deliberately not built with `defineCollection` from `astro:content`: that is a\n * virtual module, resolvable only inside an Astro app's module graph, and this\n * package is pre-compiled. `defineCollection` does nothing more than stamp this\n * `type`, so producing the object directly keeps the package importable\n * anywhere — including from a plain unit test.\n */\nexport interface AstroCollectionConfig<T = unknown> {\n type: \"content_layer\";\n loader: ContentLoader;\n /**\n * A function, so that Astro hands in its `image()`. Its result is typed as\n * `ZodType<T>` because Astro derives `CollectionEntry<\"projects\">[\"data\"]`\n * from it: left generic, every field in every template would degrade to\n * `any`, and the point of the shared model is that renaming a field breaks\n * the build.\n */\n schema: (context: { image: () => z.ZodType }) => z.ZodType<T>;\n}\n\n/** The subset of Astro's `Loader` contract this package uses. */\nexport interface ContentLoader {\n name: string;\n load(context: LoaderContext): Promise<void>;\n}\n\nexport interface LoaderContext {\n store: {\n clear(): void;\n set(entry: { id: string; data: Record<string, unknown>; digest?: string; filePath?: string }): boolean;\n keys(): string[];\n };\n logger: {\n info(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n debug(message: string): void;\n };\n meta: { get(key: string): string | undefined; set(key: string, value: string): void };\n /**\n * Astro's resolved config. Optional because this interface is a hand-written\n * subset and the tests build a context by hand; `config.root` is what turns a\n * relative content root into a path.\n */\n config?: { root: URL };\n /**\n * Present only while `astro dev` runs. Astro's watcher follows the module\n * graph, and content files are not in it — no page imports\n * `projects/haus-a/index.json` — so each loader watches its own directory\n * here, exactly as Astro's own `glob` loader does.\n */\n watcher?: {\n add(path: string): void;\n on(event: \"add\" | \"change\" | \"unlink\", handler: (path: string) => void): void;\n };\n parseData(input: {\n id: string;\n data: Record<string, unknown>;\n /** The entry's file, from the project root: Astro's `image()` resolves paths against it. */\n filePath?: string;\n }): Promise<Record<string, unknown>>;\n generateDigest(data: Record<string, unknown> | string): string;\n}\n\nexport type MicaCollections<S extends Site> = {\n [K in keyof S[\"collections\"]]: AstroCollectionConfig<CollectionDocument<S, K>>;\n} & {\n [K in keyof S[\"singletons\"]]: AstroCollectionConfig<SingletonDocument<S, K>>;\n};\n\nexport function micaCollections<S extends Site>(\n site: S,\n options: MicaCollectionsOptions = {},\n): MicaCollections<S> {\n assertKnownKinds(site);\n\n /**\n * Everything below is resolved on first load rather than here, because the\n * Astro project root is something only a loader is told — and it is what\n * turns `./content` into a directory. Memoised: one root and one source,\n * however many collections and however many reloads.\n */\n let root: string | undefined;\n let source: ContentSource | undefined;\n\n const rootFor = (context: LoaderContext): string =>\n (root ??= resolveContentRoot(options.root ?? site.root, context.config?.root));\n\n const sourceFor = (context: LoaderContext): ContentSource =>\n (source ??= options.source ?? fileSystemSource(rootFor(context)));\n\n /** Published once per process; a model change restarts the dev server. */\n let publishing: Promise<void> | undefined;\n\n const publish = (context: LoaderContext): Promise<void> =>\n (publishing ??=\n options.publishModel === false || options.source\n ? Promise.resolve()\n : publishModel(site, rootFor(context), context.logger));\n\n /** An entry's data file, from the content root and, outside tests, from the project root. */\n const entryFile = (context: LoaderContext, fields: FieldMap, name: string, slug?: string) => {\n const base = slug === undefined ? singletonPath(fields, name) : collectionEntryPath(fields, name, slug);\n const inContent = entryLayout(fields) === \"directory\" ? `${base}/${INDEX_FILE}` : base;\n const project = context.config?.root;\n const inProject =\n project && !options.source\n ? relative(fileURLToPath(project), join(rootFor(context), inContent)).split(sep).join(\"/\")\n : undefined;\n return { inContent, inProject };\n };\n\n /** Warns about broken relations and missing files, or fails the build with `strict`. */\n const checkReferences = async (\n context: LoaderContext,\n source: ContentSource,\n documents: Array<{ label: string; fields: FieldMap; data: Record<string, unknown> }>,\n ): Promise<void> => {\n const check = createReferenceCheck(site, source);\n const problems: string[] = [];\n for (const { label, fields, data } of documents) {\n for (const issue of await check.issues(fields, data)) problems.push(`${label}: ${issue.path}: ${issue.message}`);\n }\n if (problems.length === 0) return;\n if (options.strict) throw new Error(`Broken references:\\n ${problems.join(\"\\n \")}`);\n for (const problem of problems) context.logger.warn(problem);\n };\n\n /** Collections whose watcher is already registered, so a reload adds none. */\n const watching = new Set<string>();\n\n const watch = (context: LoaderContext, name: string, reload: () => Promise<void>): void => {\n const watcher = context.watcher;\n if (!watcher || options.source || watching.has(name)) return;\n watching.add(name);\n\n const base = rootFor(context);\n watcher.add(base);\n\n // A collection lives in `<root>/<name>/`; a singleton is either\n // `<root>/<name>.json` or `<root>/<name>/…`. Both are covered by this\n // prefix, and nothing outside it can concern this loader — which is what\n // keeps one collection's save from re-reading every other one.\n const prefix = join(base, name);\n const mine = (path: string): boolean =>\n path === `${prefix}.json` || path.startsWith(prefix + sep);\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const onChange = (path: string): void => {\n // The manifest is this package's own output; reloading content because\n // the model was written is a loop waiting to happen.\n if (path.endsWith(MANIFEST_PATH) || !mine(path)) return;\n\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n // Content that fails its own schema lands here. The dev server has to\n // survive it: the next save is very likely the fix.\n void reload().catch((error: unknown) => {\n context.logger.error(`Could not reload content: ${describe(error)}`);\n });\n }, options.debounceMs ?? 100);\n // A pending reload is never a reason to keep the process alive.\n timer.unref?.();\n };\n\n watcher.on(\"change\", onChange);\n watcher.on(\"add\", onChange);\n watcher.on(\"unlink\", onChange);\n };\n\n const collections = {} as Record<string, AstroCollectionConfig<unknown>>;\n\n for (const [name, def] of Object.entries(site.collections) as [string, Collection][]) {\n collections[name] = {\n type: \"content_layer\",\n schema: ({ image }) => fieldsSchema(def.fields, { image }).extend({ _mica: metaSchema }),\n loader: {\n name: `mica:${name}`,\n load: async (context) => {\n const read = async (): Promise<void> => {\n const source = sourceFor(context);\n const entries = await guard(context, source, () => readCollection(source, def, name));\n\n // A full replace, not a merge: an entry deleted from the repository\n // has to disappear from the build.\n context.store.clear();\n\n for (const entry of entries) {\n const meta: MicaMeta = {\n id: entry.slug,\n slug: entry.slug,\n collection: name,\n position: entry.position,\n };\n\n const file = entryFile(context, def.fields, name, entry.slug);\n const data = await context.parseData({\n id: entry.slug,\n data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n context.store.set({\n id: entry.slug,\n data,\n digest: context.generateDigest(data),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n }\n\n await checkReferences(\n context,\n source,\n entries.map((entry) => ({ label: `${name}/${entry.slug}`, fields: def.fields, data: entry.data })),\n );\n context.logger.info(`Loaded ${entries.length} ${entries.length === 1 ? \"entry\" : \"entries\"} of \"${def.label}\"`);\n };\n\n await publish(context);\n await read();\n watch(context, name, read);\n },\n },\n };\n }\n\n // A singleton becomes a one-entry collection whose id is its name, so the\n // website reaches it with the same `getEntry` it uses for everything else.\n for (const [name, def] of Object.entries(site.singletons) as [string, Singleton][]) {\n collections[name] = {\n type: \"content_layer\",\n schema: ({ image }) => fieldsSchema(def.fields, { image }),\n loader: {\n name: `mica:${name}`,\n load: async (context) => {\n const read = async (): Promise<void> => {\n const source = sourceFor(context);\n const data = await guard(context, source, () => readSingleton(source, def, name));\n\n context.store.clear();\n if (data === null) {\n // Not fatal. A site is normally deployed before every settings\n // form has been filled in, and the page that reads this singleton\n // reports the absence far better than the loader can.\n context.logger.warn(`No content for \"${def.label}\" yet.`);\n return;\n }\n\n await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);\n const file = entryFile(context, def.fields, name);\n const parsed = await context.parseData({\n id: name,\n data: imagePathsFromEntry(def.fields, data, file.inContent),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n context.store.set({\n id: name,\n data: parsed,\n digest: context.generateDigest(parsed),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n };\n\n await publish(context);\n await read();\n watch(context, name, read);\n },\n },\n };\n }\n\n return collections as MicaCollections<S>;\n}\n\n/**\n * The site's own model must use only kinds this package knows. Mica reads an\n * unknown kind as one from a newer version and makes its collection read-only,\n * so a typo would otherwise pass the build and lock the collection.\n */\nfunction assertKnownKinds(site: Site): void {\n const check = (fields: FieldMap, at: string) => {\n for (const [key, field] of Object.entries(fields)) {\n if (!isFieldKind(field.kind)) {\n throw new Error(`Field \"${at}.${key}\" has the unknown kind \"${String(field.kind)}\". Use the field functions from @micacms/core.`);\n }\n if (field.kind === \"object\" || field.kind === \"repeater\") check(field.fields, `${at}.${key}`);\n }\n };\n for (const [name, def] of Object.entries(site.collections)) check(def.fields, `collections.${name}`);\n for (const [name, def] of Object.entries(site.singletons)) check(def.fields, `singletons.${name}`);\n}\n\n/**\n * Writes the model into the content root, for Mica to read back.\n *\n * Mica holds no content model: the Astro project is the only place a collection\n * is defined, and this is how it says so. The file goes *inside the content\n * root* so that it travels to Mica the way content does — through Git and the\n * repository connector — with nothing to mount and no second channel to keep in\n * step. Commit it.\n *\n * Done from a loader rather than an integration because this is the moment both\n * facts are in hand: the model, and the project root that turns `./content`\n * into a directory. It also means `astro.config` needs no Mica in it, which is\n * what lets a project define Mica collections and ordinary Astro ones in the\n * same `content.config.ts`.\n */\nasync function publishModel(\n site: Site,\n root: string,\n logger: LoaderContext[\"logger\"],\n): Promise<void> {\n const target = join(root, MANIFEST_PATH);\n const contents = `${JSON.stringify(manifestFor(site), null, 2)}\\n`;\n\n try {\n // Only when it differs. The content root is watched in dev, and rewriting\n // an identical file on every start would ask for a reload for no reason.\n if ((await readFile(target, \"utf8\").catch(() => null)) === contents) return;\n\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, contents, \"utf8\");\n logger.info(`Wrote the content model to ${relative(process.cwd(), target)} — commit it for Mica to read.`);\n } catch (error) {\n // A site that builds correctly should not fail over this, and a read-only\n // checkout is a normal way to build one in CI. Mica reports a missing model\n // itself, and far more usefully.\n logger.warn(`Could not write the content model: ${describe(error)}`);\n }\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Reports a read failure against the content root that produced it.\n *\n * A build failing on unreadable content is correct — publishing a half-empty\n * site would be worse — but the message has to name the directory that was\n * read, because the usual cause is a content root pointing at the wrong place.\n */\nasync function guard<T>(\n context: LoaderContext,\n source: ContentSource,\n read: () => Promise<T>,\n): Promise<T> {\n try {\n return await read();\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n context.logger.error(detail);\n\n throw new Error(`Could not read content from ${source.origin}.\\n ${detail}`, { cause: error });\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Types for the website\n * ------------------------------------------------------------------ */\n\n/** The document type for one collection, as it appears in Astro. */\nexport type CollectionDocument<\n S extends Site,\n K extends keyof S[\"collections\"],\n> = AstroDocument<InferCollection<S[\"collections\"][K], ImageMetadata>>;\n\n/** The document type for one singleton. */\nexport type SingletonDocument<\n S extends Site,\n K extends keyof S[\"singletons\"],\n> = InferSingleton<S[\"singletons\"][K], ImageMetadata>;\n\nexport { manifestFor } from \"./manifest.js\";\nexport { DEFAULT_CONTENT_ROOT, resolveContentRoot };\nexport {\n renderRichText,\n transformRichText,\n validateRichText,\n type RichTextOptions,\n} from \"./richtext.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @micacms/astro — a Git repository as an Astro content source.\n *\n * // src/content.config.ts — the model is in src/mica.ts\n * import { micaCollections } from \"@micacms/astro\";\n * import { site } from \"./mica\";\n *\n * export const collections = micaCollections(site);\n *\n * From there the website uses ordinary Astro APIs — `getCollection(\"projects\")`\n * — and knows nothing about how the content arrived.\n *\n * There is no CMS in this path. The loaders read files that are already in the\n * working tree, so a build needs a checkout and nothing else: no running\n * server, no token, no network. That is the point of the Git-native design.\n * Publishing is a commit, and CI builds the commit.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n collectionEntryPath,\n createReferenceCheck,\n entryLayout,\n fieldsSchema,\n INDEX_FILE,\n singletonPath,\n isFieldKind,\n resolveSlugField,\n MANIFEST_PATH,\n readCollection,\n readSingleton,\n type Collection,\n type ContentSource,\n type FieldMap,\n type InferCollection,\n type InferSingleton,\n type Singleton,\n type Site,\n} from \"@micacms/core\";\nimport { fileSystemSource } from \"@micacms/core/node\";\nimport type { ImageMetadata } from \"astro\";\nimport { z } from \"zod\";\n\nimport { imagePathsFromEntry } from \"./images.js\";\nimport { manifestFor } from \"./manifest.js\";\nimport { DEFAULT_CONTENT_ROOT, resolveContentRoot } from \"./root.js\";\n\nexport interface MicaCollectionsOptions {\n /**\n * Overrides `defineSite({ root })`, which is where the content root belongs.\n *\n * Relative paths resolve against the Astro project root, like Astro's own\n * `glob({ base })`. An absolute path is used as given — which is what a\n * project whose content is a separate repository, checked out by CI\n * somewhere else, wants.\n */\n root?: string;\n /** Overrides `root` entirely. Used by tests, and by a non-filesystem source. */\n source?: ContentSource;\n /**\n * Turns off publishing the model to `<root>/.mica/schema.json`.\n *\n * Only for a project that is not edited through Mica at all: without that\n * file in the repository, Mica has no model and refuses to start.\n */\n publishModel?: false;\n /**\n * How long to wait after a content change before reloading, in dev.\n *\n * Saving one entry touches several files — `index.json` and every `.mdoc`\n * beside it — and a Git checkout touches all of them at once. One reload\n * after the burst is both faster and less confusing than one per file.\n */\n debounceMs?: number;\n /**\n * Fails the build on a relation to a missing entry or a missing media file.\n * Without it, the build logs a warning for each and goes on; a missing image\n * file fails the build either way, because Astro cannot import it.\n */\n strict?: boolean;\n}\n\n/**\n * Entry metadata attached to every collection document.\n *\n * No timestamps: in Git they are commit metadata, which a static build cannot\n * read cheaply. A date an editor cares about belongs in the model as a `date()` field.\n *\n * Namespaced because these keys share a namespace with the customer's own\n * fields, and `position` is a plausible field name.\n */\nexport interface MicaMeta {\n /** The entry slug, which is its identity in the repository. */\n id: string;\n slug: string;\n collection: string;\n /** Index in the collection's order, manual when `_order.json` says so. */\n position: number;\n}\n\nconst metaSchema = z.object({\n id: z.string(),\n slug: z.string(),\n collection: z.string(),\n position: z.number(),\n});\n\n/** What a document looks like inside Astro: the payload plus `_mica`. */\nexport type AstroDocument<T> = T & { _mica: MicaMeta };\n\n/**\n * The shape Astro's config loader expects.\n *\n * Deliberately not built with `defineCollection` from `astro:content`: that is a\n * virtual module, resolvable only inside an Astro app's module graph, and this\n * package is pre-compiled. `defineCollection` does nothing more than stamp this\n * `type`, so producing the object directly keeps the package importable\n * anywhere — including from a plain unit test.\n */\nexport interface AstroCollectionConfig<T = unknown> {\n type: \"content_layer\";\n loader: ContentLoader;\n /**\n * A function, so that Astro hands in its `image()`. Its result is typed as\n * `ZodType<T>` because Astro derives `CollectionEntry<\"projects\">[\"data\"]`\n * from it: left generic, every field in every template would degrade to\n * `any`, and the point of the shared model is that renaming a field breaks\n * the build.\n */\n schema: (context: { image: () => z.ZodType }) => z.ZodType<T>;\n}\n\n/** The subset of Astro's `Loader` contract this package uses. */\nexport interface ContentLoader {\n name: string;\n load(context: LoaderContext): Promise<void>;\n}\n\nexport interface LoaderContext {\n store: {\n clear(): void;\n set(entry: { id: string; data: Record<string, unknown>; digest?: string; filePath?: string }): boolean;\n keys(): string[];\n };\n logger: {\n info(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n debug(message: string): void;\n };\n meta: { get(key: string): string | undefined; set(key: string, value: string): void };\n /**\n * Astro's resolved config. Optional because this interface is a hand-written\n * subset and the tests build a context by hand; `config.root` is what turns a\n * relative content root into a path.\n */\n config?: { root: URL };\n /**\n * Present only while `astro dev` runs. Astro's watcher follows the module\n * graph, and content files are not in it — no page imports\n * `projects/haus-a/index.json` — so each loader watches its own directory\n * here, exactly as Astro's own `glob` loader does.\n */\n watcher?: {\n add(path: string): void;\n on(event: \"add\" | \"change\" | \"unlink\", handler: (path: string) => void): void;\n };\n parseData(input: {\n id: string;\n data: Record<string, unknown>;\n /** The entry's file, from the project root: Astro's `image()` resolves paths against it. */\n filePath?: string;\n }): Promise<Record<string, unknown>>;\n generateDigest(data: Record<string, unknown> | string): string;\n}\n\nexport type MicaCollections<S extends Site> = {\n [K in keyof S[\"collections\"]]: AstroCollectionConfig<CollectionDocument<S, K>>;\n} & {\n [K in keyof S[\"singletons\"]]: AstroCollectionConfig<SingletonDocument<S, K>>;\n};\n\nexport function micaCollections<S extends Site>(\n site: S,\n options: MicaCollectionsOptions = {},\n): MicaCollections<S> {\n assertKnownKinds(site);\n assertOneUrl(site);\n\n /**\n * Everything below is resolved on first load rather than here, because the\n * Astro project root is something only a loader is told — and it is what\n * turns `./content` into a directory. Memoised: one root and one source,\n * however many collections and however many reloads.\n */\n let root: string | undefined;\n let source: ContentSource | undefined;\n\n const rootFor = (context: LoaderContext): string =>\n (root ??= resolveContentRoot(options.root ?? site.root, context.config?.root));\n\n const sourceFor = (context: LoaderContext): ContentSource =>\n (source ??= options.source ?? fileSystemSource(rootFor(context)));\n\n /** Published once per process; a model change restarts the dev server. */\n let publishing: Promise<void> | undefined;\n\n const publish = (context: LoaderContext): Promise<void> =>\n (publishing ??=\n options.publishModel === false || options.source\n ? Promise.resolve()\n : publishModel(site, rootFor(context), context.logger));\n\n /** An entry's data file, from the content root and, outside tests, from the project root. */\n const entryFile = (context: LoaderContext, fields: FieldMap, name: string, slug?: string) => {\n const base = slug === undefined ? singletonPath(fields, name) : collectionEntryPath(fields, name, slug);\n const inContent = entryLayout(fields) === \"directory\" ? `${base}/${INDEX_FILE}` : base;\n const project = context.config?.root;\n const inProject =\n project && !options.source\n ? relative(fileURLToPath(project), join(rootFor(context), inContent)).split(sep).join(\"/\")\n : undefined;\n return { inContent, inProject };\n };\n\n /** Warns about broken relations and missing files, or fails the build with `strict`. */\n const checkReferences = async (\n context: LoaderContext,\n source: ContentSource,\n documents: Array<{ label: string; fields: FieldMap; data: Record<string, unknown> }>,\n ): Promise<void> => {\n const check = createReferenceCheck(site, source);\n const problems: string[] = [];\n for (const { label, fields, data } of documents) {\n for (const issue of await check.issues(fields, data)) problems.push(`${label}: ${issue.path}: ${issue.message}`);\n }\n if (problems.length === 0) return;\n if (options.strict) throw new Error(`Broken references:\\n ${problems.join(\"\\n \")}`);\n for (const problem of problems) context.logger.warn(problem);\n };\n\n /** Collections whose watcher is already registered, so a reload adds none. */\n const watching = new Set<string>();\n\n const watch = (context: LoaderContext, name: string, reload: () => Promise<void>): void => {\n const watcher = context.watcher;\n if (!watcher || options.source || watching.has(name)) return;\n watching.add(name);\n\n const base = rootFor(context);\n watcher.add(base);\n\n // A collection lives in `<root>/<name>/`; a singleton is either\n // `<root>/<name>.json` or `<root>/<name>/…`. Both are covered by this\n // prefix, and nothing outside it can concern this loader — which is what\n // keeps one collection's save from re-reading every other one.\n const prefix = join(base, name);\n const mine = (path: string): boolean =>\n path === `${prefix}.json` || path.startsWith(prefix + sep);\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const onChange = (path: string): void => {\n // The manifest is this package's own output; reloading content because\n // the model was written is a loop waiting to happen.\n if (path.endsWith(MANIFEST_PATH) || !mine(path)) return;\n\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n // Content that fails its own schema lands here. The dev server has to\n // survive it: the next save is very likely the fix.\n void reload().catch((error: unknown) => {\n context.logger.error(`Could not reload content: ${describe(error)}`);\n });\n }, options.debounceMs ?? 100);\n // A pending reload is never a reason to keep the process alive.\n timer.unref?.();\n };\n\n watcher.on(\"change\", onChange);\n watcher.on(\"add\", onChange);\n watcher.on(\"unlink\", onChange);\n };\n\n const collections = {} as Record<string, AstroCollectionConfig<unknown>>;\n\n for (const [name, def] of Object.entries(site.collections) as [string, Collection][]) {\n collections[name] = {\n type: \"content_layer\",\n schema: ({ image }) => fieldsSchema(def.fields, { image }).extend({ _mica: metaSchema }),\n loader: {\n name: `mica:${name}`,\n load: async (context) => {\n const read = async (): Promise<void> => {\n const source = sourceFor(context);\n const entries = await guard(context, source, () => readCollection(source, def, name));\n\n // A full replace, not a merge: an entry deleted from the repository\n // has to disappear from the build.\n context.store.clear();\n\n for (const entry of entries) {\n const meta: MicaMeta = {\n id: entry.slug,\n slug: entry.slug,\n collection: name,\n position: entry.position,\n };\n\n const file = entryFile(context, def.fields, name, entry.slug);\n const data = await draftTolerant(context, `${name}/${entry.slug}`, () =>\n context.parseData({\n id: entry.slug,\n data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },\n ...(file.inProject ? { filePath: file.inProject } : {}),\n }),\n );\n if (data === undefined) continue;\n context.store.set({\n id: entry.slug,\n data,\n digest: context.generateDigest(data),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n }\n\n await checkReferences(\n context,\n source,\n entries.map((entry) => ({ label: `${name}/${entry.slug}`, fields: def.fields, data: entry.data })),\n );\n context.logger.info(`Loaded ${entries.length} ${entries.length === 1 ? \"entry\" : \"entries\"} of \"${def.label}\"`);\n };\n\n await publish(context);\n await read();\n watch(context, name, read);\n },\n },\n };\n }\n\n // A singleton becomes a one-entry collection whose id is its name, so the\n // website reaches it with the same `getEntry` it uses for everything else.\n for (const [name, def] of Object.entries(site.singletons) as [string, Singleton][]) {\n collections[name] = {\n type: \"content_layer\",\n schema: ({ image }) => fieldsSchema(def.fields, { image }),\n loader: {\n name: `mica:${name}`,\n load: async (context) => {\n const read = async (): Promise<void> => {\n const source = sourceFor(context);\n const data = await guard(context, source, () => readSingleton(source, def, name));\n\n context.store.clear();\n if (data === null) {\n // Not fatal. A site is normally deployed before every settings\n // form has been filled in, and the page that reads this singleton\n // reports the absence far better than the loader can.\n context.logger.warn(`No content for \"${def.label}\" yet.`);\n return;\n }\n\n await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);\n const file = entryFile(context, def.fields, name);\n const parsed = await draftTolerant(context, name, () =>\n context.parseData({\n id: name,\n data: imagePathsFromEntry(def.fields, data, file.inContent),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n }),\n );\n if (parsed === undefined) return;\n context.store.set({\n id: name,\n data: parsed,\n digest: context.generateDigest(parsed),\n ...(file.inProject ? { filePath: file.inProject } : {}),\n });\n };\n\n await publish(context);\n await read();\n watch(context, name, read);\n },\n },\n };\n }\n\n return collections as MicaCollections<S>;\n}\n\n/**\n * In dev, an entry that fails its schema is left out with a warning: the editor\n * saves a draft before every required field is filled in, and Publish refuses it\n * until then. A build has no watcher, and fails as before.\n */\nasync function draftTolerant<T>(context: LoaderContext, label: string, parse: () => Promise<T>): Promise<T | undefined> {\n try {\n return await parse();\n } catch (error) {\n if (!context.watcher) throw error;\n context.logger.warn(`Left out \"${label}\" until it is valid: ${describe(error)}`);\n return undefined;\n }\n}\n\n/**\n * The site's own model must use only kinds this package knows. Mica reads an\n * unknown kind as one from a newer version and makes its collection read-only,\n * so a typo would otherwise pass the build and lock the collection.\n */\nfunction assertKnownKinds(site: Site): void {\n const check = (fields: FieldMap, at: string) => {\n for (const [key, field] of Object.entries(fields)) {\n if (!isFieldKind(field.kind)) {\n throw new Error(`Field \"${at}.${key}\" has the unknown kind \"${String(field.kind)}\". Use the field functions from @micacms/core.`);\n }\n if (field.kind === \"object\" || field.kind === \"repeater\") check(field.fields, `${at}.${key}`);\n }\n };\n for (const [name, def] of Object.entries(site.collections)) check(def.fields, `collections.${name}`);\n for (const [name, def] of Object.entries(site.singletons)) check(def.fields, `singletons.${name}`);\n}\n\n/**\n * A collection's own slug fields are typed as never null, because Mica fills\n * the URL field from the file name. A second one would be typed the same and\n * could still be null, so the site's model refuses it. (A slug in an object,\n * such as one per language, is nullable and fine.) Mica itself still reads a\n * manifest with two, so an older site keeps working with a newer Mica.\n */\nfunction assertOneUrl(site: Site): void {\n for (const [name, def] of Object.entries(site.collections)) {\n const url = resolveSlugField(def);\n const others = Object.entries(def.fields).filter(([key, field]) => field.kind === \"slug\" && key !== url).map(([key]) => key);\n if (others.length > 0) {\n throw new Error(\n `Collection \"${name}\" has slug fields that are not its URL (${others.join(\", \")}); its URL is \"${url}\". ` +\n \"An entry has one URL: use text() for the others, or put them in an object, such as one per language.\",\n );\n }\n }\n}\n\n/**\n * Writes the model into the content root, for Mica to read back.\n *\n * Mica holds no content model: the Astro project is the only place a collection\n * is defined, and this is how it says so. The file goes *inside the content\n * root* so that it travels to Mica the way content does — through Git and the\n * repository connector — with nothing to mount and no second channel to keep in\n * step. Commit it.\n *\n * Done from a loader rather than an integration because this is the moment both\n * facts are in hand: the model, and the project root that turns `./content`\n * into a directory. It also means `astro.config` needs no Mica in it, which is\n * what lets a project define Mica collections and ordinary Astro ones in the\n * same `content.config.ts`.\n */\nasync function publishModel(\n site: Site,\n root: string,\n logger: LoaderContext[\"logger\"],\n): Promise<void> {\n const target = join(root, MANIFEST_PATH);\n const contents = `${JSON.stringify(manifestFor(site), null, 2)}\\n`;\n\n try {\n // Only when it differs. The content root is watched in dev, and rewriting\n // an identical file on every start would ask for a reload for no reason.\n if ((await readFile(target, \"utf8\").catch(() => null)) === contents) return;\n\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, contents, \"utf8\");\n logger.info(`Wrote the content model to ${relative(process.cwd(), target)} — commit it for Mica to read.`);\n } catch (error) {\n // A site that builds correctly should not fail over this, and a read-only\n // checkout is a normal way to build one in CI. Mica reports a missing model\n // itself, and far more usefully.\n logger.warn(`Could not write the content model: ${describe(error)}`);\n }\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Reports a read failure against the content root that produced it.\n *\n * A build failing on unreadable content is correct — publishing a half-empty\n * site would be worse — but the message has to name the directory that was\n * read, because the usual cause is a content root pointing at the wrong place.\n */\nasync function guard<T>(\n context: LoaderContext,\n source: ContentSource,\n read: () => Promise<T>,\n): Promise<T> {\n try {\n return await read();\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n context.logger.error(detail);\n\n throw new Error(`Could not read content from ${source.origin}.\\n ${detail}`, { cause: error });\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Types for the website\n * ------------------------------------------------------------------ */\n\n/** The document type for one collection, as it appears in Astro. */\nexport type CollectionDocument<\n S extends Site,\n K extends keyof S[\"collections\"],\n> = AstroDocument<InferCollection<S[\"collections\"][K], ImageMetadata>>;\n\n/** The document type for one singleton. */\nexport type SingletonDocument<\n S extends Site,\n K extends keyof S[\"singletons\"],\n> = InferSingleton<S[\"singletons\"][K], ImageMetadata>;\n\nexport { manifestFor } from \"./manifest.js\";\nexport { DEFAULT_CONTENT_ROOT, resolveContentRoot };\nexport {\n renderRichText,\n transformRichText,\n validateRichText,\n type RichTextOptions,\n} from \"./richtext.js\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micacms/astro",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Reads a Mica content repository as Astro Content Collections via the Content Layer API.",
6
6
  "license": "MIT",
@@ -44,14 +44,14 @@
44
44
  "peerDependencies": {
45
45
  "astro": "^7.0.0",
46
46
  "zod": "^4.3.6",
47
- "@micacms/core": "^0.6.0"
47
+ "@micacms/core": "^0.7.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^22.20.1",
51
51
  "astro": "^7.2.2",
52
52
  "typescript": "^5.7.3",
53
53
  "zod": "^4.3.6",
54
- "@micacms/core": "0.6.0"
54
+ "@micacms/core": "0.7.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsc -p tsconfig.json",
@@ -43,9 +43,10 @@ Rules that `defineSite` enforces:
43
43
  - Collection, singleton and field names are lowerCamelCase and start with a
44
44
  letter: `news`, `teamMembers`, `publishedOn`. Not `team-members`.
45
45
  - A name is a collection or a singleton, not both.
46
- - Every collection has a `slug()` field. Its value is the name of the entry's
47
- file or folder, and the entry's address on the website. Use one; with more,
48
- the first is the slug unless `slugField` names another.
46
+ - Every collection has one `slug()` field among its own fields. Its value is
47
+ the name of the entry's file or folder, and the entry's address on the
48
+ website. A second one there stops the build; a slug in an object (such as one
49
+ per language) is fine and can be empty.
49
50
  - A `relation()` names a collection of the same model.
50
51
  - A `select()` has at least one option.
51
52
 
@@ -63,7 +64,7 @@ in the entry list), `readonly`, `hidden`, `group`, `width` (`"full"`, `"half"`,
63
64
 
64
65
  | Field | Stored as | Other options |
65
66
  | --- | --- | --- |
66
- | `text()` | string | `minLength`, `maxLength`, `pattern` (must match the whole value), `placeholder`, `default` |
67
+ | `text()` | string | `minLength`, `maxLength`, `pattern` (must match the whole value), `patternMessage` (what the editor reads when it does not), `placeholder`, `default` |
67
68
  | `textarea()` | string | `minLength`, `maxLength`, `rows`, `placeholder`, `default` |
68
69
  | `richText()` | Markdoc in its own `.mdoc` file | `toolbar`, `maxLength`, `default` |
69
70
  | `number()` | number | `min`, `max`, `step`, `integer`, `unit`, `default` |
@@ -78,7 +79,7 @@ in the entry list), `readonly`, `hidden`, `group`, `width` (`"full"`, `"half"`,
78
79
  | `relation()` | the slug of an entry | `collection` (required), `multiple` |
79
80
  | `media()` | a file path, or `{ path, alt }` with `alt` | `accept: "image" \| "document" \| "any"`, `multiple`, `max`, `alt: "required" \| "optional"` |
80
81
  | `color()` | `"#rrggbb"` | `presets`, `default` |
81
- | `coordinates()` | `{ "lat": 47.37, "lng": 8.54 }` | `defaultCenter`, `defaultZoom` |
82
+ | `coordinates()` | `{ "lat": 47.37, "lng": 8.54 }` | — |
82
83
 
83
84
  There is no field for a free list of strings, such as free tags. Use
84
85
  `select({ multiple: true, options })` for a fixed list, or a `repeater()` with
@@ -104,7 +105,8 @@ content/
104
105
  ```
105
106
 
106
107
  - A slug has lowercase letters, digits and single hyphens: `hello-world`. The
107
- slug field in the JSON has the same value as the folder or file name.
108
+ slug field in the JSON has the same value as the folder or file name. Where
109
+ they differ, Mica uses the name.
108
110
  - A media path in the JSON is relative to the entry's folder (`media/cover.jpg`),
109
111
  or starts with `/` and is relative to the content root (`/shared/logo.png`).
110
112
  - A rich-text field in a repeater or an object gets a dotted file name:
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  INDEX_FILE,
29
29
  singletonPath,
30
30
  isFieldKind,
31
+ resolveSlugField,
31
32
  MANIFEST_PATH,
32
33
  readCollection,
33
34
  readSingleton,
@@ -187,6 +188,7 @@ export function micaCollections<S extends Site>(
187
188
  options: MicaCollectionsOptions = {},
188
189
  ): MicaCollections<S> {
189
190
  assertKnownKinds(site);
191
+ assertOneUrl(site);
190
192
 
191
193
  /**
192
194
  * Everything below is resolved on first load rather than here, because the
@@ -309,11 +311,14 @@ export function micaCollections<S extends Site>(
309
311
  };
310
312
 
311
313
  const file = entryFile(context, def.fields, name, entry.slug);
312
- const data = await context.parseData({
313
- id: entry.slug,
314
- data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },
315
- ...(file.inProject ? { filePath: file.inProject } : {}),
316
- });
314
+ const data = await draftTolerant(context, `${name}/${entry.slug}`, () =>
315
+ context.parseData({
316
+ id: entry.slug,
317
+ data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },
318
+ ...(file.inProject ? { filePath: file.inProject } : {}),
319
+ }),
320
+ );
321
+ if (data === undefined) continue;
317
322
  context.store.set({
318
323
  id: entry.slug,
319
324
  data,
@@ -362,11 +367,14 @@ export function micaCollections<S extends Site>(
362
367
 
363
368
  await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);
364
369
  const file = entryFile(context, def.fields, name);
365
- const parsed = await context.parseData({
366
- id: name,
367
- data: imagePathsFromEntry(def.fields, data, file.inContent),
368
- ...(file.inProject ? { filePath: file.inProject } : {}),
369
- });
370
+ const parsed = await draftTolerant(context, name, () =>
371
+ context.parseData({
372
+ id: name,
373
+ data: imagePathsFromEntry(def.fields, data, file.inContent),
374
+ ...(file.inProject ? { filePath: file.inProject } : {}),
375
+ }),
376
+ );
377
+ if (parsed === undefined) return;
370
378
  context.store.set({
371
379
  id: name,
372
380
  data: parsed,
@@ -386,6 +394,21 @@ export function micaCollections<S extends Site>(
386
394
  return collections as MicaCollections<S>;
387
395
  }
388
396
 
397
+ /**
398
+ * In dev, an entry that fails its schema is left out with a warning: the editor
399
+ * saves a draft before every required field is filled in, and Publish refuses it
400
+ * until then. A build has no watcher, and fails as before.
401
+ */
402
+ async function draftTolerant<T>(context: LoaderContext, label: string, parse: () => Promise<T>): Promise<T | undefined> {
403
+ try {
404
+ return await parse();
405
+ } catch (error) {
406
+ if (!context.watcher) throw error;
407
+ context.logger.warn(`Left out "${label}" until it is valid: ${describe(error)}`);
408
+ return undefined;
409
+ }
410
+ }
411
+
389
412
  /**
390
413
  * The site's own model must use only kinds this package knows. Mica reads an
391
414
  * unknown kind as one from a newer version and makes its collection read-only,
@@ -404,6 +427,26 @@ function assertKnownKinds(site: Site): void {
404
427
  for (const [name, def] of Object.entries(site.singletons)) check(def.fields, `singletons.${name}`);
405
428
  }
406
429
 
430
+ /**
431
+ * A collection's own slug fields are typed as never null, because Mica fills
432
+ * the URL field from the file name. A second one would be typed the same and
433
+ * could still be null, so the site's model refuses it. (A slug in an object,
434
+ * such as one per language, is nullable and fine.) Mica itself still reads a
435
+ * manifest with two, so an older site keeps working with a newer Mica.
436
+ */
437
+ function assertOneUrl(site: Site): void {
438
+ for (const [name, def] of Object.entries(site.collections)) {
439
+ const url = resolveSlugField(def);
440
+ const others = Object.entries(def.fields).filter(([key, field]) => field.kind === "slug" && key !== url).map(([key]) => key);
441
+ if (others.length > 0) {
442
+ throw new Error(
443
+ `Collection "${name}" has slug fields that are not its URL (${others.join(", ")}); its URL is "${url}". ` +
444
+ "An entry has one URL: use text() for the others, or put them in an object, such as one per language.",
445
+ );
446
+ }
447
+ }
448
+ }
449
+
407
450
  /**
408
451
  * Writes the model into the content root, for Mica to read back.
409
452
  *