@micacms/astro 0.6.0-rc.2 → 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-rc.2",
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-rc.2"
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-rc.2"
54
+ "@micacms/core": "0.7.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsc -p tsconfig.json",
@@ -36,49 +36,104 @@ the integrations in `astro.config.*`. For each collection, find:
36
36
  - the number of entries, and whether the ids have folders (`2024/hello`);
37
37
  - what the bodies contain: plain Markdown, raw HTML, footnotes, MDX imports and
38
38
  components, images with relative paths;
39
- - the pages and URLs that use the collection.
40
-
41
- ## 3. Ask the user
42
-
43
- Show the inventory as a short table, and ask:
44
-
45
- 1. Which collections should Mica manage? Content that only developers change,
46
- such as documentation in the repository, can stay an Astro collection.
47
- 2. The model you propose for each collection, mapped as in step 4.
48
- 3. Every case where content cannot move without a change:
49
- - MDX components in a body. Markdoc does not run them. Options: a Markdoc
50
- tag that the website declares and renders, a field, or keep that
51
- collection in Astro. Remove the `import` lines too, and the text that
52
- refers to a component you drop.
53
- - Raw HTML in a body. Markdoc does not render it. Options: Markdown (`<b>`
54
- becomes `**`), a Unicode character, a Markdoc tag, or remove it. Some tags
55
- have no Markdown form (`<sub>`, `<sup>`, `<kbd>`, `<mark>`). Do not change
56
- HTML inside code blocks or inline code: that is text.
57
- - Footnotes (`[^1]`). Markdoc shows them as text. Options: a normal
58
- paragraph or a link. Remove the `[^1]` markers from the text too.
59
- - Images in a body (`![](./photo.jpg)`). The rich-text editor has no images.
60
- Options: a `media()` field or a `repeater()` with images, or files in
61
- `public/` with absolute URLs, which Astro does not optimize.
62
- - Ids with folders, or ids that are not slugs. They become flat slugs, so
63
- the URL can change. Options: keep the URL with Astro `redirects`, or
64
- accept the new URL.
65
- - A free list of strings, such as tags. See
66
- [reference.md](./reference.md#the-content-model).
67
- - Headings lose their `id` and code blocks lose their highlighting, unless
68
- the website adds them. See
69
- [reference.md](./reference.md#what-markdoc-renders-differently-from-astros-markdown).
70
- 4. Should editors put a collection in a manual order? If a collection has a
71
- number field that only sorts it, such as `order`, give the user the choice
72
- and the reason:
73
- - Keep it as a `number()` field. No page changes. Editors type numbers,
74
- and two entries can get the same one.
75
- - Make the collection `sortable: true`. Editors drag the entries into
76
- order, which is easier and cannot give two entries the same place. The
77
- import turns the numbers into `_order.json` (`--order`), the field goes, and each
78
- page that sorts by it sorts by `entry.data._mica.position` instead: one
79
- changed line per page.
80
-
81
- Wait for the answers.
39
+ - the pages, components and URLs that use the collection, and the lines that
40
+ read each field;
41
+ - whether the site uses Tailwind CSS 4: a stylesheet with
42
+ `@import "tailwindcss"`.
43
+
44
+ ## 3. Propose, and wait for the approval
45
+
46
+ Before you write the model or change any file, show the user one proposal and
47
+ wait for a short approval or correction. The user must know what will change
48
+ on their website, and why, before anything changes. Write for a reader who is
49
+ new to Mica and may know little about Astro: plain words, short sentences, no
50
+ code. Map the fields with the table in step 4. Use this form:
51
+
52
+ ```markdown
53
+ ## What editors will manage
54
+
55
+ **Team** (2 entries): the people on the team page.
56
+ - Name — text, required
57
+ - Role — text, required
58
+ - Photo — an image, with its alt text right under it (required)
59
+ - Editors put the people in order by dragging them.
60
+
61
+ **Stays as it is:** <collections that stay in Astro, and why>
62
+
63
+ ## What changes in the website's files
64
+
65
+ 1. **The team photos' alt text moves into the photo field.** Editors then
66
+ write it right under the photo, and Mica does not save a photo without it.
67
+ In `TeamSection.astro`, two lines read the photo and its alt text in the
68
+ new place. Visitors see no difference.
69
+ Recommended. Other option: keep the alt text as its own field; then this
70
+ file does not change.
71
+ 2. …
72
+
73
+ ## What cannot move as it is
74
+
75
+ - <each case from the list below, with the options>
76
+
77
+ Shall I go ahead like this? You can change any point.
78
+ ```
79
+
80
+ List every change to a page, a component, a layout, a style or a config file,
81
+ one per item: what changes, in which file, whether visitors can see it
82
+ (normally not), your recommendation, and the other option. Only what you list
83
+ here may change later. The usual items:
84
+
85
+ - **Manual order.** If a collection has a number field that only sorts it,
86
+ such as `order`: recommend that editors drag the entries into order. It is
87
+ easier, and two entries cannot get the same place. The import turns the
88
+ numbers into `_order.json` (`--order`), the field goes, and each page that
89
+ sorts by it sorts by `entry.data._mica.position`: one changed line per
90
+ page. Other option: keep the number field, with no page change.
91
+ - **Alt text.** If an image has its alt text in a separate field: recommend
92
+ the image field with alt text (`media({ accept: "image", alt: "required" })`,
93
+ `--map <collection>.<image>.alt=<field>`). Editors see it under the image,
94
+ and Mica requires it. The page then reads `entry.data.<image>.src` and
95
+ `.alt`, or `<Image {...entry.data.<image>} />`. Other option: keep the
96
+ separate field, with no page change.
97
+ - **Dates.** A date becomes text such as `2026-01-15`. Each page that formats
98
+ or sorts it changes (step 7). Visitors see the same date.
99
+ - **Bodies.** The Markdown body becomes Markdoc: `<Content />` is replaced
100
+ (step 7).
101
+ - **Tailwind CSS 4.** Tailwind reads every file of the project for class
102
+ names, now also `src/mica.ts` and `content/`. A word in a text, such as
103
+ "hidden", then becomes CSS, and the CSS changes when an editor changes a
104
+ text. Recommend excluding both, in the stylesheet with
105
+ `@import "tailwindcss"`, with paths from that file:
106
+ `@source not "./mica.ts";` and `@source not "../content";`. Visitors see no
107
+ difference.
108
+
109
+ What cannot move as it is, with the options to propose:
110
+
111
+ - MDX components in a body. Markdoc does not run them. Options: a Markdoc tag
112
+ that the website declares and renders, a field, or keep that collection in
113
+ Astro. Remove the `import` lines too, and the text that refers to a
114
+ component you drop.
115
+ - Raw HTML in a body. Markdoc does not render it. Options: Markdown (`<b>`
116
+ becomes `**`), a Unicode character, a Markdoc tag, or remove it. Some tags
117
+ have no Markdown form (`<sub>`, `<sup>`, `<kbd>`, `<mark>`). Do not change
118
+ HTML inside code blocks or inline code: that is text.
119
+ - Footnotes (`[^1]`). Markdoc shows them as text. Options: a normal paragraph
120
+ or a link. Remove the `[^1]` markers from the text too.
121
+ - Images in a body (`![](./photo.jpg)`). The rich-text editor has no images.
122
+ Options: a `media()` field or a `repeater()` with images, or files in
123
+ `public/` with absolute URLs, which Astro does not optimize.
124
+ - Ids with folders, or ids that are not slugs. They become flat slugs, so the
125
+ URL can change. Options: keep the URL with Astro `redirects`, or accept the
126
+ new URL.
127
+ - A free list of strings, such as tags. See
128
+ [reference.md](./reference.md#the-content-model).
129
+ - Headings lose their `id` and code blocks lose their highlighting, unless the
130
+ website adds them. See
131
+ [reference.md](./reference.md#what-markdoc-renders-differently-from-astros-markdown).
132
+
133
+ Collections that only developers change, such as documentation in the
134
+ repository, can stay Astro collections: propose that where it fits.
135
+
136
+ Wait for the answer. Change the proposal until the user agrees.
82
137
 
83
138
  ## 4. Map the schema to the model
84
139
 
@@ -158,8 +213,10 @@ from [reference.md](./reference.md#write-entries-with-a-script).
158
213
 
159
214
  ## 7. Change the pages
160
215
 
161
- Look at every file from the inventory: pages, layouts, components and
162
- endpoints such as `rss.xml.js`. See
216
+ Make the changes that the user agreed to in step 3, and no others. If you find
217
+ a change that the proposal did not list, describe it to the user and wait for
218
+ an answer first. Look at every file from the inventory: pages, layouts,
219
+ components, stylesheets and endpoints such as `rss.xml.js`. See
163
220
  [reference.md](./reference.md#render-the-content-in-astro).
164
221
 
165
222
  - `render(entry)` and `<Content />` become
package/setup/finish.md CHANGED
@@ -52,33 +52,49 @@ paths if the project uses others.
52
52
  - Before a commit that changes `content/`, `npm run check` must pass.
53
53
  ```
54
54
 
55
+ Change no other documentation, such as `README.md`, unless the user asks.
56
+
55
57
  ## 4. Tell the user
56
58
 
57
- - What you added and changed, file by file.
58
- - The decisions they made, and what was not converted, if anything.
59
- - How to commit: give the commands, and run them only if the user asks. With
60
- converted content, use two commits, so that the history keeps the setup
61
- apart from the content. Name the files of the first commit from
62
- `git status`:
63
-
64
- ```bash
65
- # 1. The setup: packages, model, collections config, agent notes, scripts.
66
- git add package.json package-lock.json src/mica.ts src/content.config.ts \
67
- content/.mica/schema.json AGENTS.md
68
- git commit -m "Add Mica"
69
- # 2. The content and the page changes: everything else that git status shows.
70
- git add -A
71
- git commit -m "Move the content to Mica"
72
- ```
73
-
74
- Use the project's lock file (`pnpm-lock.yaml`, `yarn.lock`). For a new
75
- project without converted content, one commit is enough.
76
- - How to publish the branch: on a branch that is not on the remote yet,
77
- `git push -u origin <branch>`. The editor's Publish does this too, the first
78
- time.
79
- - How to edit: `npm run mica` opens the editor on this computer. A save writes
80
- files in `content/`; Publish commits them. `npm run check` checks the content
81
- (and the types, if `@astrojs/check` is there). `@micacms/editor` is a dev
82
- dependency, so on another clone, run `npm install` first.
83
- - That a self-hosted editor for other people is described at
84
- https://micacms.com/docs/self-hosting/.
59
+ Your last message is the report. Its commands are for this one time: do not
60
+ write them into `README.md` or another file. Use this form, with the real
61
+ files from `git status`:
62
+
63
+ ````markdown
64
+ ## Done
65
+
66
+ Mica is set up. <one sentence on what editors can now edit>.
67
+
68
+ **What changed:** <file by file, one line each: what and why, in plain words>.
69
+ **Your decisions:** <each decision from the proposal, and the choice>.
70
+ **Not converted:** <anything that was not converted, or "nothing">.
71
+ **Checks:** `npm run check` and `npx astro build` pass. <how the website
72
+ compares with the version before>.
73
+
74
+ ## Commit it
75
+
76
+ ```bash
77
+ # 1. The setup: packages, model, collections config, agent notes.
78
+ git add package.json package-lock.json src/mica.ts src/content.config.ts \
79
+ content/.mica/schema.json AGENTS.md <other setup files>
80
+ git commit -m "Add Mica"
81
+ # 2. The content and the page changes.
82
+ git add -A
83
+ git commit -m "Move the content to Mica"
84
+ # 3. A branch that is not on the remote yet.
85
+ git push -u origin <branch>
86
+ ```
87
+
88
+ ## Edit the content
89
+
90
+ `npm run mica` opens the editor on this computer. A save writes files in
91
+ `content/`; Publish commits them, and pushes a new branch the first time.
92
+ `npm run check` checks the content (and the types, if `@astrojs/check` is
93
+ there). On another clone, run `npm install` first. A self-hosted editor for
94
+ other people: https://micacms.com/docs/self-hosting/.
95
+ ````
96
+
97
+ - Use the project's lock file (`pnpm-lock.yaml`, `yarn.lock`) and package
98
+ manager.
99
+ - For a new project without converted content, one commit is enough.
100
+ - Run the commands only if the user asks.
@@ -25,8 +25,24 @@ Ask these questions, and wait for the answers:
25
25
  file.
26
26
  6. Do they want example content, so that the pages show something at once?
27
27
 
28
- Suggest a model from the answers, as a short list of collections, singletons
29
- and fields. Change it until the user agrees.
28
+ Then show one proposal, and wait for a short approval or correction before
29
+ you write a file. Write for a reader who is new to Mica and may know little
30
+ about Astro: plain words, no code.
31
+
32
+ - **What editors will manage:** each collection and singleton, one line on
33
+ what it is, and its fields in plain words ("Title — text, required",
34
+ "Cover — an image, with its alt text under it").
35
+ - **The pages:** each page you will make, with its URL and what it shows.
36
+ - **Existing files that change:** in an empty Astro project, for example
37
+ `src/pages/index.astro` or a layout: what changes, and why. Nothing else
38
+ changes.
39
+ - Your recommendation where there is a choice, and the other option: manual
40
+ order by dragging, or sorting by a field; alt text required for images that
41
+ carry meaning.
42
+
43
+ End with: "Shall I go ahead like this? You can change any point." Change the
44
+ proposal until the user agrees, and later make no change that it did not
45
+ list without asking first.
30
46
 
31
47
  ## 2. Add Mica to the project
32
48
 
@@ -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
  *