@micacms/astro 0.5.0-rc.1 → 0.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/richtext.js.map +1 -1
- package/dist/root.js.map +1 -1
- package/package.json +4 -3
- package/setup/content-collections.md +211 -0
- package/setup/finish.md +49 -0
- package/setup/new-project.md +73 -0
- package/setup/other-cms.md +9 -0
- package/setup/reference.md +252 -0
package/README.md
CHANGED
|
@@ -88,6 +88,12 @@ of its collections reloads that collection only — never a foreign loader.
|
|
|
88
88
|
`transformRichText(source)` returns the renderable tree instead, for a project
|
|
89
89
|
that wants to render with its own components.
|
|
90
90
|
|
|
91
|
+
## Setup guides for coding agents
|
|
92
|
+
|
|
93
|
+
`setup/` holds the guides that https://micacms.com/setup.md sends a coding
|
|
94
|
+
agent to: a new or empty Astro project, and a project with content
|
|
95
|
+
collections. They ship with the package, so they match the installed version.
|
|
96
|
+
|
|
91
97
|
## Install
|
|
92
98
|
|
|
93
99
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @micacms/astro — a Git repository as an Astro content source.
|
|
3
3
|
*
|
|
4
|
-
* // src/content.config.ts
|
|
4
|
+
* // src/content.config.ts — the model is in src/mica.ts
|
|
5
5
|
* import { micaCollections } from "@micacms/astro";
|
|
6
|
-
* import {
|
|
6
|
+
* import { site } from "./mica";
|
|
7
7
|
*
|
|
8
|
-
* export const
|
|
9
|
-
* export const collections = micaCollections(site, { root: "./content" });
|
|
8
|
+
* export const collections = micaCollections(site);
|
|
10
9
|
*
|
|
11
10
|
* From there the website uses ordinary Astro APIs — `getCollection("projects")`
|
|
12
11
|
* — and knows nothing about how the content arrived.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @micacms/astro — a Git repository as an Astro content source.
|
|
3
3
|
*
|
|
4
|
-
* // src/content.config.ts
|
|
4
|
+
* // src/content.config.ts — the model is in src/mica.ts
|
|
5
5
|
* import { micaCollections } from "@micacms/astro";
|
|
6
|
-
* import {
|
|
6
|
+
* import { site } from "./mica";
|
|
7
7
|
*
|
|
8
|
-
* export const
|
|
9
|
-
* export const collections = micaCollections(site, { root: "./content" });
|
|
8
|
+
* export const collections = micaCollections(site);
|
|
10
9
|
*
|
|
11
10
|
* From there the website uses ordinary Astro APIs — `getCollection("projects")`
|
|
12
11
|
* — and knows nothing about how the content arrived.
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
* Publishing is a commit, and CI builds the commit.
|
|
18
17
|
*/
|
|
19
18
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
20
|
-
import { dirname, join, sep } from "node:path";
|
|
19
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
21
20
|
import { fieldsSchema, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/core";
|
|
22
21
|
import { fileSystemSource } from "@micacms/core/node";
|
|
23
22
|
import { z } from "zod";
|
|
@@ -113,7 +112,7 @@ export function micaCollections(site, options = {}) {
|
|
|
113
112
|
});
|
|
114
113
|
context.store.set({ id: entry.slug, data, digest: context.generateDigest(data) });
|
|
115
114
|
}
|
|
116
|
-
context.logger.info(`Loaded ${entries.length} ${def.label
|
|
115
|
+
context.logger.info(`Loaded ${entries.length} ${entries.length === 1 ? "entry" : "entries"} of "${def.label}"`);
|
|
117
116
|
};
|
|
118
117
|
await publish(context);
|
|
119
118
|
await read();
|
|
@@ -139,7 +138,7 @@ export function micaCollections(site, options = {}) {
|
|
|
139
138
|
// Not fatal. A site is normally deployed before every settings
|
|
140
139
|
// form has been filled in, and the page that reads this singleton
|
|
141
140
|
// reports the absence far better than the loader can.
|
|
142
|
-
context.logger.warn(`No content for
|
|
141
|
+
context.logger.warn(`No content for "${def.label}" yet.`);
|
|
143
142
|
return;
|
|
144
143
|
}
|
|
145
144
|
const parsed = await context.parseData({ id: name, data });
|
|
@@ -179,7 +178,7 @@ async function publishModel(site, root, logger) {
|
|
|
179
178
|
return;
|
|
180
179
|
await mkdir(dirname(target), { recursive: true });
|
|
181
180
|
await writeFile(target, contents, "utf8");
|
|
182
|
-
logger.info(`Wrote the content model to ${
|
|
181
|
+
logger.info(`Wrote the content model to ${relative(process.cwd(), target)} — commit it for Mica to read.`);
|
|
183
182
|
}
|
|
184
183
|
catch (error) {
|
|
185
184
|
// A site that builds correctly should not fail over this, and a read-only
|
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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"}
|
|
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\";\n\nimport {\n fieldsSchema,\n MANIFEST_PATH,\n readCollection,\n readSingleton,\n type Collection,\n type ContentSource,\n type InferCollection,\n type InferSingleton,\n type Singleton,\n type Site,\n} from \"@micacms/core\";\nimport { fileSystemSource } from \"@micacms/core/node\";\nimport { z } from \"zod\";\n\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\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 * Typed as `ZodType<T>` rather than a bare `ZodObject`, because Astro derives\n * `CollectionEntry<\"projects\">[\"data\"]` from `z.infer` of whatever this is.\n * Left generic, every field in every template would degrade to `any` — and\n * the point of the shared model is that renaming a field breaks the build.\n */\n schema: 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 }): 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 }): 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<\n AstroDocument<InferCollection<S[\"collections\"][K]>>\n >;\n} & {\n [K in keyof S[\"singletons\"]]: AstroCollectionConfig<InferSingleton<S[\"singletons\"][K]>>;\n};\n\nexport function micaCollections<S extends Site>(\n site: S,\n options: MicaCollectionsOptions = {},\n): MicaCollections<S> {\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 /** 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 // `storage` rather than `output`: a media reference is a path into the\n // repository at rest and a path into the repository once loaded. There\n // is no expansion step left to tell the two apart.\n schema: fieldsSchema(def.fields, \"storage\").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 data = await context.parseData({\n id: entry.slug,\n data: { ...entry.data, _mica: meta },\n });\n context.store.set({ id: entry.slug, data, digest: context.generateDigest(data) });\n }\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: fieldsSchema(def.fields, \"storage\"),\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 const parsed = await context.parseData({ id: name, data });\n context.store.set({ id: name, data: parsed, digest: context.generateDigest(parsed) });\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 * 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]>>;\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]>;\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\n/**\n * Re-exported for convenience, so a project that only depends on this package\n * can still build a source or read a collection. They are defined in\n * `@micacms/core`, where the Mica server can reach them too.\n */\nexport {\n ContentReadError,\n ContentSourceError,\n ORDER_FILE,\n memorySource,\n readCollection,\n readSingleton,\n type ContentSource,\n type DirectoryEntry,\n} from \"@micacms/core\";\nexport { fileSystemSource } from \"@micacms/core/node\";\n"]}
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAoC,MAAM,eAAe,CAAC;AAEjF,6EAA6E;AAC7E,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU;IACpC,OAAO,cAAc,CAAC,IAAI,EAAE,kBAAkB,eAAe,EAAE,CAAC,CAAC;AACnE,CAAC"}
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAoC,MAAM,eAAe,CAAC;AAEjF,6EAA6E;AAC7E,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU;IACpC,OAAO,cAAc,CAAC,IAAI,EAAE,kBAAkB,eAAe,EAAE,CAAC,CAAC;AACnE,CAAC","sourcesContent":["/**\n * The protocol manifest this project publishes.\n *\n * Its own module rather than a function in `index.ts`, so that the integration\n * can emit a manifest without importing the loaders, and the loaders can be\n * imported without pulling in the integration. The two files would otherwise\n * import each other.\n */\n\nimport { createManifest, type ProtocolManifest, type Site } from \"@micacms/core\";\n\n/** Replaced at publish time; the fallback keeps a workspace build honest. */\nconst PACKAGE_VERSION = \"0.0.1\";\n\n/**\n * Mica holds no model of its own. The Astro project is the only place a\n * collection is defined, and this is how it says so.\n */\nexport function manifestFor(site: Site): ProtocolManifest {\n return createManifest(site, `@micacms/astro@${PACKAGE_VERSION}`);\n}\n"]}
|
package/dist/richtext.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"richtext.js","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,OAAO,EAAE,EAAwC,MAAM,kBAAkB,CAAC;AAajF;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC,EAAE,UAA2B,EAAE;IAC7F,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,UAA2B,EAAE;IAE7B,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,UAA2B,EAAE;IAC5E,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACvE,CAAC"}
|
|
1
|
+
{"version":3,"file":"richtext.js","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,OAAO,EAAE,EAAwC,MAAM,kBAAkB,CAAC;AAajF;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC,EAAE,UAA2B,EAAE;IAC7F,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,UAA2B,EAAE;IAE7B,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,UAA2B,EAAE;IAC5E,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACvE,CAAC","sourcesContent":["/**\n * Rendering a rich-text field.\n *\n * A `richText()` field is persisted as Markdoc, and this is what turns it into\n * HTML at build time. It runs during `astro build` only; the deployed page is\n * static, and no visitor's browser ever sees Markdoc.\n *\n * Markdoc rather than Markdown-with-HTML for one reason that matters: a Markdoc\n * document can only produce what the configuration allows. An undeclared\n * `{% tag %}` renders as nothing rather than as itself, and raw HTML is inert\n * unless it is explicitly permitted — which it is not here. So the canonical\n * content format cannot carry markup that escapes the site's own styling, and a\n * compromised or careless editor session cannot inject script into a build.\n */\n\nimport Markdoc, { type Config, type RenderableTreeNode } from \"@markdoc/markdoc\";\n\nexport interface RichTextOptions {\n /**\n * Extra Markdoc configuration: custom tags, nodes or variables.\n *\n * The place a project adds a callout or a gallery block without giving up the\n * closed set that makes the format safe. A tag must be declared here to\n * render at all.\n */\n config?: Config;\n}\n\n/**\n * Markdoc source to an HTML string.\n *\n * Returns an empty string for empty input, so a caller can render the result\n * directly without a null check ahead of it.\n */\nexport function renderRichText(source: string | null | undefined, options: RichTextOptions = {}): string {\n if (!source || source.trim() === \"\") return \"\";\n\n const tree = transformRichText(source, options);\n return Markdoc.renderers.html(tree);\n}\n\n/**\n * The intermediate renderable tree, for a project that wants to render with\n * its own components rather than to an HTML string.\n */\nexport function transformRichText(\n source: string,\n options: RichTextOptions = {},\n): RenderableTreeNode {\n return Markdoc.transform(Markdoc.parse(source), options.config ?? {});\n}\n\n/**\n * Problems Markdoc found in a document, for a build that wants to be strict.\n *\n * Not called by `renderRichText`: a malformed heading should not stop a site\n * from deploying, and Markdoc degrades to plain text rather than to nothing.\n * Exposed so a project can decide otherwise.\n */\nexport function validateRichText(source: string, options: RichTextOptions = {}) {\n return Markdoc.validate(Markdoc.parse(source), options.config ?? {});\n}\n"]}
|
package/dist/root.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root.js","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEhD,MAAM,UAAU,kBAAkB,CAAC,IAAwB,EAAE,WAAiB;IAC5E,MAAM,UAAU,GAAG,IAAI,IAAI,oBAAoB,CAAC;IAEhD,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAE9C,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC;IAE7C,uEAAuE;IACvE,0EAA0E;IAC1E,iBAAiB;IACjB,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,GAAG,CAAC;IACxF,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC"}
|
|
1
|
+
{"version":3,"file":"root.js","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEhD,MAAM,UAAU,kBAAkB,CAAC,IAAwB,EAAE,WAAiB;IAC5E,MAAM,UAAU,GAAG,IAAI,IAAI,oBAAoB,CAAC;IAEhD,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAE9C,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC;IAE7C,uEAAuE;IACvE,0EAA0E;IAC1E,iBAAiB;IACjB,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,GAAG,CAAC;IACxF,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC","sourcesContent":["/**\n * Where the content root is, said once.\n *\n * The collections, the dev integration and the manifest writer all have to\n * agree on this directory, and a disagreement between them produces an empty\n * build rather than an error — the worst kind of failure. So the project states\n * it once, in `defineSite({ root })`, and everything here resolves that one\n * string the same way.\n *\n * Resolved against the **Astro project root**, not the working directory, which\n * is what Astro's own `glob({ base })` does. A relative root therefore means\n * the same thing whether the build was started from the project, from a\n * workspace root, or by an editor extension. Absolute paths are left alone, for\n * content that lives outside the project.\n */\n\nimport { isAbsolute, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const DEFAULT_CONTENT_ROOT = \"./content\";\n\nexport function resolveContentRoot(root: string | undefined, projectRoot?: URL): string {\n const configured = root ?? DEFAULT_CONTENT_ROOT;\n\n if (isAbsolute(configured)) return configured;\n\n // Only outside a loader or a hook — a plain unit test, or an Astro version\n // that stops passing its config. The working directory is the best guess\n // left, and it is right whenever the build was started from the project.\n if (!projectRoot) return resolve(configured);\n\n // A base without a trailing slash would have its last segment replaced\n // rather than appended to. Astro's `config.root` has one; nothing else is\n // guaranteed to.\n const base = projectRoot.href.endsWith(\"/\") ? projectRoot.href : `${projectRoot.href}/`;\n return fileURLToPath(new URL(configured, base));\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micacms/astro",
|
|
3
|
-
"version": "0.5.0-rc.
|
|
3
|
+
"version": "0.5.0-rc.2",
|
|
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",
|
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"setup"
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@markdoc/markdoc": "^0.5.9",
|
|
31
32
|
"zod": "^4.3.6",
|
|
32
|
-
"@micacms/core": "0.5.0-rc.
|
|
33
|
+
"@micacms/core": "0.5.0-rc.2"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"astro": "^6.0.0 || ^7.0.0"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Move Astro content collections to Mica
|
|
2
|
+
|
|
3
|
+
For an Astro project whose content is in Astro content collections, usually
|
|
4
|
+
Markdown or MDX files with frontmatter. `SETUP.md` has already installed the
|
|
5
|
+
packages. The commands are in [reference.md](./reference.md#commands).
|
|
6
|
+
|
|
7
|
+
Mica stores content in its own format (JSON and Markdoc in `content/`), so the
|
|
8
|
+
entries are converted once. The website then reads them through
|
|
9
|
+
`micaCollections()`. Collections that Mica does not manage stay as they are.
|
|
10
|
+
|
|
11
|
+
## 1. Prepare
|
|
12
|
+
|
|
13
|
+
- The Git working tree must be clean (`git status`). If it is not, ask the
|
|
14
|
+
user to commit or stash first.
|
|
15
|
+
- Ask the user whether to work on a new branch, and make it if they agree.
|
|
16
|
+
- Build the website once, and copy the result to a temporary directory
|
|
17
|
+
outside the project. Step 8 compares with it.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx astro build
|
|
21
|
+
cp -r dist <temporary directory>/dist-before
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 2. Make an inventory
|
|
25
|
+
|
|
26
|
+
Read `src/content.config.*` (or `src/content/config.*`), every page, layout and
|
|
27
|
+
endpoint that calls `getCollection`, `getEntry`, `render` or `reference`, and
|
|
28
|
+
the integrations in `astro.config.*`. For each collection, find:
|
|
29
|
+
|
|
30
|
+
- the loader and the files: `glob()` with `.md`, `.mdx`, `.json` or `.yaml`, or
|
|
31
|
+
something else;
|
|
32
|
+
- the schema, field by field;
|
|
33
|
+
- the number of entries, and whether the ids have folders (`2024/hello`);
|
|
34
|
+
- what the bodies contain: plain Markdown, raw HTML, footnotes, MDX imports and
|
|
35
|
+
components, images with relative paths;
|
|
36
|
+
- the pages and URLs that use the collection.
|
|
37
|
+
|
|
38
|
+
## 3. Ask the user
|
|
39
|
+
|
|
40
|
+
Show the inventory as a short table, and ask:
|
|
41
|
+
|
|
42
|
+
1. Which collections should Mica manage? Content that only developers change,
|
|
43
|
+
such as documentation in the repository, can stay an Astro collection.
|
|
44
|
+
2. The model you propose for each collection, mapped as in step 4.
|
|
45
|
+
3. Every case where content cannot move without a change:
|
|
46
|
+
- MDX components in a body. Markdoc does not run them. Options: a Markdoc
|
|
47
|
+
tag that the website declares and renders, a field, or keep that
|
|
48
|
+
collection in Astro. Remove the `import` lines too, and the text that
|
|
49
|
+
refers to a component you drop.
|
|
50
|
+
- Raw HTML in a body. Markdoc does not render it. Options: Markdown (`<b>`
|
|
51
|
+
becomes `**`), a Unicode character, a Markdoc tag, or remove it. Some tags
|
|
52
|
+
have no Markdown form (`<sub>`, `<sup>`, `<kbd>`, `<mark>`). Do not change
|
|
53
|
+
HTML inside code blocks or inline code: that is text.
|
|
54
|
+
- Footnotes (`[^1]`). Markdoc shows them as text. Options: a normal
|
|
55
|
+
paragraph or a link. Remove the `[^1]` markers from the text too.
|
|
56
|
+
- Images in a body (``). The rich-text editor has no images.
|
|
57
|
+
Options: a `media()` field or a `repeater()` with images, or files in
|
|
58
|
+
`public/` with absolute URLs, which Astro does not optimize.
|
|
59
|
+
- Ids with folders, or ids that are not slugs. They become flat slugs, so
|
|
60
|
+
the URL can change. Options: keep the URL with Astro `redirects`, or
|
|
61
|
+
accept the new URL.
|
|
62
|
+
- A free list of strings, such as tags. See
|
|
63
|
+
[reference.md](./reference.md#the-content-model).
|
|
64
|
+
- Headings lose their `id` and code blocks lose their highlighting, unless
|
|
65
|
+
the website adds them. See
|
|
66
|
+
[reference.md](./reference.md#what-markdoc-renders-differently-from-astros-markdown).
|
|
67
|
+
4. Should editors put a collection in a manual order?
|
|
68
|
+
|
|
69
|
+
Wait for the answers.
|
|
70
|
+
|
|
71
|
+
## 4. Map the schema to the model
|
|
72
|
+
|
|
73
|
+
| Astro schema | Mica field |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `z.string()` | `text()`, or `textarea()` for long plain text |
|
|
76
|
+
| `z.string().url()` / `.email()` | `url()` / `email()` |
|
|
77
|
+
| `z.number()` | `number()` |
|
|
78
|
+
| `z.boolean()` | `boolean()` |
|
|
79
|
+
| `z.date()`, `z.coerce.date()` | `date()` (stored as `"YYYY-MM-DD"`) |
|
|
80
|
+
| `z.enum([...])` | `select({ options })` |
|
|
81
|
+
| `z.array(z.enum([...]))` | `select({ multiple: true, options })` |
|
|
82
|
+
| `image()` | `media({ accept: "image" })` |
|
|
83
|
+
| `reference("authors")` | `relation({ collection: "authors" })` |
|
|
84
|
+
| `z.object({...})` | `object({ fields })` |
|
|
85
|
+
| `z.array(z.object({...}))` | `repeater({ fields })` |
|
|
86
|
+
| `.optional()` | the field without `required: true` |
|
|
87
|
+
| the Markdown body | `richText()`, usually named `body` |
|
|
88
|
+
| the entry id | a `slug()` field |
|
|
89
|
+
|
|
90
|
+
Give the Mica collection the same name as the Astro collection, so that
|
|
91
|
+
`getCollection("blog")` in the pages keeps working. The names must be
|
|
92
|
+
lowerCamelCase.
|
|
93
|
+
|
|
94
|
+
## 5. Add the model
|
|
95
|
+
|
|
96
|
+
1. Run `npx mica init`. It keeps your `src/content.config.ts` and prints the
|
|
97
|
+
lines to add.
|
|
98
|
+
2. Replace the starter model in `src/mica.ts` (`news` and `settings`) with the
|
|
99
|
+
model the user agreed to.
|
|
100
|
+
3. In `src/content.config.ts`, remove the definitions of the collections that
|
|
101
|
+
Mica now manages, and add `...micaCollections(site)`. Keep the others.
|
|
102
|
+
4. Run `npx astro sync`. It writes `content/.mica/schema.json`, which the
|
|
103
|
+
conversion script reads. The pages can fail now; step 7 fixes them.
|
|
104
|
+
|
|
105
|
+
## 6. Convert the entries
|
|
106
|
+
|
|
107
|
+
Write a conversion script inside the project, for example
|
|
108
|
+
`scripts/convert-to-mica.mjs`. Use `writeEntry` from
|
|
109
|
+
[reference.md](./reference.md#write-entries-with-a-script). For each entry,
|
|
110
|
+
the script:
|
|
111
|
+
|
|
112
|
+
1. Reads the file and its frontmatter. For YAML, use a parser that the project
|
|
113
|
+
can already import (Astro depends on `yaml`), or install one for the
|
|
114
|
+
conversion only and remove it in step 9.
|
|
115
|
+
2. Makes the slug from the id: lowercase, folders and other characters turned
|
|
116
|
+
into single hyphens. Two ids must not give the same slug.
|
|
117
|
+
3. Builds `data` with every field of the model, in the stored form:
|
|
118
|
+
- a date from the text in the frontmatter, as `"YYYY-MM-DD"`. Take an ISO
|
|
119
|
+
date as it is written. Parse any other text in UTC, for example
|
|
120
|
+
`` new Date(`${text} UTC`).toISOString().slice(0, 10) ``, and compare a few
|
|
121
|
+
results with the source. Never format a local-time `Date`: in a time zone
|
|
122
|
+
east of UTC, that gives the day before.
|
|
123
|
+
- a relation as the slug of the other entry;
|
|
124
|
+
- the body as a Markdoc string, changed as the user decided in step 3;
|
|
125
|
+
- the slug field equal to the slug.
|
|
126
|
+
4. Copies the image of each `image()` field to
|
|
127
|
+
`content/<collection>/<slug>/media/<file>`, and stores `media/<file>` in the
|
|
128
|
+
field. The path in the frontmatter is relative to the source entry's file.
|
|
129
|
+
Body images go where the user decided in step 3.
|
|
130
|
+
5. Calls `writeEntry(collection, slug, data)`.
|
|
131
|
+
6. Prints a line for each entry, and a list of everything it changed or could
|
|
132
|
+
not convert.
|
|
133
|
+
|
|
134
|
+
Keep the source files. Do not delete anything in this step.
|
|
135
|
+
|
|
136
|
+
Run the script, then:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx mica check
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Fix the script, not the output, until `mica check` reports 0 errors. Before
|
|
143
|
+
each run, delete the converted collection's directory in `content/` (not
|
|
144
|
+
`content/.mica/`), so that no file from an earlier run stays. Read every warning: "Not read" means that an
|
|
145
|
+
entry or a file is lost, and a Markdoc warning means that some text does not
|
|
146
|
+
render as before. If you cannot fix a warning, list it in the report with the
|
|
147
|
+
reason.
|
|
148
|
+
|
|
149
|
+
## 7. Change the pages
|
|
150
|
+
|
|
151
|
+
Look at every file from the inventory: pages, layouts, components and
|
|
152
|
+
endpoints such as `rss.xml.js`. See
|
|
153
|
+
[reference.md](./reference.md#render-the-content-in-astro).
|
|
154
|
+
|
|
155
|
+
- `render(entry)` and `<Content />` become
|
|
156
|
+
`<div set:html={renderRichText(entry.data.body)} />`.
|
|
157
|
+
- A date was a `Date` and is now a `"YYYY-MM-DD"` string:
|
|
158
|
+
- Format it in UTC. If a formatter is shared, check its other callers: a
|
|
159
|
+
`Date` made in local time now shows the day before.
|
|
160
|
+
- Sort with `b.data.pubDate.localeCompare(a.data.pubDate)`. Subtracting
|
|
161
|
+
strings gives `NaN`, and the list is then not sorted, with no error.
|
|
162
|
+
- Where an API needs a `Date`, such as the RSS `pubDate`, pass
|
|
163
|
+
`new Date(entry.data.pubDate)`.
|
|
164
|
+
- An image was `ImageMetadata` and is now a path from the content root. Create
|
|
165
|
+
`src/lib/media.ts` from [reference.md](./reference.md#images), and use
|
|
166
|
+
`mediaImage()`.
|
|
167
|
+
- A reference was `{ collection, id }` and is now a slug:
|
|
168
|
+
`getEntry("authors", entry.data.author)`.
|
|
169
|
+
- `entry.id` is the slug. If ids had folders, update the links, and add
|
|
170
|
+
`redirects` in `astro.config` if the user wants the old URLs to keep working.
|
|
171
|
+
- A spread such as `{...post.data}` now also passes `_mica` and the new types.
|
|
172
|
+
Pass the fields one by one.
|
|
173
|
+
- A layout or component typed with `CollectionEntry<"blog">["data"]` can also
|
|
174
|
+
be used by a page that is not an entry, with the old types. Give it its own
|
|
175
|
+
props type.
|
|
176
|
+
|
|
177
|
+
## 8. Compare the website
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx astro build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Compare `dist` with `dist-before` from step 1: the list of HTML files, and two
|
|
184
|
+
or three converted pages. Every page that is missing or new must have a
|
|
185
|
+
reason: a changed URL with a redirect, or a decision of the user. Expect these
|
|
186
|
+
differences, and tell the user about them:
|
|
187
|
+
|
|
188
|
+
- headings without `id`, and code blocks without highlighting;
|
|
189
|
+
- body images from `public/`, which Astro does not optimize;
|
|
190
|
+
- dates that showed the local day and now show the stored day, and times in
|
|
191
|
+
`<time datetime>` and the RSS feed that are now midnight UTC.
|
|
192
|
+
|
|
193
|
+
## 9. Clean up
|
|
194
|
+
|
|
195
|
+
Ask the user before each of these:
|
|
196
|
+
|
|
197
|
+
- Delete the source files of the converted collections, and the images that
|
|
198
|
+
are now copies in `content/`, unless another file still imports them.
|
|
199
|
+
- Remove integrations and dependencies that only the old collections needed,
|
|
200
|
+
such as `@astrojs/mdx` when no MDX is left, and those that only the
|
|
201
|
+
conversion needed. Change text in the content that describes what you
|
|
202
|
+
removed.
|
|
203
|
+
- Delete the conversion script, or keep it for the record.
|
|
204
|
+
|
|
205
|
+
Then run `npx mica check` and `npx astro build` once more.
|
|
206
|
+
|
|
207
|
+
## 10. Finish
|
|
208
|
+
|
|
209
|
+
Follow [finish.md](./finish.md). In the report, list each collection, the
|
|
210
|
+
number of entries before and after, and everything that changed or was not
|
|
211
|
+
converted.
|
package/setup/finish.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Finish the setup
|
|
2
|
+
|
|
3
|
+
## 1. Check
|
|
4
|
+
|
|
5
|
+
All three must pass:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx astro sync
|
|
9
|
+
npx mica check # 0 errors
|
|
10
|
+
npx astro build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the project has `@astrojs/check`, run `npx astro check` too. Do not install
|
|
14
|
+
it only for this. Without it, nothing checks the types in `.astro` files: tell
|
|
15
|
+
the user.
|
|
16
|
+
|
|
17
|
+
Explain each warning of `mica check` to the user, or fix it.
|
|
18
|
+
|
|
19
|
+
## 2. Add notes for the next agent
|
|
20
|
+
|
|
21
|
+
Add this section to the project's agent instructions: `AGENTS.md`,
|
|
22
|
+
`CLAUDE.md` or the file the project already uses. If `CLAUDE.md` is a link to
|
|
23
|
+
`AGENTS.md`, edit `AGENTS.md`. Make the file if there is none. Change the
|
|
24
|
+
paths if the project uses others.
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
## Content (Mica)
|
|
28
|
+
|
|
29
|
+
- The content model is `src/mica.ts`. The content is JSON and Markdoc in
|
|
30
|
+
`content/`; editors change it with `npx mica`.
|
|
31
|
+
- After a change to `src/mica.ts`, run `npx astro sync` and `npx mica check`,
|
|
32
|
+
and commit `content/.mica/schema.json` with the change.
|
|
33
|
+
- Renaming or removing a field or a collection does not move the stored
|
|
34
|
+
content. Change the content files in the same commit, or ask first.
|
|
35
|
+
- Before a commit that changes `content/`, `npx mica check` must report 0 errors.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 3. Tell the user
|
|
39
|
+
|
|
40
|
+
- What you added and changed, file by file.
|
|
41
|
+
- The decisions they made, and what was not converted, if anything.
|
|
42
|
+
- What to commit: every file that `git status` shows as new or changed,
|
|
43
|
+
including `content/.mica/schema.json`, `package.json` and the lock file. Do
|
|
44
|
+
not commit for them unless they asked.
|
|
45
|
+
- How to edit: `npx mica` opens the editor on this computer. A save writes
|
|
46
|
+
files in `content/`; Publish commits them. `@micacms/editor` is a dev
|
|
47
|
+
dependency, so on another clone, run `npm install` first.
|
|
48
|
+
- That a self-hosted editor for other people is described at
|
|
49
|
+
https://micacms.com/docs/self-hosting/.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Set up Mica in a new or empty Astro project
|
|
2
|
+
|
|
3
|
+
For an Astro project that has no content collections yet. `SETUP.md` has
|
|
4
|
+
already created the project if there was none, and installed the packages.
|
|
5
|
+
The commands are in [reference.md](./reference.md#commands).
|
|
6
|
+
|
|
7
|
+
## 1. Ask the user about the content
|
|
8
|
+
|
|
9
|
+
Ask these questions, and wait for the answers:
|
|
10
|
+
|
|
11
|
+
1. What is the website for, and which pages does it need?
|
|
12
|
+
2. Which content do editors change? For each kind, ask:
|
|
13
|
+
- Are there many of them (a collection: articles, projects, team members)
|
|
14
|
+
or one (a singleton: site settings, the home page, the navigation)?
|
|
15
|
+
- Which fields does it have? Which are required?
|
|
16
|
+
- Does it have long formatted text (rich text) or images?
|
|
17
|
+
3. Do entries refer to other entries? For example, an article has an author
|
|
18
|
+
from the team.
|
|
19
|
+
4. Should editors put a collection in a manual order?
|
|
20
|
+
5. Do images need alt text that editors write? A media field has none, so
|
|
21
|
+
each image that carries meaning needs a `text()` field beside it.
|
|
22
|
+
6. Do they want example content, so that the pages show something at once?
|
|
23
|
+
|
|
24
|
+
Suggest a model from the answers, as a short list of collections, singletons
|
|
25
|
+
and fields. Change it until the user agrees.
|
|
26
|
+
|
|
27
|
+
## 2. Add Mica to the project
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx mica init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
It writes `src/mica.ts` (a starter model) and `src/content.config.ts`. It
|
|
34
|
+
never overwrites a file. If `src/content.config.ts` already exists, it prints
|
|
35
|
+
the lines to add. Add them so that the existing collections stay.
|
|
36
|
+
|
|
37
|
+
## 3. Write the model
|
|
38
|
+
|
|
39
|
+
Replace the starter model in `src/mica.ts` (`news` and `settings`) with the
|
|
40
|
+
model the user agreed to. See
|
|
41
|
+
[reference.md](./reference.md#the-content-model) for the field types and the
|
|
42
|
+
rules. Then publish it and check it:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx astro sync
|
|
46
|
+
npx mica check
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`astro sync` fails with a clear message when the model breaks a rule. Fix
|
|
50
|
+
`src/mica.ts` and run it again. Before there is content, `mica check` warns
|
|
51
|
+
"No content for … yet" for each singleton. That is expected until step 4.
|
|
52
|
+
|
|
53
|
+
## 4. Add content (if the user wants examples)
|
|
54
|
+
|
|
55
|
+
Write one or two entries per collection, and fill in each singleton, as files
|
|
56
|
+
in `content/`. See [reference.md](./reference.md#the-files-on-disk). Use the
|
|
57
|
+
script in [reference.md](./reference.md#write-entries-with-a-script) for
|
|
58
|
+
entries, singletons and the manual order.
|
|
59
|
+
|
|
60
|
+
Then run `npx mica check` and fix every error.
|
|
61
|
+
|
|
62
|
+
## 5. Make the pages
|
|
63
|
+
|
|
64
|
+
Make the pages that the user asked for. For each collection, usually a list
|
|
65
|
+
page and a page per entry. Use the singletons in the layout, for example the
|
|
66
|
+
site name. See [reference.md](./reference.md#render-the-content-in-astro) for
|
|
67
|
+
rich text, images, relations and dates.
|
|
68
|
+
|
|
69
|
+
Keep the design simple unless the user asked for more.
|
|
70
|
+
|
|
71
|
+
## 6. Finish
|
|
72
|
+
|
|
73
|
+
Follow [finish.md](./finish.md).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Move content from WordPress or another CMS
|
|
2
|
+
|
|
3
|
+
TODO: this guide is not written yet.
|
|
4
|
+
|
|
5
|
+
Tell the user that Mica has no guide for this yet. Then offer one of these:
|
|
6
|
+
|
|
7
|
+
- Set up Mica with [new-project.md](./new-project.md) first, and move the
|
|
8
|
+
content in a second step together with the user.
|
|
9
|
+
- Wait for the guide. Ask the user to follow the Mica releases.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Mica reference for setup
|
|
2
|
+
|
|
3
|
+
The guides link here. Read the section that a step names.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | What it does |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `npx mica init` | Writes a starter `src/mica.ts` and `src/content.config.ts`. Never overwrites a file |
|
|
10
|
+
| `npx astro sync` | Runs the Mica loaders, which write the model to `content/.mica/schema.json` |
|
|
11
|
+
| `npx mica check` | Checks every entry against `content/.mica/schema.json`. Exit code 1 on an error |
|
|
12
|
+
| `npx mica` | Opens the editor in the browser. Only for the user; do not run it yourself |
|
|
13
|
+
|
|
14
|
+
`mica check` reads the model from `content/.mica/schema.json`. After every
|
|
15
|
+
change to `src/mica.ts`, run `npx astro sync` before `npx mica check`.
|
|
16
|
+
|
|
17
|
+
## The content model
|
|
18
|
+
|
|
19
|
+
`src/mica.ts` exports one `defineSite(...)`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { collection, defineSite, singleton, slug, text } from "@micacms/core";
|
|
23
|
+
|
|
24
|
+
export const site = defineSite({
|
|
25
|
+
root: "./content",
|
|
26
|
+
collections: {
|
|
27
|
+
news: collection({
|
|
28
|
+
label: "News",
|
|
29
|
+
labelSingular: "Article",
|
|
30
|
+
fields: { title: text({ required: true }), slug: slug({ from: "title" }) },
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
singletons: {
|
|
34
|
+
settings: singleton({ label: "Website", fields: { siteName: text({ required: true }) } }),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rules that `defineSite` enforces:
|
|
40
|
+
|
|
41
|
+
- Collection, singleton and field names are lowerCamelCase and start with a
|
|
42
|
+
letter: `news`, `teamMembers`, `publishedOn`. Not `team-members`.
|
|
43
|
+
- A name is a collection or a singleton, not both.
|
|
44
|
+
- Every collection has a `slug()` field. Its value is the name of the entry's
|
|
45
|
+
file or folder, and the entry's address on the website. Use one; with more,
|
|
46
|
+
the first is the slug unless `slugField` names another.
|
|
47
|
+
- A `relation()` names a collection of the same model.
|
|
48
|
+
- A `select()` has at least one option.
|
|
49
|
+
|
|
50
|
+
Collection options: `label` (required), `labelSingular`, `description`,
|
|
51
|
+
`titleField` (the field the editor shows as the entry's title; default
|
|
52
|
+
`title`, else the first `text()` field), `slugField`, `sortable` (editors can
|
|
53
|
+
order the entries by hand) and `defaultSort: { field, direction }` (a field
|
|
54
|
+
name or `"position"`). Without `defaultSort`, the editor lists the entries by
|
|
55
|
+
position: the manual order, or the slugs in alphabetical order before anybody
|
|
56
|
+
ordered them. Singleton options: `label`, `description`, `fields`.
|
|
57
|
+
|
|
58
|
+
Options of every field: `label`, `description`, `required`, `listed` (a column
|
|
59
|
+
in the entry list), `readonly`, `hidden`, `group`, `width` (`"full"`, `"half"`,
|
|
60
|
+
`"third"`).
|
|
61
|
+
|
|
62
|
+
| Field | Stored as | Other options |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `text()` | string | `minLength`, `maxLength`, `pattern`, `placeholder`, `default` |
|
|
65
|
+
| `textarea()` | string | `minLength`, `maxLength`, `rows`, `placeholder`, `default` |
|
|
66
|
+
| `richText()` | Markdoc in its own `.mdoc` file | `toolbar`, `maxLength`, `default` |
|
|
67
|
+
| `number()` | number | `min`, `max`, `step`, `integer`, `unit`, `default` |
|
|
68
|
+
| `boolean()` | boolean | `default` |
|
|
69
|
+
| `date()` | `"YYYY-MM-DD"`, or an ISO timestamp with `includeTime: true` | `includeTime`, `min`, `max`, `default` |
|
|
70
|
+
| `email()` | string | `placeholder`, `default` |
|
|
71
|
+
| `url()` | string | `placeholder`, `default` |
|
|
72
|
+
| `slug()` | string | `from` (the field to make it from), `maxLength` |
|
|
73
|
+
| `select()` | string, or string[] with `multiple` | `options: [{ label, value }]` (required), `multiple`, `default` |
|
|
74
|
+
| `object()` | object | `fields` (required), `collapsible`, `collapsed` |
|
|
75
|
+
| `repeater()` | array of objects | `fields` (required), `min`, `max`, `itemLabel`, `addLabel` |
|
|
76
|
+
| `relation()` | the slug of an entry | `collection` (required), `multiple` |
|
|
77
|
+
| `media()` | a file path | `accept: "image" \| "document" \| "any"`, `multiple`, `max` |
|
|
78
|
+
| `color()` | `"#rrggbb"` | `presets`, `default` |
|
|
79
|
+
| `coordinates()` | `{ "lat": 47.37, "lng": 8.54 }` | `defaultCenter`, `defaultZoom` |
|
|
80
|
+
|
|
81
|
+
There is no field for a free list of strings, such as free tags. Use
|
|
82
|
+
`select({ multiple: true, options })` for a fixed list, or a `repeater()` with
|
|
83
|
+
one `text()` field. Ask the user which one.
|
|
84
|
+
|
|
85
|
+
## The files on disk
|
|
86
|
+
|
|
87
|
+
A collection whose fields include `richText()` or `media()` stores each entry
|
|
88
|
+
as a folder. Any other collection stores each entry as one JSON file.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
content/
|
|
92
|
+
├── .mica/schema.json the model; written by `astro sync`, commit it
|
|
93
|
+
├── news/ has rich text or media: a folder per entry
|
|
94
|
+
│ ├── _order.json optional: the manual order of a sortable collection
|
|
95
|
+
│ └── hello-world/ the folder name is the slug
|
|
96
|
+
│ ├── index.json all fields except rich text
|
|
97
|
+
│ ├── body.mdoc one file per rich-text field, named after it
|
|
98
|
+
│ └── media/cover.jpg the entry's own files
|
|
99
|
+
├── team/ plain fields: one file per entry
|
|
100
|
+
│ └── anna-vogt.json the file name is the slug
|
|
101
|
+
└── settings.json a singleton (a folder with index.json if it has rich text or media)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- A slug has lowercase letters, digits and single hyphens: `hello-world`. The
|
|
105
|
+
slug field in the JSON has the same value as the folder or file name.
|
|
106
|
+
- A media path in the JSON is relative to the entry's folder (`media/cover.jpg`),
|
|
107
|
+
or starts with `/` and is relative to the content root (`/shared/logo.png`).
|
|
108
|
+
- A rich-text field in a repeater or an object gets a dotted file name:
|
|
109
|
+
`sections.0.body.mdoc`.
|
|
110
|
+
- An empty field is `null` in the JSON. An empty list is `[]`, an unset
|
|
111
|
+
boolean is `false`, and an `object()` holds its empty fields.
|
|
112
|
+
- Markdoc is Markdown plus `{% tags %}`. Raw HTML is not rendered: Markdoc
|
|
113
|
+
escapes it.
|
|
114
|
+
|
|
115
|
+
### Write entries with a script
|
|
116
|
+
|
|
117
|
+
To write many entries, use the functions Mica itself uses. Then the files are
|
|
118
|
+
byte for byte what the editor writes. Run `npx astro sync` first, so that the
|
|
119
|
+
model is in `content/.mica/schema.json`. Put the script inside the project, so
|
|
120
|
+
that `@micacms/core` resolves from its `node_modules`.
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// scripts/write-entries.mjs — run with `node scripts/write-entries.mjs`
|
|
124
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
125
|
+
import { dirname, join } from "node:path";
|
|
126
|
+
|
|
127
|
+
import { collectionEntryPath, entryFiles, serializeEntry, singletonPath } from "@micacms/core";
|
|
128
|
+
|
|
129
|
+
const ROOT = "content";
|
|
130
|
+
const model = JSON.parse(await readFile(join(ROOT, ".mica/schema.json"), "utf8"));
|
|
131
|
+
|
|
132
|
+
export async function writeEntry(collection, slug, data) {
|
|
133
|
+
const fields = model.collections[collection].fields;
|
|
134
|
+
const base = collectionEntryPath(fields, collection, slug);
|
|
135
|
+
for (const [path, contents] of Object.entries(entryFiles(fields, base, serializeEntry(fields, data)))) {
|
|
136
|
+
await mkdir(dirname(join(ROOT, path)), { recursive: true });
|
|
137
|
+
await writeFile(join(ROOT, path), contents);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A singleton: `content/<name>.json`, or `content/<name>/index.json` with rich text or media.
|
|
142
|
+
export async function writeSingleton(name, data) {
|
|
143
|
+
const fields = model.singletons[name].fields;
|
|
144
|
+
for (const [path, contents] of Object.entries(entryFiles(fields, singletonPath(fields, name), serializeEntry(fields, data)))) {
|
|
145
|
+
await mkdir(dirname(join(ROOT, path)), { recursive: true });
|
|
146
|
+
await writeFile(join(ROOT, path), contents);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The manual order of a sortable collection. Slugs that it does not name come after.
|
|
151
|
+
export async function writeOrder(collection, slugs) {
|
|
152
|
+
await writeFile(join(ROOT, collection, "_order.json"), `${JSON.stringify(slugs, null, 2)}\n`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Example: `data` has every field, rich text as a Markdoc string, media as `media/<file>`.
|
|
156
|
+
// await writeEntry("news", "hello-world", {
|
|
157
|
+
// title: "Hello, world",
|
|
158
|
+
// slug: "hello-world",
|
|
159
|
+
// publishedOn: "2026-01-15",
|
|
160
|
+
// cover: "media/cover.jpg",
|
|
161
|
+
// body: "The first article.",
|
|
162
|
+
// });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The script does not copy media files. Copy each file to
|
|
166
|
+
`content/<collection>/<slug>/media/<file>` yourself. Delete the script when the
|
|
167
|
+
content is written, unless the user wants to keep it.
|
|
168
|
+
|
|
169
|
+
## Render the content in Astro
|
|
170
|
+
|
|
171
|
+
`micaCollections(site)` makes one Astro collection per collection and per
|
|
172
|
+
singleton. Pages use the normal Astro API.
|
|
173
|
+
|
|
174
|
+
```astro
|
|
175
|
+
---
|
|
176
|
+
// src/pages/news/[slug].astro
|
|
177
|
+
import { getCollection, getEntry } from "astro:content";
|
|
178
|
+
import { renderRichText } from "@micacms/astro";
|
|
179
|
+
|
|
180
|
+
export async function getStaticPaths() {
|
|
181
|
+
const news = await getCollection("news");
|
|
182
|
+
return news.map((article) => ({ params: { slug: article.id }, props: { article } }));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { article } = Astro.props;
|
|
186
|
+
const settings = await getEntry("settings", "settings");
|
|
187
|
+
---
|
|
188
|
+
<h1>{article.data.title}</h1>
|
|
189
|
+
<div set:html={renderRichText(article.data.body)} />
|
|
190
|
+
<footer>{settings?.data.siteName}</footer>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `entry.id` is the slug.
|
|
194
|
+
- A singleton is a collection with one entry. Its id is the singleton's name:
|
|
195
|
+
`getEntry("settings", "settings")`. It is `undefined` until it has content.
|
|
196
|
+
- Rich text is a Markdoc string. `renderRichText()` returns HTML in an
|
|
197
|
+
`<article>` element. There is no `render(entry)` and no `<Content />` for
|
|
198
|
+
Mica collections.
|
|
199
|
+
- A date is a `"YYYY-MM-DD"` string. `new Date()` reads it as UTC midnight,
|
|
200
|
+
so format it in UTC, or it shows the day before west of UTC:
|
|
201
|
+
`new Date(value).toLocaleDateString("en-GB", { timeZone: "UTC" })`.
|
|
202
|
+
- A relation is a slug: `await getEntry("team", article.data.author)`.
|
|
203
|
+
- Do not depend on the order of `getCollection()`. Sort by a date field, or
|
|
204
|
+
by position for a `sortable` collection:
|
|
205
|
+
`entries.sort((a, b) => a.data._mica.position - b.data._mica.position)`.
|
|
206
|
+
|
|
207
|
+
### Images
|
|
208
|
+
|
|
209
|
+
On disk, a media field holds `media/cover.jpg`. In `entry.data`, the loader
|
|
210
|
+
gives the path from the content root: `news/hello-world/media/cover.jpg`. Astro can only optimize an image that the
|
|
211
|
+
build imports, so collect the images with `import.meta.glob`:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// src/lib/media.ts — the glob path is relative to this file
|
|
215
|
+
import type { ImageMetadata } from "astro";
|
|
216
|
+
|
|
217
|
+
const images = import.meta.glob<{ default: ImageMetadata }>(
|
|
218
|
+
"../../content/**/*.{jpeg,jpg,png,webp,avif,gif}",
|
|
219
|
+
{ eager: true },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
/** The image for a Mica media path, or undefined when the file is not there. */
|
|
223
|
+
export function mediaImage(path: string | null | undefined): ImageMetadata | undefined {
|
|
224
|
+
return path ? images[`../../content/${path}`]?.default : undefined;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```astro
|
|
229
|
+
---
|
|
230
|
+
import { Image } from "astro:assets";
|
|
231
|
+
import { mediaImage } from "../../lib/media";
|
|
232
|
+
const cover = mediaImage(article.data.cover);
|
|
233
|
+
---
|
|
234
|
+
{cover && <Image src={cover} alt={article.data.title} />}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A `media({ multiple: true })` field is an array of paths:
|
|
238
|
+
`project.data.gallery.map(mediaImage)`.
|
|
239
|
+
|
|
240
|
+
A media field has no alt text. Add a `text()` field for it where the image
|
|
241
|
+
carries meaning. For documents such as PDF files, use
|
|
242
|
+
`import.meta.glob("../../content/**/*.pdf", { query: "?url", import: "default", eager: true })`
|
|
243
|
+
and link to the URL.
|
|
244
|
+
|
|
245
|
+
### What Markdoc renders differently from Astro's Markdown
|
|
246
|
+
|
|
247
|
+
- Headings have no `id`, and code blocks are not highlighted. To add them,
|
|
248
|
+
pass Markdoc nodes in `renderRichText(source, { config })`; see
|
|
249
|
+
https://markdoc.dev/docs/nodes. Ask the user first.
|
|
250
|
+
- Raw HTML, MDX components and footnotes are not rendered. `mica check` warns
|
|
251
|
+
about them.
|
|
252
|
+
- A `{% tag %}` renders only if `config.tags` declares it.
|