scribe-cms 0.0.7 → 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 +534 -388
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +534 -388
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +745 -516
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +744 -517
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +292 -261
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +290 -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 +14 -4
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/create-project.d.ts.map +1 -1
- package/dist/src/create-scribe.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 -1
- 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 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts +1 -0
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/resolve-translate-config.d.ts.map +1 -1
- package/dist/src/translate/sanitize-mdx-jsx.d.ts +9 -0
- package/dist/src/translate/sanitize-mdx-jsx.d.ts.map +1 -0
- 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.js
CHANGED
|
@@ -222,14 +222,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
222
222
|
function isRoutableType(type) {
|
|
223
223
|
return typeof type.path === "string" && type.path.length > 0;
|
|
224
224
|
}
|
|
225
|
-
function assertValidPathTemplate(
|
|
226
|
-
if (!
|
|
227
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
225
|
+
function assertValidPathTemplate(path16, typeId) {
|
|
226
|
+
if (!path16.startsWith("/")) {
|
|
227
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path16}")`);
|
|
228
228
|
}
|
|
229
|
-
const count = (
|
|
229
|
+
const count = (path16.match(/\{slug\}/g) ?? []).length;
|
|
230
230
|
if (count !== 1) {
|
|
231
231
|
throw new Error(
|
|
232
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
232
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path16}")`
|
|
233
233
|
);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
@@ -243,19 +243,69 @@ function pathSuffix(template) {
|
|
|
243
243
|
if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
|
|
244
244
|
return template.slice(idx + SLUG_PLACEHOLDER.length);
|
|
245
245
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
246
|
+
function resolveDefaultLocaleRouting() {
|
|
247
|
+
return { strategy: "path-prefix", prefixDefaultLocale: false };
|
|
248
|
+
}
|
|
249
|
+
function splitPathAndSearch(pathname) {
|
|
250
|
+
const q = pathname.indexOf("?");
|
|
251
|
+
if (q === -1) return { pathname, search: "" };
|
|
252
|
+
return { pathname: pathname.slice(0, q), search: pathname.slice(q) };
|
|
253
|
+
}
|
|
254
|
+
function createUrlBuilder(config) {
|
|
255
|
+
const localeRouting = config.localeRouting ?? resolveDefaultLocaleRouting();
|
|
256
|
+
const defaultLocale = config.defaultLocale;
|
|
257
|
+
const prefixedLocales = localeRouting.strategy === "path-prefix" ? localeRouting.prefixDefaultLocale ? [...config.locales] : config.locales.filter((locale) => locale !== defaultLocale) : config.locales.filter((locale) => locale !== defaultLocale);
|
|
258
|
+
function applyLocaleToPath(pathname, locale) {
|
|
259
|
+
if (locale === defaultLocale) {
|
|
260
|
+
if (localeRouting.strategy === "path-prefix" && localeRouting.prefixDefaultLocale) {
|
|
261
|
+
return `/${defaultLocale}${pathname}`;
|
|
262
|
+
}
|
|
263
|
+
return pathname;
|
|
264
|
+
}
|
|
265
|
+
if (localeRouting.strategy === "search-param") {
|
|
266
|
+
const { pathname: base, search } = splitPathAndSearch(pathname);
|
|
267
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
268
|
+
params.set(localeRouting.param, locale);
|
|
269
|
+
const qs = params.toString();
|
|
270
|
+
return qs ? `${base}?${qs}` : base;
|
|
271
|
+
}
|
|
272
|
+
return `/${locale}${pathname}`;
|
|
273
|
+
}
|
|
274
|
+
function resolvePath(template, slug, locale) {
|
|
275
|
+
const relative = template.replace(SLUG_PLACEHOLDER, slug);
|
|
276
|
+
return applyLocaleToPath(relative, locale);
|
|
277
|
+
}
|
|
278
|
+
function extractSlugFromResolvedPath(template, resolvedPath) {
|
|
279
|
+
let pathname = resolvedPath;
|
|
280
|
+
if (localeRouting.strategy === "search-param") {
|
|
281
|
+
pathname = splitPathAndSearch(resolvedPath).pathname;
|
|
282
|
+
} else if (localeRouting.strategy === "path-prefix") {
|
|
283
|
+
for (const locale of config.locales) {
|
|
284
|
+
if (locale === defaultLocale && !localeRouting.prefixDefaultLocale) continue;
|
|
285
|
+
const prefix2 = `/${locale}`;
|
|
286
|
+
if (pathname === prefix2 || pathname.startsWith(`${prefix2}/`)) {
|
|
287
|
+
pathname = pathname.slice(prefix2.length) || "/";
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const prefix = pathPrefix(template);
|
|
293
|
+
const suffix = pathSuffix(template);
|
|
294
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
295
|
+
if (suffix && !pathname.endsWith(suffix)) return null;
|
|
296
|
+
const slugEnd = suffix ? pathname.length - suffix.length : pathname.length;
|
|
297
|
+
const slug = pathname.slice(prefix.length, slugEnd);
|
|
298
|
+
return slug.length > 0 ? slug : null;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
defaultLocale,
|
|
302
|
+
locales: config.locales,
|
|
303
|
+
localeRouting,
|
|
304
|
+
prefixedLocales,
|
|
305
|
+
resolvePath,
|
|
306
|
+
extractSlugFromResolvedPath,
|
|
307
|
+
applyLocaleToPath
|
|
308
|
+
};
|
|
259
309
|
}
|
|
260
310
|
|
|
261
311
|
// src/config/resolve-config.ts
|
|
@@ -278,6 +328,13 @@ function resolveConfig(input, baseDir) {
|
|
|
278
328
|
`scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
|
|
279
329
|
);
|
|
280
330
|
}
|
|
331
|
+
const localeRouting = raw.localeRouting ?? {
|
|
332
|
+
strategy: "path-prefix",
|
|
333
|
+
prefixDefaultLocale: false
|
|
334
|
+
};
|
|
335
|
+
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
336
|
+
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
337
|
+
}
|
|
281
338
|
const projectRoot = path4.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
282
339
|
const contentRoot = path4.resolve(projectRoot, raw.contentDir ?? "content");
|
|
283
340
|
const storePath = path4.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -305,6 +362,7 @@ function resolveConfig(input, baseDir) {
|
|
|
305
362
|
assetsPath,
|
|
306
363
|
locales: [...raw.locales],
|
|
307
364
|
defaultLocale,
|
|
365
|
+
localeRouting,
|
|
308
366
|
localePresets: raw.localePresets,
|
|
309
367
|
translate: raw.translate,
|
|
310
368
|
types
|
|
@@ -359,10 +417,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
359
417
|
}
|
|
360
418
|
function extractByPaths(data, paths) {
|
|
361
419
|
const out = {};
|
|
362
|
-
for (const
|
|
363
|
-
const value = getAtPath(data,
|
|
420
|
+
for (const path16 of paths) {
|
|
421
|
+
const value = getAtPath(data, path16);
|
|
364
422
|
if (value !== void 0) {
|
|
365
|
-
setAtPath(out,
|
|
423
|
+
setAtPath(out, path16.filter((p) => p !== "*"), value);
|
|
366
424
|
}
|
|
367
425
|
}
|
|
368
426
|
return out;
|
|
@@ -380,13 +438,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
380
438
|
const translatable = pickTranslatable(localeData, schema);
|
|
381
439
|
return deepMerge(structural, translatable);
|
|
382
440
|
}
|
|
383
|
-
function getAtPath(obj,
|
|
441
|
+
function getAtPath(obj, path16) {
|
|
384
442
|
let current = obj;
|
|
385
|
-
for (const segment of
|
|
443
|
+
for (const segment of path16) {
|
|
386
444
|
if (segment === "*") {
|
|
387
445
|
if (!Array.isArray(current)) return void 0;
|
|
388
446
|
return current.map(
|
|
389
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
447
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path16.slice(path16.indexOf("*") + 1)) : void 0
|
|
390
448
|
);
|
|
391
449
|
}
|
|
392
450
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -394,13 +452,13 @@ function getAtPath(obj, path14) {
|
|
|
394
452
|
}
|
|
395
453
|
return current;
|
|
396
454
|
}
|
|
397
|
-
function setAtPath(obj,
|
|
398
|
-
if (
|
|
399
|
-
if (
|
|
400
|
-
obj[
|
|
455
|
+
function setAtPath(obj, path16, value) {
|
|
456
|
+
if (path16.length === 0) return;
|
|
457
|
+
if (path16.length === 1) {
|
|
458
|
+
obj[path16[0]] = value;
|
|
401
459
|
return;
|
|
402
460
|
}
|
|
403
|
-
const [head, ...rest] =
|
|
461
|
+
const [head, ...rest] = path16;
|
|
404
462
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
405
463
|
obj[head] = {};
|
|
406
464
|
}
|
|
@@ -451,45 +509,23 @@ var LOCALE_BUILTIN_KEYS = [
|
|
|
451
509
|
["translationOf", "translationOf is internal; remove from locale translation"],
|
|
452
510
|
["enSlug", "enSlug is internal; remove from locale translation"]
|
|
453
511
|
];
|
|
454
|
-
|
|
512
|
+
var DEPRECATED_REDIRECT_FIELDS = [
|
|
513
|
+
[
|
|
514
|
+
"aliases",
|
|
515
|
+
"aliases frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
516
|
+
],
|
|
517
|
+
[
|
|
518
|
+
"redirect_to",
|
|
519
|
+
"redirect_to frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
520
|
+
]
|
|
521
|
+
];
|
|
522
|
+
function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
|
|
455
523
|
const issues = [];
|
|
456
524
|
const rest = { ...data };
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
let redirectTo;
|
|
462
|
-
if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
|
|
463
|
-
if (typeof redirectRaw !== "string") {
|
|
464
|
-
issues.push({
|
|
465
|
-
field: "redirect_to",
|
|
466
|
-
message: "redirect_to must be a string path",
|
|
467
|
-
level: "error"
|
|
468
|
-
});
|
|
469
|
-
} else if (!pathTemplate) {
|
|
470
|
-
issues.push({
|
|
471
|
-
field: "redirect_to",
|
|
472
|
-
message: "redirect_to is not allowed on reference-only content types",
|
|
473
|
-
level: "error"
|
|
474
|
-
});
|
|
475
|
-
} else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
|
|
476
|
-
issues.push({
|
|
477
|
-
field: "redirect_to",
|
|
478
|
-
message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
|
|
479
|
-
level: "error"
|
|
480
|
-
});
|
|
481
|
-
} else {
|
|
482
|
-
redirectTo = redirectRaw;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const aliases = aliasesResult.success ? aliasesResult.data : [];
|
|
486
|
-
if (!aliasesResult.success) {
|
|
487
|
-
for (const issue of aliasesResult.error.issues) {
|
|
488
|
-
issues.push({
|
|
489
|
-
field: `aliases.${issue.path.join(".")}`,
|
|
490
|
-
message: issue.message,
|
|
491
|
-
level: "error"
|
|
492
|
-
});
|
|
525
|
+
for (const [field2, message] of DEPRECATED_REDIRECT_FIELDS) {
|
|
526
|
+
if (rest[field2] !== void 0) {
|
|
527
|
+
issues.push({ field: field2, message, level: "error" });
|
|
528
|
+
delete rest[field2];
|
|
493
529
|
}
|
|
494
530
|
}
|
|
495
531
|
let publishedAt;
|
|
@@ -551,8 +587,6 @@ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
|
|
|
551
587
|
}
|
|
552
588
|
return {
|
|
553
589
|
builtin: {
|
|
554
|
-
aliases,
|
|
555
|
-
redirectTo,
|
|
556
590
|
publishedAt,
|
|
557
591
|
updatedAt,
|
|
558
592
|
noindex,
|
|
@@ -571,20 +605,24 @@ function validateLocaleBuiltinFields(data) {
|
|
|
571
605
|
}
|
|
572
606
|
return issues;
|
|
573
607
|
}
|
|
574
|
-
function resolveCanonicalPathname(type, doc, defaultLocale) {
|
|
608
|
+
function resolveCanonicalPathname(type, doc, defaultLocale, localeRouting) {
|
|
609
|
+
const urlBuilder = createUrlBuilder({
|
|
610
|
+
locales: [defaultLocale, ...doc.locale !== defaultLocale ? [doc.locale] : []],
|
|
611
|
+
defaultLocale,
|
|
612
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
613
|
+
});
|
|
575
614
|
if (doc.canonicalPathOverride) {
|
|
576
|
-
|
|
577
|
-
return `/${doc.locale}${doc.canonicalPathOverride}`;
|
|
615
|
+
return urlBuilder.applyLocaleToPath(doc.canonicalPathOverride, doc.locale);
|
|
578
616
|
}
|
|
579
617
|
if (!type.path) return `/${doc.slug}`;
|
|
580
|
-
return resolvePath(type.path, doc.slug, doc.locale
|
|
618
|
+
return urlBuilder.resolvePath(type.path, doc.slug, doc.locale);
|
|
581
619
|
}
|
|
582
|
-
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
|
|
620
|
+
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, localeRouting) {
|
|
583
621
|
const out = { ...frontmatter };
|
|
584
622
|
if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
|
|
585
623
|
if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
|
|
586
624
|
out.noindex = doc.noindex;
|
|
587
|
-
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
|
|
625
|
+
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
|
|
588
626
|
return out;
|
|
589
627
|
}
|
|
590
628
|
function seoFieldsFromEn(enDoc) {
|
|
@@ -592,8 +630,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
592
630
|
publishedAt: enDoc.publishedAt,
|
|
593
631
|
updatedAt: enDoc.updatedAt,
|
|
594
632
|
noindex: enDoc.noindex,
|
|
595
|
-
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
596
|
-
redirectTo: enDoc.redirectTo
|
|
633
|
+
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
597
634
|
};
|
|
598
635
|
}
|
|
599
636
|
|
|
@@ -668,11 +705,6 @@ function getTranslation(db, contentType, enSlug, locale) {
|
|
|
668
705
|
function listTranslationsForType(db, contentType) {
|
|
669
706
|
return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
|
|
670
707
|
}
|
|
671
|
-
function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
672
|
-
return db.prepare(
|
|
673
|
-
`SELECT * FROM translations WHERE content_type = ? AND en_slug = ? ORDER BY locale`
|
|
674
|
-
).all(contentType, enSlug);
|
|
675
|
-
}
|
|
676
708
|
function listTranslationsForLocale(db, contentType, locale) {
|
|
677
709
|
return db.prepare(`SELECT * FROM translations WHERE content_type = ? AND locale = ? ORDER BY en_slug`).all(contentType, locale);
|
|
678
710
|
}
|
|
@@ -756,7 +788,8 @@ function parseEnMdx(filePath, config, type) {
|
|
|
756
788
|
locale: config.defaultLocale
|
|
757
789
|
},
|
|
758
790
|
type,
|
|
759
|
-
config.defaultLocale
|
|
791
|
+
config.defaultLocale,
|
|
792
|
+
config.localeRouting
|
|
760
793
|
);
|
|
761
794
|
const crossIssues = type.crossValidate?.(result.data, {
|
|
762
795
|
locale: config.defaultLocale,
|
|
@@ -770,8 +803,6 @@ function parseEnMdx(filePath, config, type) {
|
|
|
770
803
|
slug,
|
|
771
804
|
enSlug: slug,
|
|
772
805
|
locale: config.defaultLocale,
|
|
773
|
-
aliases: builtin.aliases,
|
|
774
|
-
redirectTo: builtin.redirectTo,
|
|
775
806
|
publishedAt: builtin.publishedAt,
|
|
776
807
|
updatedAt: builtin.updatedAt,
|
|
777
808
|
noindex: builtin.noindex,
|
|
@@ -782,7 +813,7 @@ function parseEnMdx(filePath, config, type) {
|
|
|
782
813
|
};
|
|
783
814
|
return { document, issues };
|
|
784
815
|
}
|
|
785
|
-
function buildDocumentFromTranslation(row, enDoc, type) {
|
|
816
|
+
function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
786
817
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
787
818
|
const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
|
|
788
819
|
const seo = seoFieldsFromEn(enDoc);
|
|
@@ -790,14 +821,13 @@ function buildDocumentFromTranslation(row, enDoc, type) {
|
|
|
790
821
|
merged,
|
|
791
822
|
{ ...seo, slug: row.slug, locale: row.locale },
|
|
792
823
|
type,
|
|
793
|
-
|
|
824
|
+
config.defaultLocale,
|
|
825
|
+
config.localeRouting
|
|
794
826
|
);
|
|
795
827
|
return {
|
|
796
828
|
slug: row.slug,
|
|
797
829
|
enSlug: row.en_slug,
|
|
798
830
|
locale: row.locale,
|
|
799
|
-
aliases: [],
|
|
800
|
-
redirectTo: seo.redirectTo,
|
|
801
831
|
publishedAt: seo.publishedAt,
|
|
802
832
|
updatedAt: seo.updatedAt,
|
|
803
833
|
noindex: seo.noindex,
|
|
@@ -875,7 +905,7 @@ function createContentLoader(config, type) {
|
|
|
875
905
|
for (const row of rowsByLocale.get(locale) ?? []) {
|
|
876
906
|
const enDoc = englishBySlug.get(row.en_slug);
|
|
877
907
|
if (!enDoc) continue;
|
|
878
|
-
const doc = buildDocumentFromTranslation(row, enDoc, type);
|
|
908
|
+
const doc = buildDocumentFromTranslation(row, enDoc, type, config);
|
|
879
909
|
bySlug.set(doc.slug, doc);
|
|
880
910
|
byEnSlug.set(row.en_slug, doc);
|
|
881
911
|
}
|
|
@@ -924,56 +954,16 @@ function getTranslatablePayload(doc, type) {
|
|
|
924
954
|
|
|
925
955
|
// src/i18n/resolve-document.ts
|
|
926
956
|
init_esm_shims();
|
|
927
|
-
function
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
return null;
|
|
934
|
-
}
|
|
935
|
-
function enCanonicalDoc(doc, allDocs, defaultLocale) {
|
|
936
|
-
if (doc.locale === defaultLocale) return doc;
|
|
937
|
-
return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
|
|
938
|
-
}
|
|
939
|
-
function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
|
|
940
|
-
if (!type.path) return void 0;
|
|
941
|
-
const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
|
|
942
|
-
if (!enDoc.redirectTo) return void 0;
|
|
943
|
-
const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
|
|
944
|
-
if (!targetEnSlug) return void 0;
|
|
945
|
-
const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
|
|
946
|
-
const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
|
|
947
|
-
return resolvePath(type.path, targetSlug, locale, defaultLocale);
|
|
948
|
-
}
|
|
949
|
-
function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
|
|
950
|
-
if (result.shouldRedirectTo || !result.document) return result;
|
|
951
|
-
const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
|
|
952
|
-
if (!redirect) return result;
|
|
953
|
-
return {
|
|
954
|
-
document: null,
|
|
955
|
-
actualLocale: result.actualLocale,
|
|
956
|
-
shouldRedirectTo: redirect
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
957
|
+
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
|
|
958
|
+
const urlBuilder = createUrlBuilder({
|
|
959
|
+
locales: [defaultLocale, locale],
|
|
960
|
+
defaultLocale,
|
|
961
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
962
|
+
});
|
|
960
963
|
const idx = allDocs.get(locale);
|
|
961
964
|
const direct = idx?.bySlug.get(slug);
|
|
962
965
|
if (direct) {
|
|
963
|
-
return
|
|
964
|
-
}
|
|
965
|
-
const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
|
|
966
|
-
if (aliasDoc && type.path) {
|
|
967
|
-
const canonicalSlug = aliasDoc.slug;
|
|
968
|
-
const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
|
|
969
|
-
const targetSlug = localizedSlug ?? canonicalSlug;
|
|
970
|
-
if (targetSlug !== slug) {
|
|
971
|
-
return {
|
|
972
|
-
document: null,
|
|
973
|
-
actualLocale: locale,
|
|
974
|
-
shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
|
|
975
|
-
};
|
|
976
|
-
}
|
|
966
|
+
return { document: direct, actualLocale: locale };
|
|
977
967
|
}
|
|
978
968
|
for (const [docLocale, docIdx] of allDocs) {
|
|
979
969
|
const found = docIdx.bySlug.get(slug);
|
|
@@ -986,17 +976,11 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
986
976
|
return {
|
|
987
977
|
document: null,
|
|
988
978
|
actualLocale: locale,
|
|
989
|
-
shouldRedirectTo: resolvePath(type.path, correctSlug, locale
|
|
979
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
|
|
990
980
|
};
|
|
991
981
|
}
|
|
992
982
|
if (docLocale === defaultLocale) {
|
|
993
|
-
return
|
|
994
|
-
{ document: found, actualLocale: defaultLocale },
|
|
995
|
-
locale,
|
|
996
|
-
defaultLocale,
|
|
997
|
-
allDocs,
|
|
998
|
-
type
|
|
999
|
-
);
|
|
983
|
+
return { document: found, actualLocale: defaultLocale };
|
|
1000
984
|
}
|
|
1001
985
|
if (!type.path) {
|
|
1002
986
|
return { document: null, actualLocale: locale };
|
|
@@ -1004,39 +988,21 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
1004
988
|
return {
|
|
1005
989
|
document: null,
|
|
1006
990
|
actualLocale: locale,
|
|
1007
|
-
shouldRedirectTo: resolvePath(type.path, found.enSlug, locale
|
|
991
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, found.enSlug, locale)
|
|
1008
992
|
};
|
|
1009
993
|
}
|
|
1010
994
|
if (locale !== defaultLocale) {
|
|
1011
995
|
const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
1012
996
|
if (enDoc && type.indexFallback === "en") {
|
|
1013
|
-
return
|
|
1014
|
-
{ document: enDoc, actualLocale: defaultLocale },
|
|
1015
|
-
locale,
|
|
1016
|
-
defaultLocale,
|
|
1017
|
-
allDocs,
|
|
1018
|
-
type
|
|
1019
|
-
);
|
|
997
|
+
return { document: enDoc, actualLocale: defaultLocale };
|
|
1020
998
|
}
|
|
1021
999
|
const byEn = idx?.byEnSlug.get(slug);
|
|
1022
1000
|
if (byEn) {
|
|
1023
|
-
return
|
|
1024
|
-
{ document: byEn, actualLocale: locale },
|
|
1025
|
-
locale,
|
|
1026
|
-
defaultLocale,
|
|
1027
|
-
allDocs,
|
|
1028
|
-
type
|
|
1029
|
-
);
|
|
1001
|
+
return { document: byEn, actualLocale: locale };
|
|
1030
1002
|
}
|
|
1031
1003
|
const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
1032
1004
|
if (fallback && type.indexFallback === "en") {
|
|
1033
|
-
return
|
|
1034
|
-
{ document: fallback, actualLocale: defaultLocale },
|
|
1035
|
-
locale,
|
|
1036
|
-
defaultLocale,
|
|
1037
|
-
allDocs,
|
|
1038
|
-
type
|
|
1039
|
-
);
|
|
1005
|
+
return { document: fallback, actualLocale: defaultLocale };
|
|
1040
1006
|
}
|
|
1041
1007
|
}
|
|
1042
1008
|
return { document: null, actualLocale: locale };
|
|
@@ -1078,6 +1044,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1078
1044
|
}
|
|
1079
1045
|
return type.path;
|
|
1080
1046
|
}
|
|
1047
|
+
const urlBuilder = createUrlBuilder(config);
|
|
1081
1048
|
const runtime = {
|
|
1082
1049
|
id: type.id,
|
|
1083
1050
|
config: type,
|
|
@@ -1093,15 +1060,21 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1093
1060
|
return load().get(locale)?.bySlug.get(slug) ?? null;
|
|
1094
1061
|
},
|
|
1095
1062
|
resolve(slug, locale) {
|
|
1096
|
-
const result = resolveLocalizedDocument(
|
|
1063
|
+
const result = resolveLocalizedDocument(
|
|
1064
|
+
slug,
|
|
1065
|
+
locale,
|
|
1066
|
+
config.defaultLocale,
|
|
1067
|
+
load(),
|
|
1068
|
+
type,
|
|
1069
|
+
config.localeRouting
|
|
1070
|
+
);
|
|
1097
1071
|
if (result.document && type.path) {
|
|
1098
1072
|
return {
|
|
1099
1073
|
...result,
|
|
1100
|
-
canonicalPath: resolvePath(
|
|
1074
|
+
canonicalPath: urlBuilder.resolvePath(
|
|
1101
1075
|
type.path,
|
|
1102
1076
|
result.document.slug,
|
|
1103
|
-
result.actualLocale
|
|
1104
|
-
config.defaultLocale
|
|
1077
|
+
result.actualLocale
|
|
1105
1078
|
)
|
|
1106
1079
|
};
|
|
1107
1080
|
}
|
|
@@ -1125,10 +1098,9 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1125
1098
|
alternates(doc) {
|
|
1126
1099
|
const pathTemplate = assertRoutable("alternates");
|
|
1127
1100
|
const out = {
|
|
1128
|
-
[config.defaultLocale]: resolvePath(
|
|
1101
|
+
[config.defaultLocale]: urlBuilder.resolvePath(
|
|
1129
1102
|
pathTemplate,
|
|
1130
1103
|
doc.enSlug,
|
|
1131
|
-
config.defaultLocale,
|
|
1132
1104
|
config.defaultLocale
|
|
1133
1105
|
)
|
|
1134
1106
|
};
|
|
@@ -1137,7 +1109,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1137
1109
|
if (locale === config.defaultLocale) continue;
|
|
1138
1110
|
const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
|
|
1139
1111
|
if (translated) {
|
|
1140
|
-
out[locale] = resolvePath(pathTemplate, translated.slug, locale
|
|
1112
|
+
out[locale] = urlBuilder.resolvePath(pathTemplate, translated.slug, locale);
|
|
1141
1113
|
}
|
|
1142
1114
|
}
|
|
1143
1115
|
return out;
|
|
@@ -1152,7 +1124,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1152
1124
|
},
|
|
1153
1125
|
url(slug, locale) {
|
|
1154
1126
|
const pathTemplate = assertRoutable("url");
|
|
1155
|
-
return resolvePath(pathTemplate, slug, locale
|
|
1127
|
+
return urlBuilder.resolvePath(pathTemplate, slug, locale);
|
|
1156
1128
|
},
|
|
1157
1129
|
related(doc, fieldName, locale) {
|
|
1158
1130
|
const meta = relationFields.get(fieldName);
|
|
@@ -1208,6 +1180,9 @@ function createProject(config) {
|
|
|
1208
1180
|
getType: getRuntime,
|
|
1209
1181
|
listTypes() {
|
|
1210
1182
|
return Array.from(runtimes.values());
|
|
1183
|
+
},
|
|
1184
|
+
listRoutableTypes() {
|
|
1185
|
+
return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
|
|
1211
1186
|
}
|
|
1212
1187
|
};
|
|
1213
1188
|
}
|
|
@@ -1218,208 +1193,141 @@ init_esm_shims();
|
|
|
1218
1193
|
// src/redirects/build-redirects.ts
|
|
1219
1194
|
init_esm_shims();
|
|
1220
1195
|
|
|
1221
|
-
// src/core/slug-aliases.ts
|
|
1222
|
-
init_esm_shims();
|
|
1223
|
-
|
|
1224
1196
|
// src/core/alias-helpers.ts
|
|
1225
1197
|
init_esm_shims();
|
|
1226
1198
|
function enFileExists(config, type, enSlug) {
|
|
1227
1199
|
const dir = path4.join(config.rootDir, type.contentDir);
|
|
1228
1200
|
return fs2.existsSync(path4.join(dir, `${enSlug}.mdx`)) || fs2.existsSync(path4.join(dir, `${enSlug}.md`));
|
|
1229
1201
|
}
|
|
1230
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1231
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1232
|
-
}
|
|
1233
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1234
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1235
|
-
}
|
|
1236
1202
|
function listEnSlugs(rootDir, contentDir) {
|
|
1237
1203
|
const dir = path4.join(rootDir, contentDir);
|
|
1238
1204
|
if (!fs2.existsSync(dir)) return [];
|
|
1239
1205
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1240
1206
|
}
|
|
1241
1207
|
|
|
1242
|
-
// src/
|
|
1208
|
+
// src/i18n/translation-index.ts
|
|
1209
|
+
init_esm_shims();
|
|
1210
|
+
|
|
1211
|
+
// src/redirects/build-redirects.ts
|
|
1243
1212
|
init_sqlite();
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1213
|
+
|
|
1214
|
+
// src/redirects/build-json-redirects.ts
|
|
1215
|
+
init_esm_shims();
|
|
1216
|
+
init_sqlite();
|
|
1217
|
+
|
|
1218
|
+
// src/redirects/load-type-redirects.ts
|
|
1219
|
+
init_esm_shims();
|
|
1220
|
+
|
|
1221
|
+
// src/redirects/redirect-schema.ts
|
|
1222
|
+
init_esm_shims();
|
|
1223
|
+
var redirectFromSchema = z.union([
|
|
1224
|
+
slugPatternSchema,
|
|
1225
|
+
z.array(slugPatternSchema).min(1).max(20)
|
|
1226
|
+
]);
|
|
1227
|
+
var typeRedirectEntrySchema = z.object({
|
|
1228
|
+
from: redirectFromSchema,
|
|
1229
|
+
toSlug: slugPatternSchema.optional(),
|
|
1230
|
+
toType: z.string().min(1).optional(),
|
|
1231
|
+
toUrl: z.string().min(1).optional(),
|
|
1232
|
+
permanent: z.boolean().optional()
|
|
1233
|
+
}).superRefine((entry, ctx) => {
|
|
1234
|
+
const hasToSlug = entry.toSlug !== void 0;
|
|
1235
|
+
const hasToUrl = entry.toUrl !== void 0;
|
|
1236
|
+
const hasToType = entry.toType !== void 0;
|
|
1237
|
+
const targetCount = Number(hasToSlug) + Number(hasToUrl);
|
|
1238
|
+
if (targetCount !== 1) {
|
|
1239
|
+
ctx.addIssue({
|
|
1240
|
+
code: "custom",
|
|
1241
|
+
message: "Each redirect must specify exactly one of toSlug/toType+toSlug or toUrl",
|
|
1242
|
+
path: ["toSlug"]
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1253
1245
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
level: "error"
|
|
1268
|
-
});
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
if (canonicalSlugs.has(alias)) {
|
|
1272
|
-
issues.push({
|
|
1273
|
-
contentTypeId: type.id,
|
|
1274
|
-
enSlug,
|
|
1275
|
-
field: "aliases",
|
|
1276
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1277
|
-
level: "error"
|
|
1278
|
-
});
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
const existing = aliasToTarget.get(alias);
|
|
1282
|
-
if (existing) {
|
|
1283
|
-
issues.push({
|
|
1284
|
-
contentTypeId: type.id,
|
|
1285
|
-
enSlug,
|
|
1286
|
-
field: "aliases",
|
|
1287
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1288
|
-
level: "error"
|
|
1289
|
-
});
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
aliasToTarget.set(alias, {
|
|
1293
|
-
contentTypeId: type.id,
|
|
1294
|
-
canonicalSlug: enSlug,
|
|
1295
|
-
path: type.path
|
|
1296
|
-
});
|
|
1297
|
-
allAliasSlugs.add(alias);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1246
|
+
if (hasToUrl && hasToType) {
|
|
1247
|
+
ctx.addIssue({
|
|
1248
|
+
code: "custom",
|
|
1249
|
+
message: "toUrl cannot be combined with toType",
|
|
1250
|
+
path: ["toUrl"]
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (hasToType && !hasToSlug) {
|
|
1254
|
+
ctx.addIssue({
|
|
1255
|
+
code: "custom",
|
|
1256
|
+
message: "Cross-type redirects require both toType and toSlug",
|
|
1257
|
+
path: ["toSlug"]
|
|
1258
|
+
});
|
|
1300
1259
|
}
|
|
1301
|
-
|
|
1260
|
+
});
|
|
1261
|
+
var typeRedirectsFileSchema = z.object({
|
|
1262
|
+
redirects: z.array(typeRedirectEntrySchema).max(500)
|
|
1263
|
+
});
|
|
1264
|
+
function normalizeRedirectFrom(from) {
|
|
1265
|
+
return Array.isArray(from) ? from : [from];
|
|
1302
1266
|
}
|
|
1303
|
-
function
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1313
|
-
if (!known) {
|
|
1314
|
-
issues.push({
|
|
1315
|
-
contentTypeId: type.id,
|
|
1316
|
-
enSlug: target.canonicalSlug,
|
|
1317
|
-
field: "aliases",
|
|
1318
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1319
|
-
level: "warning"
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
if (enFileExists(config, type, alias)) {
|
|
1323
|
-
issues.push({
|
|
1324
|
-
contentTypeId: type.id,
|
|
1325
|
-
enSlug: target.canonicalSlug,
|
|
1326
|
-
field: "aliases",
|
|
1327
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1328
|
-
level: "warning"
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
const sqliteRows = listTranslationsForEnSlug(db, type.id, alias);
|
|
1332
|
-
if (sqliteRows.length > 0) {
|
|
1333
|
-
issues.push({
|
|
1334
|
-
contentTypeId: type.id,
|
|
1335
|
-
enSlug: target.canonicalSlug,
|
|
1336
|
-
field: "aliases",
|
|
1337
|
-
message: `${sqliteRows.length} sqlite translation(s) for alias "${alias}" retained for locale redirects and history`,
|
|
1338
|
-
level: "info"
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
for (const locale of config.locales) {
|
|
1342
|
-
if (locale === config.defaultLocale) continue;
|
|
1343
|
-
const canonicalRow = listTranslationsForEnSlug(db, type.id, target.canonicalSlug).find(
|
|
1344
|
-
(r) => r.locale === locale
|
|
1345
|
-
);
|
|
1346
|
-
if (!canonicalRow) continue;
|
|
1347
|
-
const aliasRow = sqliteRows.find((r) => r.locale === locale);
|
|
1348
|
-
if (!aliasRow && known) {
|
|
1349
|
-
issues.push({
|
|
1350
|
-
contentTypeId: type.id,
|
|
1351
|
-
enSlug: target.canonicalSlug,
|
|
1352
|
-
field: "aliases",
|
|
1353
|
-
message: `Alias "${alias}" has no locale slug mapping for ${locale} \u2014 old localized URL may miss redirect`,
|
|
1354
|
-
level: "warning"
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
} finally {
|
|
1360
|
-
db.close();
|
|
1267
|
+
function parseRedirectEntry(entry) {
|
|
1268
|
+
const fromSlugs = normalizeRedirectFrom(entry.from);
|
|
1269
|
+
if (entry.toUrl !== void 0) {
|
|
1270
|
+
return {
|
|
1271
|
+
fromSlugs,
|
|
1272
|
+
kind: "anywhere",
|
|
1273
|
+
toUrl: entry.toUrl,
|
|
1274
|
+
permanent: entry.permanent ?? true
|
|
1275
|
+
};
|
|
1361
1276
|
}
|
|
1362
|
-
|
|
1277
|
+
if (entry.toType !== void 0) {
|
|
1278
|
+
return {
|
|
1279
|
+
fromSlugs,
|
|
1280
|
+
kind: "cross-type",
|
|
1281
|
+
toType: entry.toType,
|
|
1282
|
+
toSlug: entry.toSlug,
|
|
1283
|
+
permanent: entry.permanent ?? true
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
return {
|
|
1287
|
+
fromSlugs,
|
|
1288
|
+
kind: "same-type",
|
|
1289
|
+
toSlug: entry.toSlug,
|
|
1290
|
+
permanent: entry.permanent ?? true
|
|
1291
|
+
};
|
|
1363
1292
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1293
|
+
|
|
1294
|
+
// src/redirects/load-type-redirects.ts
|
|
1295
|
+
var TYPE_REDIRECTS_FILENAME = "_redirects.json";
|
|
1296
|
+
function redirectsFilePath(config, type) {
|
|
1297
|
+
return path4.join(config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1298
|
+
}
|
|
1299
|
+
function loadTypeRedirectsFile(config, type) {
|
|
1300
|
+
const filePath = redirectsFilePath(config, type);
|
|
1301
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
1302
|
+
let raw;
|
|
1303
|
+
try {
|
|
1304
|
+
raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
|
|
1373
1308
|
);
|
|
1374
|
-
if (doc?.redirectTo) {
|
|
1375
|
-
issues.push({
|
|
1376
|
-
contentTypeId: target.contentTypeId,
|
|
1377
|
-
enSlug: target.canonicalSlug,
|
|
1378
|
-
field: "aliases",
|
|
1379
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1380
|
-
level: "error"
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
1309
|
}
|
|
1310
|
+
const parsed = typeRedirectsFileSchema.safeParse(raw);
|
|
1311
|
+
if (!parsed.success) {
|
|
1312
|
+
const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
|
|
1313
|
+
throw new Error(`${type.id}: invalid ${TYPE_REDIRECTS_FILENAME}: ${details}`);
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
contentTypeId: type.id,
|
|
1317
|
+
contentDir: type.contentDir,
|
|
1318
|
+
filePath,
|
|
1319
|
+
entries: parsed.data.redirects.map(parseRedirectEntry)
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function loadAllTypeRedirects(config) {
|
|
1323
|
+
const out = [];
|
|
1384
1324
|
for (const type of config.types) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1388
|
-
if (!doc?.redirectTo) continue;
|
|
1389
|
-
const targetSlug = extractSlugFromResolvedPath(type.path, doc.redirectTo);
|
|
1390
|
-
if (targetSlug && allAliasSlugs.has(targetSlug)) {
|
|
1391
|
-
issues.push({
|
|
1392
|
-
contentTypeId: type.id,
|
|
1393
|
-
enSlug,
|
|
1394
|
-
field: "redirect_to",
|
|
1395
|
-
message: `redirect_to target "${targetSlug}" is an alias slug \u2014 chain detected`,
|
|
1396
|
-
level: "error"
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
const targetDoc = targetSlug ? readEnDocument(config, type, targetSlug) : null;
|
|
1400
|
-
if (targetDoc?.redirectTo) {
|
|
1401
|
-
issues.push({
|
|
1402
|
-
contentTypeId: type.id,
|
|
1403
|
-
enSlug,
|
|
1404
|
-
field: "redirect_to",
|
|
1405
|
-
message: `redirect_to chain detected: "${enSlug}" \u2192 "${targetSlug}" \u2192 "${targetDoc.redirectTo}"`,
|
|
1406
|
-
level: "error"
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1325
|
+
const loaded = loadTypeRedirectsFile(config, type);
|
|
1326
|
+
if (loaded) out.push(loaded);
|
|
1410
1327
|
}
|
|
1411
|
-
return
|
|
1328
|
+
return out;
|
|
1412
1329
|
}
|
|
1413
1330
|
|
|
1414
|
-
// src/redirects/translation-index.ts
|
|
1415
|
-
init_esm_shims();
|
|
1416
|
-
|
|
1417
|
-
// src/i18n/translation-index.ts
|
|
1418
|
-
init_esm_shims();
|
|
1419
|
-
|
|
1420
|
-
// src/redirects/translation-index.ts
|
|
1421
|
-
init_sqlite();
|
|
1422
|
-
|
|
1423
1331
|
// src/sitemap/join-base-url.ts
|
|
1424
1332
|
init_esm_shims();
|
|
1425
1333
|
|
|
@@ -1471,6 +1379,173 @@ function loadConfigSync(options = {}) {
|
|
|
1471
1379
|
|
|
1472
1380
|
// src/validate/validate-project.ts
|
|
1473
1381
|
init_esm_shims();
|
|
1382
|
+
|
|
1383
|
+
// src/validate/validate-redirects.ts
|
|
1384
|
+
init_esm_shims();
|
|
1385
|
+
function liveDocExists(config, typeId, enSlug) {
|
|
1386
|
+
const type = config.types.find((entry) => entry.id === typeId);
|
|
1387
|
+
if (!type) return false;
|
|
1388
|
+
return enFileExists(config, type, enSlug);
|
|
1389
|
+
}
|
|
1390
|
+
function resolveRedirectTarget(project, sourceTypeId, entry) {
|
|
1391
|
+
if (entry.kind === "anywhere") return null;
|
|
1392
|
+
if (entry.kind === "cross-type") {
|
|
1393
|
+
return { typeId: entry.toType, enSlug: entry.toSlug };
|
|
1394
|
+
}
|
|
1395
|
+
return { typeId: sourceTypeId, enSlug: entry.toSlug };
|
|
1396
|
+
}
|
|
1397
|
+
function matchRoutableTarget(project, toUrl) {
|
|
1398
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1399
|
+
for (const type of project.config.types) {
|
|
1400
|
+
if (!type.path) continue;
|
|
1401
|
+
const enSlug = urlBuilder.extractSlugFromResolvedPath(type.path, toUrl);
|
|
1402
|
+
if (enSlug && liveDocExists(project.config, type.id, enSlug)) {
|
|
1403
|
+
return { typeId: type.id, enSlug };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
function validateTypeRedirects(project) {
|
|
1409
|
+
const issues = [];
|
|
1410
|
+
const globalFrom = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const type of project.config.types) {
|
|
1412
|
+
const filePath = path4.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1413
|
+
if (!fs2.existsSync(filePath)) continue;
|
|
1414
|
+
let loaded;
|
|
1415
|
+
try {
|
|
1416
|
+
loaded = loadTypeRedirectsFile(project.config, type);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
issues.push({
|
|
1419
|
+
level: "error",
|
|
1420
|
+
contentType: type.id,
|
|
1421
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1422
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1423
|
+
});
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (!loaded) continue;
|
|
1427
|
+
if (!isRoutableType(type)) {
|
|
1428
|
+
issues.push({
|
|
1429
|
+
level: "error",
|
|
1430
|
+
contentType: type.id,
|
|
1431
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1432
|
+
message: `Redirects are only supported on routable content types (missing path)`
|
|
1433
|
+
});
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
for (const entry of loaded.entries) {
|
|
1437
|
+
for (const from of entry.fromSlugs) {
|
|
1438
|
+
if (from === entry.toSlug) {
|
|
1439
|
+
issues.push({
|
|
1440
|
+
level: "error",
|
|
1441
|
+
contentType: type.id,
|
|
1442
|
+
enSlug: from,
|
|
1443
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1444
|
+
message: `Redirect source "${from}" must not equal its target slug`
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
if (liveDocExists(project.config, type.id, from)) {
|
|
1448
|
+
issues.push({
|
|
1449
|
+
level: "error",
|
|
1450
|
+
contentType: type.id,
|
|
1451
|
+
enSlug: from,
|
|
1452
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1453
|
+
message: `Redirect source "${from}" still has a live EN file \u2014 delete or rename the MDX first`
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
const existing = globalFrom.get(from);
|
|
1457
|
+
if (existing && existing.typeId !== type.id) {
|
|
1458
|
+
issues.push({
|
|
1459
|
+
level: "error",
|
|
1460
|
+
contentType: type.id,
|
|
1461
|
+
enSlug: from,
|
|
1462
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1463
|
+
message: `Redirect source "${from}" is already claimed by ${existing.typeId}`
|
|
1464
|
+
});
|
|
1465
|
+
} else if (existing) {
|
|
1466
|
+
issues.push({
|
|
1467
|
+
level: "error",
|
|
1468
|
+
contentType: type.id,
|
|
1469
|
+
enSlug: from,
|
|
1470
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1471
|
+
message: `Duplicate redirect source "${from}" in ${TYPE_REDIRECTS_FILENAME}`
|
|
1472
|
+
});
|
|
1473
|
+
} else {
|
|
1474
|
+
globalFrom.set(from, { typeId: type.id, filePath: loaded.filePath });
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (entry.kind === "cross-type") {
|
|
1478
|
+
const targetType = project.config.types.find((candidate) => candidate.id === entry.toType);
|
|
1479
|
+
if (!targetType) {
|
|
1480
|
+
issues.push({
|
|
1481
|
+
level: "error",
|
|
1482
|
+
contentType: type.id,
|
|
1483
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1484
|
+
message: `Unknown redirect target type "${entry.toType}"`
|
|
1485
|
+
});
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (!isRoutableType(targetType)) {
|
|
1489
|
+
issues.push({
|
|
1490
|
+
level: "error",
|
|
1491
|
+
contentType: type.id,
|
|
1492
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1493
|
+
message: `Redirect target type "${entry.toType}" is not routable (missing path)`
|
|
1494
|
+
});
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const target = resolveRedirectTarget(project, type.id, entry);
|
|
1499
|
+
if (target) {
|
|
1500
|
+
if (!liveDocExists(project.config, target.typeId, target.enSlug)) {
|
|
1501
|
+
issues.push({
|
|
1502
|
+
level: "error",
|
|
1503
|
+
contentType: type.id,
|
|
1504
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1505
|
+
message: `Redirect target ${target.typeId}/${target.enSlug} does not exist`
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (entry.kind === "anywhere" && entry.toUrl?.startsWith("/")) {
|
|
1511
|
+
const matched = matchRoutableTarget(project, entry.toUrl);
|
|
1512
|
+
if (matched && !liveDocExists(project.config, matched.typeId, matched.enSlug)) {
|
|
1513
|
+
issues.push({
|
|
1514
|
+
level: "error",
|
|
1515
|
+
contentType: type.id,
|
|
1516
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1517
|
+
message: `Redirect toUrl "${entry.toUrl}" does not resolve to a live document`
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const allLoaded = loadAllTypeRedirects(project.config);
|
|
1524
|
+
for (const file of allLoaded) {
|
|
1525
|
+
for (const entry of file.entries) {
|
|
1526
|
+
if (entry.kind === "anywhere" || !entry.toSlug) continue;
|
|
1527
|
+
const targetTypeId = entry.kind === "cross-type" ? entry.toType : file.contentTypeId;
|
|
1528
|
+
const targetType = project.config.types.find((type) => type.id === targetTypeId);
|
|
1529
|
+
if (!targetType?.path) continue;
|
|
1530
|
+
for (const from of entry.fromSlugs) {
|
|
1531
|
+
if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
|
|
1532
|
+
const chainTarget = globalFrom.get(entry.toSlug);
|
|
1533
|
+
if (chainTarget) {
|
|
1534
|
+
issues.push({
|
|
1535
|
+
level: "error",
|
|
1536
|
+
contentType: file.contentTypeId,
|
|
1537
|
+
enSlug: from,
|
|
1538
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1539
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return issues;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/validate/validate-project.ts
|
|
1474
1549
|
init_sqlite();
|
|
1475
1550
|
|
|
1476
1551
|
// src/validate/validate-relations.ts
|
|
@@ -1671,7 +1746,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1671
1746
|
}
|
|
1672
1747
|
|
|
1673
1748
|
// src/validate/validate-project.ts
|
|
1674
|
-
function
|
|
1749
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1675
1750
|
const dir = path4.join(rootDir, contentDir);
|
|
1676
1751
|
if (!fs2.existsSync(dir)) return [];
|
|
1677
1752
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1689,7 +1764,7 @@ function validateProject(config) {
|
|
|
1689
1764
|
}
|
|
1690
1765
|
const db = openStore(config, "readonly");
|
|
1691
1766
|
for (const type of config.types) {
|
|
1692
|
-
const enSlugs =
|
|
1767
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1693
1768
|
const englishSlugs = new Set(enSlugs);
|
|
1694
1769
|
if (!type.translate && isRoutableType(type)) {
|
|
1695
1770
|
issues.push({
|
|
@@ -1771,33 +1846,8 @@ function validateProject(config) {
|
|
|
1771
1846
|
}
|
|
1772
1847
|
}
|
|
1773
1848
|
db.close();
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
issues.push({
|
|
1777
|
-
level: issue.level,
|
|
1778
|
-
contentType: issue.contentTypeId,
|
|
1779
|
-
enSlug: issue.enSlug,
|
|
1780
|
-
field: issue.field,
|
|
1781
|
-
message: issue.message
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
1785
|
-
issues.push({
|
|
1786
|
-
level: issue.level,
|
|
1787
|
-
contentType: issue.contentTypeId,
|
|
1788
|
-
enSlug: issue.enSlug,
|
|
1789
|
-
field: issue.field,
|
|
1790
|
-
message: issue.message
|
|
1791
|
-
});
|
|
1792
|
-
}
|
|
1793
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
1794
|
-
issues.push({
|
|
1795
|
-
level: issue.level,
|
|
1796
|
-
contentType: issue.contentTypeId,
|
|
1797
|
-
enSlug: issue.enSlug,
|
|
1798
|
-
field: issue.field,
|
|
1799
|
-
message: issue.message
|
|
1800
|
-
});
|
|
1849
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
1850
|
+
issues.push(issue);
|
|
1801
1851
|
}
|
|
1802
1852
|
for (const issue of validateRelations(project)) {
|
|
1803
1853
|
issues.push({
|
|
@@ -1841,7 +1891,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1841
1891
|
|
|
1842
1892
|
// src/translate/worklist.ts
|
|
1843
1893
|
init_sqlite();
|
|
1844
|
-
function
|
|
1894
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
1845
1895
|
const dir = path4.join(rootDir, contentDir);
|
|
1846
1896
|
if (!fs2.existsSync(dir)) return [];
|
|
1847
1897
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1852,7 +1902,7 @@ function buildWorklist(config, options = {}) {
|
|
|
1852
1902
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
1853
1903
|
for (const type of config.types) {
|
|
1854
1904
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
1855
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
1905
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
1856
1906
|
for (const enSlug of enSlugs) {
|
|
1857
1907
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
1858
1908
|
if (!enDoc) continue;
|
|
@@ -2153,11 +2203,12 @@ init_esm_shims();
|
|
|
2153
2203
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2154
2204
|
if (slugStrategy === "localized") {
|
|
2155
2205
|
const rules = [
|
|
2156
|
-
"Provide a URL slug in JSON field `slug` that is
|
|
2206
|
+
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2157
2207
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2158
2208
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2159
2209
|
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2160
|
-
"Do NOT append locale codes to the slug (e.g. -fr, -he, -zh-cn). Locale routing is handled by the URL prefix, not the slug."
|
|
2210
|
+
"Do NOT append locale codes to the slug (e.g. -fr, -he, -zh-cn). Locale routing is handled by the URL prefix, not the slug.",
|
|
2211
|
+
'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".'
|
|
2161
2212
|
];
|
|
2162
2213
|
if (preserveTerms?.length) {
|
|
2163
2214
|
rules.push(
|
|
@@ -2190,6 +2241,96 @@ function resolveTranslateConfig(config, type) {
|
|
|
2190
2241
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2191
2242
|
}
|
|
2192
2243
|
|
|
2244
|
+
// src/translate/sanitize-mdx-jsx.ts
|
|
2245
|
+
init_esm_shims();
|
|
2246
|
+
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2247
|
+
let adjusted = false;
|
|
2248
|
+
let out = "";
|
|
2249
|
+
let i = 0;
|
|
2250
|
+
while (i < body.length) {
|
|
2251
|
+
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2252
|
+
out += body[i];
|
|
2253
|
+
i += 1;
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
const tagStart = i;
|
|
2257
|
+
i += 1;
|
|
2258
|
+
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2259
|
+
const tagName = body.slice(tagStart + 1, i);
|
|
2260
|
+
let tagOut = `<${tagName}`;
|
|
2261
|
+
let tagAdjusted = false;
|
|
2262
|
+
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2263
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2264
|
+
tagOut += body[i];
|
|
2265
|
+
i += 1;
|
|
2266
|
+
}
|
|
2267
|
+
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2268
|
+
const attrStart = i;
|
|
2269
|
+
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2270
|
+
body.slice(attrStart, i);
|
|
2271
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2272
|
+
if (body[i] !== "=") {
|
|
2273
|
+
tagOut += body.slice(attrStart, i);
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
tagOut += body.slice(attrStart, i);
|
|
2277
|
+
tagOut += "=";
|
|
2278
|
+
i += 1;
|
|
2279
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2280
|
+
const quote = body[i];
|
|
2281
|
+
if (quote !== '"' && quote !== "'") {
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
if (quote === "'") {
|
|
2285
|
+
const valStart2 = i + 1;
|
|
2286
|
+
i += 1;
|
|
2287
|
+
while (i < body.length) {
|
|
2288
|
+
if (body[i] === "\\") {
|
|
2289
|
+
i += 2;
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2292
|
+
if (body[i] === "'") break;
|
|
2293
|
+
i += 1;
|
|
2294
|
+
}
|
|
2295
|
+
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2296
|
+
i += 1;
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
const valStart = i + 1;
|
|
2300
|
+
i += 1;
|
|
2301
|
+
let closeIdx = -1;
|
|
2302
|
+
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2303
|
+
if (body[scan] !== '"') continue;
|
|
2304
|
+
let j = scan + 1;
|
|
2305
|
+
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2306
|
+
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2307
|
+
closeIdx = scan;
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (closeIdx === -1) {
|
|
2312
|
+
tagOut += body.slice(valStart - 1, i);
|
|
2313
|
+
break;
|
|
2314
|
+
}
|
|
2315
|
+
const value = body.slice(valStart, closeIdx);
|
|
2316
|
+
const hasInternalQuote = value.includes('"');
|
|
2317
|
+
if (hasInternalQuote) {
|
|
2318
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2319
|
+
tagOut += `'${escaped}'`;
|
|
2320
|
+
tagAdjusted = true;
|
|
2321
|
+
} else {
|
|
2322
|
+
tagOut += `"${value}"`;
|
|
2323
|
+
}
|
|
2324
|
+
i = closeIdx + 1;
|
|
2325
|
+
}
|
|
2326
|
+
if (tagAdjusted) adjusted = true;
|
|
2327
|
+
tagOut += body[i] ?? "";
|
|
2328
|
+
out += tagOut;
|
|
2329
|
+
i += 1;
|
|
2330
|
+
}
|
|
2331
|
+
return { body: out, adjusted };
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2193
2334
|
// src/translate/validate-translation.ts
|
|
2194
2335
|
init_esm_shims();
|
|
2195
2336
|
function formatZodIssues(error) {
|
|
@@ -2324,6 +2465,9 @@ async function translatePage(config, item, options = {}) {
|
|
|
2324
2465
|
if (!validated.ok) {
|
|
2325
2466
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2326
2467
|
}
|
|
2468
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
|
|
2469
|
+
result.parsed.body
|
|
2470
|
+
);
|
|
2327
2471
|
const writeDb = openStore(config, "readwrite");
|
|
2328
2472
|
const snapshotId = recordEnSnapshot(
|
|
2329
2473
|
config,
|
|
@@ -2342,7 +2486,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2342
2486
|
locale: item.locale,
|
|
2343
2487
|
slug,
|
|
2344
2488
|
frontmatter: validated.frontmatter,
|
|
2345
|
-
body:
|
|
2489
|
+
body: translatedBody,
|
|
2346
2490
|
enHash: currentEnHash,
|
|
2347
2491
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2348
2492
|
model: result.model,
|
|
@@ -2361,7 +2505,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2361
2505
|
usage: result.usage,
|
|
2362
2506
|
estimatedCostUsd,
|
|
2363
2507
|
durationMs: Date.now() - startedAt,
|
|
2364
|
-
slugAdjusted
|
|
2508
|
+
slugAdjusted,
|
|
2509
|
+
mdxAdjusted: mdxAdjusted || void 0
|
|
2365
2510
|
};
|
|
2366
2511
|
} catch (error) {
|
|
2367
2512
|
return {
|
|
@@ -2444,6 +2589,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2444
2589
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2445
2590
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2446
2591
|
const out = [];
|
|
2592
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2447
2593
|
for (const type of project.listTypes()) {
|
|
2448
2594
|
if (!isRoutableType(type.config)) continue;
|
|
2449
2595
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2459,7 +2605,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2459
2605
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2460
2606
|
const doc = resolved.document;
|
|
2461
2607
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2462
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2608
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2463
2609
|
out.push({
|
|
2464
2610
|
relativePath: urlPath.slice(1),
|
|
2465
2611
|
urlPath,
|