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.
- package/README.md +5 -5
- package/dist/cli/index.cjs +432 -384
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +432 -384
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +642 -512
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +642 -513
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +287 -261
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +286 -261
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/builtin-fields.d.ts +8 -8
- package/dist/src/core/builtin-fields.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +11 -4
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/create-project.d.ts.map +1 -1
- package/dist/src/export/build-static-raw-exports.d.ts.map +1 -1
- package/dist/src/i18n/build-url.d.ts +16 -3
- package/dist/src/i18n/build-url.d.ts.map +1 -1
- package/dist/src/i18n/resolve-document.d.ts +3 -2
- package/dist/src/i18n/resolve-document.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/loader/create-loader.d.ts.map +1 -1
- package/dist/src/redirects/build-json-redirects.d.ts +6 -0
- package/dist/src/redirects/build-json-redirects.d.ts.map +1 -0
- package/dist/src/redirects/build-redirects.d.ts +9 -7
- package/dist/src/redirects/build-redirects.d.ts.map +1 -1
- package/dist/src/redirects/load-type-redirects.d.ts +14 -0
- package/dist/src/redirects/load-type-redirects.d.ts.map +1 -0
- package/dist/src/redirects/redirect-schema.d.ts +31 -0
- package/dist/src/redirects/redirect-schema.d.ts.map +1 -0
- package/dist/src/runtime.d.ts +3 -1
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -1
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/src/validate/validate-redirects.d.ts +4 -0
- package/dist/src/validate/validate-redirects.d.ts.map +1 -0
- package/dist/studio/server.cjs +90 -61
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js +90 -61
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/core/slug-aliases.d.ts +0 -27
- package/dist/src/core/slug-aliases.d.ts.map +0 -1
- package/dist/src/redirects/translation-index.d.ts +0 -4
- package/dist/src/redirects/translation-index.d.ts.map +0 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -237,14 +237,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
237
237
|
function isRoutableType(type) {
|
|
238
238
|
return typeof type.path === "string" && type.path.length > 0;
|
|
239
239
|
}
|
|
240
|
-
function assertValidPathTemplate(
|
|
241
|
-
if (!
|
|
242
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
240
|
+
function assertValidPathTemplate(path15, typeId) {
|
|
241
|
+
if (!path15.startsWith("/")) {
|
|
242
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path15}")`);
|
|
243
243
|
}
|
|
244
|
-
const count = (
|
|
244
|
+
const count = (path15.match(/\{slug\}/g) ?? []).length;
|
|
245
245
|
if (count !== 1) {
|
|
246
246
|
throw new Error(
|
|
247
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
247
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path15}")`
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -258,19 +258,69 @@ function pathSuffix(template) {
|
|
|
258
258
|
if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
|
|
259
259
|
return template.slice(idx + SLUG_PLACEHOLDER.length);
|
|
260
260
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
261
|
+
function resolveDefaultLocaleRouting() {
|
|
262
|
+
return { strategy: "path-prefix", prefixDefaultLocale: false };
|
|
263
|
+
}
|
|
264
|
+
function splitPathAndSearch(pathname) {
|
|
265
|
+
const q = pathname.indexOf("?");
|
|
266
|
+
if (q === -1) return { pathname, search: "" };
|
|
267
|
+
return { pathname: pathname.slice(0, q), search: pathname.slice(q) };
|
|
268
|
+
}
|
|
269
|
+
function createUrlBuilder(config) {
|
|
270
|
+
const localeRouting = config.localeRouting ?? resolveDefaultLocaleRouting();
|
|
271
|
+
const defaultLocale = config.defaultLocale;
|
|
272
|
+
const prefixedLocales = localeRouting.strategy === "path-prefix" ? localeRouting.prefixDefaultLocale ? [...config.locales] : config.locales.filter((locale) => locale !== defaultLocale) : config.locales.filter((locale) => locale !== defaultLocale);
|
|
273
|
+
function applyLocaleToPath(pathname, locale) {
|
|
274
|
+
if (locale === defaultLocale) {
|
|
275
|
+
if (localeRouting.strategy === "path-prefix" && localeRouting.prefixDefaultLocale) {
|
|
276
|
+
return `/${defaultLocale}${pathname}`;
|
|
277
|
+
}
|
|
278
|
+
return pathname;
|
|
279
|
+
}
|
|
280
|
+
if (localeRouting.strategy === "search-param") {
|
|
281
|
+
const { pathname: base, search } = splitPathAndSearch(pathname);
|
|
282
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
283
|
+
params.set(localeRouting.param, locale);
|
|
284
|
+
const qs = params.toString();
|
|
285
|
+
return qs ? `${base}?${qs}` : base;
|
|
286
|
+
}
|
|
287
|
+
return `/${locale}${pathname}`;
|
|
288
|
+
}
|
|
289
|
+
function resolvePath(template, slug, locale) {
|
|
290
|
+
const relative = template.replace(SLUG_PLACEHOLDER, slug);
|
|
291
|
+
return applyLocaleToPath(relative, locale);
|
|
292
|
+
}
|
|
293
|
+
function extractSlugFromResolvedPath(template, resolvedPath) {
|
|
294
|
+
let pathname = resolvedPath;
|
|
295
|
+
if (localeRouting.strategy === "search-param") {
|
|
296
|
+
pathname = splitPathAndSearch(resolvedPath).pathname;
|
|
297
|
+
} else if (localeRouting.strategy === "path-prefix") {
|
|
298
|
+
for (const locale of config.locales) {
|
|
299
|
+
if (locale === defaultLocale && !localeRouting.prefixDefaultLocale) continue;
|
|
300
|
+
const prefix2 = `/${locale}`;
|
|
301
|
+
if (pathname === prefix2 || pathname.startsWith(`${prefix2}/`)) {
|
|
302
|
+
pathname = pathname.slice(prefix2.length) || "/";
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const prefix = pathPrefix(template);
|
|
308
|
+
const suffix = pathSuffix(template);
|
|
309
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
310
|
+
if (suffix && !pathname.endsWith(suffix)) return null;
|
|
311
|
+
const slugEnd = suffix ? pathname.length - suffix.length : pathname.length;
|
|
312
|
+
const slug = pathname.slice(prefix.length, slugEnd);
|
|
313
|
+
return slug.length > 0 ? slug : null;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
defaultLocale,
|
|
317
|
+
locales: config.locales,
|
|
318
|
+
localeRouting,
|
|
319
|
+
prefixedLocales,
|
|
320
|
+
resolvePath,
|
|
321
|
+
extractSlugFromResolvedPath,
|
|
322
|
+
applyLocaleToPath
|
|
323
|
+
};
|
|
274
324
|
}
|
|
275
325
|
|
|
276
326
|
// src/config/resolve-config.ts
|
|
@@ -293,6 +343,13 @@ function resolveConfig(input, baseDir) {
|
|
|
293
343
|
`scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
|
|
294
344
|
);
|
|
295
345
|
}
|
|
346
|
+
const localeRouting = raw.localeRouting ?? {
|
|
347
|
+
strategy: "path-prefix",
|
|
348
|
+
prefixDefaultLocale: false
|
|
349
|
+
};
|
|
350
|
+
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
351
|
+
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
352
|
+
}
|
|
296
353
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
297
354
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
298
355
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -320,6 +377,7 @@ function resolveConfig(input, baseDir) {
|
|
|
320
377
|
assetsPath,
|
|
321
378
|
locales: [...raw.locales],
|
|
322
379
|
defaultLocale,
|
|
380
|
+
localeRouting,
|
|
323
381
|
localePresets: raw.localePresets,
|
|
324
382
|
translate: raw.translate,
|
|
325
383
|
types
|
|
@@ -374,10 +432,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
374
432
|
}
|
|
375
433
|
function extractByPaths(data, paths) {
|
|
376
434
|
const out = {};
|
|
377
|
-
for (const
|
|
378
|
-
const value = getAtPath(data,
|
|
435
|
+
for (const path15 of paths) {
|
|
436
|
+
const value = getAtPath(data, path15);
|
|
379
437
|
if (value !== void 0) {
|
|
380
|
-
setAtPath(out,
|
|
438
|
+
setAtPath(out, path15.filter((p) => p !== "*"), value);
|
|
381
439
|
}
|
|
382
440
|
}
|
|
383
441
|
return out;
|
|
@@ -395,13 +453,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
395
453
|
const translatable = pickTranslatable(localeData, schema);
|
|
396
454
|
return deepMerge(structural, translatable);
|
|
397
455
|
}
|
|
398
|
-
function getAtPath(obj,
|
|
456
|
+
function getAtPath(obj, path15) {
|
|
399
457
|
let current = obj;
|
|
400
|
-
for (const segment of
|
|
458
|
+
for (const segment of path15) {
|
|
401
459
|
if (segment === "*") {
|
|
402
460
|
if (!Array.isArray(current)) return void 0;
|
|
403
461
|
return current.map(
|
|
404
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
462
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path15.slice(path15.indexOf("*") + 1)) : void 0
|
|
405
463
|
);
|
|
406
464
|
}
|
|
407
465
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -409,13 +467,13 @@ function getAtPath(obj, path13) {
|
|
|
409
467
|
}
|
|
410
468
|
return current;
|
|
411
469
|
}
|
|
412
|
-
function setAtPath(obj,
|
|
413
|
-
if (
|
|
414
|
-
if (
|
|
415
|
-
obj[
|
|
470
|
+
function setAtPath(obj, path15, value) {
|
|
471
|
+
if (path15.length === 0) return;
|
|
472
|
+
if (path15.length === 1) {
|
|
473
|
+
obj[path15[0]] = value;
|
|
416
474
|
return;
|
|
417
475
|
}
|
|
418
|
-
const [head, ...rest] =
|
|
476
|
+
const [head, ...rest] = path15;
|
|
419
477
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
420
478
|
obj[head] = {};
|
|
421
479
|
}
|
|
@@ -466,45 +524,23 @@ var LOCALE_BUILTIN_KEYS = [
|
|
|
466
524
|
["translationOf", "translationOf is internal; remove from locale translation"],
|
|
467
525
|
["enSlug", "enSlug is internal; remove from locale translation"]
|
|
468
526
|
];
|
|
469
|
-
|
|
527
|
+
var DEPRECATED_REDIRECT_FIELDS = [
|
|
528
|
+
[
|
|
529
|
+
"aliases",
|
|
530
|
+
"aliases frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
531
|
+
],
|
|
532
|
+
[
|
|
533
|
+
"redirect_to",
|
|
534
|
+
"redirect_to frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
535
|
+
]
|
|
536
|
+
];
|
|
537
|
+
function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
|
|
470
538
|
const issues = [];
|
|
471
539
|
const rest = { ...data };
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let redirectTo;
|
|
477
|
-
if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
|
|
478
|
-
if (typeof redirectRaw !== "string") {
|
|
479
|
-
issues.push({
|
|
480
|
-
field: "redirect_to",
|
|
481
|
-
message: "redirect_to must be a string path",
|
|
482
|
-
level: "error"
|
|
483
|
-
});
|
|
484
|
-
} else if (!pathTemplate) {
|
|
485
|
-
issues.push({
|
|
486
|
-
field: "redirect_to",
|
|
487
|
-
message: "redirect_to is not allowed on reference-only content types",
|
|
488
|
-
level: "error"
|
|
489
|
-
});
|
|
490
|
-
} else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
|
|
491
|
-
issues.push({
|
|
492
|
-
field: "redirect_to",
|
|
493
|
-
message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
|
|
494
|
-
level: "error"
|
|
495
|
-
});
|
|
496
|
-
} else {
|
|
497
|
-
redirectTo = redirectRaw;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const aliases = aliasesResult.success ? aliasesResult.data : [];
|
|
501
|
-
if (!aliasesResult.success) {
|
|
502
|
-
for (const issue of aliasesResult.error.issues) {
|
|
503
|
-
issues.push({
|
|
504
|
-
field: `aliases.${issue.path.join(".")}`,
|
|
505
|
-
message: issue.message,
|
|
506
|
-
level: "error"
|
|
507
|
-
});
|
|
540
|
+
for (const [field2, message] of DEPRECATED_REDIRECT_FIELDS) {
|
|
541
|
+
if (rest[field2] !== void 0) {
|
|
542
|
+
issues.push({ field: field2, message, level: "error" });
|
|
543
|
+
delete rest[field2];
|
|
508
544
|
}
|
|
509
545
|
}
|
|
510
546
|
let publishedAt;
|
|
@@ -566,8 +602,6 @@ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
|
|
|
566
602
|
}
|
|
567
603
|
return {
|
|
568
604
|
builtin: {
|
|
569
|
-
aliases,
|
|
570
|
-
redirectTo,
|
|
571
605
|
publishedAt,
|
|
572
606
|
updatedAt,
|
|
573
607
|
noindex,
|
|
@@ -586,20 +620,24 @@ function validateLocaleBuiltinFields(data) {
|
|
|
586
620
|
}
|
|
587
621
|
return issues;
|
|
588
622
|
}
|
|
589
|
-
function resolveCanonicalPathname(type, doc, defaultLocale) {
|
|
623
|
+
function resolveCanonicalPathname(type, doc, defaultLocale, localeRouting) {
|
|
624
|
+
const urlBuilder = createUrlBuilder({
|
|
625
|
+
locales: [defaultLocale, ...doc.locale !== defaultLocale ? [doc.locale] : []],
|
|
626
|
+
defaultLocale,
|
|
627
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
628
|
+
});
|
|
590
629
|
if (doc.canonicalPathOverride) {
|
|
591
|
-
|
|
592
|
-
return `/${doc.locale}${doc.canonicalPathOverride}`;
|
|
630
|
+
return urlBuilder.applyLocaleToPath(doc.canonicalPathOverride, doc.locale);
|
|
593
631
|
}
|
|
594
632
|
if (!type.path) return `/${doc.slug}`;
|
|
595
|
-
return resolvePath(type.path, doc.slug, doc.locale
|
|
633
|
+
return urlBuilder.resolvePath(type.path, doc.slug, doc.locale);
|
|
596
634
|
}
|
|
597
|
-
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
|
|
635
|
+
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, localeRouting) {
|
|
598
636
|
const out = { ...frontmatter };
|
|
599
637
|
if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
|
|
600
638
|
if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
|
|
601
639
|
out.noindex = doc.noindex;
|
|
602
|
-
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
|
|
640
|
+
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
|
|
603
641
|
return out;
|
|
604
642
|
}
|
|
605
643
|
function seoFieldsFromEn(enDoc) {
|
|
@@ -607,8 +645,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
607
645
|
publishedAt: enDoc.publishedAt,
|
|
608
646
|
updatedAt: enDoc.updatedAt,
|
|
609
647
|
noindex: enDoc.noindex,
|
|
610
|
-
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
611
|
-
redirectTo: enDoc.redirectTo
|
|
648
|
+
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
612
649
|
};
|
|
613
650
|
}
|
|
614
651
|
|
|
@@ -683,11 +720,6 @@ function getTranslation(db, contentType, enSlug, locale) {
|
|
|
683
720
|
function listTranslationsForType(db, contentType) {
|
|
684
721
|
return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
|
|
685
722
|
}
|
|
686
|
-
function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
687
|
-
return db.prepare(
|
|
688
|
-
`SELECT * FROM translations WHERE content_type = ? AND en_slug = ? ORDER BY locale`
|
|
689
|
-
).all(contentType, enSlug);
|
|
690
|
-
}
|
|
691
723
|
function listTranslationsForLocale(db, contentType, locale) {
|
|
692
724
|
return db.prepare(`SELECT * FROM translations WHERE content_type = ? AND locale = ? ORDER BY en_slug`).all(contentType, locale);
|
|
693
725
|
}
|
|
@@ -771,7 +803,8 @@ function parseEnMdx(filePath, config, type) {
|
|
|
771
803
|
locale: config.defaultLocale
|
|
772
804
|
},
|
|
773
805
|
type,
|
|
774
|
-
config.defaultLocale
|
|
806
|
+
config.defaultLocale,
|
|
807
|
+
config.localeRouting
|
|
775
808
|
);
|
|
776
809
|
const crossIssues = type.crossValidate?.(result.data, {
|
|
777
810
|
locale: config.defaultLocale,
|
|
@@ -785,8 +818,6 @@ function parseEnMdx(filePath, config, type) {
|
|
|
785
818
|
slug,
|
|
786
819
|
enSlug: slug,
|
|
787
820
|
locale: config.defaultLocale,
|
|
788
|
-
aliases: builtin.aliases,
|
|
789
|
-
redirectTo: builtin.redirectTo,
|
|
790
821
|
publishedAt: builtin.publishedAt,
|
|
791
822
|
updatedAt: builtin.updatedAt,
|
|
792
823
|
noindex: builtin.noindex,
|
|
@@ -797,7 +828,7 @@ function parseEnMdx(filePath, config, type) {
|
|
|
797
828
|
};
|
|
798
829
|
return { document: document2, issues };
|
|
799
830
|
}
|
|
800
|
-
function buildDocumentFromTranslation(row, enDoc, type) {
|
|
831
|
+
function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
801
832
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
802
833
|
const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
|
|
803
834
|
const seo = seoFieldsFromEn(enDoc);
|
|
@@ -805,14 +836,13 @@ function buildDocumentFromTranslation(row, enDoc, type) {
|
|
|
805
836
|
merged,
|
|
806
837
|
{ ...seo, slug: row.slug, locale: row.locale },
|
|
807
838
|
type,
|
|
808
|
-
|
|
839
|
+
config.defaultLocale,
|
|
840
|
+
config.localeRouting
|
|
809
841
|
);
|
|
810
842
|
return {
|
|
811
843
|
slug: row.slug,
|
|
812
844
|
enSlug: row.en_slug,
|
|
813
845
|
locale: row.locale,
|
|
814
|
-
aliases: [],
|
|
815
|
-
redirectTo: seo.redirectTo,
|
|
816
846
|
publishedAt: seo.publishedAt,
|
|
817
847
|
updatedAt: seo.updatedAt,
|
|
818
848
|
noindex: seo.noindex,
|
|
@@ -890,7 +920,7 @@ function createContentLoader(config, type) {
|
|
|
890
920
|
for (const row of rowsByLocale.get(locale) ?? []) {
|
|
891
921
|
const enDoc = englishBySlug.get(row.en_slug);
|
|
892
922
|
if (!enDoc) continue;
|
|
893
|
-
const doc = buildDocumentFromTranslation(row, enDoc, type);
|
|
923
|
+
const doc = buildDocumentFromTranslation(row, enDoc, type, config);
|
|
894
924
|
bySlug.set(doc.slug, doc);
|
|
895
925
|
byEnSlug.set(row.en_slug, doc);
|
|
896
926
|
}
|
|
@@ -939,56 +969,16 @@ function getTranslatablePayload(doc, type) {
|
|
|
939
969
|
|
|
940
970
|
// src/i18n/resolve-document.ts
|
|
941
971
|
init_cjs_shims();
|
|
942
|
-
function
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
|
-
function enCanonicalDoc(doc, allDocs, defaultLocale) {
|
|
951
|
-
if (doc.locale === defaultLocale) return doc;
|
|
952
|
-
return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
|
|
953
|
-
}
|
|
954
|
-
function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
|
|
955
|
-
if (!type.path) return void 0;
|
|
956
|
-
const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
|
|
957
|
-
if (!enDoc.redirectTo) return void 0;
|
|
958
|
-
const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
|
|
959
|
-
if (!targetEnSlug) return void 0;
|
|
960
|
-
const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
|
|
961
|
-
const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
|
|
962
|
-
return resolvePath(type.path, targetSlug, locale, defaultLocale);
|
|
963
|
-
}
|
|
964
|
-
function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
|
|
965
|
-
if (result.shouldRedirectTo || !result.document) return result;
|
|
966
|
-
const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
|
|
967
|
-
if (!redirect) return result;
|
|
968
|
-
return {
|
|
969
|
-
document: null,
|
|
970
|
-
actualLocale: result.actualLocale,
|
|
971
|
-
shouldRedirectTo: redirect
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
972
|
+
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
|
|
973
|
+
const urlBuilder = createUrlBuilder({
|
|
974
|
+
locales: [defaultLocale, locale],
|
|
975
|
+
defaultLocale,
|
|
976
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
977
|
+
});
|
|
975
978
|
const idx = allDocs.get(locale);
|
|
976
979
|
const direct = idx?.bySlug.get(slug);
|
|
977
980
|
if (direct) {
|
|
978
|
-
return
|
|
979
|
-
}
|
|
980
|
-
const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
|
|
981
|
-
if (aliasDoc && type.path) {
|
|
982
|
-
const canonicalSlug = aliasDoc.slug;
|
|
983
|
-
const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
|
|
984
|
-
const targetSlug = localizedSlug ?? canonicalSlug;
|
|
985
|
-
if (targetSlug !== slug) {
|
|
986
|
-
return {
|
|
987
|
-
document: null,
|
|
988
|
-
actualLocale: locale,
|
|
989
|
-
shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
|
|
990
|
-
};
|
|
991
|
-
}
|
|
981
|
+
return { document: direct, actualLocale: locale };
|
|
992
982
|
}
|
|
993
983
|
for (const [docLocale, docIdx] of allDocs) {
|
|
994
984
|
const found = docIdx.bySlug.get(slug);
|
|
@@ -1001,17 +991,11 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
1001
991
|
return {
|
|
1002
992
|
document: null,
|
|
1003
993
|
actualLocale: locale,
|
|
1004
|
-
shouldRedirectTo: resolvePath(type.path, correctSlug, locale
|
|
994
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
|
|
1005
995
|
};
|
|
1006
996
|
}
|
|
1007
997
|
if (docLocale === defaultLocale) {
|
|
1008
|
-
return
|
|
1009
|
-
{ document: found, actualLocale: defaultLocale },
|
|
1010
|
-
locale,
|
|
1011
|
-
defaultLocale,
|
|
1012
|
-
allDocs,
|
|
1013
|
-
type
|
|
1014
|
-
);
|
|
998
|
+
return { document: found, actualLocale: defaultLocale };
|
|
1015
999
|
}
|
|
1016
1000
|
if (!type.path) {
|
|
1017
1001
|
return { document: null, actualLocale: locale };
|
|
@@ -1019,39 +1003,21 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
1019
1003
|
return {
|
|
1020
1004
|
document: null,
|
|
1021
1005
|
actualLocale: locale,
|
|
1022
|
-
shouldRedirectTo: resolvePath(type.path, found.enSlug, locale
|
|
1006
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, found.enSlug, locale)
|
|
1023
1007
|
};
|
|
1024
1008
|
}
|
|
1025
1009
|
if (locale !== defaultLocale) {
|
|
1026
1010
|
const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
1027
1011
|
if (enDoc && type.indexFallback === "en") {
|
|
1028
|
-
return
|
|
1029
|
-
{ document: enDoc, actualLocale: defaultLocale },
|
|
1030
|
-
locale,
|
|
1031
|
-
defaultLocale,
|
|
1032
|
-
allDocs,
|
|
1033
|
-
type
|
|
1034
|
-
);
|
|
1012
|
+
return { document: enDoc, actualLocale: defaultLocale };
|
|
1035
1013
|
}
|
|
1036
1014
|
const byEn = idx?.byEnSlug.get(slug);
|
|
1037
1015
|
if (byEn) {
|
|
1038
|
-
return
|
|
1039
|
-
{ document: byEn, actualLocale: locale },
|
|
1040
|
-
locale,
|
|
1041
|
-
defaultLocale,
|
|
1042
|
-
allDocs,
|
|
1043
|
-
type
|
|
1044
|
-
);
|
|
1016
|
+
return { document: byEn, actualLocale: locale };
|
|
1045
1017
|
}
|
|
1046
1018
|
const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
1047
1019
|
if (fallback && type.indexFallback === "en") {
|
|
1048
|
-
return
|
|
1049
|
-
{ document: fallback, actualLocale: defaultLocale },
|
|
1050
|
-
locale,
|
|
1051
|
-
defaultLocale,
|
|
1052
|
-
allDocs,
|
|
1053
|
-
type
|
|
1054
|
-
);
|
|
1020
|
+
return { document: fallback, actualLocale: defaultLocale };
|
|
1055
1021
|
}
|
|
1056
1022
|
}
|
|
1057
1023
|
return { document: null, actualLocale: locale };
|
|
@@ -1093,6 +1059,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1093
1059
|
}
|
|
1094
1060
|
return type.path;
|
|
1095
1061
|
}
|
|
1062
|
+
const urlBuilder = createUrlBuilder(config);
|
|
1096
1063
|
const runtime = {
|
|
1097
1064
|
id: type.id,
|
|
1098
1065
|
config: type,
|
|
@@ -1108,15 +1075,21 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1108
1075
|
return load().get(locale)?.bySlug.get(slug) ?? null;
|
|
1109
1076
|
},
|
|
1110
1077
|
resolve(slug, locale) {
|
|
1111
|
-
const result = resolveLocalizedDocument(
|
|
1078
|
+
const result = resolveLocalizedDocument(
|
|
1079
|
+
slug,
|
|
1080
|
+
locale,
|
|
1081
|
+
config.defaultLocale,
|
|
1082
|
+
load(),
|
|
1083
|
+
type,
|
|
1084
|
+
config.localeRouting
|
|
1085
|
+
);
|
|
1112
1086
|
if (result.document && type.path) {
|
|
1113
1087
|
return {
|
|
1114
1088
|
...result,
|
|
1115
|
-
canonicalPath: resolvePath(
|
|
1089
|
+
canonicalPath: urlBuilder.resolvePath(
|
|
1116
1090
|
type.path,
|
|
1117
1091
|
result.document.slug,
|
|
1118
|
-
result.actualLocale
|
|
1119
|
-
config.defaultLocale
|
|
1092
|
+
result.actualLocale
|
|
1120
1093
|
)
|
|
1121
1094
|
};
|
|
1122
1095
|
}
|
|
@@ -1140,10 +1113,9 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1140
1113
|
alternates(doc) {
|
|
1141
1114
|
const pathTemplate = assertRoutable("alternates");
|
|
1142
1115
|
const out = {
|
|
1143
|
-
[config.defaultLocale]: resolvePath(
|
|
1116
|
+
[config.defaultLocale]: urlBuilder.resolvePath(
|
|
1144
1117
|
pathTemplate,
|
|
1145
1118
|
doc.enSlug,
|
|
1146
|
-
config.defaultLocale,
|
|
1147
1119
|
config.defaultLocale
|
|
1148
1120
|
)
|
|
1149
1121
|
};
|
|
@@ -1152,7 +1124,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1152
1124
|
if (locale === config.defaultLocale) continue;
|
|
1153
1125
|
const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
|
|
1154
1126
|
if (translated) {
|
|
1155
|
-
out[locale] = resolvePath(pathTemplate, translated.slug, locale
|
|
1127
|
+
out[locale] = urlBuilder.resolvePath(pathTemplate, translated.slug, locale);
|
|
1156
1128
|
}
|
|
1157
1129
|
}
|
|
1158
1130
|
return out;
|
|
@@ -1167,7 +1139,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1167
1139
|
},
|
|
1168
1140
|
url(slug, locale) {
|
|
1169
1141
|
const pathTemplate = assertRoutable("url");
|
|
1170
|
-
return resolvePath(pathTemplate, slug, locale
|
|
1142
|
+
return urlBuilder.resolvePath(pathTemplate, slug, locale);
|
|
1171
1143
|
},
|
|
1172
1144
|
related(doc, fieldName, locale) {
|
|
1173
1145
|
const meta = relationFields.get(fieldName);
|
|
@@ -1236,208 +1208,141 @@ init_cjs_shims();
|
|
|
1236
1208
|
// src/redirects/build-redirects.ts
|
|
1237
1209
|
init_cjs_shims();
|
|
1238
1210
|
|
|
1239
|
-
// src/core/slug-aliases.ts
|
|
1240
|
-
init_cjs_shims();
|
|
1241
|
-
|
|
1242
1211
|
// src/core/alias-helpers.ts
|
|
1243
1212
|
init_cjs_shims();
|
|
1244
1213
|
function enFileExists(config, type, enSlug) {
|
|
1245
1214
|
const dir = path3__default.default.join(config.rootDir, type.contentDir);
|
|
1246
1215
|
return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
|
|
1247
1216
|
}
|
|
1248
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1249
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1250
|
-
}
|
|
1251
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1252
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1253
|
-
}
|
|
1254
1217
|
function listEnSlugs(rootDir, contentDir) {
|
|
1255
1218
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1256
1219
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1257
1220
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1258
1221
|
}
|
|
1259
1222
|
|
|
1260
|
-
// src/
|
|
1223
|
+
// src/i18n/translation-index.ts
|
|
1224
|
+
init_cjs_shims();
|
|
1225
|
+
|
|
1226
|
+
// src/redirects/build-redirects.ts
|
|
1261
1227
|
init_sqlite();
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1228
|
+
|
|
1229
|
+
// src/redirects/build-json-redirects.ts
|
|
1230
|
+
init_cjs_shims();
|
|
1231
|
+
init_sqlite();
|
|
1232
|
+
|
|
1233
|
+
// src/redirects/load-type-redirects.ts
|
|
1234
|
+
init_cjs_shims();
|
|
1235
|
+
|
|
1236
|
+
// src/redirects/redirect-schema.ts
|
|
1237
|
+
init_cjs_shims();
|
|
1238
|
+
var redirectFromSchema = zod.z.union([
|
|
1239
|
+
slugPatternSchema,
|
|
1240
|
+
zod.z.array(slugPatternSchema).min(1).max(20)
|
|
1241
|
+
]);
|
|
1242
|
+
var typeRedirectEntrySchema = zod.z.object({
|
|
1243
|
+
from: redirectFromSchema,
|
|
1244
|
+
toSlug: slugPatternSchema.optional(),
|
|
1245
|
+
toType: zod.z.string().min(1).optional(),
|
|
1246
|
+
toUrl: zod.z.string().min(1).optional(),
|
|
1247
|
+
permanent: zod.z.boolean().optional()
|
|
1248
|
+
}).superRefine((entry, ctx) => {
|
|
1249
|
+
const hasToSlug = entry.toSlug !== void 0;
|
|
1250
|
+
const hasToUrl = entry.toUrl !== void 0;
|
|
1251
|
+
const hasToType = entry.toType !== void 0;
|
|
1252
|
+
const targetCount = Number(hasToSlug) + Number(hasToUrl);
|
|
1253
|
+
if (targetCount !== 1) {
|
|
1254
|
+
ctx.addIssue({
|
|
1255
|
+
code: "custom",
|
|
1256
|
+
message: "Each redirect must specify exactly one of toSlug/toType+toSlug or toUrl",
|
|
1257
|
+
path: ["toSlug"]
|
|
1258
|
+
});
|
|
1259
|
+
return;
|
|
1271
1260
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
level: "error"
|
|
1286
|
-
});
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (canonicalSlugs.has(alias)) {
|
|
1290
|
-
issues.push({
|
|
1291
|
-
contentTypeId: type.id,
|
|
1292
|
-
enSlug,
|
|
1293
|
-
field: "aliases",
|
|
1294
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1295
|
-
level: "error"
|
|
1296
|
-
});
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const existing = aliasToTarget.get(alias);
|
|
1300
|
-
if (existing) {
|
|
1301
|
-
issues.push({
|
|
1302
|
-
contentTypeId: type.id,
|
|
1303
|
-
enSlug,
|
|
1304
|
-
field: "aliases",
|
|
1305
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1306
|
-
level: "error"
|
|
1307
|
-
});
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
aliasToTarget.set(alias, {
|
|
1311
|
-
contentTypeId: type.id,
|
|
1312
|
-
canonicalSlug: enSlug,
|
|
1313
|
-
path: type.path
|
|
1314
|
-
});
|
|
1315
|
-
allAliasSlugs.add(alias);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1261
|
+
if (hasToUrl && hasToType) {
|
|
1262
|
+
ctx.addIssue({
|
|
1263
|
+
code: "custom",
|
|
1264
|
+
message: "toUrl cannot be combined with toType",
|
|
1265
|
+
path: ["toUrl"]
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
if (hasToType && !hasToSlug) {
|
|
1269
|
+
ctx.addIssue({
|
|
1270
|
+
code: "custom",
|
|
1271
|
+
message: "Cross-type redirects require both toType and toSlug",
|
|
1272
|
+
path: ["toSlug"]
|
|
1273
|
+
});
|
|
1318
1274
|
}
|
|
1319
|
-
|
|
1275
|
+
});
|
|
1276
|
+
var typeRedirectsFileSchema = zod.z.object({
|
|
1277
|
+
redirects: zod.z.array(typeRedirectEntrySchema).max(500)
|
|
1278
|
+
});
|
|
1279
|
+
function normalizeRedirectFrom(from) {
|
|
1280
|
+
return Array.isArray(from) ? from : [from];
|
|
1320
1281
|
}
|
|
1321
|
-
function
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1331
|
-
if (!known) {
|
|
1332
|
-
issues.push({
|
|
1333
|
-
contentTypeId: type.id,
|
|
1334
|
-
enSlug: target.canonicalSlug,
|
|
1335
|
-
field: "aliases",
|
|
1336
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1337
|
-
level: "warning"
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
if (enFileExists(config, type, alias)) {
|
|
1341
|
-
issues.push({
|
|
1342
|
-
contentTypeId: type.id,
|
|
1343
|
-
enSlug: target.canonicalSlug,
|
|
1344
|
-
field: "aliases",
|
|
1345
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1346
|
-
level: "warning"
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
const sqliteRows = listTranslationsForEnSlug(db, type.id, alias);
|
|
1350
|
-
if (sqliteRows.length > 0) {
|
|
1351
|
-
issues.push({
|
|
1352
|
-
contentTypeId: type.id,
|
|
1353
|
-
enSlug: target.canonicalSlug,
|
|
1354
|
-
field: "aliases",
|
|
1355
|
-
message: `${sqliteRows.length} sqlite translation(s) for alias "${alias}" retained for locale redirects and history`,
|
|
1356
|
-
level: "info"
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
for (const locale of config.locales) {
|
|
1360
|
-
if (locale === config.defaultLocale) continue;
|
|
1361
|
-
const canonicalRow = listTranslationsForEnSlug(db, type.id, target.canonicalSlug).find(
|
|
1362
|
-
(r) => r.locale === locale
|
|
1363
|
-
);
|
|
1364
|
-
if (!canonicalRow) continue;
|
|
1365
|
-
const aliasRow = sqliteRows.find((r) => r.locale === locale);
|
|
1366
|
-
if (!aliasRow && known) {
|
|
1367
|
-
issues.push({
|
|
1368
|
-
contentTypeId: type.id,
|
|
1369
|
-
enSlug: target.canonicalSlug,
|
|
1370
|
-
field: "aliases",
|
|
1371
|
-
message: `Alias "${alias}" has no locale slug mapping for ${locale} \u2014 old localized URL may miss redirect`,
|
|
1372
|
-
level: "warning"
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
} finally {
|
|
1378
|
-
db.close();
|
|
1282
|
+
function parseRedirectEntry(entry) {
|
|
1283
|
+
const fromSlugs = normalizeRedirectFrom(entry.from);
|
|
1284
|
+
if (entry.toUrl !== void 0) {
|
|
1285
|
+
return {
|
|
1286
|
+
fromSlugs,
|
|
1287
|
+
kind: "anywhere",
|
|
1288
|
+
toUrl: entry.toUrl,
|
|
1289
|
+
permanent: entry.permanent ?? true
|
|
1290
|
+
};
|
|
1379
1291
|
}
|
|
1380
|
-
|
|
1292
|
+
if (entry.toType !== void 0) {
|
|
1293
|
+
return {
|
|
1294
|
+
fromSlugs,
|
|
1295
|
+
kind: "cross-type",
|
|
1296
|
+
toType: entry.toType,
|
|
1297
|
+
toSlug: entry.toSlug,
|
|
1298
|
+
permanent: entry.permanent ?? true
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
fromSlugs,
|
|
1303
|
+
kind: "same-type",
|
|
1304
|
+
toSlug: entry.toSlug,
|
|
1305
|
+
permanent: entry.permanent ?? true
|
|
1306
|
+
};
|
|
1381
1307
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1308
|
+
|
|
1309
|
+
// src/redirects/load-type-redirects.ts
|
|
1310
|
+
var TYPE_REDIRECTS_FILENAME = "_redirects.json";
|
|
1311
|
+
function redirectsFilePath(config, type) {
|
|
1312
|
+
return path3__default.default.join(config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1313
|
+
}
|
|
1314
|
+
function loadTypeRedirectsFile(config, type) {
|
|
1315
|
+
const filePath = redirectsFilePath(config, type);
|
|
1316
|
+
if (!fs2__default.default.existsSync(filePath)) return null;
|
|
1317
|
+
let raw;
|
|
1318
|
+
try {
|
|
1319
|
+
raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
|
|
1391
1323
|
);
|
|
1392
|
-
if (doc?.redirectTo) {
|
|
1393
|
-
issues.push({
|
|
1394
|
-
contentTypeId: target.contentTypeId,
|
|
1395
|
-
enSlug: target.canonicalSlug,
|
|
1396
|
-
field: "aliases",
|
|
1397
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1398
|
-
level: "error"
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
1324
|
}
|
|
1325
|
+
const parsed = typeRedirectsFileSchema.safeParse(raw);
|
|
1326
|
+
if (!parsed.success) {
|
|
1327
|
+
const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
|
|
1328
|
+
throw new Error(`${type.id}: invalid ${TYPE_REDIRECTS_FILENAME}: ${details}`);
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
contentTypeId: type.id,
|
|
1332
|
+
contentDir: type.contentDir,
|
|
1333
|
+
filePath,
|
|
1334
|
+
entries: parsed.data.redirects.map(parseRedirectEntry)
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function loadAllTypeRedirects(config) {
|
|
1338
|
+
const out = [];
|
|
1402
1339
|
for (const type of config.types) {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1406
|
-
if (!doc?.redirectTo) continue;
|
|
1407
|
-
const targetSlug = extractSlugFromResolvedPath(type.path, doc.redirectTo);
|
|
1408
|
-
if (targetSlug && allAliasSlugs.has(targetSlug)) {
|
|
1409
|
-
issues.push({
|
|
1410
|
-
contentTypeId: type.id,
|
|
1411
|
-
enSlug,
|
|
1412
|
-
field: "redirect_to",
|
|
1413
|
-
message: `redirect_to target "${targetSlug}" is an alias slug \u2014 chain detected`,
|
|
1414
|
-
level: "error"
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
const targetDoc = targetSlug ? readEnDocument(config, type, targetSlug) : null;
|
|
1418
|
-
if (targetDoc?.redirectTo) {
|
|
1419
|
-
issues.push({
|
|
1420
|
-
contentTypeId: type.id,
|
|
1421
|
-
enSlug,
|
|
1422
|
-
field: "redirect_to",
|
|
1423
|
-
message: `redirect_to chain detected: "${enSlug}" \u2192 "${targetSlug}" \u2192 "${targetDoc.redirectTo}"`,
|
|
1424
|
-
level: "error"
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1340
|
+
const loaded = loadTypeRedirectsFile(config, type);
|
|
1341
|
+
if (loaded) out.push(loaded);
|
|
1428
1342
|
}
|
|
1429
|
-
return
|
|
1343
|
+
return out;
|
|
1430
1344
|
}
|
|
1431
1345
|
|
|
1432
|
-
// src/redirects/translation-index.ts
|
|
1433
|
-
init_cjs_shims();
|
|
1434
|
-
|
|
1435
|
-
// src/i18n/translation-index.ts
|
|
1436
|
-
init_cjs_shims();
|
|
1437
|
-
|
|
1438
|
-
// src/redirects/translation-index.ts
|
|
1439
|
-
init_sqlite();
|
|
1440
|
-
|
|
1441
1346
|
// src/sitemap/join-base-url.ts
|
|
1442
1347
|
init_cjs_shims();
|
|
1443
1348
|
|
|
@@ -1489,6 +1394,173 @@ function loadConfigSync(options = {}) {
|
|
|
1489
1394
|
|
|
1490
1395
|
// src/validate/validate-project.ts
|
|
1491
1396
|
init_cjs_shims();
|
|
1397
|
+
|
|
1398
|
+
// src/validate/validate-redirects.ts
|
|
1399
|
+
init_cjs_shims();
|
|
1400
|
+
function liveDocExists(config, typeId, enSlug) {
|
|
1401
|
+
const type = config.types.find((entry) => entry.id === typeId);
|
|
1402
|
+
if (!type) return false;
|
|
1403
|
+
return enFileExists(config, type, enSlug);
|
|
1404
|
+
}
|
|
1405
|
+
function resolveRedirectTarget(project, sourceTypeId, entry) {
|
|
1406
|
+
if (entry.kind === "anywhere") return null;
|
|
1407
|
+
if (entry.kind === "cross-type") {
|
|
1408
|
+
return { typeId: entry.toType, enSlug: entry.toSlug };
|
|
1409
|
+
}
|
|
1410
|
+
return { typeId: sourceTypeId, enSlug: entry.toSlug };
|
|
1411
|
+
}
|
|
1412
|
+
function matchRoutableTarget(project, toUrl) {
|
|
1413
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1414
|
+
for (const type of project.config.types) {
|
|
1415
|
+
if (!type.path) continue;
|
|
1416
|
+
const enSlug = urlBuilder.extractSlugFromResolvedPath(type.path, toUrl);
|
|
1417
|
+
if (enSlug && liveDocExists(project.config, type.id, enSlug)) {
|
|
1418
|
+
return { typeId: type.id, enSlug };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
function validateTypeRedirects(project) {
|
|
1424
|
+
const issues = [];
|
|
1425
|
+
const globalFrom = /* @__PURE__ */ new Map();
|
|
1426
|
+
for (const type of project.config.types) {
|
|
1427
|
+
const filePath = path3__default.default.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1428
|
+
if (!fs2__default.default.existsSync(filePath)) continue;
|
|
1429
|
+
let loaded;
|
|
1430
|
+
try {
|
|
1431
|
+
loaded = loadTypeRedirectsFile(project.config, type);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
issues.push({
|
|
1434
|
+
level: "error",
|
|
1435
|
+
contentType: type.id,
|
|
1436
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1437
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1438
|
+
});
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (!loaded) continue;
|
|
1442
|
+
if (!isRoutableType(type)) {
|
|
1443
|
+
issues.push({
|
|
1444
|
+
level: "error",
|
|
1445
|
+
contentType: type.id,
|
|
1446
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1447
|
+
message: `Redirects are only supported on routable content types (missing path)`
|
|
1448
|
+
});
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
for (const entry of loaded.entries) {
|
|
1452
|
+
for (const from of entry.fromSlugs) {
|
|
1453
|
+
if (from === entry.toSlug) {
|
|
1454
|
+
issues.push({
|
|
1455
|
+
level: "error",
|
|
1456
|
+
contentType: type.id,
|
|
1457
|
+
enSlug: from,
|
|
1458
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1459
|
+
message: `Redirect source "${from}" must not equal its target slug`
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
if (liveDocExists(project.config, type.id, from)) {
|
|
1463
|
+
issues.push({
|
|
1464
|
+
level: "error",
|
|
1465
|
+
contentType: type.id,
|
|
1466
|
+
enSlug: from,
|
|
1467
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1468
|
+
message: `Redirect source "${from}" still has a live EN file \u2014 delete or rename the MDX first`
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
const existing = globalFrom.get(from);
|
|
1472
|
+
if (existing && existing.typeId !== type.id) {
|
|
1473
|
+
issues.push({
|
|
1474
|
+
level: "error",
|
|
1475
|
+
contentType: type.id,
|
|
1476
|
+
enSlug: from,
|
|
1477
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1478
|
+
message: `Redirect source "${from}" is already claimed by ${existing.typeId}`
|
|
1479
|
+
});
|
|
1480
|
+
} else if (existing) {
|
|
1481
|
+
issues.push({
|
|
1482
|
+
level: "error",
|
|
1483
|
+
contentType: type.id,
|
|
1484
|
+
enSlug: from,
|
|
1485
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1486
|
+
message: `Duplicate redirect source "${from}" in ${TYPE_REDIRECTS_FILENAME}`
|
|
1487
|
+
});
|
|
1488
|
+
} else {
|
|
1489
|
+
globalFrom.set(from, { typeId: type.id, filePath: loaded.filePath });
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (entry.kind === "cross-type") {
|
|
1493
|
+
const targetType = project.config.types.find((candidate) => candidate.id === entry.toType);
|
|
1494
|
+
if (!targetType) {
|
|
1495
|
+
issues.push({
|
|
1496
|
+
level: "error",
|
|
1497
|
+
contentType: type.id,
|
|
1498
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1499
|
+
message: `Unknown redirect target type "${entry.toType}"`
|
|
1500
|
+
});
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (!isRoutableType(targetType)) {
|
|
1504
|
+
issues.push({
|
|
1505
|
+
level: "error",
|
|
1506
|
+
contentType: type.id,
|
|
1507
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1508
|
+
message: `Redirect target type "${entry.toType}" is not routable (missing path)`
|
|
1509
|
+
});
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const target = resolveRedirectTarget(project, type.id, entry);
|
|
1514
|
+
if (target) {
|
|
1515
|
+
if (!liveDocExists(project.config, target.typeId, target.enSlug)) {
|
|
1516
|
+
issues.push({
|
|
1517
|
+
level: "error",
|
|
1518
|
+
contentType: type.id,
|
|
1519
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1520
|
+
message: `Redirect target ${target.typeId}/${target.enSlug} does not exist`
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (entry.kind === "anywhere" && entry.toUrl?.startsWith("/")) {
|
|
1526
|
+
const matched = matchRoutableTarget(project, entry.toUrl);
|
|
1527
|
+
if (matched && !liveDocExists(project.config, matched.typeId, matched.enSlug)) {
|
|
1528
|
+
issues.push({
|
|
1529
|
+
level: "error",
|
|
1530
|
+
contentType: type.id,
|
|
1531
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1532
|
+
message: `Redirect toUrl "${entry.toUrl}" does not resolve to a live document`
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const allLoaded = loadAllTypeRedirects(project.config);
|
|
1539
|
+
for (const file of allLoaded) {
|
|
1540
|
+
for (const entry of file.entries) {
|
|
1541
|
+
if (entry.kind === "anywhere" || !entry.toSlug) continue;
|
|
1542
|
+
const targetTypeId = entry.kind === "cross-type" ? entry.toType : file.contentTypeId;
|
|
1543
|
+
const targetType = project.config.types.find((type) => type.id === targetTypeId);
|
|
1544
|
+
if (!targetType?.path) continue;
|
|
1545
|
+
for (const from of entry.fromSlugs) {
|
|
1546
|
+
if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
|
|
1547
|
+
const chainTarget = globalFrom.get(entry.toSlug);
|
|
1548
|
+
if (chainTarget) {
|
|
1549
|
+
issues.push({
|
|
1550
|
+
level: "error",
|
|
1551
|
+
contentType: file.contentTypeId,
|
|
1552
|
+
enSlug: from,
|
|
1553
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1554
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return issues;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/validate/validate-project.ts
|
|
1492
1564
|
init_sqlite();
|
|
1493
1565
|
|
|
1494
1566
|
// src/validate/validate-relations.ts
|
|
@@ -1689,7 +1761,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1689
1761
|
}
|
|
1690
1762
|
|
|
1691
1763
|
// src/validate/validate-project.ts
|
|
1692
|
-
function
|
|
1764
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1693
1765
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1694
1766
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1695
1767
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1707,7 +1779,7 @@ function validateProject(config) {
|
|
|
1707
1779
|
}
|
|
1708
1780
|
const db = openStore(config, "readonly");
|
|
1709
1781
|
for (const type of config.types) {
|
|
1710
|
-
const enSlugs =
|
|
1782
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1711
1783
|
const englishSlugs = new Set(enSlugs);
|
|
1712
1784
|
if (!type.translate && isRoutableType(type)) {
|
|
1713
1785
|
issues.push({
|
|
@@ -1789,33 +1861,8 @@ function validateProject(config) {
|
|
|
1789
1861
|
}
|
|
1790
1862
|
}
|
|
1791
1863
|
db.close();
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
issues.push({
|
|
1795
|
-
level: issue.level,
|
|
1796
|
-
contentType: issue.contentTypeId,
|
|
1797
|
-
enSlug: issue.enSlug,
|
|
1798
|
-
field: issue.field,
|
|
1799
|
-
message: issue.message
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
1803
|
-
issues.push({
|
|
1804
|
-
level: issue.level,
|
|
1805
|
-
contentType: issue.contentTypeId,
|
|
1806
|
-
enSlug: issue.enSlug,
|
|
1807
|
-
field: issue.field,
|
|
1808
|
-
message: issue.message
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
1812
|
-
issues.push({
|
|
1813
|
-
level: issue.level,
|
|
1814
|
-
contentType: issue.contentTypeId,
|
|
1815
|
-
enSlug: issue.enSlug,
|
|
1816
|
-
field: issue.field,
|
|
1817
|
-
message: issue.message
|
|
1818
|
-
});
|
|
1864
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
1865
|
+
issues.push(issue);
|
|
1819
1866
|
}
|
|
1820
1867
|
for (const issue of validateRelations(project)) {
|
|
1821
1868
|
issues.push({
|
|
@@ -1859,7 +1906,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1859
1906
|
|
|
1860
1907
|
// src/translate/worklist.ts
|
|
1861
1908
|
init_sqlite();
|
|
1862
|
-
function
|
|
1909
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
1863
1910
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1864
1911
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1865
1912
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1870,7 +1917,7 @@ function buildWorklist(config, options = {}) {
|
|
|
1870
1917
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
1871
1918
|
for (const type of config.types) {
|
|
1872
1919
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
1873
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
1920
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
1874
1921
|
for (const enSlug of enSlugs) {
|
|
1875
1922
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
1876
1923
|
if (!enDoc) continue;
|
|
@@ -2557,6 +2604,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2557
2604
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2558
2605
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2559
2606
|
const out = [];
|
|
2607
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2560
2608
|
for (const type of project.listTypes()) {
|
|
2561
2609
|
if (!isRoutableType(type.config)) continue;
|
|
2562
2610
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2572,7 +2620,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2572
2620
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2573
2621
|
const doc = resolved.document;
|
|
2574
2622
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2575
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2623
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2576
2624
|
out.push({
|
|
2577
2625
|
relativePath: urlPath.slice(1),
|
|
2578
2626
|
urlPath,
|