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/index.js CHANGED
@@ -128,14 +128,14 @@ var SLUG_PLACEHOLDER = "{slug}";
128
128
  function isRoutableType(type) {
129
129
  return typeof type.path === "string" && type.path.length > 0;
130
130
  }
131
- function assertValidPathTemplate(path11, typeId) {
132
- if (!path11.startsWith("/")) {
133
- throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
131
+ function assertValidPathTemplate(path13, typeId) {
132
+ if (!path13.startsWith("/")) {
133
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
134
134
  }
135
- const count = (path11.match(/\{slug\}/g) ?? []).length;
135
+ const count = (path13.match(/\{slug\}/g) ?? []).length;
136
136
  if (count !== 1) {
137
137
  throw new Error(
138
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
138
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
139
139
  );
140
140
  }
141
141
  }
@@ -149,19 +149,69 @@ function pathSuffix(template) {
149
149
  if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
150
150
  return template.slice(idx + SLUG_PLACEHOLDER.length);
151
151
  }
152
- function resolvePath(template, slug, locale, defaultLocale) {
153
- const relative = template.replace(SLUG_PLACEHOLDER, slug);
154
- if (locale === defaultLocale) return relative;
155
- return `/${locale}${relative}`;
156
- }
157
- function extractSlugFromResolvedPath(template, resolvedPath) {
158
- const prefix = pathPrefix(template);
159
- const suffix = pathSuffix(template);
160
- if (!resolvedPath.startsWith(prefix)) return null;
161
- if (suffix && !resolvedPath.endsWith(suffix)) return null;
162
- const slugEnd = suffix ? resolvedPath.length - suffix.length : resolvedPath.length;
163
- const slug = resolvedPath.slice(prefix.length, slugEnd);
164
- return slug.length > 0 ? slug : null;
152
+ function resolveDefaultLocaleRouting() {
153
+ return { strategy: "path-prefix", prefixDefaultLocale: false };
154
+ }
155
+ function splitPathAndSearch(pathname) {
156
+ const q = pathname.indexOf("?");
157
+ if (q === -1) return { pathname, search: "" };
158
+ return { pathname: pathname.slice(0, q), search: pathname.slice(q) };
159
+ }
160
+ function createUrlBuilder(config) {
161
+ const localeRouting = config.localeRouting ?? resolveDefaultLocaleRouting();
162
+ const defaultLocale = config.defaultLocale;
163
+ const prefixedLocales = localeRouting.strategy === "path-prefix" ? localeRouting.prefixDefaultLocale ? [...config.locales] : config.locales.filter((locale) => locale !== defaultLocale) : config.locales.filter((locale) => locale !== defaultLocale);
164
+ function applyLocaleToPath(pathname, locale) {
165
+ if (locale === defaultLocale) {
166
+ if (localeRouting.strategy === "path-prefix" && localeRouting.prefixDefaultLocale) {
167
+ return `/${defaultLocale}${pathname}`;
168
+ }
169
+ return pathname;
170
+ }
171
+ if (localeRouting.strategy === "search-param") {
172
+ const { pathname: base, search } = splitPathAndSearch(pathname);
173
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
174
+ params.set(localeRouting.param, locale);
175
+ const qs = params.toString();
176
+ return qs ? `${base}?${qs}` : base;
177
+ }
178
+ return `/${locale}${pathname}`;
179
+ }
180
+ function resolvePath(template, slug, locale) {
181
+ const relative = template.replace(SLUG_PLACEHOLDER, slug);
182
+ return applyLocaleToPath(relative, locale);
183
+ }
184
+ function extractSlugFromResolvedPath(template, resolvedPath) {
185
+ let pathname = resolvedPath;
186
+ if (localeRouting.strategy === "search-param") {
187
+ pathname = splitPathAndSearch(resolvedPath).pathname;
188
+ } else if (localeRouting.strategy === "path-prefix") {
189
+ for (const locale of config.locales) {
190
+ if (locale === defaultLocale && !localeRouting.prefixDefaultLocale) continue;
191
+ const prefix2 = `/${locale}`;
192
+ if (pathname === prefix2 || pathname.startsWith(`${prefix2}/`)) {
193
+ pathname = pathname.slice(prefix2.length) || "/";
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ const prefix = pathPrefix(template);
199
+ const suffix = pathSuffix(template);
200
+ if (!pathname.startsWith(prefix)) return null;
201
+ if (suffix && !pathname.endsWith(suffix)) return null;
202
+ const slugEnd = suffix ? pathname.length - suffix.length : pathname.length;
203
+ const slug = pathname.slice(prefix.length, slugEnd);
204
+ return slug.length > 0 ? slug : null;
205
+ }
206
+ return {
207
+ defaultLocale,
208
+ locales: config.locales,
209
+ localeRouting,
210
+ prefixedLocales,
211
+ resolvePath,
212
+ extractSlugFromResolvedPath,
213
+ applyLocaleToPath
214
+ };
165
215
  }
166
216
 
167
217
  // src/config/resolve-config.ts
@@ -184,6 +234,13 @@ function resolveConfig(input, baseDir) {
184
234
  `scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
185
235
  );
186
236
  }
237
+ const localeRouting = raw.localeRouting ?? {
238
+ strategy: "path-prefix",
239
+ prefixDefaultLocale: false
240
+ };
241
+ if (localeRouting.strategy === "search-param" && !localeRouting.param) {
242
+ throw new Error('scribe config: localeRouting search-param requires a "param" name');
243
+ }
187
244
  const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
188
245
  const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
189
246
  const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
@@ -211,6 +268,7 @@ function resolveConfig(input, baseDir) {
211
268
  assetsPath,
212
269
  locales: [...raw.locales],
213
270
  defaultLocale,
271
+ localeRouting,
214
272
  localePresets: raw.localePresets,
215
273
  translate: raw.translate,
216
274
  types
@@ -261,10 +319,10 @@ function introspectSchema(schema, prefix = []) {
261
319
  }
262
320
  function extractByPaths(data, paths) {
263
321
  const out = {};
264
- for (const path11 of paths) {
265
- const value = getAtPath(data, path11);
322
+ for (const path13 of paths) {
323
+ const value = getAtPath(data, path13);
266
324
  if (value !== void 0) {
267
- setAtPath(out, path11.filter((p) => p !== "*"), value);
325
+ setAtPath(out, path13.filter((p) => p !== "*"), value);
268
326
  }
269
327
  }
270
328
  return out;
@@ -282,13 +340,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
282
340
  const translatable = pickTranslatable(localeData, schema);
283
341
  return deepMerge(structural, translatable);
284
342
  }
285
- function getAtPath(obj, path11) {
343
+ function getAtPath(obj, path13) {
286
344
  let current = obj;
287
- for (const segment of path11) {
345
+ for (const segment of path13) {
288
346
  if (segment === "*") {
289
347
  if (!Array.isArray(current)) return void 0;
290
348
  return current.map(
291
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
349
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path13.slice(path13.indexOf("*") + 1)) : void 0
292
350
  );
293
351
  }
294
352
  if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
@@ -296,13 +354,13 @@ function getAtPath(obj, path11) {
296
354
  }
297
355
  return current;
298
356
  }
299
- function setAtPath(obj, path11, value) {
300
- if (path11.length === 0) return;
301
- if (path11.length === 1) {
302
- obj[path11[0]] = value;
357
+ function setAtPath(obj, path13, value) {
358
+ if (path13.length === 0) return;
359
+ if (path13.length === 1) {
360
+ obj[path13[0]] = value;
303
361
  return;
304
362
  }
305
- const [head, ...rest] = path11;
363
+ const [head, ...rest] = path13;
306
364
  if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
307
365
  obj[head] = {};
308
366
  }
@@ -347,45 +405,23 @@ var LOCALE_BUILTIN_KEYS = [
347
405
  ["translationOf", "translationOf is internal; remove from locale translation"],
348
406
  ["enSlug", "enSlug is internal; remove from locale translation"]
349
407
  ];
350
- function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
408
+ var DEPRECATED_REDIRECT_FIELDS = [
409
+ [
410
+ "aliases",
411
+ "aliases frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
412
+ ],
413
+ [
414
+ "redirect_to",
415
+ "redirect_to frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
416
+ ]
417
+ ];
418
+ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
351
419
  const issues = [];
352
420
  const rest = { ...data };
353
- const aliasesResult = z.array(slugPatternSchema).max(20).optional().default([]).safeParse(rest.aliases ?? []);
354
- delete rest.aliases;
355
- const redirectRaw = rest.redirect_to;
356
- delete rest.redirect_to;
357
- let redirectTo;
358
- if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
359
- if (typeof redirectRaw !== "string") {
360
- issues.push({
361
- field: "redirect_to",
362
- message: "redirect_to must be a string path",
363
- level: "error"
364
- });
365
- } else if (!pathTemplate) {
366
- issues.push({
367
- field: "redirect_to",
368
- message: "redirect_to is not allowed on reference-only content types",
369
- level: "error"
370
- });
371
- } else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
372
- issues.push({
373
- field: "redirect_to",
374
- message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
375
- level: "error"
376
- });
377
- } else {
378
- redirectTo = redirectRaw;
379
- }
380
- }
381
- const aliases = aliasesResult.success ? aliasesResult.data : [];
382
- if (!aliasesResult.success) {
383
- for (const issue of aliasesResult.error.issues) {
384
- issues.push({
385
- field: `aliases.${issue.path.join(".")}`,
386
- message: issue.message,
387
- level: "error"
388
- });
421
+ for (const [field2, message] of DEPRECATED_REDIRECT_FIELDS) {
422
+ if (rest[field2] !== void 0) {
423
+ issues.push({ field: field2, message, level: "error" });
424
+ delete rest[field2];
389
425
  }
390
426
  }
391
427
  let publishedAt;
@@ -447,8 +483,6 @@ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
447
483
  }
448
484
  return {
449
485
  builtin: {
450
- aliases,
451
- redirectTo,
452
486
  publishedAt,
453
487
  updatedAt,
454
488
  noindex,
@@ -473,20 +507,24 @@ function documentLastModified(doc) {
473
507
  const date = new Date(raw);
474
508
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
475
509
  }
476
- function resolveCanonicalPathname(type, doc, defaultLocale) {
510
+ function resolveCanonicalPathname(type, doc, defaultLocale, localeRouting) {
511
+ const urlBuilder = createUrlBuilder({
512
+ locales: [defaultLocale, ...doc.locale !== defaultLocale ? [doc.locale] : []],
513
+ defaultLocale,
514
+ localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
515
+ });
477
516
  if (doc.canonicalPathOverride) {
478
- if (doc.locale === defaultLocale) return doc.canonicalPathOverride;
479
- return `/${doc.locale}${doc.canonicalPathOverride}`;
517
+ return urlBuilder.applyLocaleToPath(doc.canonicalPathOverride, doc.locale);
480
518
  }
481
519
  if (!type.path) return `/${doc.slug}`;
482
- return resolvePath(type.path, doc.slug, doc.locale, defaultLocale);
520
+ return urlBuilder.resolvePath(type.path, doc.slug, doc.locale);
483
521
  }
484
- function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
522
+ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, localeRouting) {
485
523
  const out = { ...frontmatter };
486
524
  if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
487
525
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
488
526
  out.noindex = doc.noindex;
489
- out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
527
+ out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
490
528
  return out;
491
529
  }
492
530
  function seoFieldsFromEn(enDoc) {
@@ -494,8 +532,7 @@ function seoFieldsFromEn(enDoc) {
494
532
  publishedAt: enDoc.publishedAt,
495
533
  updatedAt: enDoc.updatedAt,
496
534
  noindex: enDoc.noindex,
497
- canonicalPathOverride: enDoc.canonicalPathOverride,
498
- redirectTo: enDoc.redirectTo
535
+ canonicalPathOverride: enDoc.canonicalPathOverride
499
536
  };
500
537
  }
501
538
  var SCHEMA_VERSION = 4;
@@ -725,7 +762,8 @@ function parseEnMdx(filePath, config, type) {
725
762
  locale: config.defaultLocale
726
763
  },
727
764
  type,
728
- config.defaultLocale
765
+ config.defaultLocale,
766
+ config.localeRouting
729
767
  );
730
768
  const crossIssues = type.crossValidate?.(result.data, {
731
769
  locale: config.defaultLocale,
@@ -739,8 +777,6 @@ function parseEnMdx(filePath, config, type) {
739
777
  slug,
740
778
  enSlug: slug,
741
779
  locale: config.defaultLocale,
742
- aliases: builtin.aliases,
743
- redirectTo: builtin.redirectTo,
744
780
  publishedAt: builtin.publishedAt,
745
781
  updatedAt: builtin.updatedAt,
746
782
  noindex: builtin.noindex,
@@ -751,7 +787,7 @@ function parseEnMdx(filePath, config, type) {
751
787
  };
752
788
  return { document, issues };
753
789
  }
754
- function buildDocumentFromTranslation(row, enDoc, type) {
790
+ function buildDocumentFromTranslation(row, enDoc, type, config) {
755
791
  const localeFm = JSON.parse(row.frontmatter_json);
756
792
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
757
793
  const seo = seoFieldsFromEn(enDoc);
@@ -759,14 +795,13 @@ function buildDocumentFromTranslation(row, enDoc, type) {
759
795
  merged,
760
796
  { ...seo, slug: row.slug, locale: row.locale },
761
797
  type,
762
- enDoc.locale
798
+ config.defaultLocale,
799
+ config.localeRouting
763
800
  );
764
801
  return {
765
802
  slug: row.slug,
766
803
  enSlug: row.en_slug,
767
804
  locale: row.locale,
768
- aliases: [],
769
- redirectTo: seo.redirectTo,
770
805
  publishedAt: seo.publishedAt,
771
806
  updatedAt: seo.updatedAt,
772
807
  noindex: seo.noindex,
@@ -844,7 +879,7 @@ function createContentLoader(config, type) {
844
879
  for (const row of rowsByLocale.get(locale) ?? []) {
845
880
  const enDoc = englishBySlug.get(row.en_slug);
846
881
  if (!enDoc) continue;
847
- const doc = buildDocumentFromTranslation(row, enDoc, type);
882
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config);
848
883
  bySlug.set(doc.slug, doc);
849
884
  byEnSlug.set(row.en_slug, doc);
850
885
  }
@@ -892,56 +927,16 @@ function getTranslatablePayload(doc, type) {
892
927
  }
893
928
 
894
929
  // src/i18n/resolve-document.ts
895
- function findEnDocByAlias(slug, allDocs, defaultLocale) {
896
- const enIdx = allDocs.get(defaultLocale);
897
- if (!enIdx) return null;
898
- for (const doc of enIdx.bySlug.values()) {
899
- if (doc.aliases.includes(slug)) return doc;
900
- }
901
- return null;
902
- }
903
- function enCanonicalDoc(doc, allDocs, defaultLocale) {
904
- if (doc.locale === defaultLocale) return doc;
905
- return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
906
- }
907
- function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
908
- if (!type.path) return void 0;
909
- const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
910
- if (!enDoc.redirectTo) return void 0;
911
- const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
912
- if (!targetEnSlug) return void 0;
913
- const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
914
- const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
915
- return resolvePath(type.path, targetSlug, locale, defaultLocale);
916
- }
917
- function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
918
- if (result.shouldRedirectTo || !result.document) return result;
919
- const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
920
- if (!redirect) return result;
921
- return {
922
- document: null,
923
- actualLocale: result.actualLocale,
924
- shouldRedirectTo: redirect
925
- };
926
- }
927
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
930
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
931
+ const urlBuilder = createUrlBuilder({
932
+ locales: [defaultLocale, locale],
933
+ defaultLocale,
934
+ localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
935
+ });
928
936
  const idx = allDocs.get(locale);
929
937
  const direct = idx?.bySlug.get(slug);
930
938
  if (direct) {
931
- return withRedirectTo({ document: direct, actualLocale: locale }, locale, defaultLocale, allDocs, type);
932
- }
933
- const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
934
- if (aliasDoc && type.path) {
935
- const canonicalSlug = aliasDoc.slug;
936
- const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
937
- const targetSlug = localizedSlug ?? canonicalSlug;
938
- if (targetSlug !== slug) {
939
- return {
940
- document: null,
941
- actualLocale: locale,
942
- shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
943
- };
944
- }
939
+ return { document: direct, actualLocale: locale };
945
940
  }
946
941
  for (const [docLocale, docIdx] of allDocs) {
947
942
  const found = docIdx.bySlug.get(slug);
@@ -954,17 +949,11 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
954
949
  return {
955
950
  document: null,
956
951
  actualLocale: locale,
957
- shouldRedirectTo: resolvePath(type.path, correctSlug, locale, defaultLocale)
952
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
958
953
  };
959
954
  }
960
955
  if (docLocale === defaultLocale) {
961
- return withRedirectTo(
962
- { document: found, actualLocale: defaultLocale },
963
- locale,
964
- defaultLocale,
965
- allDocs,
966
- type
967
- );
956
+ return { document: found, actualLocale: defaultLocale };
968
957
  }
969
958
  if (!type.path) {
970
959
  return { document: null, actualLocale: locale };
@@ -972,39 +961,21 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
972
961
  return {
973
962
  document: null,
974
963
  actualLocale: locale,
975
- shouldRedirectTo: resolvePath(type.path, found.enSlug, locale, defaultLocale)
964
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, found.enSlug, locale)
976
965
  };
977
966
  }
978
967
  if (locale !== defaultLocale) {
979
968
  const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
980
969
  if (enDoc && type.indexFallback === "en") {
981
- return withRedirectTo(
982
- { document: enDoc, actualLocale: defaultLocale },
983
- locale,
984
- defaultLocale,
985
- allDocs,
986
- type
987
- );
970
+ return { document: enDoc, actualLocale: defaultLocale };
988
971
  }
989
972
  const byEn = idx?.byEnSlug.get(slug);
990
973
  if (byEn) {
991
- return withRedirectTo(
992
- { document: byEn, actualLocale: locale },
993
- locale,
994
- defaultLocale,
995
- allDocs,
996
- type
997
- );
974
+ return { document: byEn, actualLocale: locale };
998
975
  }
999
976
  const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
1000
977
  if (fallback && type.indexFallback === "en") {
1001
- return withRedirectTo(
1002
- { document: fallback, actualLocale: defaultLocale },
1003
- locale,
1004
- defaultLocale,
1005
- allDocs,
1006
- type
1007
- );
978
+ return { document: fallback, actualLocale: defaultLocale };
1008
979
  }
1009
980
  }
1010
981
  return { document: null, actualLocale: locale };
@@ -1046,6 +1017,7 @@ function buildRuntime(config, type, getRuntime) {
1046
1017
  }
1047
1018
  return type.path;
1048
1019
  }
1020
+ const urlBuilder = createUrlBuilder(config);
1049
1021
  const runtime = {
1050
1022
  id: type.id,
1051
1023
  config: type,
@@ -1061,15 +1033,21 @@ function buildRuntime(config, type, getRuntime) {
1061
1033
  return load().get(locale)?.bySlug.get(slug) ?? null;
1062
1034
  },
1063
1035
  resolve(slug, locale) {
1064
- const result = resolveLocalizedDocument(slug, locale, config.defaultLocale, load(), type);
1036
+ const result = resolveLocalizedDocument(
1037
+ slug,
1038
+ locale,
1039
+ config.defaultLocale,
1040
+ load(),
1041
+ type,
1042
+ config.localeRouting
1043
+ );
1065
1044
  if (result.document && type.path) {
1066
1045
  return {
1067
1046
  ...result,
1068
- canonicalPath: resolvePath(
1047
+ canonicalPath: urlBuilder.resolvePath(
1069
1048
  type.path,
1070
1049
  result.document.slug,
1071
- result.actualLocale,
1072
- config.defaultLocale
1050
+ result.actualLocale
1073
1051
  )
1074
1052
  };
1075
1053
  }
@@ -1093,10 +1071,9 @@ function buildRuntime(config, type, getRuntime) {
1093
1071
  alternates(doc) {
1094
1072
  const pathTemplate = assertRoutable("alternates");
1095
1073
  const out = {
1096
- [config.defaultLocale]: resolvePath(
1074
+ [config.defaultLocale]: urlBuilder.resolvePath(
1097
1075
  pathTemplate,
1098
1076
  doc.enSlug,
1099
- config.defaultLocale,
1100
1077
  config.defaultLocale
1101
1078
  )
1102
1079
  };
@@ -1105,7 +1082,7 @@ function buildRuntime(config, type, getRuntime) {
1105
1082
  if (locale === config.defaultLocale) continue;
1106
1083
  const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
1107
1084
  if (translated) {
1108
- out[locale] = resolvePath(pathTemplate, translated.slug, locale, config.defaultLocale);
1085
+ out[locale] = urlBuilder.resolvePath(pathTemplate, translated.slug, locale);
1109
1086
  }
1110
1087
  }
1111
1088
  return out;
@@ -1120,7 +1097,7 @@ function buildRuntime(config, type, getRuntime) {
1120
1097
  },
1121
1098
  url(slug, locale) {
1122
1099
  const pathTemplate = assertRoutable("url");
1123
- return resolvePath(pathTemplate, slug, locale, config.defaultLocale);
1100
+ return urlBuilder.resolvePath(pathTemplate, slug, locale);
1124
1101
  },
1125
1102
  related(doc, fieldName, locale) {
1126
1103
  const meta = relationFields.get(fieldName);
@@ -1186,207 +1163,329 @@ function enFileExists(config, type, enSlug) {
1186
1163
  const dir = path3.join(config.rootDir, type.contentDir);
1187
1164
  return fs2.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs2.existsSync(path3.join(dir, `${enSlug}.md`));
1188
1165
  }
1189
- function sqliteHasTranslations(db, contentTypeId, enSlug) {
1190
- return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
1191
- }
1192
- function isAliasKnown(config, type, aliasEnSlug, db) {
1193
- return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
1194
- }
1195
1166
  function listEnSlugs(rootDir, contentDir) {
1196
1167
  const dir = path3.join(rootDir, contentDir);
1197
1168
  if (!fs2.existsSync(dir)) return [];
1198
1169
  return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1199
1170
  }
1200
1171
 
1201
- // src/core/slug-aliases.ts
1202
- function buildGlobalAliasIndex(project) {
1203
- const { config } = project;
1204
- const aliasToTarget = /* @__PURE__ */ new Map();
1205
- const allAliasSlugs = /* @__PURE__ */ new Set();
1206
- const issues = [];
1207
- const canonicalSlugsByType = /* @__PURE__ */ new Map();
1208
- for (const type of config.types) {
1209
- const slugs = listEnSlugs(config.rootDir, type.contentDir);
1210
- canonicalSlugsByType.set(type.id, new Set(slugs));
1172
+ // src/i18n/translation-index.ts
1173
+ function buildTranslationIndex(allDocs, locales, defaultLocale) {
1174
+ const index = /* @__PURE__ */ new Map();
1175
+ for (const locale of locales) {
1176
+ if (locale === defaultLocale) continue;
1177
+ const idx = allDocs.get(locale);
1178
+ if (!idx) continue;
1179
+ const localeMap = /* @__PURE__ */ new Map();
1180
+ for (const [enSlug, doc] of idx.byEnSlug) {
1181
+ localeMap.set(enSlug, doc.slug);
1182
+ }
1183
+ index.set(locale, localeMap);
1184
+ }
1185
+ return index;
1186
+ }
1187
+ var redirectFromSchema = z.union([
1188
+ slugPatternSchema,
1189
+ z.array(slugPatternSchema).min(1).max(20)
1190
+ ]);
1191
+ var typeRedirectEntrySchema = z.object({
1192
+ from: redirectFromSchema,
1193
+ toSlug: slugPatternSchema.optional(),
1194
+ toType: z.string().min(1).optional(),
1195
+ toUrl: z.string().min(1).optional(),
1196
+ permanent: z.boolean().optional()
1197
+ }).superRefine((entry, ctx) => {
1198
+ const hasToSlug = entry.toSlug !== void 0;
1199
+ const hasToUrl = entry.toUrl !== void 0;
1200
+ const hasToType = entry.toType !== void 0;
1201
+ const targetCount = Number(hasToSlug) + Number(hasToUrl);
1202
+ if (targetCount !== 1) {
1203
+ ctx.addIssue({
1204
+ code: "custom",
1205
+ message: "Each redirect must specify exactly one of toSlug/toType+toSlug or toUrl",
1206
+ path: ["toSlug"]
1207
+ });
1208
+ return;
1209
+ }
1210
+ if (hasToUrl && hasToType) {
1211
+ ctx.addIssue({
1212
+ code: "custom",
1213
+ message: "toUrl cannot be combined with toType",
1214
+ path: ["toUrl"]
1215
+ });
1216
+ }
1217
+ if (hasToType && !hasToSlug) {
1218
+ ctx.addIssue({
1219
+ code: "custom",
1220
+ message: "Cross-type redirects require both toType and toSlug",
1221
+ path: ["toSlug"]
1222
+ });
1223
+ }
1224
+ });
1225
+ var typeRedirectsFileSchema = z.object({
1226
+ redirects: z.array(typeRedirectEntrySchema).max(500)
1227
+ });
1228
+ function normalizeRedirectFrom(from) {
1229
+ return Array.isArray(from) ? from : [from];
1230
+ }
1231
+ function parseRedirectEntry(entry) {
1232
+ const fromSlugs = normalizeRedirectFrom(entry.from);
1233
+ if (entry.toUrl !== void 0) {
1234
+ return {
1235
+ fromSlugs,
1236
+ kind: "anywhere",
1237
+ toUrl: entry.toUrl,
1238
+ permanent: entry.permanent ?? true
1239
+ };
1240
+ }
1241
+ if (entry.toType !== void 0) {
1242
+ return {
1243
+ fromSlugs,
1244
+ kind: "cross-type",
1245
+ toType: entry.toType,
1246
+ toSlug: entry.toSlug,
1247
+ permanent: entry.permanent ?? true
1248
+ };
1249
+ }
1250
+ return {
1251
+ fromSlugs,
1252
+ kind: "same-type",
1253
+ toSlug: entry.toSlug,
1254
+ permanent: entry.permanent ?? true
1255
+ };
1256
+ }
1257
+
1258
+ // src/redirects/load-type-redirects.ts
1259
+ var TYPE_REDIRECTS_FILENAME = "_redirects.json";
1260
+ function redirectsFilePath(config, type) {
1261
+ return path3.join(config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1262
+ }
1263
+ function loadTypeRedirectsFile(config, type) {
1264
+ const filePath = redirectsFilePath(config, type);
1265
+ if (!fs2.existsSync(filePath)) return null;
1266
+ let raw;
1267
+ try {
1268
+ raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
1269
+ } catch (error) {
1270
+ throw new Error(
1271
+ `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1272
+ );
1273
+ }
1274
+ const parsed = typeRedirectsFileSchema.safeParse(raw);
1275
+ if (!parsed.success) {
1276
+ const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
1277
+ throw new Error(`${type.id}: invalid ${TYPE_REDIRECTS_FILENAME}: ${details}`);
1211
1278
  }
1279
+ return {
1280
+ contentTypeId: type.id,
1281
+ contentDir: type.contentDir,
1282
+ filePath,
1283
+ entries: parsed.data.redirects.map(parseRedirectEntry)
1284
+ };
1285
+ }
1286
+ function loadAllTypeRedirects(config) {
1287
+ const out = [];
1212
1288
  for (const type of config.types) {
1213
- if (!type.path) continue;
1214
- const canonicalSlugs = canonicalSlugsByType.get(type.id) ?? /* @__PURE__ */ new Set();
1215
- for (const enSlug of canonicalSlugs) {
1216
- const doc = readEnDocument(config, type, enSlug);
1217
- if (!doc) continue;
1218
- for (const alias of doc.aliases) {
1219
- if (alias === enSlug) {
1220
- issues.push({
1221
- contentTypeId: type.id,
1222
- enSlug,
1223
- field: "aliases",
1224
- message: `Alias "${alias}" must not equal the canonical slug`,
1225
- level: "error"
1226
- });
1227
- continue;
1228
- }
1229
- if (canonicalSlugs.has(alias)) {
1230
- issues.push({
1231
- contentTypeId: type.id,
1232
- enSlug,
1233
- field: "aliases",
1234
- message: `Alias "${alias}" collides with another document's canonical slug`,
1235
- level: "error"
1236
- });
1237
- continue;
1238
- }
1239
- const existing = aliasToTarget.get(alias);
1240
- if (existing) {
1241
- issues.push({
1242
- contentTypeId: type.id,
1243
- enSlug,
1244
- field: "aliases",
1245
- message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
1246
- level: "error"
1247
- });
1248
- continue;
1249
- }
1250
- aliasToTarget.set(alias, {
1251
- contentTypeId: type.id,
1252
- canonicalSlug: enSlug,
1253
- path: type.path
1254
- });
1255
- allAliasSlugs.add(alias);
1289
+ const loaded = loadTypeRedirectsFile(config, type);
1290
+ if (loaded) out.push(loaded);
1291
+ }
1292
+ return out;
1293
+ }
1294
+ function collectRedirectSourceSlugs(loaded) {
1295
+ const slugs = /* @__PURE__ */ new Set();
1296
+ for (const file of loaded) {
1297
+ for (const entry of file.entries) {
1298
+ for (const from of entry.fromSlugs) {
1299
+ slugs.add(from);
1256
1300
  }
1257
1301
  }
1258
1302
  }
1259
- return { aliasToTarget, allAliasSlugs, issues };
1303
+ return slugs;
1260
1304
  }
1261
- function validateAliasCoverage(project) {
1262
- const { config } = project;
1263
- const { aliasToTarget } = buildGlobalAliasIndex(project);
1264
- const issues = [];
1265
- const db = openStore(config, "readonly");
1266
- try {
1267
- for (const [alias, target] of aliasToTarget) {
1268
- const type = config.types.find((t) => t.id === target.contentTypeId);
1269
- if (!type) continue;
1270
- const known = isAliasKnown(config, type, alias, db);
1271
- if (!known) {
1272
- issues.push({
1273
- contentTypeId: type.id,
1274
- enSlug: target.canonicalSlug,
1275
- field: "aliases",
1276
- message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
1277
- level: "warning"
1278
- });
1279
- }
1280
- if (enFileExists(config, type, alias)) {
1281
- issues.push({
1282
- contentTypeId: type.id,
1283
- enSlug: target.canonicalSlug,
1284
- field: "aliases",
1285
- message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
1286
- level: "warning"
1287
- });
1305
+ function collectOutboundRedirectSourcesByType(loaded) {
1306
+ const out = /* @__PURE__ */ new Map();
1307
+ for (const file of loaded) {
1308
+ const set = out.get(file.contentTypeId) ?? /* @__PURE__ */ new Set();
1309
+ for (const entry of file.entries) {
1310
+ for (const from of entry.fromSlugs) {
1311
+ set.add(from);
1288
1312
  }
1289
- const sqliteRows = listTranslationsForEnSlug(db, type.id, alias);
1290
- if (sqliteRows.length > 0) {
1291
- issues.push({
1292
- contentTypeId: type.id,
1293
- enSlug: target.canonicalSlug,
1294
- field: "aliases",
1295
- message: `${sqliteRows.length} sqlite translation(s) for alias "${alias}" retained for locale redirects and history`,
1296
- level: "info"
1297
- });
1313
+ }
1314
+ out.set(file.contentTypeId, set);
1315
+ }
1316
+ return out;
1317
+ }
1318
+
1319
+ // src/redirects/build-json-redirects.ts
1320
+ function localizedSlug(db, contentTypeId, enSlug, locale, defaultLocale) {
1321
+ if (locale === defaultLocale) return enSlug;
1322
+ const row = listTranslationsForEnSlug(db, contentTypeId, enSlug).find(
1323
+ (translation) => translation.locale === locale
1324
+ );
1325
+ return row?.slug ?? enSlug;
1326
+ }
1327
+ function resolveSlugTargetDestination(project, db, targetType, toSlug, locale, urlBuilder) {
1328
+ const targetSlug = localizedSlug(
1329
+ db,
1330
+ targetType.id,
1331
+ toSlug,
1332
+ locale,
1333
+ project.config.defaultLocale
1334
+ );
1335
+ return urlBuilder.resolvePath(targetType.path, targetSlug, locale);
1336
+ }
1337
+ function buildEntryRules(project, sourceType, entry, db, urlBuilder) {
1338
+ if (!isRoutableType(sourceType)) return [];
1339
+ const out = [];
1340
+ const { defaultLocale } = project.config;
1341
+ let targetType = sourceType;
1342
+ if (entry.kind === "cross-type") {
1343
+ const resolvedTarget = project.config.types.find((type) => type.id === entry.toType);
1344
+ if (!resolvedTarget?.path) return out;
1345
+ targetType = resolvedTarget;
1346
+ }
1347
+ for (const fromEnSlug of entry.fromSlugs) {
1348
+ const locales = project.config.locales;
1349
+ for (const locale of locales) {
1350
+ const fromSlug = localizedSlug(db, sourceType.id, fromEnSlug, locale, defaultLocale);
1351
+ const source = urlBuilder.resolvePath(sourceType.path, fromSlug, locale);
1352
+ let destination;
1353
+ if (entry.kind === "anywhere") {
1354
+ destination = entry.toUrl;
1355
+ } else {
1356
+ destination = resolveSlugTargetDestination(
1357
+ project,
1358
+ db,
1359
+ entry.kind === "cross-type" ? targetType : sourceType,
1360
+ entry.toSlug,
1361
+ locale,
1362
+ urlBuilder
1363
+ );
1298
1364
  }
1299
- for (const locale of config.locales) {
1300
- if (locale === config.defaultLocale) continue;
1301
- const canonicalRow = listTranslationsForEnSlug(db, type.id, target.canonicalSlug).find(
1302
- (r) => r.locale === locale
1365
+ if (source === destination) continue;
1366
+ out.push({
1367
+ source,
1368
+ destination,
1369
+ permanent: entry.permanent
1370
+ });
1371
+ }
1372
+ if (sourceType.id === "blog") {
1373
+ let destinationEn;
1374
+ if (entry.kind === "anywhere") {
1375
+ destinationEn = entry.toUrl;
1376
+ } else {
1377
+ destinationEn = resolveSlugTargetDestination(
1378
+ project,
1379
+ db,
1380
+ entry.kind === "cross-type" ? targetType : sourceType,
1381
+ entry.toSlug,
1382
+ defaultLocale,
1383
+ urlBuilder
1303
1384
  );
1304
- if (!canonicalRow) continue;
1305
- const aliasRow = sqliteRows.find((r) => r.locale === locale);
1306
- if (!aliasRow && known) {
1307
- issues.push({
1308
- contentTypeId: type.id,
1309
- enSlug: target.canonicalSlug,
1310
- field: "aliases",
1311
- message: `Alias "${alias}" has no locale slug mapping for ${locale} \u2014 old localized URL may miss redirect`,
1312
- level: "warning"
1313
- });
1385
+ }
1386
+ out.push({
1387
+ source: `/${fromEnSlug}`,
1388
+ destination: destinationEn,
1389
+ permanent: entry.permanent
1390
+ });
1391
+ for (const locale of urlBuilder.prefixedLocales) {
1392
+ const fromSlug = localizedSlug(db, sourceType.id, fromEnSlug, locale, defaultLocale);
1393
+ let destination;
1394
+ if (entry.kind === "anywhere") {
1395
+ destination = entry.toUrl;
1396
+ } else {
1397
+ destination = resolveSlugTargetDestination(
1398
+ project,
1399
+ db,
1400
+ entry.kind === "cross-type" ? targetType : sourceType,
1401
+ entry.toSlug,
1402
+ locale,
1403
+ urlBuilder
1404
+ );
1314
1405
  }
1406
+ out.push({
1407
+ source: `/${locale}/${fromSlug}`,
1408
+ destination,
1409
+ permanent: entry.permanent
1410
+ });
1411
+ out.push({
1412
+ source: `/${locale}/${fromEnSlug}`,
1413
+ destination,
1414
+ permanent: entry.permanent
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ return out;
1420
+ }
1421
+ function buildJsonRedirects(project) {
1422
+ const urlBuilder = createUrlBuilder(project.config);
1423
+ const db = openStore(project.config, "readonly");
1424
+ const out = [];
1425
+ try {
1426
+ for (const type of project.config.types) {
1427
+ if (!isRoutableType(type)) continue;
1428
+ const loaded = loadTypeRedirectsFile(project.config, type);
1429
+ if (!loaded) continue;
1430
+ for (const entry of loaded.entries) {
1431
+ out.push(...buildEntryRules(project, type, entry, db, urlBuilder));
1315
1432
  }
1316
1433
  }
1317
1434
  } finally {
1318
1435
  db.close();
1319
1436
  }
1320
- return issues;
1437
+ return out;
1321
1438
  }
1322
- function validateAliasRedirectChains(project) {
1323
- const { config } = project;
1324
- const { aliasToTarget, allAliasSlugs } = buildGlobalAliasIndex(project);
1325
- const issues = [];
1326
- for (const [alias, target] of aliasToTarget) {
1327
- const doc = readEnDocument(
1328
- config,
1329
- project.getType(target.contentTypeId).config,
1330
- target.canonicalSlug
1331
- );
1332
- if (doc?.redirectTo) {
1333
- issues.push({
1334
- contentTypeId: target.contentTypeId,
1335
- enSlug: target.canonicalSlug,
1336
- field: "aliases",
1337
- message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
1338
- level: "error"
1339
- });
1439
+ function buildRedirectSourceSlugSet(project) {
1440
+ const out = /* @__PURE__ */ new Set();
1441
+ for (const type of project.config.types) {
1442
+ const loaded = loadTypeRedirectsFile(project.config, type);
1443
+ if (!loaded) continue;
1444
+ for (const entry of loaded.entries) {
1445
+ for (const from of entry.fromSlugs) {
1446
+ out.add(`${type.id}:${from}`);
1447
+ }
1340
1448
  }
1341
1449
  }
1342
- for (const type of config.types) {
1343
- if (!type.path) continue;
1344
- for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
1345
- const doc = readEnDocument(config, type, enSlug);
1346
- if (!doc?.redirectTo) continue;
1347
- const targetSlug = extractSlugFromResolvedPath(type.path, doc.redirectTo);
1348
- if (targetSlug && allAliasSlugs.has(targetSlug)) {
1349
- issues.push({
1350
- contentTypeId: type.id,
1351
- enSlug,
1352
- field: "redirect_to",
1353
- message: `redirect_to target "${targetSlug}" is an alias slug \u2014 chain detected`,
1354
- level: "error"
1355
- });
1356
- }
1357
- const targetDoc = targetSlug ? readEnDocument(config, type, targetSlug) : null;
1358
- if (targetDoc?.redirectTo) {
1359
- issues.push({
1360
- contentTypeId: type.id,
1450
+ return out;
1451
+ }
1452
+ function buildLegacyNoBlogPathRedirects(project, redirectSourceKeys) {
1453
+ const blogType = project.config.types.find((type) => type.id === "blog" && type.path);
1454
+ if (!blogType?.path) return [];
1455
+ const urlBuilder = createUrlBuilder(project.config);
1456
+ const db = openStore(project.config, "readonly");
1457
+ const out = [];
1458
+ try {
1459
+ for (const enSlug of listEnSlugs(project.config.rootDir, blogType.contentDir)) {
1460
+ if (redirectSourceKeys.has(`blog:${enSlug}`)) continue;
1461
+ out.push({
1462
+ source: `/${enSlug}`,
1463
+ destination: urlBuilder.resolvePath(blogType.path, enSlug, project.config.defaultLocale),
1464
+ permanent: true
1465
+ });
1466
+ for (const locale of urlBuilder.prefixedLocales) {
1467
+ const translatedSlug = localizedSlug(
1468
+ db,
1469
+ blogType.id,
1361
1470
  enSlug,
1362
- field: "redirect_to",
1363
- message: `redirect_to chain detected: "${enSlug}" \u2192 "${targetSlug}" \u2192 "${targetDoc.redirectTo}"`,
1364
- level: "error"
1471
+ locale,
1472
+ project.config.defaultLocale
1473
+ );
1474
+ out.push({
1475
+ source: `/${locale}/${enSlug}`,
1476
+ destination: urlBuilder.resolvePath(blogType.path, translatedSlug, locale),
1477
+ permanent: true
1365
1478
  });
1366
1479
  }
1367
1480
  }
1481
+ } finally {
1482
+ db.close();
1368
1483
  }
1369
- return issues;
1370
- }
1371
-
1372
- // src/i18n/translation-index.ts
1373
- function buildTranslationIndex(allDocs, locales, defaultLocale) {
1374
- const index = /* @__PURE__ */ new Map();
1375
- for (const locale of locales) {
1376
- if (locale === defaultLocale) continue;
1377
- const idx = allDocs.get(locale);
1378
- if (!idx) continue;
1379
- const localeMap = /* @__PURE__ */ new Map();
1380
- for (const [enSlug, doc] of idx.byEnSlug) {
1381
- localeMap.set(enSlug, doc.slug);
1382
- }
1383
- index.set(locale, localeMap);
1384
- }
1385
- return index;
1484
+ return out;
1386
1485
  }
1387
1486
 
1388
- // src/redirects/translation-index.ts
1389
- function buildRedirectTranslationIndex(project, typeId) {
1487
+ // src/redirects/build-redirects.ts
1488
+ function buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs) {
1390
1489
  const runtime = project.getType(typeId);
1391
1490
  const { config } = project;
1392
1491
  const merged = buildTranslationIndex(
@@ -1394,11 +1493,9 @@ function buildRedirectTranslationIndex(project, typeId) {
1394
1493
  config.locales,
1395
1494
  config.defaultLocale
1396
1495
  );
1397
- const { aliasToTarget } = buildGlobalAliasIndex(project);
1398
1496
  const db = openStore(project.config, "readonly");
1399
1497
  try {
1400
- for (const [alias, target] of aliasToTarget) {
1401
- if (target.contentTypeId !== typeId) continue;
1498
+ for (const alias of redirectSourceSlugs) {
1402
1499
  for (const row of listTranslationsForEnSlug(db, typeId, alias)) {
1403
1500
  const localeMap = merged.get(row.locale) ?? /* @__PURE__ */ new Map();
1404
1501
  if (!localeMap.has(alias)) {
@@ -1412,168 +1509,63 @@ function buildRedirectTranslationIndex(project, typeId) {
1412
1509
  }
1413
1510
  return merged;
1414
1511
  }
1415
-
1416
- // src/redirects/build-redirects.ts
1417
- function buildAliasRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
1418
- const out = [];
1419
- const { aliasToTarget } = aliasIndex;
1420
- const pathTemplate = type.path;
1421
- for (const [alias, target] of aliasToTarget) {
1422
- if (target.contentTypeId !== type.id) continue;
1423
- const canonical = target.canonicalSlug;
1424
- out.push({
1425
- source: resolvePath(pathTemplate, alias, defaultLocale, defaultLocale),
1426
- destination: resolvePath(pathTemplate, canonical, defaultLocale, defaultLocale),
1427
- permanent: true
1428
- });
1429
- for (const locale of prefixedLocales) {
1430
- const localeMap = translationIndex.get(locale);
1431
- const toLocale = localeMap?.get(canonical) ?? canonical;
1432
- const fromLocale = localeMap?.get(alias);
1433
- if (fromLocale) {
1434
- out.push({
1435
- source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
1436
- destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1437
- permanent: true
1438
- });
1439
- }
1440
- out.push({
1441
- source: resolvePath(pathTemplate, alias, locale, defaultLocale),
1442
- destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1443
- permanent: true
1444
- });
1445
- }
1446
- }
1447
- return out;
1448
- }
1449
- function buildAliasLegacyNoBlogPathRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
1450
- if (type.id !== "blog") return [];
1451
- const out = [];
1452
- for (const [alias, target] of aliasIndex.aliasToTarget) {
1453
- if (target.contentTypeId !== type.id) continue;
1454
- for (const locale of prefixedLocales) {
1455
- const localeMap = translationIndex.get(locale);
1456
- const toLocale = localeMap?.get(target.canonicalSlug) ?? target.canonicalSlug;
1457
- out.push({
1458
- source: `/${locale}/${alias}`,
1459
- destination: resolvePath(type.path, toLocale, locale, defaultLocale),
1460
- permanent: true
1461
- });
1462
- }
1463
- }
1464
- return out;
1465
- }
1466
- function buildRedirectToRules(type, config, translationIndex, prefixedLocales, defaultLocale) {
1467
- const out = [];
1468
- const pathTemplate = type.path;
1469
- for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
1470
- const doc = readEnDocument(config, type, enSlug);
1471
- if (!doc?.redirectTo) continue;
1472
- out.push({
1473
- source: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
1474
- destination: doc.redirectTo,
1475
- permanent: true
1476
- });
1477
- for (const locale of prefixedLocales) {
1478
- const localeMap = translationIndex.get(locale);
1479
- const fromLocale = localeMap?.get(enSlug);
1480
- if (!fromLocale) continue;
1481
- const targetEnSlug = extractSlugFromResolvedPath(pathTemplate, doc.redirectTo);
1482
- if (!targetEnSlug) continue;
1483
- const toLocale = localeMap?.get(targetEnSlug) ?? targetEnSlug;
1484
- out.push({
1485
- source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
1486
- destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
1487
- permanent: true
1488
- });
1489
- }
1490
- }
1491
- return out;
1492
- }
1493
- function buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale) {
1512
+ function buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs) {
1513
+ const type = project.getType(typeId).config;
1514
+ if (!isRoutableType(type)) return [];
1515
+ const urlBuilder = createUrlBuilder(project.config);
1516
+ const translationIndex = buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs);
1494
1517
  const out = [];
1495
1518
  const pathTemplate = type.path;
1496
1519
  for (const [locale, localeMap] of translationIndex) {
1497
1520
  for (const [enSlug, translatedSlug] of localeMap) {
1498
1521
  if (enSlug === translatedSlug) continue;
1499
- if (aliasSourceSlugs.has(enSlug)) continue;
1522
+ if (redirectSourceSlugs.has(enSlug)) continue;
1500
1523
  out.push({
1501
- source: resolvePath(pathTemplate, enSlug, locale, defaultLocale),
1502
- destination: resolvePath(pathTemplate, translatedSlug, locale, defaultLocale),
1524
+ source: urlBuilder.resolvePath(pathTemplate, enSlug, locale),
1525
+ destination: urlBuilder.resolvePath(pathTemplate, translatedSlug, locale),
1503
1526
  permanent: true
1504
1527
  });
1505
1528
  }
1506
1529
  }
1507
1530
  return out;
1508
1531
  }
1509
- function buildLegacyNoBlogPathRedirects(type, config, translationIndex, prefixedLocales, defaultLocale) {
1510
- if (type.id !== "blog") return [];
1511
- const out = [];
1512
- const enSlugs = listEnSlugs(config.rootDir, type.contentDir);
1513
- const pathTemplate = type.path;
1514
- for (const enSlug of enSlugs) {
1515
- out.push({
1516
- source: `/${enSlug}`,
1517
- destination: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
1518
- permanent: true
1519
- });
1520
- for (const locale of prefixedLocales) {
1521
- const translatedSlug = translationIndex.get(locale)?.get(enSlug);
1522
- const canonicalSlug = translatedSlug ?? enSlug;
1523
- out.push({
1524
- source: `/${locale}/${enSlug}`,
1525
- destination: resolvePath(pathTemplate, canonicalSlug, locale, defaultLocale),
1526
- permanent: true
1527
- });
1532
+ function buildTypeRedirects(project, typeId) {
1533
+ const loaded = loadAllTypeRedirects(project.config).find((file) => file.contentTypeId === typeId);
1534
+ const redirectSourceSlugs = /* @__PURE__ */ new Set();
1535
+ if (loaded) {
1536
+ for (const entry of loaded.entries) {
1537
+ for (const from of entry.fromSlugs) {
1538
+ redirectSourceSlugs.add(from);
1539
+ }
1528
1540
  }
1529
1541
  }
1530
- return out;
1531
- }
1532
- function buildTypeRedirects(project, typeId, aliasIndex, options) {
1533
- const type = project.getType(typeId).config;
1534
- if (!isRoutableType(type)) return [];
1535
- const { defaultLocale } = project.config;
1536
- const translationIndex = buildRedirectTranslationIndex(project, typeId);
1537
- const aliasSourceSlugs = /* @__PURE__ */ new Set();
1538
- for (const [alias, target] of aliasIndex.aliasToTarget) {
1539
- if (target.contentTypeId === typeId) aliasSourceSlugs.add(alias);
1540
- }
1541
1542
  return [
1542
- ...buildAliasRedirects(type, aliasIndex, translationIndex, options.prefixedLocales, defaultLocale),
1543
- ...buildAliasLegacyNoBlogPathRedirects(
1544
- type,
1545
- aliasIndex,
1546
- translationIndex,
1547
- options.prefixedLocales,
1548
- defaultLocale
1549
- ),
1550
- ...buildRedirectToRules(type, project.config, translationIndex, options.prefixedLocales, defaultLocale),
1551
- ...buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale),
1552
- ...buildLegacyNoBlogPathRedirects(type, project.config, translationIndex, options.prefixedLocales, defaultLocale)
1543
+ ...buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs)
1553
1544
  ];
1554
1545
  }
1555
- function buildAllContentRedirects(project, options) {
1556
- const aliasIndex = buildGlobalAliasIndex(project);
1557
- const out = [];
1546
+ function buildAllContentRedirects(project, _options = {}) {
1547
+ const redirectSourceKeys = buildRedirectSourceSlugSet(project);
1548
+ const out = [
1549
+ ...buildJsonRedirects(project),
1550
+ ...buildLegacyNoBlogPathRedirects(project, redirectSourceKeys)
1551
+ ];
1558
1552
  for (const type of project.config.types) {
1559
1553
  if (!isRoutableType(type)) continue;
1560
- out.push(...buildTypeRedirects(project, type.id, aliasIndex, options));
1554
+ out.push(...buildTypeRedirects(project, type.id));
1561
1555
  }
1562
1556
  return out;
1563
1557
  }
1564
1558
  function getRedirectSourceSlugs(project) {
1565
- const aliasIndex = buildGlobalAliasIndex(project);
1566
- const outboundByType = /* @__PURE__ */ new Map();
1559
+ const loaded = loadAllTypeRedirects(project.config);
1560
+ const aliasSlugs = collectRedirectSourceSlugs(loaded);
1561
+ const outboundByType = collectOutboundRedirectSourcesByType(loaded);
1567
1562
  for (const type of project.config.types) {
1568
- const outbound = /* @__PURE__ */ new Set();
1569
- for (const enSlug of listEnSlugs(project.config.rootDir, type.contentDir)) {
1570
- const doc = readEnDocument(project.config, type, enSlug);
1571
- if (doc?.redirectTo) outbound.add(enSlug);
1563
+ if (!outboundByType.has(type.id)) {
1564
+ outboundByType.set(type.id, /* @__PURE__ */ new Set());
1572
1565
  }
1573
- outboundByType.set(type.id, outbound);
1574
1566
  }
1575
1567
  return {
1576
- aliasSlugs: aliasIndex.allAliasSlugs,
1568
+ aliasSlugs,
1577
1569
  outboundByType
1578
1570
  };
1579
1571
  }
@@ -1581,13 +1573,12 @@ function getRedirectSourceSlugs(project) {
1581
1573
  // src/sitemap/join-base-url.ts
1582
1574
  function joinBaseUrl(baseUrl, pathname) {
1583
1575
  const origin = baseUrl.replace(/\/$/, "");
1584
- const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1585
- return `${origin}${path11}`;
1576
+ const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1577
+ return `${origin}${path13}`;
1586
1578
  }
1587
1579
 
1588
1580
  // src/sitemap/generate-sitemap.ts
1589
1581
  function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
1590
- if (enDoc.redirectTo) return false;
1591
1582
  if (redirectSources.aliasSlugs.has(enSlug)) return false;
1592
1583
  const outbound = redirectSources.outboundByType.get(contentTypeId);
1593
1584
  if (outbound?.has(enSlug)) return false;
@@ -1727,6 +1718,168 @@ function loadConfigSync(options = {}) {
1727
1718
  }
1728
1719
  return resolveConfig(config, path3.dirname(configPath));
1729
1720
  }
1721
+ function liveDocExists(config, typeId, enSlug) {
1722
+ const type = config.types.find((entry) => entry.id === typeId);
1723
+ if (!type) return false;
1724
+ return enFileExists(config, type, enSlug);
1725
+ }
1726
+ function resolveRedirectTarget(project, sourceTypeId, entry) {
1727
+ if (entry.kind === "anywhere") return null;
1728
+ if (entry.kind === "cross-type") {
1729
+ return { typeId: entry.toType, enSlug: entry.toSlug };
1730
+ }
1731
+ return { typeId: sourceTypeId, enSlug: entry.toSlug };
1732
+ }
1733
+ function matchRoutableTarget(project, toUrl) {
1734
+ const urlBuilder = createUrlBuilder(project.config);
1735
+ for (const type of project.config.types) {
1736
+ if (!type.path) continue;
1737
+ const enSlug = urlBuilder.extractSlugFromResolvedPath(type.path, toUrl);
1738
+ if (enSlug && liveDocExists(project.config, type.id, enSlug)) {
1739
+ return { typeId: type.id, enSlug };
1740
+ }
1741
+ }
1742
+ return null;
1743
+ }
1744
+ function validateTypeRedirects(project) {
1745
+ const issues = [];
1746
+ const globalFrom = /* @__PURE__ */ new Map();
1747
+ for (const type of project.config.types) {
1748
+ const filePath = path3.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1749
+ if (!fs2.existsSync(filePath)) continue;
1750
+ let loaded;
1751
+ try {
1752
+ loaded = loadTypeRedirectsFile(project.config, type);
1753
+ } catch (error) {
1754
+ issues.push({
1755
+ level: "error",
1756
+ contentType: type.id,
1757
+ field: TYPE_REDIRECTS_FILENAME,
1758
+ message: error instanceof Error ? error.message : String(error)
1759
+ });
1760
+ continue;
1761
+ }
1762
+ if (!loaded) continue;
1763
+ if (!isRoutableType(type)) {
1764
+ issues.push({
1765
+ level: "error",
1766
+ contentType: type.id,
1767
+ field: TYPE_REDIRECTS_FILENAME,
1768
+ message: `Redirects are only supported on routable content types (missing path)`
1769
+ });
1770
+ continue;
1771
+ }
1772
+ for (const entry of loaded.entries) {
1773
+ for (const from of entry.fromSlugs) {
1774
+ if (from === entry.toSlug) {
1775
+ issues.push({
1776
+ level: "error",
1777
+ contentType: type.id,
1778
+ enSlug: from,
1779
+ field: TYPE_REDIRECTS_FILENAME,
1780
+ message: `Redirect source "${from}" must not equal its target slug`
1781
+ });
1782
+ }
1783
+ if (liveDocExists(project.config, type.id, from)) {
1784
+ issues.push({
1785
+ level: "error",
1786
+ contentType: type.id,
1787
+ enSlug: from,
1788
+ field: TYPE_REDIRECTS_FILENAME,
1789
+ message: `Redirect source "${from}" still has a live EN file \u2014 delete or rename the MDX first`
1790
+ });
1791
+ }
1792
+ const existing = globalFrom.get(from);
1793
+ if (existing && existing.typeId !== type.id) {
1794
+ issues.push({
1795
+ level: "error",
1796
+ contentType: type.id,
1797
+ enSlug: from,
1798
+ field: TYPE_REDIRECTS_FILENAME,
1799
+ message: `Redirect source "${from}" is already claimed by ${existing.typeId}`
1800
+ });
1801
+ } else if (existing) {
1802
+ issues.push({
1803
+ level: "error",
1804
+ contentType: type.id,
1805
+ enSlug: from,
1806
+ field: TYPE_REDIRECTS_FILENAME,
1807
+ message: `Duplicate redirect source "${from}" in ${TYPE_REDIRECTS_FILENAME}`
1808
+ });
1809
+ } else {
1810
+ globalFrom.set(from, { typeId: type.id, filePath: loaded.filePath });
1811
+ }
1812
+ }
1813
+ if (entry.kind === "cross-type") {
1814
+ const targetType = project.config.types.find((candidate) => candidate.id === entry.toType);
1815
+ if (!targetType) {
1816
+ issues.push({
1817
+ level: "error",
1818
+ contentType: type.id,
1819
+ field: TYPE_REDIRECTS_FILENAME,
1820
+ message: `Unknown redirect target type "${entry.toType}"`
1821
+ });
1822
+ continue;
1823
+ }
1824
+ if (!isRoutableType(targetType)) {
1825
+ issues.push({
1826
+ level: "error",
1827
+ contentType: type.id,
1828
+ field: TYPE_REDIRECTS_FILENAME,
1829
+ message: `Redirect target type "${entry.toType}" is not routable (missing path)`
1830
+ });
1831
+ continue;
1832
+ }
1833
+ }
1834
+ const target = resolveRedirectTarget(project, type.id, entry);
1835
+ if (target) {
1836
+ if (!liveDocExists(project.config, target.typeId, target.enSlug)) {
1837
+ issues.push({
1838
+ level: "error",
1839
+ contentType: type.id,
1840
+ field: TYPE_REDIRECTS_FILENAME,
1841
+ message: `Redirect target ${target.typeId}/${target.enSlug} does not exist`
1842
+ });
1843
+ }
1844
+ continue;
1845
+ }
1846
+ if (entry.kind === "anywhere" && entry.toUrl?.startsWith("/")) {
1847
+ const matched = matchRoutableTarget(project, entry.toUrl);
1848
+ if (matched && !liveDocExists(project.config, matched.typeId, matched.enSlug)) {
1849
+ issues.push({
1850
+ level: "error",
1851
+ contentType: type.id,
1852
+ field: TYPE_REDIRECTS_FILENAME,
1853
+ message: `Redirect toUrl "${entry.toUrl}" does not resolve to a live document`
1854
+ });
1855
+ }
1856
+ }
1857
+ }
1858
+ }
1859
+ const allLoaded = loadAllTypeRedirects(project.config);
1860
+ for (const file of allLoaded) {
1861
+ for (const entry of file.entries) {
1862
+ if (entry.kind === "anywhere" || !entry.toSlug) continue;
1863
+ const targetTypeId = entry.kind === "cross-type" ? entry.toType : file.contentTypeId;
1864
+ const targetType = project.config.types.find((type) => type.id === targetTypeId);
1865
+ if (!targetType?.path) continue;
1866
+ for (const from of entry.fromSlugs) {
1867
+ if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
1868
+ const chainTarget = globalFrom.get(entry.toSlug);
1869
+ if (chainTarget) {
1870
+ issues.push({
1871
+ level: "error",
1872
+ contentType: file.contentTypeId,
1873
+ enSlug: from,
1874
+ field: TYPE_REDIRECTS_FILENAME,
1875
+ message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
1876
+ });
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ return issues;
1882
+ }
1730
1883
  function getAtPath2(obj, fieldPath) {
1731
1884
  let current = obj;
1732
1885
  for (const segment of fieldPath) {
@@ -1916,7 +2069,7 @@ function validateTranslationSlugSuffixes(config, db) {
1916
2069
  }
1917
2070
 
1918
2071
  // src/validate/validate-project.ts
1919
- function listEnSlugs2(rootDir, contentDir) {
2072
+ function listEnSlugs3(rootDir, contentDir) {
1920
2073
  const dir = path3.join(rootDir, contentDir);
1921
2074
  if (!fs2.existsSync(dir)) return [];
1922
2075
  return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
@@ -1934,7 +2087,7 @@ function validateProject(config) {
1934
2087
  }
1935
2088
  const db = openStore(config, "readonly");
1936
2089
  for (const type of config.types) {
1937
- const enSlugs = listEnSlugs2(config.rootDir, type.contentDir);
2090
+ const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
1938
2091
  const englishSlugs = new Set(enSlugs);
1939
2092
  if (!type.translate && isRoutableType(type)) {
1940
2093
  issues.push({
@@ -2016,33 +2169,8 @@ function validateProject(config) {
2016
2169
  }
2017
2170
  }
2018
2171
  db.close();
2019
- const aliasIndex = buildGlobalAliasIndex(project);
2020
- for (const issue of aliasIndex.issues) {
2021
- issues.push({
2022
- level: issue.level,
2023
- contentType: issue.contentTypeId,
2024
- enSlug: issue.enSlug,
2025
- field: issue.field,
2026
- message: issue.message
2027
- });
2028
- }
2029
- for (const issue of validateAliasRedirectChains(project)) {
2030
- issues.push({
2031
- level: issue.level,
2032
- contentType: issue.contentTypeId,
2033
- enSlug: issue.enSlug,
2034
- field: issue.field,
2035
- message: issue.message
2036
- });
2037
- }
2038
- for (const issue of validateAliasCoverage(project)) {
2039
- issues.push({
2040
- level: issue.level,
2041
- contentType: issue.contentTypeId,
2042
- enSlug: issue.enSlug,
2043
- field: issue.field,
2044
- message: issue.message
2045
- });
2172
+ for (const issue of validateTypeRedirects(project)) {
2173
+ issues.push(issue);
2046
2174
  }
2047
2175
  for (const issue of validateRelations(project)) {
2048
2176
  issues.push({
@@ -2079,7 +2207,7 @@ function computePageEnHash(translatableFrontmatter, body) {
2079
2207
  }
2080
2208
 
2081
2209
  // src/translate/worklist.ts
2082
- function listEnSlugs3(rootDir, contentDir) {
2210
+ function listEnSlugs4(rootDir, contentDir) {
2083
2211
  const dir = path3.join(rootDir, contentDir);
2084
2212
  if (!fs2.existsSync(dir)) return [];
2085
2213
  return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
@@ -2090,7 +2218,7 @@ function buildWorklist(config, options = {}) {
2090
2218
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
2091
2219
  for (const type of config.types) {
2092
2220
  if (options.contentType && type.id !== options.contentType) continue;
2093
- const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs3(config.rootDir, type.contentDir);
2221
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
2094
2222
  for (const enSlug of enSlugs) {
2095
2223
  const enDoc = readEnDocument(config, type, enSlug);
2096
2224
  if (!enDoc) continue;
@@ -2740,6 +2868,7 @@ function buildStaticRawExports(project, options = {}) {
2740
2868
  const excludeNoindex = options.excludeNoindex ?? false;
2741
2869
  const typeFilter = options.types ? new Set(options.types) : null;
2742
2870
  const out = [];
2871
+ const urlBuilder = createUrlBuilder(config);
2743
2872
  for (const type of project.listTypes()) {
2744
2873
  if (!isRoutableType(type.config)) continue;
2745
2874
  if (typeFilter && !typeFilter.has(type.id)) continue;
@@ -2755,7 +2884,7 @@ function buildStaticRawExports(project, options = {}) {
2755
2884
  if (excludeNoindex && resolved.document.noindex) continue;
2756
2885
  const doc = resolved.document;
2757
2886
  const slugWithExt = `${doc.slug}${extension}`;
2758
- const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
2887
+ const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
2759
2888
  out.push({
2760
2889
  relativePath: urlPath.slice(1),
2761
2890
  urlPath,
@@ -2798,6 +2927,6 @@ function writeStaticRawExports(project, options = {}) {
2798
2927
  return { exports, written: exports.length };
2799
2928
  }
2800
2929
 
2801
- export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, isRoutableType, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
2930
+ export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, createUrlBuilder, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, isRoutableType, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
2802
2931
  //# sourceMappingURL=index.js.map
2803
2932
  //# sourceMappingURL=index.js.map