@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 +9 -3
- package/dist/images.d.ts +12 -0
- package/dist/images.d.ts.map +1 -0
- package/dist/images.js +41 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -7
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/setup/content-collections.md +150 -80
- package/setup/finish.md +45 -12
- package/setup/new-project.md +23 -4
- package/setup/reference.md +13 -29
- package/src/images.ts +49 -0
- package/src/index.ts +57 -19
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
package/dist/images.d.ts
ADDED
|
@@ -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
|
-
*
|
|
89
|
-
* `CollectionEntry<"projects">["data"]`
|
|
90
|
-
*
|
|
91
|
-
* the point of the shared model is that renaming a field breaks
|
|
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:
|
|
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<
|
|
153
|
+
[K in keyof S["collections"]]: AstroCollectionConfig<CollectionDocument<S, K>>;
|
|
146
154
|
} & {
|
|
147
|
-
[K in keyof S["singletons"]]: AstroCollectionConfig<
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
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 {
|
|
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
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
17
|
-
outside the project
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 (``). 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
|
-
|
|
170
|
+
import reads. The pages can fail now; step 7 fixes them.
|
|
104
171
|
|
|
105
172
|
## 6. Convert the entries
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
165
|
-
`alt
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
package/setup/new-project.md
CHANGED
|
@@ -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
|
|
22
|
-
its alt text. A decorative image gets
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
package/setup/reference.md
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
221
|
+
{cover && <Image {...cover} />}
|
|
238
222
|
```
|
|
239
223
|
|
|
240
|
-
|
|
241
|
-
`
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
`
|
|
245
|
-
|
|
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
|
-
*
|
|
119
|
-
* `CollectionEntry<"projects">["data"]`
|
|
120
|
-
*
|
|
121
|
-
* the point of the shared model is that renaming a field breaks
|
|
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<
|
|
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
|
|
337
|
-
|
|
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 };
|