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.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);
|
|
@@ -1223,6 +1195,9 @@ function createProject(config) {
|
|
|
1223
1195
|
getType: getRuntime,
|
|
1224
1196
|
listTypes() {
|
|
1225
1197
|
return Array.from(runtimes.values());
|
|
1198
|
+
},
|
|
1199
|
+
listRoutableTypes() {
|
|
1200
|
+
return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
|
|
1226
1201
|
}
|
|
1227
1202
|
};
|
|
1228
1203
|
}
|
|
@@ -1233,208 +1208,141 @@ init_cjs_shims();
|
|
|
1233
1208
|
// src/redirects/build-redirects.ts
|
|
1234
1209
|
init_cjs_shims();
|
|
1235
1210
|
|
|
1236
|
-
// src/core/slug-aliases.ts
|
|
1237
|
-
init_cjs_shims();
|
|
1238
|
-
|
|
1239
1211
|
// src/core/alias-helpers.ts
|
|
1240
1212
|
init_cjs_shims();
|
|
1241
1213
|
function enFileExists(config, type, enSlug) {
|
|
1242
1214
|
const dir = path3__default.default.join(config.rootDir, type.contentDir);
|
|
1243
1215
|
return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
|
|
1244
1216
|
}
|
|
1245
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1246
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1247
|
-
}
|
|
1248
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1249
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1250
|
-
}
|
|
1251
1217
|
function listEnSlugs(rootDir, contentDir) {
|
|
1252
1218
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1253
1219
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1254
1220
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1255
1221
|
}
|
|
1256
1222
|
|
|
1257
|
-
// src/
|
|
1223
|
+
// src/i18n/translation-index.ts
|
|
1224
|
+
init_cjs_shims();
|
|
1225
|
+
|
|
1226
|
+
// src/redirects/build-redirects.ts
|
|
1258
1227
|
init_sqlite();
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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;
|
|
1268
1260
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
level: "error"
|
|
1283
|
-
});
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
if (canonicalSlugs.has(alias)) {
|
|
1287
|
-
issues.push({
|
|
1288
|
-
contentTypeId: type.id,
|
|
1289
|
-
enSlug,
|
|
1290
|
-
field: "aliases",
|
|
1291
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1292
|
-
level: "error"
|
|
1293
|
-
});
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
const existing = aliasToTarget.get(alias);
|
|
1297
|
-
if (existing) {
|
|
1298
|
-
issues.push({
|
|
1299
|
-
contentTypeId: type.id,
|
|
1300
|
-
enSlug,
|
|
1301
|
-
field: "aliases",
|
|
1302
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1303
|
-
level: "error"
|
|
1304
|
-
});
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1307
|
-
aliasToTarget.set(alias, {
|
|
1308
|
-
contentTypeId: type.id,
|
|
1309
|
-
canonicalSlug: enSlug,
|
|
1310
|
-
path: type.path
|
|
1311
|
-
});
|
|
1312
|
-
allAliasSlugs.add(alias);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
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
|
+
});
|
|
1315
1274
|
}
|
|
1316
|
-
|
|
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];
|
|
1317
1281
|
}
|
|
1318
|
-
function
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1328
|
-
if (!known) {
|
|
1329
|
-
issues.push({
|
|
1330
|
-
contentTypeId: type.id,
|
|
1331
|
-
enSlug: target.canonicalSlug,
|
|
1332
|
-
field: "aliases",
|
|
1333
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1334
|
-
level: "warning"
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
if (enFileExists(config, type, alias)) {
|
|
1338
|
-
issues.push({
|
|
1339
|
-
contentTypeId: type.id,
|
|
1340
|
-
enSlug: target.canonicalSlug,
|
|
1341
|
-
field: "aliases",
|
|
1342
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1343
|
-
level: "warning"
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
const sqliteRows = listTranslationsForEnSlug(db, type.id, alias);
|
|
1347
|
-
if (sqliteRows.length > 0) {
|
|
1348
|
-
issues.push({
|
|
1349
|
-
contentTypeId: type.id,
|
|
1350
|
-
enSlug: target.canonicalSlug,
|
|
1351
|
-
field: "aliases",
|
|
1352
|
-
message: `${sqliteRows.length} sqlite translation(s) for alias "${alias}" retained for locale redirects and history`,
|
|
1353
|
-
level: "info"
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
for (const locale of config.locales) {
|
|
1357
|
-
if (locale === config.defaultLocale) continue;
|
|
1358
|
-
const canonicalRow = listTranslationsForEnSlug(db, type.id, target.canonicalSlug).find(
|
|
1359
|
-
(r) => r.locale === locale
|
|
1360
|
-
);
|
|
1361
|
-
if (!canonicalRow) continue;
|
|
1362
|
-
const aliasRow = sqliteRows.find((r) => r.locale === locale);
|
|
1363
|
-
if (!aliasRow && known) {
|
|
1364
|
-
issues.push({
|
|
1365
|
-
contentTypeId: type.id,
|
|
1366
|
-
enSlug: target.canonicalSlug,
|
|
1367
|
-
field: "aliases",
|
|
1368
|
-
message: `Alias "${alias}" has no locale slug mapping for ${locale} \u2014 old localized URL may miss redirect`,
|
|
1369
|
-
level: "warning"
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
} finally {
|
|
1375
|
-
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
|
+
};
|
|
1376
1291
|
}
|
|
1377
|
-
|
|
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
|
+
};
|
|
1378
1307
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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)}`
|
|
1388
1323
|
);
|
|
1389
|
-
if (doc?.redirectTo) {
|
|
1390
|
-
issues.push({
|
|
1391
|
-
contentTypeId: target.contentTypeId,
|
|
1392
|
-
enSlug: target.canonicalSlug,
|
|
1393
|
-
field: "aliases",
|
|
1394
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1395
|
-
level: "error"
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
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 = [];
|
|
1399
1339
|
for (const type of config.types) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1403
|
-
if (!doc?.redirectTo) continue;
|
|
1404
|
-
const targetSlug = extractSlugFromResolvedPath(type.path, doc.redirectTo);
|
|
1405
|
-
if (targetSlug && allAliasSlugs.has(targetSlug)) {
|
|
1406
|
-
issues.push({
|
|
1407
|
-
contentTypeId: type.id,
|
|
1408
|
-
enSlug,
|
|
1409
|
-
field: "redirect_to",
|
|
1410
|
-
message: `redirect_to target "${targetSlug}" is an alias slug \u2014 chain detected`,
|
|
1411
|
-
level: "error"
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
const targetDoc = targetSlug ? readEnDocument(config, type, targetSlug) : null;
|
|
1415
|
-
if (targetDoc?.redirectTo) {
|
|
1416
|
-
issues.push({
|
|
1417
|
-
contentTypeId: type.id,
|
|
1418
|
-
enSlug,
|
|
1419
|
-
field: "redirect_to",
|
|
1420
|
-
message: `redirect_to chain detected: "${enSlug}" \u2192 "${targetSlug}" \u2192 "${targetDoc.redirectTo}"`,
|
|
1421
|
-
level: "error"
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1340
|
+
const loaded = loadTypeRedirectsFile(config, type);
|
|
1341
|
+
if (loaded) out.push(loaded);
|
|
1425
1342
|
}
|
|
1426
|
-
return
|
|
1343
|
+
return out;
|
|
1427
1344
|
}
|
|
1428
1345
|
|
|
1429
|
-
// src/redirects/translation-index.ts
|
|
1430
|
-
init_cjs_shims();
|
|
1431
|
-
|
|
1432
|
-
// src/i18n/translation-index.ts
|
|
1433
|
-
init_cjs_shims();
|
|
1434
|
-
|
|
1435
|
-
// src/redirects/translation-index.ts
|
|
1436
|
-
init_sqlite();
|
|
1437
|
-
|
|
1438
1346
|
// src/sitemap/join-base-url.ts
|
|
1439
1347
|
init_cjs_shims();
|
|
1440
1348
|
|
|
@@ -1486,6 +1394,173 @@ function loadConfigSync(options = {}) {
|
|
|
1486
1394
|
|
|
1487
1395
|
// src/validate/validate-project.ts
|
|
1488
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
|
|
1489
1564
|
init_sqlite();
|
|
1490
1565
|
|
|
1491
1566
|
// src/validate/validate-relations.ts
|
|
@@ -1686,7 +1761,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1686
1761
|
}
|
|
1687
1762
|
|
|
1688
1763
|
// src/validate/validate-project.ts
|
|
1689
|
-
function
|
|
1764
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1690
1765
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1691
1766
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1692
1767
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1704,7 +1779,7 @@ function validateProject(config) {
|
|
|
1704
1779
|
}
|
|
1705
1780
|
const db = openStore(config, "readonly");
|
|
1706
1781
|
for (const type of config.types) {
|
|
1707
|
-
const enSlugs =
|
|
1782
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1708
1783
|
const englishSlugs = new Set(enSlugs);
|
|
1709
1784
|
if (!type.translate && isRoutableType(type)) {
|
|
1710
1785
|
issues.push({
|
|
@@ -1786,33 +1861,8 @@ function validateProject(config) {
|
|
|
1786
1861
|
}
|
|
1787
1862
|
}
|
|
1788
1863
|
db.close();
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
issues.push({
|
|
1792
|
-
level: issue.level,
|
|
1793
|
-
contentType: issue.contentTypeId,
|
|
1794
|
-
enSlug: issue.enSlug,
|
|
1795
|
-
field: issue.field,
|
|
1796
|
-
message: issue.message
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
1800
|
-
issues.push({
|
|
1801
|
-
level: issue.level,
|
|
1802
|
-
contentType: issue.contentTypeId,
|
|
1803
|
-
enSlug: issue.enSlug,
|
|
1804
|
-
field: issue.field,
|
|
1805
|
-
message: issue.message
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
1809
|
-
issues.push({
|
|
1810
|
-
level: issue.level,
|
|
1811
|
-
contentType: issue.contentTypeId,
|
|
1812
|
-
enSlug: issue.enSlug,
|
|
1813
|
-
field: issue.field,
|
|
1814
|
-
message: issue.message
|
|
1815
|
-
});
|
|
1864
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
1865
|
+
issues.push(issue);
|
|
1816
1866
|
}
|
|
1817
1867
|
for (const issue of validateRelations(project)) {
|
|
1818
1868
|
issues.push({
|
|
@@ -1856,7 +1906,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1856
1906
|
|
|
1857
1907
|
// src/translate/worklist.ts
|
|
1858
1908
|
init_sqlite();
|
|
1859
|
-
function
|
|
1909
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
1860
1910
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1861
1911
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1862
1912
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1867,7 +1917,7 @@ function buildWorklist(config, options = {}) {
|
|
|
1867
1917
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
1868
1918
|
for (const type of config.types) {
|
|
1869
1919
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
1870
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
1920
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
1871
1921
|
for (const enSlug of enSlugs) {
|
|
1872
1922
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
1873
1923
|
if (!enDoc) continue;
|
|
@@ -2168,11 +2218,12 @@ init_cjs_shims();
|
|
|
2168
2218
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2169
2219
|
if (slugStrategy === "localized") {
|
|
2170
2220
|
const rules = [
|
|
2171
|
-
"Provide a URL slug in JSON field `slug` that is
|
|
2221
|
+
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2172
2222
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2173
2223
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2174
2224
|
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2175
|
-
"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."
|
|
2225
|
+
"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.",
|
|
2226
|
+
'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 \\".'
|
|
2176
2227
|
];
|
|
2177
2228
|
if (preserveTerms?.length) {
|
|
2178
2229
|
rules.push(
|
|
@@ -2205,6 +2256,96 @@ function resolveTranslateConfig(config, type) {
|
|
|
2205
2256
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2206
2257
|
}
|
|
2207
2258
|
|
|
2259
|
+
// src/translate/sanitize-mdx-jsx.ts
|
|
2260
|
+
init_cjs_shims();
|
|
2261
|
+
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2262
|
+
let adjusted = false;
|
|
2263
|
+
let out = "";
|
|
2264
|
+
let i = 0;
|
|
2265
|
+
while (i < body.length) {
|
|
2266
|
+
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2267
|
+
out += body[i];
|
|
2268
|
+
i += 1;
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const tagStart = i;
|
|
2272
|
+
i += 1;
|
|
2273
|
+
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2274
|
+
const tagName = body.slice(tagStart + 1, i);
|
|
2275
|
+
let tagOut = `<${tagName}`;
|
|
2276
|
+
let tagAdjusted = false;
|
|
2277
|
+
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2278
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2279
|
+
tagOut += body[i];
|
|
2280
|
+
i += 1;
|
|
2281
|
+
}
|
|
2282
|
+
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2283
|
+
const attrStart = i;
|
|
2284
|
+
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2285
|
+
body.slice(attrStart, i);
|
|
2286
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2287
|
+
if (body[i] !== "=") {
|
|
2288
|
+
tagOut += body.slice(attrStart, i);
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
tagOut += body.slice(attrStart, i);
|
|
2292
|
+
tagOut += "=";
|
|
2293
|
+
i += 1;
|
|
2294
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2295
|
+
const quote = body[i];
|
|
2296
|
+
if (quote !== '"' && quote !== "'") {
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (quote === "'") {
|
|
2300
|
+
const valStart2 = i + 1;
|
|
2301
|
+
i += 1;
|
|
2302
|
+
while (i < body.length) {
|
|
2303
|
+
if (body[i] === "\\") {
|
|
2304
|
+
i += 2;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (body[i] === "'") break;
|
|
2308
|
+
i += 1;
|
|
2309
|
+
}
|
|
2310
|
+
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2311
|
+
i += 1;
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
const valStart = i + 1;
|
|
2315
|
+
i += 1;
|
|
2316
|
+
let closeIdx = -1;
|
|
2317
|
+
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2318
|
+
if (body[scan] !== '"') continue;
|
|
2319
|
+
let j = scan + 1;
|
|
2320
|
+
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2321
|
+
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2322
|
+
closeIdx = scan;
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (closeIdx === -1) {
|
|
2327
|
+
tagOut += body.slice(valStart - 1, i);
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
const value = body.slice(valStart, closeIdx);
|
|
2331
|
+
const hasInternalQuote = value.includes('"');
|
|
2332
|
+
if (hasInternalQuote) {
|
|
2333
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2334
|
+
tagOut += `'${escaped}'`;
|
|
2335
|
+
tagAdjusted = true;
|
|
2336
|
+
} else {
|
|
2337
|
+
tagOut += `"${value}"`;
|
|
2338
|
+
}
|
|
2339
|
+
i = closeIdx + 1;
|
|
2340
|
+
}
|
|
2341
|
+
if (tagAdjusted) adjusted = true;
|
|
2342
|
+
tagOut += body[i] ?? "";
|
|
2343
|
+
out += tagOut;
|
|
2344
|
+
i += 1;
|
|
2345
|
+
}
|
|
2346
|
+
return { body: out, adjusted };
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2208
2349
|
// src/translate/validate-translation.ts
|
|
2209
2350
|
init_cjs_shims();
|
|
2210
2351
|
function formatZodIssues(error) {
|
|
@@ -2339,6 +2480,9 @@ async function translatePage(config, item, options = {}) {
|
|
|
2339
2480
|
if (!validated.ok) {
|
|
2340
2481
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2341
2482
|
}
|
|
2483
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
|
|
2484
|
+
result.parsed.body
|
|
2485
|
+
);
|
|
2342
2486
|
const writeDb = openStore(config, "readwrite");
|
|
2343
2487
|
const snapshotId = recordEnSnapshot(
|
|
2344
2488
|
config,
|
|
@@ -2357,7 +2501,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2357
2501
|
locale: item.locale,
|
|
2358
2502
|
slug,
|
|
2359
2503
|
frontmatter: validated.frontmatter,
|
|
2360
|
-
body:
|
|
2504
|
+
body: translatedBody,
|
|
2361
2505
|
enHash: currentEnHash,
|
|
2362
2506
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2363
2507
|
model: result.model,
|
|
@@ -2376,7 +2520,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2376
2520
|
usage: result.usage,
|
|
2377
2521
|
estimatedCostUsd,
|
|
2378
2522
|
durationMs: Date.now() - startedAt,
|
|
2379
|
-
slugAdjusted
|
|
2523
|
+
slugAdjusted,
|
|
2524
|
+
mdxAdjusted: mdxAdjusted || void 0
|
|
2380
2525
|
};
|
|
2381
2526
|
} catch (error) {
|
|
2382
2527
|
return {
|
|
@@ -2459,6 +2604,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2459
2604
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2460
2605
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2461
2606
|
const out = [];
|
|
2607
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2462
2608
|
for (const type of project.listTypes()) {
|
|
2463
2609
|
if (!isRoutableType(type.config)) continue;
|
|
2464
2610
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2474,7 +2620,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2474
2620
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2475
2621
|
const doc = resolved.document;
|
|
2476
2622
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2477
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2623
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2478
2624
|
out.push({
|
|
2479
2625
|
relativePath: urlPath.slice(1),
|
|
2480
2626
|
urlPath,
|