scribe-cms 0.0.8 → 0.0.9

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 (50) hide show
  1. package/README.md +5 -5
  2. package/dist/cli/index.cjs +432 -384
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +432 -384
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +642 -512
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +642 -513
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime.cjs +287 -261
  11. package/dist/runtime.cjs.map +1 -1
  12. package/dist/runtime.js +286 -261
  13. package/dist/runtime.js.map +1 -1
  14. package/dist/src/config/resolve-config.d.ts.map +1 -1
  15. package/dist/src/core/builtin-fields.d.ts +8 -8
  16. package/dist/src/core/builtin-fields.d.ts.map +1 -1
  17. package/dist/src/core/types.d.ts +11 -4
  18. package/dist/src/core/types.d.ts.map +1 -1
  19. package/dist/src/create-project.d.ts.map +1 -1
  20. package/dist/src/export/build-static-raw-exports.d.ts.map +1 -1
  21. package/dist/src/i18n/build-url.d.ts +16 -3
  22. package/dist/src/i18n/build-url.d.ts.map +1 -1
  23. package/dist/src/i18n/resolve-document.d.ts +3 -2
  24. package/dist/src/i18n/resolve-document.d.ts.map +1 -1
  25. package/dist/src/index.d.ts +3 -2
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/loader/create-loader.d.ts.map +1 -1
  28. package/dist/src/redirects/build-json-redirects.d.ts +6 -0
  29. package/dist/src/redirects/build-json-redirects.d.ts.map +1 -0
  30. package/dist/src/redirects/build-redirects.d.ts +9 -7
  31. package/dist/src/redirects/build-redirects.d.ts.map +1 -1
  32. package/dist/src/redirects/load-type-redirects.d.ts +14 -0
  33. package/dist/src/redirects/load-type-redirects.d.ts.map +1 -0
  34. package/dist/src/redirects/redirect-schema.d.ts +31 -0
  35. package/dist/src/redirects/redirect-schema.d.ts.map +1 -0
  36. package/dist/src/runtime.d.ts +3 -1
  37. package/dist/src/runtime.d.ts.map +1 -1
  38. package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -1
  39. package/dist/src/validate/validate-project.d.ts.map +1 -1
  40. package/dist/src/validate/validate-redirects.d.ts +4 -0
  41. package/dist/src/validate/validate-redirects.d.ts.map +1 -0
  42. package/dist/studio/server.cjs +90 -61
  43. package/dist/studio/server.cjs.map +1 -1
  44. package/dist/studio/server.js +90 -61
  45. package/dist/studio/server.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/src/core/slug-aliases.d.ts +0 -27
  48. package/dist/src/core/slug-aliases.d.ts.map +0 -1
  49. package/dist/src/redirects/translation-index.d.ts +0 -4
  50. package/dist/src/redirects/translation-index.d.ts.map +0 -1
package/dist/runtime.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var path3 = require('path');
3
+ var path = require('path');
4
4
  var zod = require('zod');
5
5
  var fs2 = require('fs');
6
6
  var matter = require('gray-matter');
@@ -8,7 +8,7 @@ var Database = require('better-sqlite3');
8
8
 
9
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
11
- var path3__default = /*#__PURE__*/_interopDefault(path3);
11
+ var path__default = /*#__PURE__*/_interopDefault(path);
12
12
  var fs2__default = /*#__PURE__*/_interopDefault(fs2);
13
13
  var matter__default = /*#__PURE__*/_interopDefault(matter);
14
14
  var Database__default = /*#__PURE__*/_interopDefault(Database);
@@ -20,14 +20,14 @@ var SLUG_PLACEHOLDER = "{slug}";
20
20
  function isRoutableType(type) {
21
21
  return typeof type.path === "string" && type.path.length > 0;
22
22
  }
23
- function assertValidPathTemplate(path5, typeId) {
24
- if (!path5.startsWith("/")) {
25
- throw new Error(`Content type "${typeId}": path must start with / (got "${path5}")`);
23
+ function assertValidPathTemplate(path6, typeId) {
24
+ if (!path6.startsWith("/")) {
25
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path6}")`);
26
26
  }
27
- const count = (path5.match(/\{slug\}/g) ?? []).length;
27
+ const count = (path6.match(/\{slug\}/g) ?? []).length;
28
28
  if (count !== 1) {
29
29
  throw new Error(
30
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path5}")`
30
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path6}")`
31
31
  );
32
32
  }
33
33
  }
@@ -41,19 +41,69 @@ function pathSuffix(template) {
41
41
  if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
42
42
  return template.slice(idx + SLUG_PLACEHOLDER.length);
43
43
  }
44
- function resolvePath(template, slug, locale, defaultLocale) {
45
- const relative = template.replace(SLUG_PLACEHOLDER, slug);
46
- if (locale === defaultLocale) return relative;
47
- return `/${locale}${relative}`;
44
+ function resolveDefaultLocaleRouting() {
45
+ return { strategy: "path-prefix", prefixDefaultLocale: false };
48
46
  }
49
- function extractSlugFromResolvedPath(template, resolvedPath) {
50
- const prefix = pathPrefix(template);
51
- const suffix = pathSuffix(template);
52
- if (!resolvedPath.startsWith(prefix)) return null;
53
- if (suffix && !resolvedPath.endsWith(suffix)) return null;
54
- const slugEnd = suffix ? resolvedPath.length - suffix.length : resolvedPath.length;
55
- const slug = resolvedPath.slice(prefix.length, slugEnd);
56
- return slug.length > 0 ? slug : null;
47
+ function splitPathAndSearch(pathname) {
48
+ const q = pathname.indexOf("?");
49
+ if (q === -1) return { pathname, search: "" };
50
+ return { pathname: pathname.slice(0, q), search: pathname.slice(q) };
51
+ }
52
+ function createUrlBuilder(config) {
53
+ const localeRouting = config.localeRouting ?? resolveDefaultLocaleRouting();
54
+ const defaultLocale = config.defaultLocale;
55
+ const prefixedLocales = localeRouting.strategy === "path-prefix" ? localeRouting.prefixDefaultLocale ? [...config.locales] : config.locales.filter((locale) => locale !== defaultLocale) : config.locales.filter((locale) => locale !== defaultLocale);
56
+ function applyLocaleToPath(pathname, locale) {
57
+ if (locale === defaultLocale) {
58
+ if (localeRouting.strategy === "path-prefix" && localeRouting.prefixDefaultLocale) {
59
+ return `/${defaultLocale}${pathname}`;
60
+ }
61
+ return pathname;
62
+ }
63
+ if (localeRouting.strategy === "search-param") {
64
+ const { pathname: base, search } = splitPathAndSearch(pathname);
65
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
66
+ params.set(localeRouting.param, locale);
67
+ const qs = params.toString();
68
+ return qs ? `${base}?${qs}` : base;
69
+ }
70
+ return `/${locale}${pathname}`;
71
+ }
72
+ function resolvePath(template, slug, locale) {
73
+ const relative = template.replace(SLUG_PLACEHOLDER, slug);
74
+ return applyLocaleToPath(relative, locale);
75
+ }
76
+ function extractSlugFromResolvedPath(template, resolvedPath) {
77
+ let pathname = resolvedPath;
78
+ if (localeRouting.strategy === "search-param") {
79
+ pathname = splitPathAndSearch(resolvedPath).pathname;
80
+ } else if (localeRouting.strategy === "path-prefix") {
81
+ for (const locale of config.locales) {
82
+ if (locale === defaultLocale && !localeRouting.prefixDefaultLocale) continue;
83
+ const prefix2 = `/${locale}`;
84
+ if (pathname === prefix2 || pathname.startsWith(`${prefix2}/`)) {
85
+ pathname = pathname.slice(prefix2.length) || "/";
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ const prefix = pathPrefix(template);
91
+ const suffix = pathSuffix(template);
92
+ if (!pathname.startsWith(prefix)) return null;
93
+ if (suffix && !pathname.endsWith(suffix)) return null;
94
+ const slugEnd = suffix ? pathname.length - suffix.length : pathname.length;
95
+ const slug = pathname.slice(prefix.length, slugEnd);
96
+ return slug.length > 0 ? slug : null;
97
+ }
98
+ return {
99
+ defaultLocale,
100
+ locales: config.locales,
101
+ localeRouting,
102
+ prefixedLocales,
103
+ resolvePath,
104
+ extractSlugFromResolvedPath,
105
+ applyLocaleToPath
106
+ };
57
107
  }
58
108
 
59
109
  // src/config/resolve-config.ts
@@ -76,10 +126,17 @@ function resolveConfig(input, baseDir) {
76
126
  `scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
77
127
  );
78
128
  }
79
- const projectRoot = path3__default.default.resolve(process.cwd(), raw.rootDir);
80
- const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
81
- const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
82
- const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
129
+ const localeRouting = raw.localeRouting ?? {
130
+ strategy: "path-prefix",
131
+ prefixDefaultLocale: false
132
+ };
133
+ if (localeRouting.strategy === "search-param" && !localeRouting.param) {
134
+ throw new Error('scribe config: localeRouting search-param requires a "param" name');
135
+ }
136
+ const projectRoot = path__default.default.resolve(process.cwd(), raw.rootDir);
137
+ const contentRoot = path__default.default.resolve(projectRoot, raw.contentDir ?? "content");
138
+ const storePath = path__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
139
+ const assetsPath = raw.assetsDir ? path__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
83
140
  const seenIds = /* @__PURE__ */ new Set();
84
141
  const types = raw.types.map((type) => {
85
142
  if (seenIds.has(type.id)) {
@@ -103,6 +160,7 @@ function resolveConfig(input, baseDir) {
103
160
  assetsPath,
104
161
  locales: [...raw.locales],
105
162
  defaultLocale,
163
+ localeRouting,
106
164
  localePresets: raw.localePresets,
107
165
  translate: raw.translate,
108
166
  types
@@ -202,10 +260,10 @@ function introspectSchema(schema, prefix = []) {
202
260
  }
203
261
  function extractByPaths(data, paths) {
204
262
  const out = {};
205
- for (const path5 of paths) {
206
- const value = getAtPath(data, path5);
263
+ for (const path6 of paths) {
264
+ const value = getAtPath(data, path6);
207
265
  if (value !== void 0) {
208
- setAtPath(out, path5.filter((p) => p !== "*"), value);
266
+ setAtPath(out, path6.filter((p) => p !== "*"), value);
209
267
  }
210
268
  }
211
269
  return out;
@@ -223,13 +281,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
223
281
  const translatable = pickTranslatable(localeData, schema);
224
282
  return deepMerge(structural, translatable);
225
283
  }
226
- function getAtPath(obj, path5) {
284
+ function getAtPath(obj, path6) {
227
285
  let current = obj;
228
- for (const segment of path5) {
286
+ for (const segment of path6) {
229
287
  if (segment === "*") {
230
288
  if (!Array.isArray(current)) return void 0;
231
289
  return current.map(
232
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path5.slice(path5.indexOf("*") + 1)) : void 0
290
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path6.slice(path6.indexOf("*") + 1)) : void 0
233
291
  );
234
292
  }
235
293
  if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
@@ -237,13 +295,13 @@ function getAtPath(obj, path5) {
237
295
  }
238
296
  return current;
239
297
  }
240
- function setAtPath(obj, path5, value) {
241
- if (path5.length === 0) return;
242
- if (path5.length === 1) {
243
- obj[path5[0]] = value;
298
+ function setAtPath(obj, path6, value) {
299
+ if (path6.length === 0) return;
300
+ if (path6.length === 1) {
301
+ obj[path6[0]] = value;
244
302
  return;
245
303
  }
246
- const [head, ...rest] = path5;
304
+ const [head, ...rest] = path6;
247
305
  if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
248
306
  obj[head] = {};
249
307
  }
@@ -278,45 +336,23 @@ var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
278
336
  var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
279
337
  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");
280
338
  var canonicalPathSchema = zod.z.string().regex(/^\//, "canonicalPath must start with /");
281
- function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
339
+ var DEPRECATED_REDIRECT_FIELDS = [
340
+ [
341
+ "aliases",
342
+ "aliases frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
343
+ ],
344
+ [
345
+ "redirect_to",
346
+ "redirect_to frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
347
+ ]
348
+ ];
349
+ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
282
350
  const issues = [];
283
351
  const rest = { ...data };
284
- const aliasesResult = zod.z.array(slugPatternSchema).max(20).optional().default([]).safeParse(rest.aliases ?? []);
285
- delete rest.aliases;
286
- const redirectRaw = rest.redirect_to;
287
- delete rest.redirect_to;
288
- let redirectTo;
289
- if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
290
- if (typeof redirectRaw !== "string") {
291
- issues.push({
292
- field: "redirect_to",
293
- message: "redirect_to must be a string path",
294
- level: "error"
295
- });
296
- } else if (!pathTemplate) {
297
- issues.push({
298
- field: "redirect_to",
299
- message: "redirect_to is not allowed on reference-only content types",
300
- level: "error"
301
- });
302
- } else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
303
- issues.push({
304
- field: "redirect_to",
305
- message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
306
- level: "error"
307
- });
308
- } else {
309
- redirectTo = redirectRaw;
310
- }
311
- }
312
- const aliases = aliasesResult.success ? aliasesResult.data : [];
313
- if (!aliasesResult.success) {
314
- for (const issue of aliasesResult.error.issues) {
315
- issues.push({
316
- field: `aliases.${issue.path.join(".")}`,
317
- message: issue.message,
318
- level: "error"
319
- });
352
+ for (const [field, message] of DEPRECATED_REDIRECT_FIELDS) {
353
+ if (rest[field] !== void 0) {
354
+ issues.push({ field, message, level: "error" });
355
+ delete rest[field];
320
356
  }
321
357
  }
322
358
  let publishedAt;
@@ -378,8 +414,6 @@ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
378
414
  }
379
415
  return {
380
416
  builtin: {
381
- aliases,
382
- redirectTo,
383
417
  publishedAt,
384
418
  updatedAt,
385
419
  noindex,
@@ -395,20 +429,24 @@ function documentLastModified(doc) {
395
429
  const date = new Date(raw);
396
430
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
397
431
  }
398
- function resolveCanonicalPathname(type, doc, defaultLocale) {
432
+ function resolveCanonicalPathname(type, doc, defaultLocale, localeRouting) {
433
+ const urlBuilder = createUrlBuilder({
434
+ locales: [defaultLocale, ...doc.locale !== defaultLocale ? [doc.locale] : []],
435
+ defaultLocale,
436
+ localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
437
+ });
399
438
  if (doc.canonicalPathOverride) {
400
- if (doc.locale === defaultLocale) return doc.canonicalPathOverride;
401
- return `/${doc.locale}${doc.canonicalPathOverride}`;
439
+ return urlBuilder.applyLocaleToPath(doc.canonicalPathOverride, doc.locale);
402
440
  }
403
441
  if (!type.path) return `/${doc.slug}`;
404
- return resolvePath(type.path, doc.slug, doc.locale, defaultLocale);
442
+ return urlBuilder.resolvePath(type.path, doc.slug, doc.locale);
405
443
  }
406
- function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
444
+ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, localeRouting) {
407
445
  const out = { ...frontmatter };
408
446
  if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
409
447
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
410
448
  out.noindex = doc.noindex;
411
- out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
449
+ out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
412
450
  return out;
413
451
  }
414
452
  function seoFieldsFromEn(enDoc) {
@@ -416,8 +454,7 @@ function seoFieldsFromEn(enDoc) {
416
454
  publishedAt: enDoc.publishedAt,
417
455
  updatedAt: enDoc.updatedAt,
418
456
  noindex: enDoc.noindex,
419
- canonicalPathOverride: enDoc.canonicalPathOverride,
420
- redirectTo: enDoc.redirectTo
457
+ canonicalPathOverride: enDoc.canonicalPathOverride
421
458
  };
422
459
  }
423
460
  var SCHEMA_VERSION = 4;
@@ -476,7 +513,7 @@ function resolveStorePath(config) {
476
513
  function openStore(config, mode = "readwrite") {
477
514
  const storePath = resolveStorePath(config);
478
515
  if (mode === "readwrite") {
479
- fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
516
+ fs2__default.default.mkdirSync(path__default.default.dirname(storePath), { recursive: true });
480
517
  }
481
518
  const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
482
519
  if (mode === "readwrite") {
@@ -540,10 +577,10 @@ function isPostFile(name) {
540
577
  }
541
578
  function listEnFiles(contentDir) {
542
579
  if (!fs2__default.default.existsSync(contentDir)) return [];
543
- return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path3__default.default.join(contentDir, f));
580
+ return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path__default.default.join(contentDir, f));
544
581
  }
545
582
  function slugFromPath(filePath) {
546
- return path3__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
583
+ return path__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
547
584
  }
548
585
  function parseEnMdx(filePath, config, type) {
549
586
  const raw = fs2__default.default.readFileSync(filePath, "utf8");
@@ -589,7 +626,8 @@ function parseEnMdx(filePath, config, type) {
589
626
  locale: config.defaultLocale
590
627
  },
591
628
  type,
592
- config.defaultLocale
629
+ config.defaultLocale,
630
+ config.localeRouting
593
631
  );
594
632
  const crossIssues = type.crossValidate?.(result.data, {
595
633
  locale: config.defaultLocale,
@@ -603,8 +641,6 @@ function parseEnMdx(filePath, config, type) {
603
641
  slug,
604
642
  enSlug: slug,
605
643
  locale: config.defaultLocale,
606
- aliases: builtin.aliases,
607
- redirectTo: builtin.redirectTo,
608
644
  publishedAt: builtin.publishedAt,
609
645
  updatedAt: builtin.updatedAt,
610
646
  noindex: builtin.noindex,
@@ -615,7 +651,7 @@ function parseEnMdx(filePath, config, type) {
615
651
  };
616
652
  return { document, issues };
617
653
  }
618
- function buildDocumentFromTranslation(row, enDoc, type) {
654
+ function buildDocumentFromTranslation(row, enDoc, type, config) {
619
655
  const localeFm = JSON.parse(row.frontmatter_json);
620
656
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
621
657
  const seo = seoFieldsFromEn(enDoc);
@@ -623,14 +659,13 @@ function buildDocumentFromTranslation(row, enDoc, type) {
623
659
  merged,
624
660
  { ...seo, slug: row.slug, locale: row.locale },
625
661
  type,
626
- enDoc.locale
662
+ config.defaultLocale,
663
+ config.localeRouting
627
664
  );
628
665
  return {
629
666
  slug: row.slug,
630
667
  enSlug: row.en_slug,
631
668
  locale: row.locale,
632
- aliases: [],
633
- redirectTo: seo.redirectTo,
634
669
  publishedAt: seo.publishedAt,
635
670
  updatedAt: seo.updatedAt,
636
671
  noindex: seo.noindex,
@@ -644,7 +679,7 @@ function createContentLoader(config, type) {
644
679
  let cached = null;
645
680
  let signature = "";
646
681
  let lastCheck = 0;
647
- const contentDir = path3__default.default.join(
682
+ const contentDir = path__default.default.join(
648
683
  /* turbopackIgnore: true */
649
684
  config.rootDir,
650
685
  type.contentDir
@@ -708,7 +743,7 @@ function createContentLoader(config, type) {
708
743
  for (const row of rowsByLocale.get(locale) ?? []) {
709
744
  const enDoc = englishBySlug.get(row.en_slug);
710
745
  if (!enDoc) continue;
711
- const doc = buildDocumentFromTranslation(row, enDoc, type);
746
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config);
712
747
  bySlug.set(doc.slug, doc);
713
748
  byEnSlug.set(row.en_slug, doc);
714
749
  }
@@ -734,72 +769,18 @@ function createContentLoader(config, type) {
734
769
  return cached;
735
770
  };
736
771
  }
737
- function readEnDocument(config, type, enSlug) {
738
- const contentDir = path3__default.default.join(
739
- /* turbopackIgnore: true */
740
- config.rootDir,
741
- type.contentDir
742
- );
743
- for (const ext of [".mdx", ".md"]) {
744
- const filePath = path3__default.default.join(contentDir, `${enSlug}${ext}`);
745
- if (!fs2__default.default.existsSync(filePath)) continue;
746
- const { document } = parseEnMdx(filePath, config, type);
747
- return document;
748
- }
749
- return null;
750
- }
751
772
 
752
773
  // src/i18n/resolve-document.ts
753
- function findEnDocByAlias(slug, allDocs, defaultLocale) {
754
- const enIdx = allDocs.get(defaultLocale);
755
- if (!enIdx) return null;
756
- for (const doc of enIdx.bySlug.values()) {
757
- if (doc.aliases.includes(slug)) return doc;
758
- }
759
- return null;
760
- }
761
- function enCanonicalDoc(doc, allDocs, defaultLocale) {
762
- if (doc.locale === defaultLocale) return doc;
763
- return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
764
- }
765
- function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
766
- if (!type.path) return void 0;
767
- const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
768
- if (!enDoc.redirectTo) return void 0;
769
- const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
770
- if (!targetEnSlug) return void 0;
771
- const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
772
- const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
773
- return resolvePath(type.path, targetSlug, locale, defaultLocale);
774
- }
775
- function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
776
- if (result.shouldRedirectTo || !result.document) return result;
777
- const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
778
- if (!redirect) return result;
779
- return {
780
- document: null,
781
- actualLocale: result.actualLocale,
782
- shouldRedirectTo: redirect
783
- };
784
- }
785
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
774
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
775
+ const urlBuilder = createUrlBuilder({
776
+ locales: [defaultLocale, locale],
777
+ defaultLocale,
778
+ localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
779
+ });
786
780
  const idx = allDocs.get(locale);
787
781
  const direct = idx?.bySlug.get(slug);
788
782
  if (direct) {
789
- return withRedirectTo({ document: direct, actualLocale: locale }, locale, defaultLocale, allDocs, type);
790
- }
791
- const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
792
- if (aliasDoc && type.path) {
793
- const canonicalSlug = aliasDoc.slug;
794
- const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
795
- const targetSlug = localizedSlug ?? canonicalSlug;
796
- if (targetSlug !== slug) {
797
- return {
798
- document: null,
799
- actualLocale: locale,
800
- shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
801
- };
802
- }
783
+ return { document: direct, actualLocale: locale };
803
784
  }
804
785
  for (const [docLocale, docIdx] of allDocs) {
805
786
  const found = docIdx.bySlug.get(slug);
@@ -812,17 +793,11 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
812
793
  return {
813
794
  document: null,
814
795
  actualLocale: locale,
815
- shouldRedirectTo: resolvePath(type.path, correctSlug, locale, defaultLocale)
796
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
816
797
  };
817
798
  }
818
799
  if (docLocale === defaultLocale) {
819
- return withRedirectTo(
820
- { document: found, actualLocale: defaultLocale },
821
- locale,
822
- defaultLocale,
823
- allDocs,
824
- type
825
- );
800
+ return { document: found, actualLocale: defaultLocale };
826
801
  }
827
802
  if (!type.path) {
828
803
  return { document: null, actualLocale: locale };
@@ -830,39 +805,21 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
830
805
  return {
831
806
  document: null,
832
807
  actualLocale: locale,
833
- shouldRedirectTo: resolvePath(type.path, found.enSlug, locale, defaultLocale)
808
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, found.enSlug, locale)
834
809
  };
835
810
  }
836
811
  if (locale !== defaultLocale) {
837
812
  const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
838
813
  if (enDoc && type.indexFallback === "en") {
839
- return withRedirectTo(
840
- { document: enDoc, actualLocale: defaultLocale },
841
- locale,
842
- defaultLocale,
843
- allDocs,
844
- type
845
- );
814
+ return { document: enDoc, actualLocale: defaultLocale };
846
815
  }
847
816
  const byEn = idx?.byEnSlug.get(slug);
848
817
  if (byEn) {
849
- return withRedirectTo(
850
- { document: byEn, actualLocale: locale },
851
- locale,
852
- defaultLocale,
853
- allDocs,
854
- type
855
- );
818
+ return { document: byEn, actualLocale: locale };
856
819
  }
857
820
  const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
858
821
  if (fallback && type.indexFallback === "en") {
859
- return withRedirectTo(
860
- { document: fallback, actualLocale: defaultLocale },
861
- locale,
862
- defaultLocale,
863
- allDocs,
864
- type
865
- );
822
+ return { document: fallback, actualLocale: defaultLocale };
866
823
  }
867
824
  }
868
825
  return { document: null, actualLocale: locale };
@@ -904,6 +861,7 @@ function buildRuntime(config, type, getRuntime) {
904
861
  }
905
862
  return type.path;
906
863
  }
864
+ const urlBuilder = createUrlBuilder(config);
907
865
  const runtime = {
908
866
  id: type.id,
909
867
  config: type,
@@ -919,15 +877,21 @@ function buildRuntime(config, type, getRuntime) {
919
877
  return load().get(locale)?.bySlug.get(slug) ?? null;
920
878
  },
921
879
  resolve(slug, locale) {
922
- const result = resolveLocalizedDocument(slug, locale, config.defaultLocale, load(), type);
880
+ const result = resolveLocalizedDocument(
881
+ slug,
882
+ locale,
883
+ config.defaultLocale,
884
+ load(),
885
+ type,
886
+ config.localeRouting
887
+ );
923
888
  if (result.document && type.path) {
924
889
  return {
925
890
  ...result,
926
- canonicalPath: resolvePath(
891
+ canonicalPath: urlBuilder.resolvePath(
927
892
  type.path,
928
893
  result.document.slug,
929
- result.actualLocale,
930
- config.defaultLocale
894
+ result.actualLocale
931
895
  )
932
896
  };
933
897
  }
@@ -951,10 +915,9 @@ function buildRuntime(config, type, getRuntime) {
951
915
  alternates(doc) {
952
916
  const pathTemplate = assertRoutable("alternates");
953
917
  const out = {
954
- [config.defaultLocale]: resolvePath(
918
+ [config.defaultLocale]: urlBuilder.resolvePath(
955
919
  pathTemplate,
956
920
  doc.enSlug,
957
- config.defaultLocale,
958
921
  config.defaultLocale
959
922
  )
960
923
  };
@@ -963,7 +926,7 @@ function buildRuntime(config, type, getRuntime) {
963
926
  if (locale === config.defaultLocale) continue;
964
927
  const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
965
928
  if (translated) {
966
- out[locale] = resolvePath(pathTemplate, translated.slug, locale, config.defaultLocale);
929
+ out[locale] = urlBuilder.resolvePath(pathTemplate, translated.slug, locale);
967
930
  }
968
931
  }
969
932
  return out;
@@ -978,7 +941,7 @@ function buildRuntime(config, type, getRuntime) {
978
941
  },
979
942
  url(slug, locale) {
980
943
  const pathTemplate = assertRoutable("url");
981
- return resolvePath(pathTemplate, slug, locale, config.defaultLocale);
944
+ return urlBuilder.resolvePath(pathTemplate, slug, locale);
982
945
  },
983
946
  related(doc, fieldName, locale) {
984
947
  const meta = relationFields.get(fieldName);
@@ -1040,87 +1003,150 @@ function createProject(config) {
1040
1003
  }
1041
1004
  };
1042
1005
  }
1043
- function listEnSlugs(rootDir, contentDir) {
1044
- const dir = path3__default.default.join(rootDir, contentDir);
1045
- if (!fs2__default.default.existsSync(dir)) return [];
1046
- return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1006
+ var redirectFromSchema = zod.z.union([
1007
+ slugPatternSchema,
1008
+ zod.z.array(slugPatternSchema).min(1).max(20)
1009
+ ]);
1010
+ var typeRedirectEntrySchema = zod.z.object({
1011
+ from: redirectFromSchema,
1012
+ toSlug: slugPatternSchema.optional(),
1013
+ toType: zod.z.string().min(1).optional(),
1014
+ toUrl: zod.z.string().min(1).optional(),
1015
+ permanent: zod.z.boolean().optional()
1016
+ }).superRefine((entry, ctx) => {
1017
+ const hasToSlug = entry.toSlug !== void 0;
1018
+ const hasToUrl = entry.toUrl !== void 0;
1019
+ const hasToType = entry.toType !== void 0;
1020
+ const targetCount = Number(hasToSlug) + Number(hasToUrl);
1021
+ if (targetCount !== 1) {
1022
+ ctx.addIssue({
1023
+ code: "custom",
1024
+ message: "Each redirect must specify exactly one of toSlug/toType+toSlug or toUrl",
1025
+ path: ["toSlug"]
1026
+ });
1027
+ return;
1028
+ }
1029
+ if (hasToUrl && hasToType) {
1030
+ ctx.addIssue({
1031
+ code: "custom",
1032
+ message: "toUrl cannot be combined with toType",
1033
+ path: ["toUrl"]
1034
+ });
1035
+ }
1036
+ if (hasToType && !hasToSlug) {
1037
+ ctx.addIssue({
1038
+ code: "custom",
1039
+ message: "Cross-type redirects require both toType and toSlug",
1040
+ path: ["toSlug"]
1041
+ });
1042
+ }
1043
+ });
1044
+ var typeRedirectsFileSchema = zod.z.object({
1045
+ redirects: zod.z.array(typeRedirectEntrySchema).max(500)
1046
+ });
1047
+ function normalizeRedirectFrom(from) {
1048
+ return Array.isArray(from) ? from : [from];
1049
+ }
1050
+ function parseRedirectEntry(entry) {
1051
+ const fromSlugs = normalizeRedirectFrom(entry.from);
1052
+ if (entry.toUrl !== void 0) {
1053
+ return {
1054
+ fromSlugs,
1055
+ kind: "anywhere",
1056
+ toUrl: entry.toUrl,
1057
+ permanent: entry.permanent ?? true
1058
+ };
1059
+ }
1060
+ if (entry.toType !== void 0) {
1061
+ return {
1062
+ fromSlugs,
1063
+ kind: "cross-type",
1064
+ toType: entry.toType,
1065
+ toSlug: entry.toSlug,
1066
+ permanent: entry.permanent ?? true
1067
+ };
1068
+ }
1069
+ return {
1070
+ fromSlugs,
1071
+ kind: "same-type",
1072
+ toSlug: entry.toSlug,
1073
+ permanent: entry.permanent ?? true
1074
+ };
1047
1075
  }
1048
1076
 
1049
- // src/core/slug-aliases.ts
1050
- function buildGlobalAliasIndex(project) {
1051
- const { config } = project;
1052
- const aliasToTarget = /* @__PURE__ */ new Map();
1053
- const allAliasSlugs = /* @__PURE__ */ new Set();
1054
- const issues = [];
1055
- const canonicalSlugsByType = /* @__PURE__ */ new Map();
1056
- for (const type of config.types) {
1057
- const slugs = listEnSlugs(config.rootDir, type.contentDir);
1058
- canonicalSlugsByType.set(type.id, new Set(slugs));
1077
+ // src/redirects/load-type-redirects.ts
1078
+ var TYPE_REDIRECTS_FILENAME = "_redirects.json";
1079
+ function redirectsFilePath(config, type) {
1080
+ return path__default.default.join(config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1081
+ }
1082
+ function loadTypeRedirectsFile(config, type) {
1083
+ const filePath = redirectsFilePath(config, type);
1084
+ if (!fs2__default.default.existsSync(filePath)) return null;
1085
+ let raw;
1086
+ try {
1087
+ raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
1088
+ } catch (error) {
1089
+ throw new Error(
1090
+ `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1091
+ );
1092
+ }
1093
+ const parsed = typeRedirectsFileSchema.safeParse(raw);
1094
+ if (!parsed.success) {
1095
+ const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
1096
+ throw new Error(`${type.id}: invalid ${TYPE_REDIRECTS_FILENAME}: ${details}`);
1059
1097
  }
1098
+ return {
1099
+ contentTypeId: type.id,
1100
+ contentDir: type.contentDir,
1101
+ filePath,
1102
+ entries: parsed.data.redirects.map(parseRedirectEntry)
1103
+ };
1104
+ }
1105
+ function loadAllTypeRedirects(config) {
1106
+ const out = [];
1060
1107
  for (const type of config.types) {
1061
- if (!type.path) continue;
1062
- const canonicalSlugs = canonicalSlugsByType.get(type.id) ?? /* @__PURE__ */ new Set();
1063
- for (const enSlug of canonicalSlugs) {
1064
- const doc = readEnDocument(config, type, enSlug);
1065
- if (!doc) continue;
1066
- for (const alias of doc.aliases) {
1067
- if (alias === enSlug) {
1068
- issues.push({
1069
- contentTypeId: type.id,
1070
- enSlug,
1071
- field: "aliases",
1072
- message: `Alias "${alias}" must not equal the canonical slug`,
1073
- level: "error"
1074
- });
1075
- continue;
1076
- }
1077
- if (canonicalSlugs.has(alias)) {
1078
- issues.push({
1079
- contentTypeId: type.id,
1080
- enSlug,
1081
- field: "aliases",
1082
- message: `Alias "${alias}" collides with another document's canonical slug`,
1083
- level: "error"
1084
- });
1085
- continue;
1086
- }
1087
- const existing = aliasToTarget.get(alias);
1088
- if (existing) {
1089
- issues.push({
1090
- contentTypeId: type.id,
1091
- enSlug,
1092
- field: "aliases",
1093
- message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
1094
- level: "error"
1095
- });
1096
- continue;
1097
- }
1098
- aliasToTarget.set(alias, {
1099
- contentTypeId: type.id,
1100
- canonicalSlug: enSlug,
1101
- path: type.path
1102
- });
1103
- allAliasSlugs.add(alias);
1108
+ const loaded = loadTypeRedirectsFile(config, type);
1109
+ if (loaded) out.push(loaded);
1110
+ }
1111
+ return out;
1112
+ }
1113
+ function collectRedirectSourceSlugs(loaded) {
1114
+ const slugs = /* @__PURE__ */ new Set();
1115
+ for (const file of loaded) {
1116
+ for (const entry of file.entries) {
1117
+ for (const from of entry.fromSlugs) {
1118
+ slugs.add(from);
1119
+ }
1120
+ }
1121
+ }
1122
+ return slugs;
1123
+ }
1124
+ function collectOutboundRedirectSourcesByType(loaded) {
1125
+ const out = /* @__PURE__ */ new Map();
1126
+ for (const file of loaded) {
1127
+ const set = out.get(file.contentTypeId) ?? /* @__PURE__ */ new Set();
1128
+ for (const entry of file.entries) {
1129
+ for (const from of entry.fromSlugs) {
1130
+ set.add(from);
1104
1131
  }
1105
1132
  }
1133
+ out.set(file.contentTypeId, set);
1106
1134
  }
1107
- return { aliasToTarget, allAliasSlugs, issues };
1135
+ return out;
1108
1136
  }
1109
1137
 
1110
1138
  // src/redirects/build-redirects.ts
1111
1139
  function getRedirectSourceSlugs(project) {
1112
- const aliasIndex = buildGlobalAliasIndex(project);
1113
- const outboundByType = /* @__PURE__ */ new Map();
1140
+ const loaded = loadAllTypeRedirects(project.config);
1141
+ const aliasSlugs = collectRedirectSourceSlugs(loaded);
1142
+ const outboundByType = collectOutboundRedirectSourcesByType(loaded);
1114
1143
  for (const type of project.config.types) {
1115
- const outbound = /* @__PURE__ */ new Set();
1116
- for (const enSlug of listEnSlugs(project.config.rootDir, type.contentDir)) {
1117
- const doc = readEnDocument(project.config, type, enSlug);
1118
- if (doc?.redirectTo) outbound.add(enSlug);
1144
+ if (!outboundByType.has(type.id)) {
1145
+ outboundByType.set(type.id, /* @__PURE__ */ new Set());
1119
1146
  }
1120
- outboundByType.set(type.id, outbound);
1121
1147
  }
1122
1148
  return {
1123
- aliasSlugs: aliasIndex.allAliasSlugs,
1149
+ aliasSlugs,
1124
1150
  outboundByType
1125
1151
  };
1126
1152
  }
@@ -1128,13 +1154,12 @@ function getRedirectSourceSlugs(project) {
1128
1154
  // src/sitemap/join-base-url.ts
1129
1155
  function joinBaseUrl(baseUrl, pathname) {
1130
1156
  const origin = baseUrl.replace(/\/$/, "");
1131
- const path5 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1132
- return `${origin}${path5}`;
1157
+ const path6 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1158
+ return `${origin}${path6}`;
1133
1159
  }
1134
1160
 
1135
1161
  // src/sitemap/generate-sitemap.ts
1136
1162
  function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
1137
- if (enDoc.redirectTo) return false;
1138
1163
  if (redirectSources.aliasSlugs.has(enSlug)) return false;
1139
1164
  const outbound = redirectSources.outboundByType.get(contentTypeId);
1140
1165
  if (outbound?.has(enSlug)) return false;
@@ -1233,6 +1258,7 @@ function createScribe(input) {
1233
1258
  }
1234
1259
 
1235
1260
  exports.createScribe = createScribe;
1261
+ exports.createUrlBuilder = createUrlBuilder;
1236
1262
  exports.isRoutableType = isRoutableType;
1237
1263
  //# sourceMappingURL=runtime.cjs.map
1238
1264
  //# sourceMappingURL=runtime.cjs.map