@micacms/astro 0.6.0-rc.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,9 +68,15 @@ Astro hands a loader exactly what they require:
68
68
  loader watches its own directory and coalesces the burst of writes that saving
69
69
  one entry produces (`debounceMs`, 100 ms by default).
70
70
 
71
- The loaders also check references: a relation to a missing entry, or a media
72
- path to a missing file, is a warning in the build log. `strict: true` makes it
73
- fail the build. An entry in the other layout always fails the build, with the
71
+ Image fields reach Astro as Astro images: a `media({ accept: "image" })` field
72
+ is `ImageMetadata`, optimised like Astro's own `image()`, and with `alt` it is
73
+ `{ src, alt }` — `<Image {...entry.data.cover} />` needs no helper. Other media
74
+ stay paths from the content root.
75
+
76
+ The loaders also check references: a relation to a missing entry, or a missing
77
+ file of a media field that is not an image, is a warning in the build log.
78
+ `strict: true` makes it fail the build. A missing image file always fails the
79
+ build, as with Astro's own `image()`. An entry in the other layout always fails the build, with the
74
80
  files and `npx mica migrate-layout`.
75
81
 
76
82
  ## Mixing with your own collections
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Image fields reach Astro through its own `image()`, which resolves a path
3
+ * against the entry's file, optimizes the image and gives `ImageMetadata`. A
4
+ * site then needs no helper: `<Image {...entry.data.cover} />`.
5
+ */
6
+ import type { FieldMap } from "@micacms/core";
7
+ /**
8
+ * Rewrites the paths of `media({ accept: "image" })` fields, which the reader
9
+ * gives from the content root, to paths from the entry's data file.
10
+ */
11
+ export declare function imagePathsFromEntry(fields: FieldMap, data: Record<string, unknown>, dataFile: string): Record<string, unknown>;
12
+ //# sourceMappingURL=images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../src/images.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
package/dist/images.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Image fields reach Astro through its own `image()`, which resolves a path
3
+ * against the entry's file, optimizes the image and gives `ImageMetadata`. A
4
+ * site then needs no helper: `<Image {...entry.data.cover} />`.
5
+ */
6
+ import { posix } from "node:path";
7
+ /**
8
+ * Rewrites the paths of `media({ accept: "image" })` fields, which the reader
9
+ * gives from the content root, to paths from the entry's data file.
10
+ */
11
+ export function imagePathsFromEntry(fields, data, dataFile) {
12
+ const from = posix.dirname(dataFile);
13
+ const relative = (path) => {
14
+ if (typeof path !== "string" || path === "")
15
+ return path;
16
+ const result = posix.relative(from, path);
17
+ return result.startsWith(".") ? result : `./${result}`;
18
+ };
19
+ const one = (value) => isRecord(value) ? { ...value, path: relative(value["path"]) } : relative(value);
20
+ const visit = (map, record) => {
21
+ const result = { ...record };
22
+ for (const [key, field] of Object.entries(map)) {
23
+ const value = record[key];
24
+ if (field.kind === "media" && field.accept === "image") {
25
+ result[key] = Array.isArray(value) ? value.map(one) : one(value);
26
+ }
27
+ else if (field.kind === "object" && isRecord(value)) {
28
+ result[key] = visit(field.fields, value);
29
+ }
30
+ else if (field.kind === "repeater" && Array.isArray(value)) {
31
+ result[key] = value.map((row) => (isRecord(row) ? visit(field.fields, row) : row));
32
+ }
33
+ }
34
+ return result;
35
+ };
36
+ return visit(fields, data);
37
+ }
38
+ function isRecord(value) {
39
+ return value !== null && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.js","sourceRoot":"","sources":["../src/images.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAIlC;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAgB,EAChB,IAA6B,EAC7B,QAAgB;IAEhB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,CAAC,IAAa,EAAW,EAAE;QAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QACzD,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;IACzD,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,CAAC,KAAc,EAAW,EAAE,CACtC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElF,MAAM,KAAK,GAAG,CAAC,GAAa,EAAE,MAA+B,EAA2B,EAAE;QACxF,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;QAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;gBACvD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnE,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC7D,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;IAEF,OAAO,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["/**\n * Image fields reach Astro through its own `image()`, which resolves a path\n * against the entry's file, optimizes the image and gives `ImageMetadata`. A\n * site then needs no helper: `<Image {...entry.data.cover} />`.\n */\n\nimport { posix } from \"node:path\";\n\nimport type { FieldMap } from \"@micacms/core\";\n\n/**\n * Rewrites the paths of `media({ accept: \"image\" })` fields, which the reader\n * gives from the content root, to paths from the entry's data file.\n */\nexport function imagePathsFromEntry(\n fields: FieldMap,\n data: Record<string, unknown>,\n dataFile: string,\n): Record<string, unknown> {\n const from = posix.dirname(dataFile);\n const relative = (path: unknown): unknown => {\n if (typeof path !== \"string\" || path === \"\") return path;\n const result = posix.relative(from, path);\n return result.startsWith(\".\") ? result : `./${result}`;\n };\n const one = (value: unknown): unknown =>\n isRecord(value) ? { ...value, path: relative(value[\"path\"]) } : relative(value);\n\n const visit = (map: FieldMap, record: Record<string, unknown>): Record<string, unknown> => {\n const result = { ...record };\n for (const [key, field] of Object.entries(map)) {\n const value = record[key];\n if (field.kind === \"media\" && field.accept === \"image\") {\n result[key] = Array.isArray(value) ? value.map(one) : one(value);\n } else if (field.kind === \"object\" && isRecord(value)) {\n result[key] = visit(field.fields, value);\n } else if (field.kind === \"repeater\" && Array.isArray(value)) {\n result[key] = value.map((row) => (isRecord(row) ? visit(field.fields, row) : row));\n }\n }\n return result;\n };\n\n return visit(fields, data);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * Publishing is a commit, and CI builds the commit.
17
17
  */
18
18
  import { type ContentSource, type InferCollection, type InferSingleton, type Site } from "@micacms/core";
19
+ import type { ImageMetadata } from "astro";
19
20
  import { z } from "zod";
20
21
  import { DEFAULT_CONTENT_ROOT, resolveContentRoot } from "./root.js";
21
22
  export interface MicaCollectionsOptions {
@@ -47,7 +48,8 @@ export interface MicaCollectionsOptions {
47
48
  debounceMs?: number;
48
49
  /**
49
50
  * Fails the build on a relation to a missing entry or a missing media file.
50
- * Without it, the build logs a warning for each and goes on.
51
+ * Without it, the build logs a warning for each and goes on; a missing image
52
+ * file fails the build either way, because Astro cannot import it.
51
53
  */
52
54
  strict?: boolean;
53
55
  }
@@ -85,12 +87,15 @@ export interface AstroCollectionConfig<T = unknown> {
85
87
  type: "content_layer";
86
88
  loader: ContentLoader;
87
89
  /**
88
- * Typed as `ZodType<T>` rather than a bare `ZodObject`, because Astro derives
89
- * `CollectionEntry<"projects">["data"]` from `z.infer` of whatever this is.
90
- * Left generic, every field in every template would degrade to `any` — and
91
- * the point of the shared model is that renaming a field breaks the build.
90
+ * A function, so that Astro hands in its `image()`. Its result is typed as
91
+ * `ZodType<T>` because Astro derives `CollectionEntry<"projects">["data"]`
92
+ * from it: left generic, every field in every template would degrade to
93
+ * `any`, and the point of the shared model is that renaming a field breaks
94
+ * the build.
92
95
  */
93
- schema: z.ZodType<T>;
96
+ schema: (context: {
97
+ image: () => z.ZodType;
98
+ }) => z.ZodType<T>;
94
99
  }
95
100
  /** The subset of Astro's `Loader` contract this package uses. */
96
101
  export interface ContentLoader {
@@ -104,6 +109,7 @@ export interface LoaderContext {
104
109
  id: string;
105
110
  data: Record<string, unknown>;
106
111
  digest?: string;
112
+ filePath?: string;
107
113
  }): boolean;
108
114
  keys(): string[];
109
115
  };
@@ -138,19 +144,21 @@ export interface LoaderContext {
138
144
  parseData(input: {
139
145
  id: string;
140
146
  data: Record<string, unknown>;
147
+ /** The entry's file, from the project root: Astro's `image()` resolves paths against it. */
148
+ filePath?: string;
141
149
  }): Promise<Record<string, unknown>>;
142
150
  generateDigest(data: Record<string, unknown> | string): string;
143
151
  }
144
152
  export type MicaCollections<S extends Site> = {
145
- [K in keyof S["collections"]]: AstroCollectionConfig<AstroDocument<InferCollection<S["collections"][K]>>>;
153
+ [K in keyof S["collections"]]: AstroCollectionConfig<CollectionDocument<S, K>>;
146
154
  } & {
147
- [K in keyof S["singletons"]]: AstroCollectionConfig<InferSingleton<S["singletons"][K]>>;
155
+ [K in keyof S["singletons"]]: AstroCollectionConfig<SingletonDocument<S, K>>;
148
156
  };
149
157
  export declare function micaCollections<S extends Site>(site: S, options?: MicaCollectionsOptions): MicaCollections<S>;
150
158
  /** The document type for one collection, as it appears in Astro. */
151
- export type CollectionDocument<S extends Site, K extends keyof S["collections"]> = AstroDocument<InferCollection<S["collections"][K]>>;
159
+ export type CollectionDocument<S extends Site, K extends keyof S["collections"]> = AstroDocument<InferCollection<S["collections"][K], ImageMetadata>>;
152
160
  /** The document type for one singleton. */
153
- export type SingletonDocument<S extends Site, K extends keyof S["singletons"]> = InferSingleton<S["singletons"][K]>;
161
+ export type SingletonDocument<S extends Site, K extends keyof S["singletons"]> = InferSingleton<S["singletons"][K], ImageMetadata>;
154
162
  export { manifestFor } from "./manifest.js";
155
163
  export { DEFAULT_CONTENT_ROOT, resolveContentRoot };
156
164
  export { renderRichText, transformRichText, validateRichText, type RichTextOptions, } from "./richtext.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAQL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,yEAAyE;AACzE,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,OAAO;IAChD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACtB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,KAAK,IAAI,IAAI,CAAC;QACd,GAAG,CAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;QACpF,IAAI,IAAI,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,MAAM,EAAE;QACN,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,CAAC;IACF,IAAI,EAAE;QAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtF;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;KAC/E,CAAC;IACF,SAAS,CAAC,KAAK,EAAE;QACf,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;CAChE;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,IAAI,IAAI;KAC3C,CAAC,IAAI,MAAM,CAAC,CAAC,aAAa,CAAC,GAAG,qBAAqB,CAClD,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACpD;CACF,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,CAAC,YAAY,CAAC,GAAG,qBAAqB,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACxF,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,SAAS,IAAI,EAC5C,IAAI,EAAE,CAAC,EACP,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAAC,CAAC,CAAC,CA0KpB;AAyFD,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,CAC5B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,MAAM,CAAC,CAAC,aAAa,CAAC,IAC9B,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAExD,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,CAC3B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,MAAM,CAAC,CAAC,YAAY,CAAC,IAC7B,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,CAAC;AACpD,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -17,9 +17,11 @@
17
17
  */
18
18
  import { mkdir, readFile, writeFile } from "node:fs/promises";
19
19
  import { dirname, join, relative, sep } from "node:path";
20
- import { createReferenceCheck, fieldsSchema, isFieldKind, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/core";
20
+ import { fileURLToPath } from "node:url";
21
+ import { collectionEntryPath, createReferenceCheck, entryLayout, fieldsSchema, INDEX_FILE, singletonPath, isFieldKind, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/core";
21
22
  import { fileSystemSource } from "@micacms/core/node";
22
23
  import { z } from "zod";
24
+ import { imagePathsFromEntry } from "./images.js";
23
25
  import { manifestFor } from "./manifest.js";
24
26
  import { DEFAULT_CONTENT_ROOT, resolveContentRoot } from "./root.js";
25
27
  const metaSchema = z.object({
@@ -46,6 +48,16 @@ export function micaCollections(site, options = {}) {
46
48
  options.publishModel === false || options.source
47
49
  ? Promise.resolve()
48
50
  : publishModel(site, rootFor(context), context.logger));
51
+ /** An entry's data file, from the content root and, outside tests, from the project root. */
52
+ const entryFile = (context, fields, name, slug) => {
53
+ const base = slug === undefined ? singletonPath(fields, name) : collectionEntryPath(fields, name, slug);
54
+ const inContent = entryLayout(fields) === "directory" ? `${base}/${INDEX_FILE}` : base;
55
+ const project = context.config?.root;
56
+ const inProject = project && !options.source
57
+ ? relative(fileURLToPath(project), join(rootFor(context), inContent)).split(sep).join("/")
58
+ : undefined;
59
+ return { inContent, inProject };
60
+ };
49
61
  /** Warns about broken relations and missing files, or fails the build with `strict`. */
50
62
  const checkReferences = async (context, source, documents) => {
51
63
  const check = createReferenceCheck(site, source);
@@ -102,7 +114,7 @@ export function micaCollections(site, options = {}) {
102
114
  for (const [name, def] of Object.entries(site.collections)) {
103
115
  collections[name] = {
104
116
  type: "content_layer",
105
- schema: fieldsSchema(def.fields).extend({ _mica: metaSchema }),
117
+ schema: ({ image }) => fieldsSchema(def.fields, { image }).extend({ _mica: metaSchema }),
106
118
  loader: {
107
119
  name: `mica:${name}`,
108
120
  load: async (context) => {
@@ -119,11 +131,18 @@ export function micaCollections(site, options = {}) {
119
131
  collection: name,
120
132
  position: entry.position,
121
133
  };
134
+ const file = entryFile(context, def.fields, name, entry.slug);
122
135
  const data = await context.parseData({
123
136
  id: entry.slug,
124
- data: { ...entry.data, _mica: meta },
137
+ data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },
138
+ ...(file.inProject ? { filePath: file.inProject } : {}),
139
+ });
140
+ context.store.set({
141
+ id: entry.slug,
142
+ data,
143
+ digest: context.generateDigest(data),
144
+ ...(file.inProject ? { filePath: file.inProject } : {}),
125
145
  });
126
- context.store.set({ id: entry.slug, data, digest: context.generateDigest(data) });
127
146
  }
128
147
  await checkReferences(context, source, entries.map((entry) => ({ label: `${name}/${entry.slug}`, fields: def.fields, data: entry.data })));
129
148
  context.logger.info(`Loaded ${entries.length} ${entries.length === 1 ? "entry" : "entries"} of "${def.label}"`);
@@ -140,7 +159,7 @@ export function micaCollections(site, options = {}) {
140
159
  for (const [name, def] of Object.entries(site.singletons)) {
141
160
  collections[name] = {
142
161
  type: "content_layer",
143
- schema: fieldsSchema(def.fields),
162
+ schema: ({ image }) => fieldsSchema(def.fields, { image }),
144
163
  loader: {
145
164
  name: `mica:${name}`,
146
165
  load: async (context) => {
@@ -156,8 +175,18 @@ export function micaCollections(site, options = {}) {
156
175
  return;
157
176
  }
158
177
  await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);
159
- const parsed = await context.parseData({ id: name, data });
160
- context.store.set({ id: name, data: parsed, digest: context.generateDigest(parsed) });
178
+ const file = entryFile(context, def.fields, name);
179
+ const parsed = await context.parseData({
180
+ id: name,
181
+ data: imagePathsFromEntry(def.fields, data, file.inContent),
182
+ ...(file.inProject ? { filePath: file.inProject } : {}),
183
+ });
184
+ context.store.set({
185
+ id: name,
186
+ data: parsed,
187
+ digest: context.generateDigest(parsed),
188
+ ...(file.inProject ? { filePath: file.inProject } : {}),
189
+ });
161
190
  };
162
191
  await publish(context);
163
192
  await read();
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\";\n\nimport {\n createReferenceCheck,\n fieldsSchema,\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 { 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 * 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.\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 * 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 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 /** 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: fieldsSchema(def.fields).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 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: fieldsSchema(def.fields),\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 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 * 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]>>;\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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micacms/astro",
3
- "version": "0.6.0-rc.1",
3
+ "version": "0.6.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.1"
47
+ "@micacms/core": "^0.6.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.1"
54
+ "@micacms/core": "0.6.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsc -p tsconfig.json",
@@ -13,12 +13,15 @@ entries are converted once. The website then reads them through
13
13
  - The Git working tree must be clean (`git status`). If it is not, ask the
14
14
  user to commit or stash first.
15
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.
16
+ - Build the website once, and copy two results to a temporary directory
17
+ outside the project: the website, which step 8 compares with, and Astro's
18
+ data store, which `mica import` reads in step 6. Astro writes the store
19
+ again when the collections change, so this copy is the only one.
18
20
 
19
21
  ```bash
20
22
  npx astro build
21
23
  cp -r dist <temporary directory>/dist-before
24
+ cp node_modules/.astro/data-store.json <temporary directory>/data-store-before.json
22
25
  ```
23
26
 
24
27
  ## 2. Make an inventory
@@ -33,40 +36,104 @@ the integrations in `astro.config.*`. For each collection, find:
33
36
  - the number of entries, and whether the ids have folders (`2024/hello`);
34
37
  - what the bodies contain: plain Markdown, raw HTML, footnotes, MDX imports and
35
38
  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 (`![](./photo.jpg)`). 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.
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.
70
137
 
71
138
  ## 4. Map the schema to the model
72
139
 
@@ -100,56 +167,56 @@ lowerCamelCase.
100
167
  3. In `src/content.config.ts`, remove the definitions of the collections that
101
168
  Mica now manages, and add `...micaCollections(site)`. Keep the others.
102
169
  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.
170
+ import reads. The pages can fail now; step 7 fixes them.
104
171
 
105
172
  ## 6. Convert the entries
106
173
 
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:
174
+ `mica import` reads the copy of the data store from step 1, where Astro keeps
175
+ every entry after its own loader and schema. Each field of the Mica model takes
176
+ the value with the same name. The command converts it to the stored form: a
177
+ date to `"YYYY-MM-DD"` in UTC, a reference to a slug, an `image()` path to a
178
+ copy in the entry's `media/` folder, and the Markdown body to the rich-text
179
+ field `body`. It makes each slug from the entry id, and writes the files the
180
+ way the editor does.
181
+
182
+ ```bash
183
+ npx mica import --from <temporary directory>/data-store-before.json \
184
+ --map blog.cover.alt=coverAlt --order team=order
185
+ ```
186
+
187
+ - `--map <collection>.<field>=<key>` takes a field from another key. Use it
188
+ when a name changed, for the alt text of an image (`blog.cover.alt=coverAlt`),
189
+ and for a body that goes to a rich-text field with another name
190
+ (`blog.content=@body`). `--map` does not reach into the rows of a repeater.
191
+ - `--order <collection>=<field>` writes the manual order of a `sortable`
192
+ collection from a number field, if the user chose that in step 3.
193
+ - Names after the options import only those collections.
194
+
195
+ The command lists every key it did not import, every id that gives the same
196
+ slug as another, and every image it did not find. Fix the model or add a
197
+ `--map`, delete the collection's directory in `content/`, and run it again.
198
+ Then make the changes to the bodies that the user decided in step 3 directly
199
+ in the `.mdoc` files, and run:
137
200
 
138
201
  ```bash
139
202
  npx mica check
140
203
  ```
141
204
 
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.
205
+ Read every warning: "Not read" means that an entry or a file is lost, and a
206
+ Markdoc warning means that some text does not render as before. If you cannot
207
+ fix a warning, list it in the report with the reason. Keep the source files.
208
+ Do not delete anything in this step.
209
+
210
+ If the import cannot express a conversion, for example a value that the model
211
+ splits into two fields, write a script for that part only, with `writeEntry`
212
+ from [reference.md](./reference.md#write-entries-with-a-script).
148
213
 
149
214
  ## 7. Change the pages
150
215
 
151
- Look at every file from the inventory: pages, layouts, components and
152
- 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
153
220
  [reference.md](./reference.md#render-the-content-in-astro).
154
221
 
155
222
  - `render(entry)` and `<Content />` become
@@ -161,10 +228,12 @@ endpoints such as `rss.xml.js`. See
161
228
  strings gives `NaN`, and the list is then not sorted, with no error.
162
229
  - Where an API needs a `Date`, such as the RSS `pubDate`, pass
163
230
  `new Date(entry.data.pubDate)`.
164
- - An image was `ImageMetadata` and is now `{ path, alt }` (or a path, without
165
- `alt`). Create `src/lib/media.ts` from [reference.md](./reference.md#images),
166
- and use `mediaImage(value.path)`. Move alt text from the old frontmatter, such
167
- as `coverAlt`, into `alt`.
231
+ - An `image()` field is still `ImageMetadata` with `media({ accept: "image" })`,
232
+ so its pages do not change. With `alt`, the value is `{ src, alt }`: use
233
+ `entry.data.cover.src` where the page used `entry.data.cover`, and
234
+ `entry.data.cover.alt` in place of the old alt-text field, or spread it:
235
+ `<Image {...entry.data.cover} />`. See
236
+ [reference.md](./reference.md#images).
168
237
  - A reference was `{ collection, id }` and is now a slug:
169
238
  `getEntry("authors", entry.data.author)`.
170
239
  - `entry.id` is the slug. If ids had folders, update the links, and add
@@ -201,7 +270,8 @@ Ask the user before each of these:
201
270
  such as `@astrojs/mdx` when no MDX is left, and those that only the
202
271
  conversion needed. Change text in the content that describes what you
203
272
  removed.
204
- - Delete the conversion script, or keep it for the record.
273
+ - Delete the temporary directory with `dist-before` and the data store copy,
274
+ and a conversion script if you wrote one.
205
275
 
206
276
  Then run `npx mica check` and `npx astro build` once more.
207
277
 
package/setup/finish.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Add two scripts to `package.json`, so that the user has short commands:
6
6
 
7
7
  ```json
8
- "mica": "mica",
8
+ "mica": "mica start",
9
9
  "check": "mica check && astro check"
10
10
  ```
11
11
 
@@ -52,16 +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
- - What to commit: every file that `git status` shows as new or changed,
60
- including `content/.mica/schema.json`, `package.json` and the lock file. Do
61
- not commit for them unless they asked.
62
- - How to edit: `npm run mica` opens the editor on this computer. A save writes
63
- files in `content/`; Publish commits them. `npm run check` checks the content
64
- (and the types, if `@astrojs/check` is there). `@micacms/editor` is a dev
65
- dependency, so on another clone, run `npm install` first.
66
- - That a self-hosted editor for other people is described at
67
- 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.
@@ -18,12 +18,31 @@ Ask these questions, and wait for the answers:
18
18
  from the team.
19
19
  4. Should editors put a collection in a manual order?
20
20
  5. Which images carry meaning, and which are decorative? An image that
21
- carries meaning gets `media({ alt: "required" })`, so that editors write
22
- its alt text. A decorative image gets `media()`.
21
+ carries meaning gets `media({ accept: "image", alt: "required" })`, so that
22
+ editors write its alt text. A decorative image gets
23
+ `media({ accept: "image" })`. `accept: "image"` makes the field an Astro
24
+ image on the pages; without it, the field is a path, for a PDF or another
25
+ file.
23
26
  6. Do they want example content, so that the pages show something at once?
24
27
 
25
- Suggest a model from the answers, as a short list of collections, singletons
26
- 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.
27
46
 
28
47
  ## 2. Add Mica to the project
29
48
 
@@ -9,8 +9,9 @@ The guides link here. Read the section that a step names.
9
9
  | `npx mica init` | Writes a starter `src/mica.ts` and `src/content.config.ts`. Never overwrites a file |
10
10
  | `npx astro sync` | Runs the Mica loaders, which write the model to `content/.mica/schema.json` |
11
11
  | `npx mica check` | Checks every entry against `content/.mica/schema.json`. Exit code 1 on an error |
12
+ | `npx mica import` | Moves Astro collections into Mica, from Astro's data store. See [content-collections.md](./content-collections.md#6-convert-the-entries) |
12
13
  | `npx mica migrate-layout` | Moves entries into the layout of the model, after the first or the last rich-text or media field of a collection was added or removed |
13
- | `npx mica` | Opens the editor in the browser. Only for the user; do not run it yourself |
14
+ | `npx mica start` | Opens the editor in the browser. Only for the user; do not run it yourself |
14
15
 
15
16
  `mica check` reads the model from `content/.mica/schema.json`. After every
16
17
  change to `src/mica.ts`, run `npx astro sync` before `npx mica check`.
@@ -208,41 +209,24 @@ const settings = await getEntry("settings", "settings");
208
209
 
209
210
  ### Images
210
211
 
211
- On disk, a media field holds `media/cover.jpg`. In `entry.data`, the loader
212
- gives the path from the content root: `news/hello-world/media/cover.jpg`. Astro can only optimize an image that the
213
- build imports, so collect the images with `import.meta.glob`:
214
-
215
- ```ts
216
- // src/lib/media.ts — the glob path is relative to this file
217
- import type { ImageMetadata } from "astro";
218
-
219
- const images = import.meta.glob<{ default: ImageMetadata }>(
220
- "../../content/**/*.{jpeg,jpg,png,webp,avif,gif}",
221
- { eager: true },
222
- );
223
-
224
- /** The image for a Mica media path, or undefined when the file is not there. */
225
- export function mediaImage(path: string | null | undefined): ImageMetadata | undefined {
226
- return path ? images[`../../content/${path}`]?.default : undefined;
227
- }
228
- ```
212
+ A `media({ accept: "image" })` field reaches Astro as an image that Astro
213
+ optimizes, the same as `image()` in an Astro collection: `ImageMetadata`. With
214
+ `alt`, it is `{ src, alt }`. No helper is needed:
229
215
 
230
216
  ```astro
231
217
  ---
232
218
  import { Image } from "astro:assets";
233
- import { mediaImage } from "../../lib/media";
234
- const cover = article.data.cover; // media({ alt: "required" }): { path, alt }
235
- const image = mediaImage(cover?.path);
219
+ const { cover } = article.data; // media({ accept: "image", alt: "required" })
236
220
  ---
237
- {image && <Image src={image} alt={cover.alt} />}
221
+ {cover && <Image {...cover} />}
238
222
  ```
239
223
 
240
- A `media({ multiple: true })` field is an array:
241
- `project.data.gallery.map((item) => mediaImage(item.path))`. A `media()`
242
- without `alt` is a path only: use it for a decorative image, with `alt=""`. For
243
- documents such as PDF files, use
244
- `import.meta.glob("../../content/**/*.pdf", { query: "?url", import: "default", eager: true })`
245
- and link to the URL.
224
+ - Without `alt`, the field is the image itself: `<Image src={logo} alt="" />`.
225
+ - A `media({ accept: "image", multiple: true })` field is an array of them.
226
+ - On disk, the field still holds `media/cover.jpg`, or `{ path, alt }`.
227
+ - A `media()` without `accept: "image"`, for example a PDF, is a path from the
228
+ content root: `news/hello-world/media/plan.pdf`. To link to it, use
229
+ `import.meta.glob("../../content/**/*.pdf", { query: "?url", import: "default", eager: true })`.
246
230
 
247
231
  ### What Markdoc renders differently from Astro's Markdown
248
232
 
package/src/images.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Image fields reach Astro through its own `image()`, which resolves a path
3
+ * against the entry's file, optimizes the image and gives `ImageMetadata`. A
4
+ * site then needs no helper: `<Image {...entry.data.cover} />`.
5
+ */
6
+
7
+ import { posix } from "node:path";
8
+
9
+ import type { FieldMap } from "@micacms/core";
10
+
11
+ /**
12
+ * Rewrites the paths of `media({ accept: "image" })` fields, which the reader
13
+ * gives from the content root, to paths from the entry's data file.
14
+ */
15
+ export function imagePathsFromEntry(
16
+ fields: FieldMap,
17
+ data: Record<string, unknown>,
18
+ dataFile: string,
19
+ ): Record<string, unknown> {
20
+ const from = posix.dirname(dataFile);
21
+ const relative = (path: unknown): unknown => {
22
+ if (typeof path !== "string" || path === "") return path;
23
+ const result = posix.relative(from, path);
24
+ return result.startsWith(".") ? result : `./${result}`;
25
+ };
26
+ const one = (value: unknown): unknown =>
27
+ isRecord(value) ? { ...value, path: relative(value["path"]) } : relative(value);
28
+
29
+ const visit = (map: FieldMap, record: Record<string, unknown>): Record<string, unknown> => {
30
+ const result = { ...record };
31
+ for (const [key, field] of Object.entries(map)) {
32
+ const value = record[key];
33
+ if (field.kind === "media" && field.accept === "image") {
34
+ result[key] = Array.isArray(value) ? value.map(one) : one(value);
35
+ } else if (field.kind === "object" && isRecord(value)) {
36
+ result[key] = visit(field.fields, value);
37
+ } else if (field.kind === "repeater" && Array.isArray(value)) {
38
+ result[key] = value.map((row) => (isRecord(row) ? visit(field.fields, row) : row));
39
+ }
40
+ }
41
+ return result;
42
+ };
43
+
44
+ return visit(fields, data);
45
+ }
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return value !== null && typeof value === "object" && !Array.isArray(value);
49
+ }
package/src/index.ts CHANGED
@@ -18,10 +18,15 @@
18
18
 
19
19
  import { mkdir, readFile, writeFile } from "node:fs/promises";
20
20
  import { dirname, join, relative, sep } from "node:path";
21
+ import { fileURLToPath } from "node:url";
21
22
 
22
23
  import {
24
+ collectionEntryPath,
23
25
  createReferenceCheck,
26
+ entryLayout,
24
27
  fieldsSchema,
28
+ INDEX_FILE,
29
+ singletonPath,
25
30
  isFieldKind,
26
31
  MANIFEST_PATH,
27
32
  readCollection,
@@ -35,8 +40,10 @@ import {
35
40
  type Site,
36
41
  } from "@micacms/core";
37
42
  import { fileSystemSource } from "@micacms/core/node";
43
+ import type { ImageMetadata } from "astro";
38
44
  import { z } from "zod";
39
45
 
46
+ import { imagePathsFromEntry } from "./images.js";
40
47
  import { manifestFor } from "./manifest.js";
41
48
  import { DEFAULT_CONTENT_ROOT, resolveContentRoot } from "./root.js";
42
49
 
@@ -69,7 +76,8 @@ export interface MicaCollectionsOptions {
69
76
  debounceMs?: number;
70
77
  /**
71
78
  * Fails the build on a relation to a missing entry or a missing media file.
72
- * Without it, the build logs a warning for each and goes on.
79
+ * Without it, the build logs a warning for each and goes on; a missing image
80
+ * file fails the build either way, because Astro cannot import it.
73
81
  */
74
82
  strict?: boolean;
75
83
  }
@@ -115,12 +123,13 @@ export interface AstroCollectionConfig<T = unknown> {
115
123
  type: "content_layer";
116
124
  loader: ContentLoader;
117
125
  /**
118
- * Typed as `ZodType<T>` rather than a bare `ZodObject`, because Astro derives
119
- * `CollectionEntry<"projects">["data"]` from `z.infer` of whatever this is.
120
- * Left generic, every field in every template would degrade to `any` — and
121
- * the point of the shared model is that renaming a field breaks the build.
126
+ * A function, so that Astro hands in its `image()`. Its result is typed as
127
+ * `ZodType<T>` because Astro derives `CollectionEntry<"projects">["data"]`
128
+ * from it: left generic, every field in every template would degrade to
129
+ * `any`, and the point of the shared model is that renaming a field breaks
130
+ * the build.
122
131
  */
123
- schema: z.ZodType<T>;
132
+ schema: (context: { image: () => z.ZodType }) => z.ZodType<T>;
124
133
  }
125
134
 
126
135
  /** The subset of Astro's `Loader` contract this package uses. */
@@ -132,7 +141,7 @@ export interface ContentLoader {
132
141
  export interface LoaderContext {
133
142
  store: {
134
143
  clear(): void;
135
- set(entry: { id: string; data: Record<string, unknown>; digest?: string }): boolean;
144
+ set(entry: { id: string; data: Record<string, unknown>; digest?: string; filePath?: string }): boolean;
136
145
  keys(): string[];
137
146
  };
138
147
  logger: {
@@ -161,16 +170,16 @@ export interface LoaderContext {
161
170
  parseData(input: {
162
171
  id: string;
163
172
  data: Record<string, unknown>;
173
+ /** The entry's file, from the project root: Astro's `image()` resolves paths against it. */
174
+ filePath?: string;
164
175
  }): Promise<Record<string, unknown>>;
165
176
  generateDigest(data: Record<string, unknown> | string): string;
166
177
  }
167
178
 
168
179
  export type MicaCollections<S extends Site> = {
169
- [K in keyof S["collections"]]: AstroCollectionConfig<
170
- AstroDocument<InferCollection<S["collections"][K]>>
171
- >;
180
+ [K in keyof S["collections"]]: AstroCollectionConfig<CollectionDocument<S, K>>;
172
181
  } & {
173
- [K in keyof S["singletons"]]: AstroCollectionConfig<InferSingleton<S["singletons"][K]>>;
182
+ [K in keyof S["singletons"]]: AstroCollectionConfig<SingletonDocument<S, K>>;
174
183
  };
175
184
 
176
185
  export function micaCollections<S extends Site>(
@@ -203,6 +212,18 @@ export function micaCollections<S extends Site>(
203
212
  ? Promise.resolve()
204
213
  : publishModel(site, rootFor(context), context.logger));
205
214
 
215
+ /** An entry's data file, from the content root and, outside tests, from the project root. */
216
+ const entryFile = (context: LoaderContext, fields: FieldMap, name: string, slug?: string) => {
217
+ const base = slug === undefined ? singletonPath(fields, name) : collectionEntryPath(fields, name, slug);
218
+ const inContent = entryLayout(fields) === "directory" ? `${base}/${INDEX_FILE}` : base;
219
+ const project = context.config?.root;
220
+ const inProject =
221
+ project && !options.source
222
+ ? relative(fileURLToPath(project), join(rootFor(context), inContent)).split(sep).join("/")
223
+ : undefined;
224
+ return { inContent, inProject };
225
+ };
226
+
206
227
  /** Warns about broken relations and missing files, or fails the build with `strict`. */
207
228
  const checkReferences = async (
208
229
  context: LoaderContext,
@@ -267,7 +288,7 @@ export function micaCollections<S extends Site>(
267
288
  for (const [name, def] of Object.entries(site.collections) as [string, Collection][]) {
268
289
  collections[name] = {
269
290
  type: "content_layer",
270
- schema: fieldsSchema(def.fields).extend({ _mica: metaSchema }),
291
+ schema: ({ image }) => fieldsSchema(def.fields, { image }).extend({ _mica: metaSchema }),
271
292
  loader: {
272
293
  name: `mica:${name}`,
273
294
  load: async (context) => {
@@ -287,11 +308,18 @@ export function micaCollections<S extends Site>(
287
308
  position: entry.position,
288
309
  };
289
310
 
311
+ const file = entryFile(context, def.fields, name, entry.slug);
290
312
  const data = await context.parseData({
291
313
  id: entry.slug,
292
- data: { ...entry.data, _mica: meta },
314
+ data: { ...imagePathsFromEntry(def.fields, entry.data, file.inContent), _mica: meta },
315
+ ...(file.inProject ? { filePath: file.inProject } : {}),
316
+ });
317
+ context.store.set({
318
+ id: entry.slug,
319
+ data,
320
+ digest: context.generateDigest(data),
321
+ ...(file.inProject ? { filePath: file.inProject } : {}),
293
322
  });
294
- context.store.set({ id: entry.slug, data, digest: context.generateDigest(data) });
295
323
  }
296
324
 
297
325
  await checkReferences(
@@ -315,7 +343,7 @@ export function micaCollections<S extends Site>(
315
343
  for (const [name, def] of Object.entries(site.singletons) as [string, Singleton][]) {
316
344
  collections[name] = {
317
345
  type: "content_layer",
318
- schema: fieldsSchema(def.fields),
346
+ schema: ({ image }) => fieldsSchema(def.fields, { image }),
319
347
  loader: {
320
348
  name: `mica:${name}`,
321
349
  load: async (context) => {
@@ -333,8 +361,18 @@ export function micaCollections<S extends Site>(
333
361
  }
334
362
 
335
363
  await checkReferences(context, source, [{ label: name, fields: def.fields, data }]);
336
- const parsed = await context.parseData({ id: name, data });
337
- context.store.set({ id: name, data: parsed, digest: context.generateDigest(parsed) });
364
+ 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
+ context.store.set({
371
+ id: name,
372
+ data: parsed,
373
+ digest: context.generateDigest(parsed),
374
+ ...(file.inProject ? { filePath: file.inProject } : {}),
375
+ });
338
376
  };
339
377
 
340
378
  await publish(context);
@@ -439,13 +477,13 @@ async function guard<T>(
439
477
  export type CollectionDocument<
440
478
  S extends Site,
441
479
  K extends keyof S["collections"],
442
- > = AstroDocument<InferCollection<S["collections"][K]>>;
480
+ > = AstroDocument<InferCollection<S["collections"][K], ImageMetadata>>;
443
481
 
444
482
  /** The document type for one singleton. */
445
483
  export type SingletonDocument<
446
484
  S extends Site,
447
485
  K extends keyof S["singletons"],
448
- > = InferSingleton<S["singletons"][K]>;
486
+ > = InferSingleton<S["singletons"][K], ImageMetadata>;
449
487
 
450
488
  export { manifestFor } from "./manifest.js";
451
489
  export { DEFAULT_CONTENT_ROOT, resolveContentRoot };