scribe-cms 0.0.1

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.
Files changed (96) hide show
  1. package/README.md +150 -0
  2. package/dist/cli/index.cjs +2303 -0
  3. package/dist/cli/index.cjs.map +1 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +2289 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/index.cjs +2268 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.js +2238 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/runtime.cjs +1216 -0
  13. package/dist/runtime.cjs.map +1 -0
  14. package/dist/runtime.js +1207 -0
  15. package/dist/runtime.js.map +1 -0
  16. package/dist/src/config/load-config.d.ts +10 -0
  17. package/dist/src/config/load-config.d.ts.map +1 -0
  18. package/dist/src/config/load-env.d.ts +3 -0
  19. package/dist/src/config/load-env.d.ts.map +1 -0
  20. package/dist/src/config/resolve-config.d.ts +13 -0
  21. package/dist/src/config/resolve-config.d.ts.map +1 -0
  22. package/dist/src/core/alias-helpers.d.ts +7 -0
  23. package/dist/src/core/alias-helpers.d.ts.map +1 -0
  24. package/dist/src/core/builtin-fields.d.ts +38 -0
  25. package/dist/src/core/builtin-fields.d.ts.map +1 -0
  26. package/dist/src/core/field.d.ts +60 -0
  27. package/dist/src/core/field.d.ts.map +1 -0
  28. package/dist/src/core/introspect-schema.d.ts +20 -0
  29. package/dist/src/core/introspect-schema.d.ts.map +1 -0
  30. package/dist/src/core/slug-aliases.d.ts +27 -0
  31. package/dist/src/core/slug-aliases.d.ts.map +1 -0
  32. package/dist/src/core/types.d.ts +261 -0
  33. package/dist/src/core/types.d.ts.map +1 -0
  34. package/dist/src/create-project.d.ts +10 -0
  35. package/dist/src/create-project.d.ts.map +1 -0
  36. package/dist/src/create-scribe.d.ts +11 -0
  37. package/dist/src/create-scribe.d.ts.map +1 -0
  38. package/dist/src/hash/page-hash.d.ts +7 -0
  39. package/dist/src/hash/page-hash.d.ts.map +1 -0
  40. package/dist/src/history/record-revision.d.ts +12 -0
  41. package/dist/src/history/record-revision.d.ts.map +1 -0
  42. package/dist/src/i18n/build-url.d.ts +14 -0
  43. package/dist/src/i18n/build-url.d.ts.map +1 -0
  44. package/dist/src/i18n/resolve-document.d.ts +12 -0
  45. package/dist/src/i18n/resolve-document.d.ts.map +1 -0
  46. package/dist/src/i18n/translation-index.d.ts +7 -0
  47. package/dist/src/i18n/translation-index.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +18 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/loader/create-loader.d.ts +17 -0
  51. package/dist/src/loader/create-loader.d.ts.map +1 -0
  52. package/dist/src/loader/normalize-en.d.ts +3 -0
  53. package/dist/src/loader/normalize-en.d.ts.map +1 -0
  54. package/dist/src/loader/parse-mdx.d.ts +7 -0
  55. package/dist/src/loader/parse-mdx.d.ts.map +1 -0
  56. package/dist/src/redirects/build-redirects.d.ts +17 -0
  57. package/dist/src/redirects/build-redirects.d.ts.map +1 -0
  58. package/dist/src/redirects/translation-index.d.ts +4 -0
  59. package/dist/src/redirects/translation-index.d.ts.map +1 -0
  60. package/dist/src/redirects/types.d.ts +6 -0
  61. package/dist/src/redirects/types.d.ts.map +1 -0
  62. package/dist/src/runtime.d.ts +5 -0
  63. package/dist/src/runtime.d.ts.map +1 -0
  64. package/dist/src/sitemap/generate-sitemap.d.ts +5 -0
  65. package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -0
  66. package/dist/src/sitemap/join-base-url.d.ts +3 -0
  67. package/dist/src/sitemap/join-base-url.d.ts.map +1 -0
  68. package/dist/src/sitemap/types.d.ts +39 -0
  69. package/dist/src/sitemap/types.d.ts.map +1 -0
  70. package/dist/src/storage/sqlite.d.ts +9 -0
  71. package/dist/src/storage/sqlite.d.ts.map +1 -0
  72. package/dist/src/storage/translations.d.ts +58 -0
  73. package/dist/src/storage/translations.d.ts.map +1 -0
  74. package/dist/src/translate/gemini-client.d.ts +15 -0
  75. package/dist/src/translate/gemini-client.d.ts.map +1 -0
  76. package/dist/src/translate/page-translator.d.ts +24 -0
  77. package/dist/src/translate/page-translator.d.ts.map +1 -0
  78. package/dist/src/translate/prompts/translation-prompt.d.ts +14 -0
  79. package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -0
  80. package/dist/src/translate/resolve-translate-config.d.ts +10 -0
  81. package/dist/src/translate/resolve-translate-config.d.ts.map +1 -0
  82. package/dist/src/translate/worklist.d.ts +19 -0
  83. package/dist/src/translate/worklist.d.ts.map +1 -0
  84. package/dist/src/validate/validate-project.d.ts +16 -0
  85. package/dist/src/validate/validate-project.d.ts.map +1 -0
  86. package/dist/src/validate/validate-relations.d.ts +19 -0
  87. package/dist/src/validate/validate-relations.d.ts.map +1 -0
  88. package/dist/src/validate/validate-slug-suffix.d.ts +12 -0
  89. package/dist/src/validate/validate-slug-suffix.d.ts.map +1 -0
  90. package/dist/studio/server.cjs +673 -0
  91. package/dist/studio/server.cjs.map +1 -0
  92. package/dist/studio/server.d.ts +7 -0
  93. package/dist/studio/server.d.ts.map +1 -0
  94. package/dist/studio/server.js +664 -0
  95. package/dist/studio/server.js.map +1 -0
  96. package/package.json +70 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2268 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var path3 = require('path');
5
+ var fs2 = require('fs');
6
+ var matter = require('gray-matter');
7
+ var Database = require('better-sqlite3');
8
+ var jiti = require('jiti');
9
+ var crypto = require('crypto');
10
+ var genai = require('@google/genai');
11
+
12
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
+
14
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
15
+ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
16
+ var matter__default = /*#__PURE__*/_interopDefault(matter);
17
+ var Database__default = /*#__PURE__*/_interopDefault(Database);
18
+
19
+ // ../../node_modules/tsup/assets/cjs_shims.js
20
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
21
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
22
+ var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
23
+ var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
24
+ function getFieldKind(schema) {
25
+ const tagged = schema;
26
+ return tagged[FIELD_KIND] ?? "structural";
27
+ }
28
+ function getRelationTarget(schema) {
29
+ let current = schema;
30
+ for (let i = 0; i < 8; i++) {
31
+ const tagged = current;
32
+ if (tagged[RELATION_META]) {
33
+ return tagged[RELATION_META];
34
+ }
35
+ if (typeof tagged.unwrap === "function") {
36
+ current = tagged.unwrap();
37
+ continue;
38
+ }
39
+ if (typeof tagged.removeDefault === "function") {
40
+ current = tagged.removeDefault();
41
+ continue;
42
+ }
43
+ if (tagged._def?.innerType) {
44
+ current = tagged._def.innerType;
45
+ continue;
46
+ }
47
+ break;
48
+ }
49
+ return null;
50
+ }
51
+ function mark(schema, kind) {
52
+ Object.defineProperty(schema, FIELD_KIND, {
53
+ value: kind,
54
+ enumerable: false,
55
+ configurable: true
56
+ });
57
+ return schema;
58
+ }
59
+ function markRelation(schema, meta) {
60
+ Object.defineProperty(schema, RELATION_META, {
61
+ value: meta,
62
+ enumerable: false,
63
+ configurable: true
64
+ });
65
+ return mark(schema, "structural");
66
+ }
67
+ var field = {
68
+ translatable: (schema) => mark(schema, "translatable"),
69
+ structural: (schema) => mark(schema, "structural"),
70
+ relation: (typeId, options) => {
71
+ const multiple = options?.multiple ?? false;
72
+ const optional = options?.optional ?? false;
73
+ if ((options?.min !== void 0 || options?.max !== void 0) && !multiple) {
74
+ throw new Error(
75
+ `field.relation("${typeId}"): min/max require { multiple: true }`
76
+ );
77
+ }
78
+ let inner;
79
+ if (multiple) {
80
+ let arr = zod.z.array(zod.z.string().min(1));
81
+ if (options?.min !== void 0) arr = arr.min(options.min);
82
+ if (options?.max !== void 0) arr = arr.max(options.max);
83
+ inner = arr;
84
+ } else {
85
+ inner = zod.z.string().min(1);
86
+ }
87
+ markRelation(inner, { typeId, multiple, optional });
88
+ const schema = optional ? inner.optional() : inner;
89
+ return schema;
90
+ }
91
+ };
92
+ function unwrapSchema(schema) {
93
+ let current = schema;
94
+ for (let i = 0; i < 8; i++) {
95
+ const anySchema = current;
96
+ if (typeof anySchema.unwrap === "function") {
97
+ current = anySchema.unwrap();
98
+ continue;
99
+ }
100
+ if (typeof anySchema.removeDefault === "function") {
101
+ current = anySchema.removeDefault();
102
+ continue;
103
+ }
104
+ if (anySchema._def?.innerType) {
105
+ current = anySchema._def.innerType;
106
+ continue;
107
+ }
108
+ break;
109
+ }
110
+ return current;
111
+ }
112
+
113
+ // src/core/types.ts
114
+ function defineConfig(config) {
115
+ return config;
116
+ }
117
+ function defineContentType(config) {
118
+ return config;
119
+ }
120
+
121
+ // src/i18n/build-url.ts
122
+ var SLUG_PLACEHOLDER = "{slug}";
123
+ function isRoutableType(type) {
124
+ return typeof type.path === "string" && type.path.length > 0;
125
+ }
126
+ function assertValidPathTemplate(path9, typeId) {
127
+ if (!path9.startsWith("/")) {
128
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path9}")`);
129
+ }
130
+ const count = (path9.match(/\{slug\}/g) ?? []).length;
131
+ if (count !== 1) {
132
+ throw new Error(
133
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path9}")`
134
+ );
135
+ }
136
+ }
137
+ function pathPrefix(template) {
138
+ const idx = template.indexOf(SLUG_PLACEHOLDER);
139
+ if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
140
+ return template.slice(0, idx);
141
+ }
142
+ function pathSuffix(template) {
143
+ const idx = template.indexOf(SLUG_PLACEHOLDER);
144
+ if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
145
+ return template.slice(idx + SLUG_PLACEHOLDER.length);
146
+ }
147
+ function resolvePath(template, slug, locale, defaultLocale) {
148
+ const relative = template.replace(SLUG_PLACEHOLDER, slug);
149
+ if (locale === defaultLocale) return relative;
150
+ return `/${locale}${relative}`;
151
+ }
152
+ function extractSlugFromResolvedPath(template, resolvedPath) {
153
+ const prefix = pathPrefix(template);
154
+ const suffix = pathSuffix(template);
155
+ if (!resolvedPath.startsWith(prefix)) return null;
156
+ if (suffix && !resolvedPath.endsWith(suffix)) return null;
157
+ const slugEnd = suffix ? resolvedPath.length - suffix.length : resolvedPath.length;
158
+ const slug = resolvedPath.slice(prefix.length, slugEnd);
159
+ return slug.length > 0 ? slug : null;
160
+ }
161
+
162
+ // src/config/resolve-config.ts
163
+ var RESOLVED = /* @__PURE__ */ Symbol.for("@genlook/scribe/resolvedConfig");
164
+ function isResolvedConfig(config) {
165
+ return typeof config === "object" && config !== null && config[RESOLVED] === true;
166
+ }
167
+ function resolveConfig(input, baseDir) {
168
+ if (isResolvedConfig(input)) return input;
169
+ const raw = input;
170
+ if (!raw.rootDir) {
171
+ throw new Error("scribe config: rootDir is required");
172
+ }
173
+ if (!raw.locales || raw.locales.length === 0) {
174
+ throw new Error("scribe config: locales must be a non-empty array");
175
+ }
176
+ const defaultLocale = raw.defaultLocale ?? "en";
177
+ if (!raw.locales.includes(defaultLocale)) {
178
+ throw new Error(
179
+ `scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
180
+ );
181
+ }
182
+ const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
183
+ const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
184
+ const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
185
+ const seenIds = /* @__PURE__ */ new Set();
186
+ const types = raw.types.map((type) => {
187
+ if (seenIds.has(type.id)) {
188
+ throw new Error(`scribe config: duplicate content type id "${type.id}"`);
189
+ }
190
+ seenIds.add(type.id);
191
+ if (type.path) {
192
+ assertValidPathTemplate(type.path, type.id);
193
+ }
194
+ return {
195
+ ...type,
196
+ contentDir: type.contentDir ?? type.id,
197
+ label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
198
+ slugStrategy: type.slugStrategy ?? "fixed",
199
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
200
+ };
201
+ });
202
+ const config = {
203
+ rootDir: contentRoot,
204
+ storePath,
205
+ locales: [...raw.locales],
206
+ defaultLocale,
207
+ localePresets: raw.localePresets,
208
+ translate: raw.translate,
209
+ types
210
+ };
211
+ Object.defineProperty(config, RESOLVED, {
212
+ value: true,
213
+ enumerable: false
214
+ });
215
+ return config;
216
+ }
217
+
218
+ // src/core/introspect-schema.ts
219
+ function introspectSchema(schema, prefix = []) {
220
+ const relation = getRelationTarget(schema);
221
+ if (relation && prefix.length > 0) {
222
+ return [
223
+ {
224
+ path: prefix,
225
+ kind: "relation",
226
+ relationTarget: relation.typeId,
227
+ relationMultiple: relation.multiple,
228
+ relationOptional: relation.optional
229
+ }
230
+ ];
231
+ }
232
+ const base = unwrapSchema(schema);
233
+ if (base instanceof Object && "shape" in base) {
234
+ const shape = base.shape;
235
+ const fields = [];
236
+ for (const [key, child] of Object.entries(shape)) {
237
+ fields.push(...introspectSchema(child, [...prefix, key]));
238
+ }
239
+ return fields;
240
+ }
241
+ if (base instanceof Object && "element" in base) {
242
+ const element = base.element;
243
+ const elementBase = unwrapSchema(element);
244
+ if (elementBase instanceof Object && "shape" in elementBase) {
245
+ const shape = elementBase.shape;
246
+ const fields = [];
247
+ for (const [key, child] of Object.entries(shape)) {
248
+ fields.push(...introspectSchema(child, [...prefix, "*", key]));
249
+ }
250
+ return fields;
251
+ }
252
+ }
253
+ return [{ path: prefix, kind: getFieldKind(schema) }];
254
+ }
255
+ function extractByPaths(data, paths) {
256
+ const out = {};
257
+ for (const path9 of paths) {
258
+ const value = getAtPath(data, path9);
259
+ if (value !== void 0) {
260
+ setAtPath(out, path9.filter((p) => p !== "*"), value);
261
+ }
262
+ }
263
+ return out;
264
+ }
265
+ function pickTranslatable(data, schema) {
266
+ const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
267
+ return extractByPaths(data, translatablePaths);
268
+ }
269
+ function pickStructural(data, schema) {
270
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
271
+ return extractByPaths(data, structuralPaths);
272
+ }
273
+ function mergeStructuralOntoLocale(localeData, enData, schema) {
274
+ const structural = pickStructural(enData, schema);
275
+ const translatable = pickTranslatable(localeData, schema);
276
+ return deepMerge(structural, translatable);
277
+ }
278
+ function getAtPath(obj, path9) {
279
+ let current = obj;
280
+ for (const segment of path9) {
281
+ if (segment === "*") {
282
+ if (!Array.isArray(current)) return void 0;
283
+ return current.map(
284
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path9.slice(path9.indexOf("*") + 1)) : void 0
285
+ );
286
+ }
287
+ if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
288
+ current = current[segment];
289
+ }
290
+ return current;
291
+ }
292
+ function setAtPath(obj, path9, value) {
293
+ if (path9.length === 0) return;
294
+ if (path9.length === 1) {
295
+ obj[path9[0]] = value;
296
+ return;
297
+ }
298
+ const [head, ...rest] = path9;
299
+ if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
300
+ obj[head] = {};
301
+ }
302
+ setAtPath(obj[head], rest, value);
303
+ }
304
+ function deepMerge(base, overlay) {
305
+ const out = { ...base };
306
+ for (const [key, value] of Object.entries(overlay)) {
307
+ if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
308
+ out[key] = deepMerge(out[key], value);
309
+ } else if (Array.isArray(value) && Array.isArray(out[key])) {
310
+ out[key] = mergeArrayOverlay(out[key], value);
311
+ } else {
312
+ out[key] = value;
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+ function mergeArrayOverlay(base, overlay) {
318
+ return base.map((item, index) => {
319
+ const overlayItem = overlay[index];
320
+ if (item && typeof item === "object" && !Array.isArray(item) && overlayItem && typeof overlayItem === "object" && !Array.isArray(overlayItem)) {
321
+ return deepMerge(item, overlayItem);
322
+ }
323
+ return overlayItem ?? item;
324
+ });
325
+ }
326
+ function listRelationFields(schema) {
327
+ return introspectSchema(schema).filter((f) => f.kind === "relation");
328
+ }
329
+ var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
330
+ var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
331
+ var isoDateSchema = zod.z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
332
+ var canonicalPathSchema = zod.z.string().regex(/^\//, "canonicalPath must start with /");
333
+ var LOCALE_BUILTIN_KEYS = [
334
+ ["publishedAt", "publishedAt is EN-only; inherited from the EN parent at load time"],
335
+ ["updatedAt", "updatedAt is EN-only; inherited from the EN parent at load time"],
336
+ ["noindex", "noindex is EN-only; inherited from the EN parent at load time"],
337
+ ["canonicalPath", "canonicalPath is EN-only; inherited from the EN parent at load time"],
338
+ ["aliases", "aliases is EN-only; remove from locale translation"],
339
+ ["redirect_to", "redirect_to is EN-only; remove from locale translation"],
340
+ ["translationOf", "translationOf is internal; remove from locale translation"],
341
+ ["enSlug", "enSlug is internal; remove from locale translation"]
342
+ ];
343
+ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
344
+ const issues = [];
345
+ const rest = { ...data };
346
+ const aliasesResult = zod.z.array(slugPatternSchema).max(20).optional().default([]).safeParse(rest.aliases ?? []);
347
+ delete rest.aliases;
348
+ const redirectRaw = rest.redirect_to;
349
+ delete rest.redirect_to;
350
+ let redirectTo;
351
+ if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
352
+ if (typeof redirectRaw !== "string") {
353
+ issues.push({
354
+ field: "redirect_to",
355
+ message: "redirect_to must be a string path",
356
+ level: "error"
357
+ });
358
+ } else if (!pathTemplate) {
359
+ issues.push({
360
+ field: "redirect_to",
361
+ message: "redirect_to is not allowed on reference-only content types",
362
+ level: "error"
363
+ });
364
+ } else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
365
+ issues.push({
366
+ field: "redirect_to",
367
+ message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
368
+ level: "error"
369
+ });
370
+ } else {
371
+ redirectTo = redirectRaw;
372
+ }
373
+ }
374
+ const aliases = aliasesResult.success ? aliasesResult.data : [];
375
+ if (!aliasesResult.success) {
376
+ for (const issue of aliasesResult.error.issues) {
377
+ issues.push({
378
+ field: `aliases.${issue.path.join(".")}`,
379
+ message: issue.message,
380
+ level: "error"
381
+ });
382
+ }
383
+ }
384
+ let publishedAt;
385
+ if (rest.publishedAt !== void 0 && rest.publishedAt !== null && rest.publishedAt !== "") {
386
+ const parsed = isoDateSchema.safeParse(rest.publishedAt);
387
+ if (parsed.success) {
388
+ publishedAt = parsed.data;
389
+ } else {
390
+ for (const issue of parsed.error.issues) {
391
+ issues.push({ field: "publishedAt", message: issue.message, level: "error" });
392
+ }
393
+ }
394
+ delete rest.publishedAt;
395
+ }
396
+ let updatedAt;
397
+ if (rest.updatedAt !== void 0 && rest.updatedAt !== null && rest.updatedAt !== "") {
398
+ const parsed = isoDateSchema.safeParse(rest.updatedAt);
399
+ if (parsed.success) {
400
+ updatedAt = parsed.data;
401
+ } else {
402
+ for (const issue of parsed.error.issues) {
403
+ issues.push({ field: "updatedAt", message: issue.message, level: "error" });
404
+ }
405
+ }
406
+ delete rest.updatedAt;
407
+ } else if (publishedAt) {
408
+ updatedAt = publishedAt;
409
+ }
410
+ let noindex = false;
411
+ if (rest.noindex !== void 0 && rest.noindex !== null && rest.noindex !== "") {
412
+ if (typeof rest.noindex === "boolean") {
413
+ noindex = rest.noindex;
414
+ } else {
415
+ issues.push({ field: "noindex", message: "noindex must be a boolean", level: "error" });
416
+ }
417
+ delete rest.noindex;
418
+ }
419
+ let canonicalPathOverride;
420
+ if (rest.canonicalPath !== void 0 && rest.canonicalPath !== null && rest.canonicalPath !== "") {
421
+ const parsed = canonicalPathSchema.safeParse(rest.canonicalPath);
422
+ if (parsed.success) {
423
+ canonicalPathOverride = parsed.data;
424
+ } else {
425
+ for (const issue of parsed.error.issues) {
426
+ issues.push({ field: "canonicalPath", message: issue.message, level: "error" });
427
+ }
428
+ }
429
+ delete rest.canonicalPath;
430
+ }
431
+ for (const internalKey of ["translationOf", "enSlug"]) {
432
+ if (rest[internalKey] !== void 0) {
433
+ issues.push({
434
+ field: internalKey,
435
+ message: `${internalKey} is managed internally by Scribe; remove from frontmatter`,
436
+ level: "warning"
437
+ });
438
+ delete rest[internalKey];
439
+ }
440
+ }
441
+ return {
442
+ builtin: {
443
+ aliases,
444
+ redirectTo,
445
+ publishedAt,
446
+ updatedAt,
447
+ noindex,
448
+ canonicalPathOverride
449
+ },
450
+ rest,
451
+ issues
452
+ };
453
+ }
454
+ function validateLocaleBuiltinFields(data) {
455
+ const issues = [];
456
+ for (const [key, message] of LOCALE_BUILTIN_KEYS) {
457
+ if (data[key] !== void 0) {
458
+ issues.push({ field: key, message, level: "error" });
459
+ }
460
+ }
461
+ return issues;
462
+ }
463
+ function documentLastModified(doc) {
464
+ const raw = doc.updatedAt ?? doc.publishedAt;
465
+ if (!raw) return void 0;
466
+ const date = new Date(raw);
467
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
468
+ }
469
+ function resolveCanonicalPathname(type, doc, defaultLocale) {
470
+ if (doc.canonicalPathOverride) {
471
+ if (doc.locale === defaultLocale) return doc.canonicalPathOverride;
472
+ return `/${doc.locale}${doc.canonicalPathOverride}`;
473
+ }
474
+ if (!type.path) return `/${doc.slug}`;
475
+ return resolvePath(type.path, doc.slug, doc.locale, defaultLocale);
476
+ }
477
+ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
478
+ const out = { ...frontmatter };
479
+ if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
480
+ if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
481
+ out.noindex = doc.noindex;
482
+ out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
483
+ return out;
484
+ }
485
+ function seoFieldsFromEn(enDoc) {
486
+ return {
487
+ publishedAt: enDoc.publishedAt,
488
+ updatedAt: enDoc.updatedAt,
489
+ noindex: enDoc.noindex,
490
+ canonicalPathOverride: enDoc.canonicalPathOverride,
491
+ redirectTo: enDoc.redirectTo
492
+ };
493
+ }
494
+ var SCHEMA_VERSION = 2;
495
+ var MIGRATIONS = [
496
+ `CREATE TABLE IF NOT EXISTS meta (
497
+ key TEXT PRIMARY KEY,
498
+ value TEXT NOT NULL
499
+ )`,
500
+ `CREATE TABLE IF NOT EXISTS translations (
501
+ content_type TEXT NOT NULL,
502
+ en_slug TEXT NOT NULL,
503
+ locale TEXT NOT NULL,
504
+ slug TEXT NOT NULL,
505
+ frontmatter_json TEXT NOT NULL,
506
+ body TEXT NOT NULL,
507
+ en_hash TEXT NOT NULL,
508
+ translated_at TEXT NOT NULL,
509
+ model TEXT NOT NULL,
510
+ PRIMARY KEY (content_type, en_slug, locale)
511
+ )`,
512
+ `CREATE INDEX IF NOT EXISTS idx_translations_type_locale
513
+ ON translations(content_type, locale)`,
514
+ `CREATE TABLE IF NOT EXISTS revisions (
515
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
516
+ content_type TEXT NOT NULL,
517
+ en_slug TEXT NOT NULL,
518
+ locale TEXT,
519
+ revision_kind TEXT NOT NULL,
520
+ en_hash TEXT NOT NULL,
521
+ body_hash TEXT NOT NULL,
522
+ created_at TEXT NOT NULL,
523
+ model TEXT,
524
+ body_preview TEXT
525
+ )`,
526
+ `CREATE INDEX IF NOT EXISTS idx_revisions_lookup
527
+ ON revisions(content_type, en_slug, locale, created_at DESC)`,
528
+ `CREATE TABLE IF NOT EXISTS slug_aliases (
529
+ content_type TEXT NOT NULL,
530
+ canonical_en_slug TEXT NOT NULL,
531
+ alias_en_slug TEXT NOT NULL,
532
+ created_at TEXT NOT NULL,
533
+ PRIMARY KEY (content_type, alias_en_slug)
534
+ )`,
535
+ `CREATE INDEX IF NOT EXISTS idx_slug_aliases_canonical
536
+ ON slug_aliases(content_type, canonical_en_slug)`,
537
+ `CREATE TABLE IF NOT EXISTS alias_locale_slugs (
538
+ content_type TEXT NOT NULL,
539
+ alias_en_slug TEXT NOT NULL,
540
+ locale TEXT NOT NULL,
541
+ locale_slug TEXT NOT NULL,
542
+ captured_at TEXT NOT NULL,
543
+ PRIMARY KEY (content_type, alias_en_slug, locale)
544
+ )`
545
+ ];
546
+ function resolveStorePath(config) {
547
+ return config.storePath;
548
+ }
549
+ function openStore(config, mode = "readwrite") {
550
+ const storePath = resolveStorePath(config);
551
+ fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
552
+ const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
553
+ if (mode === "readwrite") {
554
+ migrate(db);
555
+ }
556
+ return db;
557
+ }
558
+ function migrate(db) {
559
+ for (const sql of MIGRATIONS) {
560
+ db.exec(sql);
561
+ }
562
+ db.prepare(
563
+ `INSERT INTO meta(key, value) VALUES('schema_version', ?)
564
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
565
+ ).run(String(SCHEMA_VERSION));
566
+ }
567
+
568
+ // src/storage/translations.ts
569
+ function upsertTranslation(db, input) {
570
+ db.prepare(
571
+ `INSERT INTO translations (
572
+ content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
573
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
574
+ ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
575
+ slug = excluded.slug,
576
+ frontmatter_json = excluded.frontmatter_json,
577
+ body = excluded.body,
578
+ en_hash = excluded.en_hash,
579
+ translated_at = excluded.translated_at,
580
+ model = excluded.model`
581
+ ).run(
582
+ input.contentType,
583
+ input.enSlug,
584
+ input.locale,
585
+ input.slug,
586
+ JSON.stringify(input.frontmatter),
587
+ input.body,
588
+ input.enHash,
589
+ input.translatedAt,
590
+ input.model
591
+ );
592
+ }
593
+ function getTranslation(db, contentType, enSlug, locale) {
594
+ return db.prepare(
595
+ `SELECT * FROM translations WHERE content_type = ? AND en_slug = ? AND locale = ?`
596
+ ).get(contentType, enSlug, locale);
597
+ }
598
+ function listTranslationsForType(db, contentType) {
599
+ return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
600
+ }
601
+ function listTranslationsForEnSlug(db, contentType, enSlug) {
602
+ return db.prepare(
603
+ `SELECT * FROM translations WHERE content_type = ? AND en_slug = ? ORDER BY locale`
604
+ ).all(contentType, enSlug);
605
+ }
606
+ function bulkLoadTranslations(db) {
607
+ return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
608
+ }
609
+ function appendRevision(db, input) {
610
+ const result = db.prepare(
611
+ `INSERT INTO revisions (
612
+ content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at, model, body_preview
613
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
614
+ ).run(
615
+ input.contentType,
616
+ input.enSlug,
617
+ input.locale,
618
+ input.revisionKind,
619
+ input.enHash,
620
+ input.bodyHash,
621
+ input.createdAt,
622
+ input.model ?? null,
623
+ input.bodyPreview ?? null
624
+ );
625
+ return Number(result.lastInsertRowid);
626
+ }
627
+
628
+ // src/loader/normalize-en.ts
629
+ function normalizeEnFrontmatter(data) {
630
+ const out = { ...data };
631
+ if (!out.publishedAt && typeof out.date === "string") {
632
+ out.publishedAt = out.date;
633
+ }
634
+ if (!out.heroImage && typeof out.image === "string" && out.image.startsWith("/")) {
635
+ out.heroImage = out.image;
636
+ }
637
+ if (!out.updatedAt && typeof out.publishedAt === "string") {
638
+ out.updatedAt = out.publishedAt;
639
+ }
640
+ return out;
641
+ }
642
+ function isPublishableContentFile(name) {
643
+ if (!name.endsWith(".md") && !name.endsWith(".mdx")) return false;
644
+ const first = name.charAt(0);
645
+ return first === first.toLowerCase() && first !== "_";
646
+ }
647
+
648
+ // src/loader/create-loader.ts
649
+ function isPostFile(name) {
650
+ return isPublishableContentFile(name);
651
+ }
652
+ function listEnFiles(contentDir) {
653
+ if (!fs2__default.default.existsSync(contentDir)) return [];
654
+ return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path3__default.default.join(contentDir, f));
655
+ }
656
+ function slugFromPath(filePath) {
657
+ return path3__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
658
+ }
659
+ function parseEnMdx(filePath, config, type) {
660
+ const raw = fs2__default.default.readFileSync(filePath, "utf8");
661
+ const parsed = matter__default.default(raw);
662
+ const slug = slugFromPath(filePath);
663
+ const normalized = normalizeEnFrontmatter(parsed.data);
664
+ const { builtin, rest, issues: builtinIssues } = extractBuiltinEnFields(
665
+ normalized,
666
+ type.path,
667
+ slug,
668
+ config.defaultLocale
669
+ );
670
+ const issues = builtinIssues.map((issue) => ({
671
+ field: issue.field,
672
+ message: issue.message,
673
+ level: issue.level
674
+ }));
675
+ const result = type.schema.safeParse(rest);
676
+ if (!result.success) {
677
+ return {
678
+ document: null,
679
+ issues: [
680
+ ...issues,
681
+ ...result.error.issues.map((issue) => ({
682
+ field: issue.path.join("."),
683
+ message: issue.message,
684
+ level: "error"
685
+ }))
686
+ ]
687
+ };
688
+ }
689
+ if (issues.some((i) => i.level === "error")) {
690
+ return { document: null, issues };
691
+ }
692
+ const frontmatter = mergeBuiltinsIntoFrontmatter(
693
+ result.data,
694
+ {
695
+ publishedAt: builtin.publishedAt,
696
+ updatedAt: builtin.updatedAt,
697
+ noindex: builtin.noindex,
698
+ canonicalPathOverride: builtin.canonicalPathOverride,
699
+ slug,
700
+ locale: config.defaultLocale
701
+ },
702
+ type,
703
+ config.defaultLocale
704
+ );
705
+ const crossIssues = type.crossValidate?.(result.data, {
706
+ locale: config.defaultLocale,
707
+ defaultLocale: config.defaultLocale,
708
+ slug,
709
+ enSlug: slug,
710
+ knownLocales: config.locales
711
+ }) ?? [];
712
+ issues.push(...crossIssues);
713
+ const document2 = {
714
+ slug,
715
+ enSlug: slug,
716
+ locale: config.defaultLocale,
717
+ aliases: builtin.aliases,
718
+ redirectTo: builtin.redirectTo,
719
+ publishedAt: builtin.publishedAt,
720
+ updatedAt: builtin.updatedAt,
721
+ noindex: builtin.noindex,
722
+ canonicalPathOverride: builtin.canonicalPathOverride,
723
+ frontmatter,
724
+ content: parsed.content,
725
+ filePath
726
+ };
727
+ return { document: document2, issues };
728
+ }
729
+ function buildDocumentFromTranslation(row, enDoc, type) {
730
+ const localeFm = JSON.parse(row.frontmatter_json);
731
+ const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
732
+ const seo = seoFieldsFromEn(enDoc);
733
+ const frontmatter = mergeBuiltinsIntoFrontmatter(
734
+ merged,
735
+ { ...seo, slug: row.slug, locale: row.locale },
736
+ type,
737
+ enDoc.locale
738
+ );
739
+ return {
740
+ slug: row.slug,
741
+ enSlug: row.en_slug,
742
+ locale: row.locale,
743
+ aliases: [],
744
+ redirectTo: seo.redirectTo,
745
+ publishedAt: seo.publishedAt,
746
+ updatedAt: seo.updatedAt,
747
+ noindex: seo.noindex,
748
+ canonicalPathOverride: seo.canonicalPathOverride,
749
+ frontmatter,
750
+ content: row.body
751
+ };
752
+ }
753
+ var DEV_REVALIDATE_MS = 1500;
754
+ function createContentLoader(config, type) {
755
+ let cached = null;
756
+ let signature = "";
757
+ let lastCheck = 0;
758
+ const contentDir = path3__default.default.join(
759
+ /* turbopackIgnore: true */
760
+ config.rootDir,
761
+ type.contentDir
762
+ );
763
+ const storePath = resolveStorePath(config);
764
+ const isProd = process.env.NODE_ENV === "production";
765
+ function computeSignature() {
766
+ const files = listEnFiles(contentDir);
767
+ let newest = 0;
768
+ for (const f of files) {
769
+ try {
770
+ newest = Math.max(newest, fs2__default.default.statSync(f).mtimeMs);
771
+ } catch {
772
+ }
773
+ }
774
+ let store = 0;
775
+ try {
776
+ store = fs2__default.default.statSync(storePath).mtimeMs;
777
+ } catch {
778
+ }
779
+ return `${files.length}:${newest}:${store}`;
780
+ }
781
+ function build() {
782
+ const out = /* @__PURE__ */ new Map();
783
+ const englishBySlug = /* @__PURE__ */ new Map();
784
+ for (const file of listEnFiles(contentDir)) {
785
+ const { document: document2, issues } = parseEnMdx(file, config, type);
786
+ if (document2) {
787
+ englishBySlug.set(document2.slug, document2);
788
+ if (issues.length > 0) {
789
+ for (const issue of issues) {
790
+ console.warn(
791
+ `[scribe:${type.id}] ${file} ${issue.level}: ${issue.field}: ${issue.message}`
792
+ );
793
+ }
794
+ }
795
+ } else {
796
+ console.warn(
797
+ `[scribe:${type.id}] Skipping ${file} \u2014 validation failed:
798
+ ` + issues.map((i) => ` - ${i.field}: ${i.message}`).join("\n")
799
+ );
800
+ }
801
+ }
802
+ out.set(config.defaultLocale, {
803
+ bySlug: englishBySlug,
804
+ byEnSlug: englishBySlug
805
+ });
806
+ const db = openStore(config, "readonly");
807
+ const rows = listTranslationsForType(db, type.id);
808
+ db.close();
809
+ const rowsByLocale = /* @__PURE__ */ new Map();
810
+ for (const row of rows) {
811
+ const list = rowsByLocale.get(row.locale) ?? [];
812
+ list.push(row);
813
+ rowsByLocale.set(row.locale, list);
814
+ }
815
+ for (const locale of config.locales) {
816
+ if (locale === config.defaultLocale) continue;
817
+ const bySlug = /* @__PURE__ */ new Map();
818
+ const byEnSlug = /* @__PURE__ */ new Map();
819
+ for (const row of rowsByLocale.get(locale) ?? []) {
820
+ const enDoc = englishBySlug.get(row.en_slug);
821
+ if (!enDoc) continue;
822
+ const doc = buildDocumentFromTranslation(row, enDoc, type);
823
+ bySlug.set(doc.slug, doc);
824
+ byEnSlug.set(row.en_slug, doc);
825
+ }
826
+ out.set(locale, { bySlug, byEnSlug });
827
+ }
828
+ return out;
829
+ }
830
+ return () => {
831
+ if (cached) {
832
+ if (isProd) return cached;
833
+ const now = Date.now();
834
+ if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
835
+ lastCheck = now;
836
+ const sig = computeSignature();
837
+ if (sig === signature) return cached;
838
+ cached = build();
839
+ signature = sig;
840
+ return cached;
841
+ }
842
+ lastCheck = Date.now();
843
+ cached = build();
844
+ signature = isProd ? "" : computeSignature();
845
+ return cached;
846
+ };
847
+ }
848
+ function readEnDocument(config, type, enSlug) {
849
+ const contentDir = path3__default.default.join(
850
+ /* turbopackIgnore: true */
851
+ config.rootDir,
852
+ type.contentDir
853
+ );
854
+ for (const ext of [".mdx", ".md"]) {
855
+ const filePath = path3__default.default.join(contentDir, `${enSlug}${ext}`);
856
+ if (!fs2__default.default.existsSync(filePath)) continue;
857
+ const { document: document2 } = parseEnMdx(filePath, config, type);
858
+ return document2;
859
+ }
860
+ return null;
861
+ }
862
+ function getTranslatablePayload(doc, type) {
863
+ return {
864
+ frontmatter: pickTranslatable(doc.frontmatter, type.schema),
865
+ body: doc.content
866
+ };
867
+ }
868
+
869
+ // src/i18n/resolve-document.ts
870
+ function findEnDocByAlias(slug, allDocs, defaultLocale) {
871
+ const enIdx = allDocs.get(defaultLocale);
872
+ if (!enIdx) return null;
873
+ for (const doc of enIdx.bySlug.values()) {
874
+ if (doc.aliases.includes(slug)) return doc;
875
+ }
876
+ return null;
877
+ }
878
+ function enCanonicalDoc(doc, allDocs, defaultLocale) {
879
+ if (doc.locale === defaultLocale) return doc;
880
+ return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
881
+ }
882
+ function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
883
+ if (!type.path) return void 0;
884
+ const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
885
+ if (!enDoc.redirectTo) return void 0;
886
+ const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
887
+ if (!targetEnSlug) return void 0;
888
+ const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
889
+ const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
890
+ return resolvePath(type.path, targetSlug, locale, defaultLocale);
891
+ }
892
+ function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
893
+ if (result.shouldRedirectTo || !result.document) return result;
894
+ const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
895
+ if (!redirect) return result;
896
+ return {
897
+ document: null,
898
+ actualLocale: result.actualLocale,
899
+ shouldRedirectTo: redirect
900
+ };
901
+ }
902
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
903
+ const idx = allDocs.get(locale);
904
+ const direct = idx?.bySlug.get(slug);
905
+ if (direct) {
906
+ return withRedirectTo({ document: direct, actualLocale: locale }, locale, defaultLocale, allDocs, type);
907
+ }
908
+ const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
909
+ if (aliasDoc && type.path) {
910
+ const canonicalSlug = aliasDoc.slug;
911
+ const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
912
+ const targetSlug = localizedSlug ?? canonicalSlug;
913
+ if (targetSlug !== slug) {
914
+ return {
915
+ document: null,
916
+ actualLocale: locale,
917
+ shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
918
+ };
919
+ }
920
+ }
921
+ for (const [docLocale, docIdx] of allDocs) {
922
+ const found = docIdx.bySlug.get(slug);
923
+ if (!found) continue;
924
+ const correctSlug = getSlugForLocale(found, docLocale, locale, allDocs, defaultLocale);
925
+ if (correctSlug && correctSlug !== slug) {
926
+ if (!type.path) {
927
+ return { document: null, actualLocale: locale };
928
+ }
929
+ return {
930
+ document: null,
931
+ actualLocale: locale,
932
+ shouldRedirectTo: resolvePath(type.path, correctSlug, locale, defaultLocale)
933
+ };
934
+ }
935
+ if (docLocale === defaultLocale) {
936
+ return withRedirectTo(
937
+ { document: found, actualLocale: defaultLocale },
938
+ locale,
939
+ defaultLocale,
940
+ allDocs,
941
+ type
942
+ );
943
+ }
944
+ if (!type.path) {
945
+ return { document: null, actualLocale: locale };
946
+ }
947
+ return {
948
+ document: null,
949
+ actualLocale: locale,
950
+ shouldRedirectTo: resolvePath(type.path, found.enSlug, locale, defaultLocale)
951
+ };
952
+ }
953
+ if (locale !== defaultLocale) {
954
+ const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
955
+ if (enDoc && type.indexFallback === "en") {
956
+ return withRedirectTo(
957
+ { document: enDoc, actualLocale: defaultLocale },
958
+ locale,
959
+ defaultLocale,
960
+ allDocs,
961
+ type
962
+ );
963
+ }
964
+ const byEn = idx?.byEnSlug.get(slug);
965
+ if (byEn) {
966
+ return withRedirectTo(
967
+ { document: byEn, actualLocale: locale },
968
+ locale,
969
+ defaultLocale,
970
+ allDocs,
971
+ type
972
+ );
973
+ }
974
+ const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
975
+ if (fallback && type.indexFallback === "en") {
976
+ return withRedirectTo(
977
+ { document: fallback, actualLocale: defaultLocale },
978
+ locale,
979
+ defaultLocale,
980
+ allDocs,
981
+ type
982
+ );
983
+ }
984
+ }
985
+ return { document: null, actualLocale: locale };
986
+ }
987
+ function getSlugForLocale(document2, sourceLocale, targetLocale, allDocs, defaultLocale) {
988
+ if (sourceLocale === targetLocale) return document2.slug;
989
+ const englishSlug = sourceLocale === defaultLocale ? document2.slug : document2.enSlug;
990
+ if (!englishSlug) return null;
991
+ if (targetLocale === defaultLocale) return englishSlug;
992
+ return allDocs.get(targetLocale)?.byEnSlug.get(englishSlug)?.slug ?? null;
993
+ }
994
+
995
+ // src/create-project.ts
996
+ function comparatorFor(orderBy) {
997
+ if (typeof orderBy === "function") return orderBy;
998
+ switch (orderBy) {
999
+ case "publishedAt":
1000
+ return (a, b) => (a.publishedAt ?? "").localeCompare(b.publishedAt ?? "");
1001
+ case "-publishedAt":
1002
+ return (a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? "");
1003
+ case "updatedAt":
1004
+ return (a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? "");
1005
+ case "-updatedAt":
1006
+ return (a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
1007
+ default:
1008
+ return (a, b) => a.slug.localeCompare(b.slug);
1009
+ }
1010
+ }
1011
+ function buildRuntime(config, type, getRuntime) {
1012
+ const load = createContentLoader(config, type);
1013
+ const relationFields = new Map(
1014
+ listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1015
+ );
1016
+ function assertRoutable(method) {
1017
+ if (!type.path) {
1018
+ throw new Error(
1019
+ `Content type "${type.id}" has no path template \u2014 ${method}() requires one`
1020
+ );
1021
+ }
1022
+ return type.path;
1023
+ }
1024
+ const runtime = {
1025
+ id: type.id,
1026
+ config: type,
1027
+ load,
1028
+ list(locale = config.defaultLocale, options = {}) {
1029
+ const idx = load().get(locale);
1030
+ if (!idx) return [];
1031
+ const docs = Array.from(idx.bySlug.values());
1032
+ docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1033
+ return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1034
+ },
1035
+ get(slug, locale = config.defaultLocale) {
1036
+ return load().get(locale)?.bySlug.get(slug) ?? null;
1037
+ },
1038
+ resolve(slug, locale) {
1039
+ const result = resolveLocalizedDocument(slug, locale, config.defaultLocale, load(), type);
1040
+ if (result.document && type.path) {
1041
+ return {
1042
+ ...result,
1043
+ canonicalPath: resolvePath(
1044
+ type.path,
1045
+ result.document.slug,
1046
+ result.actualLocale,
1047
+ config.defaultLocale
1048
+ )
1049
+ };
1050
+ }
1051
+ return result;
1052
+ },
1053
+ staticParams(options = {}) {
1054
+ assertRoutable("staticParams");
1055
+ const all = load();
1056
+ const enIdx = all.get(config.defaultLocale);
1057
+ if (!enIdx) return [];
1058
+ const params = [];
1059
+ for (const locale of options.locales ?? config.locales) {
1060
+ const localeIdx = all.get(locale);
1061
+ for (const doc of enIdx.bySlug.values()) {
1062
+ const slug = locale === config.defaultLocale ? doc.slug : localeIdx?.byEnSlug.get(doc.slug)?.slug ?? doc.slug;
1063
+ params.push({ locale, slug });
1064
+ }
1065
+ }
1066
+ return params;
1067
+ },
1068
+ alternates(doc) {
1069
+ const pathTemplate = assertRoutable("alternates");
1070
+ const out = {
1071
+ [config.defaultLocale]: resolvePath(
1072
+ pathTemplate,
1073
+ doc.enSlug,
1074
+ config.defaultLocale,
1075
+ config.defaultLocale
1076
+ )
1077
+ };
1078
+ const all = load();
1079
+ for (const locale of config.locales) {
1080
+ if (locale === config.defaultLocale) continue;
1081
+ const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
1082
+ if (translated) {
1083
+ out[locale] = resolvePath(pathTemplate, translated.slug, locale, config.defaultLocale);
1084
+ }
1085
+ }
1086
+ return out;
1087
+ },
1088
+ translation(doc, targetLocale) {
1089
+ if (targetLocale === doc.locale) return doc;
1090
+ const all = load();
1091
+ if (targetLocale === config.defaultLocale) {
1092
+ return all.get(config.defaultLocale)?.bySlug.get(doc.enSlug) ?? null;
1093
+ }
1094
+ return all.get(targetLocale)?.byEnSlug.get(doc.enSlug) ?? null;
1095
+ },
1096
+ url(slug, locale) {
1097
+ const pathTemplate = assertRoutable("url");
1098
+ return resolvePath(pathTemplate, slug, locale, config.defaultLocale);
1099
+ },
1100
+ related(doc, fieldName, locale) {
1101
+ const meta = relationFields.get(fieldName);
1102
+ if (!meta) {
1103
+ throw new Error(
1104
+ `Content type "${type.id}" has no top-level relation field "${fieldName}"`
1105
+ );
1106
+ }
1107
+ const target = getRuntime(meta.relationTarget);
1108
+ const lookupLocale = locale ?? doc.locale;
1109
+ const value = doc.frontmatter[fieldName];
1110
+ const deref = (enSlug) => {
1111
+ const all = target.load();
1112
+ return all.get(lookupLocale)?.byEnSlug.get(enSlug) ?? all.get(config.defaultLocale)?.bySlug.get(enSlug) ?? null;
1113
+ };
1114
+ if (meta.relationMultiple) {
1115
+ if (!Array.isArray(value)) return [];
1116
+ return value.filter((slug) => typeof slug === "string" && slug.length > 0).map(deref).filter((d) => d !== null);
1117
+ }
1118
+ if (typeof value !== "string" || value.length === 0) {
1119
+ if (meta.relationOptional) return null;
1120
+ throw new Error(
1121
+ `${type.id} "${doc.enSlug}": required relation "${fieldName}" is missing`
1122
+ );
1123
+ }
1124
+ const resolved = deref(value);
1125
+ if (!resolved && !meta.relationOptional) {
1126
+ throw new Error(
1127
+ `${type.id} "${doc.enSlug}": relation "${fieldName}" references missing ${meta.relationTarget} "${value}" \u2014 run \`scribe validate\``
1128
+ );
1129
+ }
1130
+ return resolved;
1131
+ }
1132
+ };
1133
+ return runtime;
1134
+ }
1135
+ function createProject(config) {
1136
+ const runtimes = /* @__PURE__ */ new Map();
1137
+ const getRuntime = (id) => {
1138
+ const runtime = runtimes.get(id);
1139
+ if (!runtime) {
1140
+ throw new Error(`Unknown content type "${id}"`);
1141
+ }
1142
+ return runtime;
1143
+ };
1144
+ for (const type of config.types) {
1145
+ runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1146
+ }
1147
+ return {
1148
+ config,
1149
+ rootDir: config.rootDir,
1150
+ storePath: config.storePath,
1151
+ getType: getRuntime,
1152
+ listTypes() {
1153
+ return Array.from(runtimes.values());
1154
+ }
1155
+ };
1156
+ }
1157
+ function enFileExists(config, type, enSlug) {
1158
+ const dir = path3__default.default.join(config.rootDir, type.contentDir);
1159
+ return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
1160
+ }
1161
+ function sqliteHasTranslations(db, contentTypeId, enSlug) {
1162
+ return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
1163
+ }
1164
+ function isAliasKnown(config, type, aliasEnSlug, db) {
1165
+ return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
1166
+ }
1167
+ function listEnSlugs(rootDir, contentDir) {
1168
+ const dir = path3__default.default.join(rootDir, contentDir);
1169
+ if (!fs2__default.default.existsSync(dir)) return [];
1170
+ return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1171
+ }
1172
+
1173
+ // src/core/slug-aliases.ts
1174
+ function buildGlobalAliasIndex(project) {
1175
+ const { config } = project;
1176
+ const aliasToTarget = /* @__PURE__ */ new Map();
1177
+ const allAliasSlugs = /* @__PURE__ */ new Set();
1178
+ const issues = [];
1179
+ const canonicalSlugsByType = /* @__PURE__ */ new Map();
1180
+ for (const type of config.types) {
1181
+ const slugs = listEnSlugs(config.rootDir, type.contentDir);
1182
+ canonicalSlugsByType.set(type.id, new Set(slugs));
1183
+ }
1184
+ for (const type of config.types) {
1185
+ if (!type.path) continue;
1186
+ const canonicalSlugs = canonicalSlugsByType.get(type.id) ?? /* @__PURE__ */ new Set();
1187
+ for (const enSlug of canonicalSlugs) {
1188
+ const doc = readEnDocument(config, type, enSlug);
1189
+ if (!doc) continue;
1190
+ for (const alias of doc.aliases) {
1191
+ if (alias === enSlug) {
1192
+ issues.push({
1193
+ contentTypeId: type.id,
1194
+ enSlug,
1195
+ field: "aliases",
1196
+ message: `Alias "${alias}" must not equal the canonical slug`,
1197
+ level: "error"
1198
+ });
1199
+ continue;
1200
+ }
1201
+ if (canonicalSlugs.has(alias)) {
1202
+ issues.push({
1203
+ contentTypeId: type.id,
1204
+ enSlug,
1205
+ field: "aliases",
1206
+ message: `Alias "${alias}" collides with another document's canonical slug`,
1207
+ level: "error"
1208
+ });
1209
+ continue;
1210
+ }
1211
+ const existing = aliasToTarget.get(alias);
1212
+ if (existing) {
1213
+ issues.push({
1214
+ contentTypeId: type.id,
1215
+ enSlug,
1216
+ field: "aliases",
1217
+ message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
1218
+ level: "error"
1219
+ });
1220
+ continue;
1221
+ }
1222
+ aliasToTarget.set(alias, {
1223
+ contentTypeId: type.id,
1224
+ canonicalSlug: enSlug,
1225
+ path: type.path
1226
+ });
1227
+ allAliasSlugs.add(alias);
1228
+ }
1229
+ }
1230
+ }
1231
+ return { aliasToTarget, allAliasSlugs, issues };
1232
+ }
1233
+ function validateAliasCoverage(project) {
1234
+ const { config } = project;
1235
+ const { aliasToTarget } = buildGlobalAliasIndex(project);
1236
+ const issues = [];
1237
+ const db = openStore(config, "readonly");
1238
+ try {
1239
+ for (const [alias, target] of aliasToTarget) {
1240
+ const type = config.types.find((t) => t.id === target.contentTypeId);
1241
+ if (!type) continue;
1242
+ const known = isAliasKnown(config, type, alias, db);
1243
+ if (!known) {
1244
+ issues.push({
1245
+ contentTypeId: type.id,
1246
+ enSlug: target.canonicalSlug,
1247
+ field: "aliases",
1248
+ message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
1249
+ level: "warning"
1250
+ });
1251
+ }
1252
+ if (enFileExists(config, type, alias)) {
1253
+ issues.push({
1254
+ contentTypeId: type.id,
1255
+ enSlug: target.canonicalSlug,
1256
+ field: "aliases",
1257
+ message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
1258
+ level: "warning"
1259
+ });
1260
+ }
1261
+ const sqliteRows = listTranslationsForEnSlug(db, type.id, alias);
1262
+ if (sqliteRows.length > 0) {
1263
+ issues.push({
1264
+ contentTypeId: type.id,
1265
+ enSlug: target.canonicalSlug,
1266
+ field: "aliases",
1267
+ message: `${sqliteRows.length} sqlite translation(s) for alias "${alias}" retained for locale redirects and history`,
1268
+ level: "info"
1269
+ });
1270
+ }
1271
+ for (const locale of config.locales) {
1272
+ if (locale === config.defaultLocale) continue;
1273
+ const canonicalRow = listTranslationsForEnSlug(db, type.id, target.canonicalSlug).find(
1274
+ (r) => r.locale === locale
1275
+ );
1276
+ if (!canonicalRow) continue;
1277
+ const aliasRow = sqliteRows.find((r) => r.locale === locale);
1278
+ if (!aliasRow && known) {
1279
+ issues.push({
1280
+ contentTypeId: type.id,
1281
+ enSlug: target.canonicalSlug,
1282
+ field: "aliases",
1283
+ message: `Alias "${alias}" has no locale slug mapping for ${locale} \u2014 old localized URL may miss redirect`,
1284
+ level: "warning"
1285
+ });
1286
+ }
1287
+ }
1288
+ }
1289
+ } finally {
1290
+ db.close();
1291
+ }
1292
+ return issues;
1293
+ }
1294
+ function validateAliasRedirectChains(project) {
1295
+ const { config } = project;
1296
+ const { aliasToTarget, allAliasSlugs } = buildGlobalAliasIndex(project);
1297
+ const issues = [];
1298
+ for (const [alias, target] of aliasToTarget) {
1299
+ const doc = readEnDocument(
1300
+ config,
1301
+ project.getType(target.contentTypeId).config,
1302
+ target.canonicalSlug
1303
+ );
1304
+ if (doc?.redirectTo) {
1305
+ issues.push({
1306
+ contentTypeId: target.contentTypeId,
1307
+ enSlug: target.canonicalSlug,
1308
+ field: "aliases",
1309
+ message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
1310
+ level: "error"
1311
+ });
1312
+ }
1313
+ }
1314
+ for (const type of config.types) {
1315
+ if (!type.path) continue;
1316
+ for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
1317
+ const doc = readEnDocument(config, type, enSlug);
1318
+ if (!doc?.redirectTo) continue;
1319
+ const targetSlug = extractSlugFromResolvedPath(type.path, doc.redirectTo);
1320
+ if (targetSlug && allAliasSlugs.has(targetSlug)) {
1321
+ issues.push({
1322
+ contentTypeId: type.id,
1323
+ enSlug,
1324
+ field: "redirect_to",
1325
+ message: `redirect_to target "${targetSlug}" is an alias slug \u2014 chain detected`,
1326
+ level: "error"
1327
+ });
1328
+ }
1329
+ const targetDoc = targetSlug ? readEnDocument(config, type, targetSlug) : null;
1330
+ if (targetDoc?.redirectTo) {
1331
+ issues.push({
1332
+ contentTypeId: type.id,
1333
+ enSlug,
1334
+ field: "redirect_to",
1335
+ message: `redirect_to chain detected: "${enSlug}" \u2192 "${targetSlug}" \u2192 "${targetDoc.redirectTo}"`,
1336
+ level: "error"
1337
+ });
1338
+ }
1339
+ }
1340
+ }
1341
+ return issues;
1342
+ }
1343
+
1344
+ // src/i18n/translation-index.ts
1345
+ function buildTranslationIndex(allDocs, locales, defaultLocale) {
1346
+ const index = /* @__PURE__ */ new Map();
1347
+ for (const locale of locales) {
1348
+ if (locale === defaultLocale) continue;
1349
+ const idx = allDocs.get(locale);
1350
+ if (!idx) continue;
1351
+ const localeMap = /* @__PURE__ */ new Map();
1352
+ for (const [enSlug, doc] of idx.byEnSlug) {
1353
+ localeMap.set(enSlug, doc.slug);
1354
+ }
1355
+ index.set(locale, localeMap);
1356
+ }
1357
+ return index;
1358
+ }
1359
+
1360
+ // src/redirects/translation-index.ts
1361
+ function buildRedirectTranslationIndex(project, typeId) {
1362
+ const runtime = project.getType(typeId);
1363
+ const { config } = project;
1364
+ const merged = buildTranslationIndex(
1365
+ runtime.load(),
1366
+ config.locales,
1367
+ config.defaultLocale
1368
+ );
1369
+ const { aliasToTarget } = buildGlobalAliasIndex(project);
1370
+ const db = openStore(project.config, "readonly");
1371
+ try {
1372
+ for (const [alias, target] of aliasToTarget) {
1373
+ if (target.contentTypeId !== typeId) continue;
1374
+ for (const row of listTranslationsForEnSlug(db, typeId, alias)) {
1375
+ const localeMap = merged.get(row.locale) ?? /* @__PURE__ */ new Map();
1376
+ if (!localeMap.has(alias)) {
1377
+ localeMap.set(alias, row.slug);
1378
+ }
1379
+ merged.set(row.locale, localeMap);
1380
+ }
1381
+ }
1382
+ } finally {
1383
+ db.close();
1384
+ }
1385
+ return merged;
1386
+ }
1387
+
1388
+ // src/redirects/build-redirects.ts
1389
+ function buildAliasRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
1390
+ const out = [];
1391
+ const { aliasToTarget } = aliasIndex;
1392
+ const pathTemplate = type.path;
1393
+ for (const [alias, target] of aliasToTarget) {
1394
+ if (target.contentTypeId !== type.id) continue;
1395
+ const canonical = target.canonicalSlug;
1396
+ out.push({
1397
+ source: resolvePath(pathTemplate, alias, defaultLocale, defaultLocale),
1398
+ destination: resolvePath(pathTemplate, canonical, defaultLocale, defaultLocale),
1399
+ permanent: true
1400
+ });
1401
+ for (const locale of prefixedLocales) {
1402
+ const localeMap = translationIndex.get(locale);
1403
+ const toLocale = localeMap?.get(canonical) ?? canonical;
1404
+ const fromLocale = localeMap?.get(alias);
1405
+ if (fromLocale) {
1406
+ out.push({
1407
+ source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
1408
+ destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1409
+ permanent: true
1410
+ });
1411
+ }
1412
+ out.push({
1413
+ source: resolvePath(pathTemplate, alias, locale, defaultLocale),
1414
+ destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1415
+ permanent: true
1416
+ });
1417
+ }
1418
+ }
1419
+ return out;
1420
+ }
1421
+ function buildAliasLegacyNoBlogPathRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
1422
+ if (type.id !== "blog") return [];
1423
+ const out = [];
1424
+ for (const [alias, target] of aliasIndex.aliasToTarget) {
1425
+ if (target.contentTypeId !== type.id) continue;
1426
+ for (const locale of prefixedLocales) {
1427
+ const localeMap = translationIndex.get(locale);
1428
+ const toLocale = localeMap?.get(target.canonicalSlug) ?? target.canonicalSlug;
1429
+ out.push({
1430
+ source: `/${locale}/${alias}`,
1431
+ destination: resolvePath(type.path, toLocale, locale, defaultLocale),
1432
+ permanent: true
1433
+ });
1434
+ }
1435
+ }
1436
+ return out;
1437
+ }
1438
+ function buildRedirectToRules(type, config, translationIndex, prefixedLocales, defaultLocale) {
1439
+ const out = [];
1440
+ const pathTemplate = type.path;
1441
+ for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
1442
+ const doc = readEnDocument(config, type, enSlug);
1443
+ if (!doc?.redirectTo) continue;
1444
+ out.push({
1445
+ source: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
1446
+ destination: doc.redirectTo,
1447
+ permanent: true
1448
+ });
1449
+ for (const locale of prefixedLocales) {
1450
+ const localeMap = translationIndex.get(locale);
1451
+ const fromLocale = localeMap?.get(enSlug);
1452
+ if (!fromLocale) continue;
1453
+ const targetEnSlug = extractSlugFromResolvedPath(pathTemplate, doc.redirectTo);
1454
+ if (!targetEnSlug) continue;
1455
+ const toLocale = localeMap?.get(targetEnSlug) ?? targetEnSlug;
1456
+ out.push({
1457
+ source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
1458
+ destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1459
+ permanent: true
1460
+ });
1461
+ }
1462
+ }
1463
+ return out;
1464
+ }
1465
+ function buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale) {
1466
+ const out = [];
1467
+ const pathTemplate = type.path;
1468
+ for (const [locale, localeMap] of translationIndex) {
1469
+ for (const [enSlug, translatedSlug] of localeMap) {
1470
+ if (enSlug === translatedSlug) continue;
1471
+ if (aliasSourceSlugs.has(enSlug)) continue;
1472
+ out.push({
1473
+ source: resolvePath(pathTemplate, enSlug, locale, defaultLocale),
1474
+ destination: resolvePath(pathTemplate, translatedSlug, locale, defaultLocale),
1475
+ permanent: true
1476
+ });
1477
+ }
1478
+ }
1479
+ return out;
1480
+ }
1481
+ function buildLegacyNoBlogPathRedirects(type, config, translationIndex, prefixedLocales, defaultLocale) {
1482
+ if (type.id !== "blog") return [];
1483
+ const out = [];
1484
+ const enSlugs = listEnSlugs(config.rootDir, type.contentDir);
1485
+ const pathTemplate = type.path;
1486
+ for (const enSlug of enSlugs) {
1487
+ out.push({
1488
+ source: `/${enSlug}`,
1489
+ destination: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
1490
+ permanent: true
1491
+ });
1492
+ for (const locale of prefixedLocales) {
1493
+ const translatedSlug = translationIndex.get(locale)?.get(enSlug);
1494
+ const canonicalSlug = translatedSlug ?? enSlug;
1495
+ out.push({
1496
+ source: `/${locale}/${enSlug}`,
1497
+ destination: resolvePath(pathTemplate, canonicalSlug, locale, defaultLocale),
1498
+ permanent: true
1499
+ });
1500
+ }
1501
+ }
1502
+ return out;
1503
+ }
1504
+ function buildTypeRedirects(project, typeId, aliasIndex, options) {
1505
+ const type = project.getType(typeId).config;
1506
+ if (!isRoutableType(type)) return [];
1507
+ const { defaultLocale } = project.config;
1508
+ const translationIndex = buildRedirectTranslationIndex(project, typeId);
1509
+ const aliasSourceSlugs = /* @__PURE__ */ new Set();
1510
+ for (const [alias, target] of aliasIndex.aliasToTarget) {
1511
+ if (target.contentTypeId === typeId) aliasSourceSlugs.add(alias);
1512
+ }
1513
+ return [
1514
+ ...buildAliasRedirects(type, aliasIndex, translationIndex, options.prefixedLocales, defaultLocale),
1515
+ ...buildAliasLegacyNoBlogPathRedirects(
1516
+ type,
1517
+ aliasIndex,
1518
+ translationIndex,
1519
+ options.prefixedLocales,
1520
+ defaultLocale
1521
+ ),
1522
+ ...buildRedirectToRules(type, project.config, translationIndex, options.prefixedLocales, defaultLocale),
1523
+ ...buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale),
1524
+ ...buildLegacyNoBlogPathRedirects(type, project.config, translationIndex, options.prefixedLocales, defaultLocale)
1525
+ ];
1526
+ }
1527
+ function buildAllContentRedirects(project, options) {
1528
+ const aliasIndex = buildGlobalAliasIndex(project);
1529
+ const out = [];
1530
+ for (const type of project.config.types) {
1531
+ if (!isRoutableType(type)) continue;
1532
+ out.push(...buildTypeRedirects(project, type.id, aliasIndex, options));
1533
+ }
1534
+ return out;
1535
+ }
1536
+ function getRedirectSourceSlugs(project) {
1537
+ const aliasIndex = buildGlobalAliasIndex(project);
1538
+ const outboundByType = /* @__PURE__ */ new Map();
1539
+ for (const type of project.config.types) {
1540
+ const outbound = /* @__PURE__ */ new Set();
1541
+ for (const enSlug of listEnSlugs(project.config.rootDir, type.contentDir)) {
1542
+ const doc = readEnDocument(project.config, type, enSlug);
1543
+ if (doc?.redirectTo) outbound.add(enSlug);
1544
+ }
1545
+ outboundByType.set(type.id, outbound);
1546
+ }
1547
+ return {
1548
+ aliasSlugs: aliasIndex.allAliasSlugs,
1549
+ outboundByType
1550
+ };
1551
+ }
1552
+
1553
+ // src/sitemap/join-base-url.ts
1554
+ function joinBaseUrl(baseUrl, pathname) {
1555
+ const origin = baseUrl.replace(/\/$/, "");
1556
+ const path9 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1557
+ return `${origin}${path9}`;
1558
+ }
1559
+
1560
+ // src/sitemap/generate-sitemap.ts
1561
+ function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
1562
+ if (enDoc.redirectTo) return false;
1563
+ if (redirectSources.aliasSlugs.has(enSlug)) return false;
1564
+ const outbound = redirectSources.outboundByType.get(contentTypeId);
1565
+ if (outbound?.has(enSlug)) return false;
1566
+ if (excludeNoindex && enDoc.noindex) return false;
1567
+ return true;
1568
+ }
1569
+ function resolveSitemapOptions(options) {
1570
+ return {
1571
+ ...options,
1572
+ resolveUrl: options.resolveUrl ?? ((_locale, pathname) => joinBaseUrl(options.baseUrl, pathname))
1573
+ };
1574
+ }
1575
+ async function buildClusterEntry(type, enSlug, enDoc, allDocs, config, options) {
1576
+ const { defaultLocale, locales } = config;
1577
+ const excludeNoindex = options.excludeNoindex !== false;
1578
+ const includeXDefault = options.includeXDefault !== false;
1579
+ const typeDefaults = options.typeDefaults?.[type.id];
1580
+ const pathnameFor = (doc) => options.resolvePathname?.(type, doc, defaultLocale) ?? resolveCanonicalPathname(type, doc, defaultLocale);
1581
+ const alternates = {};
1582
+ for (const locale of locales) {
1583
+ const idx = allDocs.get(locale);
1584
+ if (!idx) continue;
1585
+ const doc = locale === defaultLocale ? idx.bySlug.get(enSlug) : idx.byEnSlug.get(enSlug);
1586
+ if (!doc) continue;
1587
+ if (excludeNoindex && doc.noindex) continue;
1588
+ const pathname = pathnameFor(doc);
1589
+ alternates[locale] = await options.resolveUrl(locale, pathname);
1590
+ }
1591
+ if (Object.keys(alternates).length === 0) return null;
1592
+ const mainUrl = alternates[defaultLocale] ?? alternates[Object.keys(alternates)[0]];
1593
+ if (includeXDefault && alternates[defaultLocale]) {
1594
+ alternates["x-default"] = alternates[defaultLocale];
1595
+ }
1596
+ const lastModified = documentLastModified(enDoc);
1597
+ return {
1598
+ url: mainUrl,
1599
+ ...lastModified ? { lastModified } : {},
1600
+ ...typeDefaults?.changeFrequency ? { changeFrequency: typeDefaults.changeFrequency } : {},
1601
+ ...typeDefaults?.priority !== void 0 ? { priority: typeDefaults.priority } : {},
1602
+ alternates: { languages: alternates }
1603
+ };
1604
+ }
1605
+ async function generateSitemap(project, options) {
1606
+ const resolvedOptions = resolveSitemapOptions(options);
1607
+ const { config } = project;
1608
+ const redirectSources = getRedirectSourceSlugs(project);
1609
+ const excludeNoindex = options.excludeNoindex !== false;
1610
+ const typeIds = options.contentTypes ?? config.types.filter(isRoutableType).map((t) => t.id);
1611
+ const entries = [];
1612
+ for (const typeId of typeIds) {
1613
+ const runtime = project.getType(typeId);
1614
+ const type = runtime.config;
1615
+ if (!isRoutableType(type)) continue;
1616
+ const allDocs = runtime.load();
1617
+ const enIndex = allDocs.get(config.defaultLocale);
1618
+ if (!enIndex) continue;
1619
+ const processed = /* @__PURE__ */ new Set();
1620
+ for (const [enSlug, enDoc] of enIndex.bySlug) {
1621
+ if (processed.has(enSlug)) continue;
1622
+ processed.add(enSlug);
1623
+ if (!shouldIncludeEnDoc(enDoc, enSlug, redirectSources, typeId, excludeNoindex)) {
1624
+ continue;
1625
+ }
1626
+ const entry = await buildClusterEntry(
1627
+ type,
1628
+ enSlug,
1629
+ enDoc,
1630
+ allDocs,
1631
+ config,
1632
+ resolvedOptions
1633
+ );
1634
+ if (entry) entries.push(entry);
1635
+ }
1636
+ }
1637
+ return entries;
1638
+ }
1639
+
1640
+ // src/create-scribe.ts
1641
+ function createScribe(input) {
1642
+ const config = resolveConfig(input);
1643
+ const project = createProject(config);
1644
+ const scribe = {
1645
+ config,
1646
+ project,
1647
+ getType: project.getType,
1648
+ listTypes: project.listTypes,
1649
+ sitemap(options) {
1650
+ return generateSitemap(project, options);
1651
+ }
1652
+ };
1653
+ for (const type of config.types) {
1654
+ scribe[type.id] = project.getType(type.id);
1655
+ }
1656
+ return scribe;
1657
+ }
1658
+ function findConfigPath(cwd) {
1659
+ const candidates = [
1660
+ path3__default.default.join(
1661
+ /* turbopackIgnore: true */
1662
+ cwd,
1663
+ "scribe.config.ts"
1664
+ ),
1665
+ path3__default.default.join(
1666
+ /* turbopackIgnore: true */
1667
+ cwd,
1668
+ "scribe.config.mts"
1669
+ ),
1670
+ path3__default.default.join(
1671
+ /* turbopackIgnore: true */
1672
+ cwd,
1673
+ "scribe.config.js"
1674
+ )
1675
+ ];
1676
+ for (const candidate of candidates) {
1677
+ if (fs2__default.default.existsSync(candidate) && fs2__default.default.statSync(candidate).isFile()) {
1678
+ return candidate;
1679
+ }
1680
+ }
1681
+ return null;
1682
+ }
1683
+ function loadConfigSync(options = {}) {
1684
+ const cwd = options.cwd ?? /* turbopackIgnore: true */
1685
+ process.cwd();
1686
+ const configPath = options.configPath ?? findConfigPath(cwd);
1687
+ if (!configPath) {
1688
+ throw new Error(`No scribe.config.ts found in ${cwd} (pass --config)`);
1689
+ }
1690
+ const jiti$1 = jiti.createJiti(importMetaUrl, {
1691
+ interopDefault: true,
1692
+ moduleCache: false
1693
+ });
1694
+ const loaded = jiti$1(configPath);
1695
+ const config = "default" in loaded ? loaded.default : loaded;
1696
+ if (!config?.rootDir) {
1697
+ throw new Error(`Invalid scribe config at ${configPath}: missing rootDir`);
1698
+ }
1699
+ return resolveConfig(config, path3__default.default.dirname(configPath));
1700
+ }
1701
+ function sha256(input) {
1702
+ return crypto.createHash("sha256").update(input, "utf8").digest("hex");
1703
+ }
1704
+ function computePageEnHash(translatableFrontmatter, body) {
1705
+ const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1706
+ return sha256(payload);
1707
+ }
1708
+ function computeBodyHash(body) {
1709
+ return sha256(body);
1710
+ }
1711
+
1712
+ // src/history/record-revision.ts
1713
+ function recordRevision(config, input) {
1714
+ const db = openStore(config, "readwrite");
1715
+ const id = appendRevision(db, {
1716
+ contentType: input.contentType,
1717
+ enSlug: input.enSlug,
1718
+ locale: input.locale,
1719
+ revisionKind: input.revisionKind,
1720
+ enHash: input.enHash,
1721
+ bodyHash: computeBodyHash(input.body),
1722
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1723
+ model: input.model,
1724
+ bodyPreview: input.body.slice(0, 200)
1725
+ });
1726
+ db.close();
1727
+ return id;
1728
+ }
1729
+ function getAtPath2(obj, fieldPath) {
1730
+ let current = obj;
1731
+ for (const segment of fieldPath) {
1732
+ if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
1733
+ current = current[segment];
1734
+ }
1735
+ return current;
1736
+ }
1737
+ function buildEnSlugIndex(config) {
1738
+ const index = /* @__PURE__ */ new Map();
1739
+ for (const type of config.types) {
1740
+ const slugs = new Set(listEnSlugs(config.rootDir, type.contentDir));
1741
+ index.set(type.id, slugs);
1742
+ }
1743
+ return index;
1744
+ }
1745
+ function listEnSlugsForType(rootDir, contentDir) {
1746
+ const dir = path3__default.default.join(rootDir, contentDir);
1747
+ if (!fs2__default.default.existsSync(dir)) return [];
1748
+ return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1749
+ }
1750
+ function validateRelations(project) {
1751
+ const { config } = project;
1752
+ const slugIndex = buildEnSlugIndex(config);
1753
+ const issues = [];
1754
+ const typeIds = new Set(config.types.map((t) => t.id));
1755
+ for (const type of config.types) {
1756
+ const relationFields = listRelationFields(type.schema);
1757
+ if (relationFields.length === 0) continue;
1758
+ for (const enSlug of listEnSlugsForType(config.rootDir, type.contentDir)) {
1759
+ const doc = readEnDocument(config, type, enSlug);
1760
+ if (!doc) continue;
1761
+ for (const fieldMeta of relationFields) {
1762
+ const targetTypeId = fieldMeta.relationTarget;
1763
+ const danglingLevel = fieldMeta.relationOptional || fieldMeta.relationMultiple ? "warning" : "error";
1764
+ if (!typeIds.has(targetTypeId)) {
1765
+ issues.push({
1766
+ contentTypeId: type.id,
1767
+ enSlug,
1768
+ field: fieldMeta.path.join("."),
1769
+ message: `Relation target type "${targetTypeId}" is not registered`,
1770
+ level: "error"
1771
+ });
1772
+ continue;
1773
+ }
1774
+ const value = getAtPath2(doc.frontmatter, fieldMeta.path);
1775
+ if (value === void 0 || value === null || value === "") continue;
1776
+ const targetSlugs = slugIndex.get(targetTypeId) ?? /* @__PURE__ */ new Set();
1777
+ const fieldLabel = fieldMeta.path.join(".");
1778
+ if (fieldMeta.relationMultiple) {
1779
+ if (!Array.isArray(value)) continue;
1780
+ for (const slug of value) {
1781
+ if (typeof slug !== "string" || !slug) continue;
1782
+ if (!targetSlugs.has(slug)) {
1783
+ issues.push({
1784
+ contentTypeId: type.id,
1785
+ enSlug,
1786
+ field: fieldLabel,
1787
+ message: `${fieldLabel} references "${slug}" \u2014 no ${targetTypeId} doc with that slug`,
1788
+ level: danglingLevel
1789
+ });
1790
+ }
1791
+ }
1792
+ } else if (typeof value === "string") {
1793
+ if (!targetSlugs.has(value)) {
1794
+ issues.push({
1795
+ contentTypeId: type.id,
1796
+ enSlug,
1797
+ field: fieldLabel,
1798
+ message: `${fieldLabel} references "${value}" \u2014 no ${targetTypeId} doc with slug "${value}"`,
1799
+ level: danglingLevel
1800
+ });
1801
+ }
1802
+ }
1803
+ }
1804
+ }
1805
+ }
1806
+ return issues;
1807
+ }
1808
+
1809
+ // src/validate/validate-slug-suffix.ts
1810
+ function validateTranslationSlugSuffixes(config, db) {
1811
+ const issues = [];
1812
+ const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
1813
+ for (const row of bulkLoadTranslations(db)) {
1814
+ if (row.locale === config.defaultLocale) continue;
1815
+ for (const code of localeCodes) {
1816
+ if (row.slug.endsWith(`-${code}`)) {
1817
+ issues.push({
1818
+ contentTypeId: row.content_type,
1819
+ enSlug: row.en_slug,
1820
+ locale: row.locale,
1821
+ slug: row.slug,
1822
+ message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
1823
+ });
1824
+ break;
1825
+ }
1826
+ }
1827
+ }
1828
+ return issues;
1829
+ }
1830
+
1831
+ // src/validate/validate-project.ts
1832
+ function listEnSlugs2(rootDir, contentDir) {
1833
+ const dir = path3__default.default.join(rootDir, contentDir);
1834
+ if (!fs2__default.default.existsSync(dir)) return [];
1835
+ return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1836
+ }
1837
+ function validateProject(config) {
1838
+ const issues = [];
1839
+ const project = createProject(config);
1840
+ const storePath = project.storePath;
1841
+ if (!fs2__default.default.existsSync(storePath)) {
1842
+ issues.push({
1843
+ level: "error",
1844
+ message: `Missing store.sqlite at ${storePath}`
1845
+ });
1846
+ return { ok: false, issues };
1847
+ }
1848
+ const db = openStore(config, "readonly");
1849
+ for (const type of config.types) {
1850
+ const enSlugs = listEnSlugs2(config.rootDir, type.contentDir);
1851
+ const englishSlugs = new Set(enSlugs);
1852
+ if (!type.translate && isRoutableType(type)) {
1853
+ issues.push({
1854
+ level: "warning",
1855
+ contentType: type.id,
1856
+ message: `No translate config on content type "${type.id}" \u2014 using Scribe defaults`
1857
+ });
1858
+ }
1859
+ for (const enSlug of enSlugs) {
1860
+ const enDoc = readEnDocument(config, type, enSlug);
1861
+ if (!enDoc) {
1862
+ issues.push({
1863
+ level: "error",
1864
+ contentType: type.id,
1865
+ enSlug,
1866
+ message: "Failed to parse EN document"
1867
+ });
1868
+ continue;
1869
+ }
1870
+ const payload = getTranslatablePayload(enDoc, type);
1871
+ const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
1872
+ const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
1873
+ locale: config.defaultLocale,
1874
+ defaultLocale: config.defaultLocale,
1875
+ slug: enSlug,
1876
+ enSlug,
1877
+ knownLocales: config.locales,
1878
+ englishSlugs
1879
+ }) ?? [];
1880
+ for (const issue of crossIssues) {
1881
+ issues.push({
1882
+ level: issue.level,
1883
+ contentType: type.id,
1884
+ enSlug,
1885
+ field: issue.field,
1886
+ message: issue.message
1887
+ });
1888
+ }
1889
+ for (const locale of config.locales) {
1890
+ if (locale === config.defaultLocale) continue;
1891
+ const row = getTranslation(db, type.id, enSlug, locale);
1892
+ if (!row) continue;
1893
+ if (row.en_hash !== currentEnHash) {
1894
+ issues.push({
1895
+ level: "warning",
1896
+ contentType: type.id,
1897
+ enSlug,
1898
+ locale,
1899
+ field: "en_hash",
1900
+ message: "Translation is stale (en_hash mismatch)"
1901
+ });
1902
+ recordRevision(config, {
1903
+ contentType: type.id,
1904
+ enSlug,
1905
+ locale,
1906
+ revisionKind: "en_edit_detected",
1907
+ enHash: currentEnHash,
1908
+ body: row.body
1909
+ });
1910
+ }
1911
+ const localeFm = JSON.parse(row.frontmatter_json);
1912
+ for (const issue of validateLocaleBuiltinFields(localeFm)) {
1913
+ issues.push({
1914
+ level: issue.level,
1915
+ contentType: type.id,
1916
+ enSlug,
1917
+ locale,
1918
+ field: issue.field,
1919
+ message: issue.message
1920
+ });
1921
+ }
1922
+ }
1923
+ }
1924
+ try {
1925
+ project.getType(type.id).load();
1926
+ } catch (error) {
1927
+ issues.push({
1928
+ level: "error",
1929
+ contentType: type.id,
1930
+ message: error instanceof Error ? error.message : String(error)
1931
+ });
1932
+ }
1933
+ }
1934
+ db.close();
1935
+ const aliasIndex = buildGlobalAliasIndex(project);
1936
+ for (const issue of aliasIndex.issues) {
1937
+ issues.push({
1938
+ level: issue.level,
1939
+ contentType: issue.contentTypeId,
1940
+ enSlug: issue.enSlug,
1941
+ field: issue.field,
1942
+ message: issue.message
1943
+ });
1944
+ }
1945
+ for (const issue of validateAliasRedirectChains(project)) {
1946
+ issues.push({
1947
+ level: issue.level,
1948
+ contentType: issue.contentTypeId,
1949
+ enSlug: issue.enSlug,
1950
+ field: issue.field,
1951
+ message: issue.message
1952
+ });
1953
+ }
1954
+ for (const issue of validateAliasCoverage(project)) {
1955
+ issues.push({
1956
+ level: issue.level,
1957
+ contentType: issue.contentTypeId,
1958
+ enSlug: issue.enSlug,
1959
+ field: issue.field,
1960
+ message: issue.message
1961
+ });
1962
+ }
1963
+ for (const issue of validateRelations(project)) {
1964
+ issues.push({
1965
+ level: issue.level,
1966
+ contentType: issue.contentTypeId,
1967
+ enSlug: issue.enSlug,
1968
+ field: issue.field,
1969
+ message: issue.message
1970
+ });
1971
+ }
1972
+ const dbForSuffix = openStore(config, "readonly");
1973
+ try {
1974
+ for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
1975
+ issues.push({
1976
+ level: "error",
1977
+ contentType: issue.contentTypeId,
1978
+ enSlug: issue.enSlug,
1979
+ locale: issue.locale,
1980
+ field: "slug",
1981
+ message: issue.message
1982
+ });
1983
+ }
1984
+ } finally {
1985
+ dbForSuffix.close();
1986
+ }
1987
+ return { ok: issues.every((i) => i.level !== "error"), issues };
1988
+ }
1989
+ function listEnSlugs3(rootDir, contentDir) {
1990
+ const dir = path3__default.default.join(rootDir, contentDir);
1991
+ if (!fs2__default.default.existsSync(dir)) return [];
1992
+ return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1993
+ }
1994
+ function buildWorklist(config, options = {}) {
1995
+ const db = openStore(config, "readonly");
1996
+ const items = [];
1997
+ const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
1998
+ for (const type of config.types) {
1999
+ if (options.contentType && type.id !== options.contentType) continue;
2000
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs3(config.rootDir, type.contentDir);
2001
+ for (const enSlug of enSlugs) {
2002
+ const enDoc = readEnDocument(config, type, enSlug);
2003
+ if (!enDoc) continue;
2004
+ const payload = getTranslatablePayload(enDoc, type);
2005
+ const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
2006
+ for (const locale of locales) {
2007
+ if (locale === config.defaultLocale) continue;
2008
+ const existing = getTranslation(db, type.id, enSlug, locale);
2009
+ if (!existing) {
2010
+ items.push({
2011
+ contentType: type.id,
2012
+ enSlug,
2013
+ locale,
2014
+ reason: "missing",
2015
+ currentEnHash
2016
+ });
2017
+ continue;
2018
+ }
2019
+ if (existing.en_hash !== currentEnHash) {
2020
+ items.push({
2021
+ contentType: type.id,
2022
+ enSlug,
2023
+ locale,
2024
+ reason: "stale",
2025
+ currentEnHash,
2026
+ storedEnHash: existing.en_hash
2027
+ });
2028
+ }
2029
+ }
2030
+ }
2031
+ }
2032
+ db.close();
2033
+ return items;
2034
+ }
2035
+ function resolveLocalesFromPreset(config, preset, explicitLocales) {
2036
+ if (explicitLocales?.length) return explicitLocales;
2037
+ if (preset && config.localePresets?.[preset]) {
2038
+ return config.localePresets[preset].filter((l) => l !== config.defaultLocale);
2039
+ }
2040
+ return config.locales.filter((l) => l !== config.defaultLocale);
2041
+ }
2042
+ var DEFAULT_MODEL = "gemini-2.5-pro";
2043
+ function extractJson(text) {
2044
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
2045
+ if (fenced?.[1]) return fenced[1].trim();
2046
+ const start = text.indexOf("{");
2047
+ const end = text.lastIndexOf("}");
2048
+ if (start >= 0 && end > start) return text.slice(start, end + 1);
2049
+ return text.trim();
2050
+ }
2051
+ async function translatePageWithGemini(input) {
2052
+ const apiKey = input.apiKey ?? process.env.GEMINI_API_KEY;
2053
+ if (!apiKey) {
2054
+ throw new Error("GEMINI_API_KEY is required for scribe translate");
2055
+ }
2056
+ const model = input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL;
2057
+ const ai = new genai.GoogleGenAI({ apiKey });
2058
+ const response = await ai.models.generateContent({
2059
+ model,
2060
+ contents: input.prompt,
2061
+ config: {
2062
+ responseMimeType: "application/json"
2063
+ }
2064
+ });
2065
+ const raw = response.text ?? "";
2066
+ const parsed = JSON.parse(extractJson(raw));
2067
+ if (!parsed.frontmatter || typeof parsed.body !== "string") {
2068
+ throw new Error("Gemini response missing frontmatter/body");
2069
+ }
2070
+ return { model, raw, parsed };
2071
+ }
2072
+
2073
+ // src/translate/prompts/translation-prompt.ts
2074
+ var LOCALE_NAMES = {
2075
+ fr: "French",
2076
+ es: "Spanish",
2077
+ de: "German",
2078
+ nl: "Dutch",
2079
+ uk: "Ukrainian",
2080
+ tr: "Turkish",
2081
+ da: "Danish",
2082
+ he: "Hebrew",
2083
+ "zh-CN": "Simplified Chinese",
2084
+ "zh-TW": "Traditional Chinese",
2085
+ pt: "Portuguese",
2086
+ ja: "Japanese",
2087
+ ru: "Russian",
2088
+ it: "Italian",
2089
+ ar: "Arabic"
2090
+ };
2091
+ function defaultLocalizationPrompt(localeName, locale) {
2092
+ return [
2093
+ `Localize the content below into natural ${localeName} (${locale}).`,
2094
+ "Do not translate word-for-word.",
2095
+ "Preserve the source tone and brand voice.",
2096
+ "Write as if a native speaker authored it for the target market."
2097
+ ].join(" ");
2098
+ }
2099
+ function buildPageTranslationPrompt(input) {
2100
+ const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2101
+ const prompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
2102
+ const lines = [
2103
+ prompt,
2104
+ "",
2105
+ ...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
2106
+ "## Rules",
2107
+ ...input.resolved.rules.map((rule) => `- ${rule}`),
2108
+ "",
2109
+ "## Output format",
2110
+ "Return ONLY valid JSON with keys:",
2111
+ input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
2112
+ "",
2113
+ "## EN metadata",
2114
+ `title: ${input.enTitle}`,
2115
+ `description: ${input.enDescription}`,
2116
+ "",
2117
+ "## EN translatable frontmatter (JSON)",
2118
+ JSON.stringify(input.translatableFrontmatter, null, 2),
2119
+ "",
2120
+ "## EN body (MDX)",
2121
+ input.enBody
2122
+ ];
2123
+ return lines.join("\n");
2124
+ }
2125
+
2126
+ // src/translate/resolve-translate-config.ts
2127
+ function slugStrategyRules(slugStrategy, preserveTerms) {
2128
+ if (slugStrategy === "localized") {
2129
+ const rules = [
2130
+ "Provide a localized URL slug in JSON field `slug`.",
2131
+ "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2132
+ "For non-Latin locales, transliterate into Latin script."
2133
+ ];
2134
+ if (preserveTerms?.length) {
2135
+ rules.push(
2136
+ `Preserve these terms verbatim in slugs (lowercase): ${preserveTerms.join(", ")}.`
2137
+ );
2138
+ } else {
2139
+ rules.push("Preserve 4-digit years and proper nouns in slugs.");
2140
+ }
2141
+ return rules;
2142
+ }
2143
+ return ["Do NOT output a slug \u2014 slugStrategy is fixed."];
2144
+ }
2145
+ function mergeContext(project, type) {
2146
+ const parts = [project?.context, type?.context].filter(Boolean);
2147
+ return parts.length > 0 ? parts.join("\n\n") : void 0;
2148
+ }
2149
+ function mergeTranslateConfig(project, type, slugStrategy) {
2150
+ return {
2151
+ promptOverride: type?.prompt ?? project?.prompt,
2152
+ context: mergeContext(project, type),
2153
+ rules: [
2154
+ ...project?.rules ?? [],
2155
+ ...type?.rules ?? [],
2156
+ ...slugStrategyRules(slugStrategy, project?.slugPreserveTerms)
2157
+ ],
2158
+ model: type?.model ?? project?.defaultModel
2159
+ };
2160
+ }
2161
+ function resolveTranslateConfig(config, type) {
2162
+ return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
2163
+ }
2164
+
2165
+ // src/translate/page-translator.ts
2166
+ async function translatePage(config, item, options = {}) {
2167
+ const type = config.types.find((t) => t.id === item.contentType);
2168
+ if (!type) throw new Error(`Unknown content type ${item.contentType}`);
2169
+ const enDoc = readEnDocument(config, type, item.enSlug);
2170
+ if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
2171
+ const payload = getTranslatablePayload(enDoc, type);
2172
+ const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
2173
+ const db = openStore(config, "readonly");
2174
+ const existing = getTranslation(db, type.id, item.enSlug, item.locale);
2175
+ db.close();
2176
+ if (!options.force && existing && existing.en_hash === currentEnHash) {
2177
+ return {
2178
+ contentType: item.contentType,
2179
+ enSlug: item.enSlug,
2180
+ locale: item.locale,
2181
+ skipped: true,
2182
+ reason: "fresh"
2183
+ };
2184
+ }
2185
+ const resolvedTranslate = resolveTranslateConfig(config, type);
2186
+ const prompt = buildPageTranslationPrompt({
2187
+ resolved: resolvedTranslate,
2188
+ targetLocale: item.locale,
2189
+ enTitle: String(enDoc.frontmatter.title ?? item.enSlug),
2190
+ enDescription: String(enDoc.frontmatter.description ?? ""),
2191
+ translatableFrontmatter: payload.frontmatter,
2192
+ enBody: payload.body,
2193
+ slugStrategy: type.slugStrategy
2194
+ });
2195
+ if (options.dryRun) {
2196
+ return {
2197
+ contentType: item.contentType,
2198
+ enSlug: item.enSlug,
2199
+ locale: item.locale,
2200
+ skipped: false,
2201
+ model: options.model
2202
+ };
2203
+ }
2204
+ const result = await translatePageWithGemini({
2205
+ prompt,
2206
+ model: options.model ?? resolvedTranslate.model
2207
+ });
2208
+ const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2209
+ const writeDb = openStore(config, "readwrite");
2210
+ upsertTranslation(writeDb, {
2211
+ contentType: type.id,
2212
+ enSlug: item.enSlug,
2213
+ locale: item.locale,
2214
+ slug,
2215
+ frontmatter: result.parsed.frontmatter,
2216
+ body: result.parsed.body,
2217
+ enHash: currentEnHash,
2218
+ translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2219
+ model: result.model
2220
+ });
2221
+ writeDb.close();
2222
+ recordRevision(config, {
2223
+ contentType: type.id,
2224
+ enSlug: item.enSlug,
2225
+ locale: item.locale,
2226
+ revisionKind: "translation",
2227
+ enHash: currentEnHash,
2228
+ body: result.parsed.body,
2229
+ model: result.model
2230
+ });
2231
+ return {
2232
+ contentType: item.contentType,
2233
+ enSlug: item.enSlug,
2234
+ locale: item.locale,
2235
+ skipped: false,
2236
+ model: result.model
2237
+ };
2238
+ }
2239
+ async function translateWorklist(config, items, options = {}) {
2240
+ const results = [];
2241
+ for (const item of items) {
2242
+ results.push(await translatePage(config, item, options));
2243
+ }
2244
+ return results;
2245
+ }
2246
+
2247
+ exports.buildAllContentRedirects = buildAllContentRedirects;
2248
+ exports.buildWorklist = buildWorklist;
2249
+ exports.createProject = createProject;
2250
+ exports.createScribe = createScribe;
2251
+ exports.defineConfig = defineConfig;
2252
+ exports.defineContentType = defineContentType;
2253
+ exports.field = field;
2254
+ exports.findConfigPath = findConfigPath;
2255
+ exports.generateSitemap = generateSitemap;
2256
+ exports.getFieldKind = getFieldKind;
2257
+ exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
2258
+ exports.getRelationTarget = getRelationTarget;
2259
+ exports.isResolvedConfig = isResolvedConfig;
2260
+ exports.loadConfigSync = loadConfigSync;
2261
+ exports.resolveConfig = resolveConfig;
2262
+ exports.resolveLocalesFromPreset = resolveLocalesFromPreset;
2263
+ exports.translatePage = translatePage;
2264
+ exports.translateWorklist = translateWorklist;
2265
+ exports.unwrapSchema = unwrapSchema;
2266
+ exports.validateProject = validateProject;
2267
+ //# sourceMappingURL=index.cjs.map
2268
+ //# sourceMappingURL=index.cjs.map