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/index.cjs
CHANGED
|
@@ -139,14 +139,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
139
139
|
function isRoutableType(type) {
|
|
140
140
|
return typeof type.path === "string" && type.path.length > 0;
|
|
141
141
|
}
|
|
142
|
-
function assertValidPathTemplate(
|
|
143
|
-
if (!
|
|
144
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
142
|
+
function assertValidPathTemplate(path13, typeId) {
|
|
143
|
+
if (!path13.startsWith("/")) {
|
|
144
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
|
|
145
145
|
}
|
|
146
|
-
const count = (
|
|
146
|
+
const count = (path13.match(/\{slug\}/g) ?? []).length;
|
|
147
147
|
if (count !== 1) {
|
|
148
148
|
throw new Error(
|
|
149
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
149
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -160,19 +160,69 @@ function pathSuffix(template) {
|
|
|
160
160
|
if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
|
|
161
161
|
return template.slice(idx + SLUG_PLACEHOLDER.length);
|
|
162
162
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
163
|
+
function resolveDefaultLocaleRouting() {
|
|
164
|
+
return { strategy: "path-prefix", prefixDefaultLocale: false };
|
|
165
|
+
}
|
|
166
|
+
function splitPathAndSearch(pathname) {
|
|
167
|
+
const q = pathname.indexOf("?");
|
|
168
|
+
if (q === -1) return { pathname, search: "" };
|
|
169
|
+
return { pathname: pathname.slice(0, q), search: pathname.slice(q) };
|
|
170
|
+
}
|
|
171
|
+
function createUrlBuilder(config) {
|
|
172
|
+
const localeRouting = config.localeRouting ?? resolveDefaultLocaleRouting();
|
|
173
|
+
const defaultLocale = config.defaultLocale;
|
|
174
|
+
const prefixedLocales = localeRouting.strategy === "path-prefix" ? localeRouting.prefixDefaultLocale ? [...config.locales] : config.locales.filter((locale) => locale !== defaultLocale) : config.locales.filter((locale) => locale !== defaultLocale);
|
|
175
|
+
function applyLocaleToPath(pathname, locale) {
|
|
176
|
+
if (locale === defaultLocale) {
|
|
177
|
+
if (localeRouting.strategy === "path-prefix" && localeRouting.prefixDefaultLocale) {
|
|
178
|
+
return `/${defaultLocale}${pathname}`;
|
|
179
|
+
}
|
|
180
|
+
return pathname;
|
|
181
|
+
}
|
|
182
|
+
if (localeRouting.strategy === "search-param") {
|
|
183
|
+
const { pathname: base, search } = splitPathAndSearch(pathname);
|
|
184
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
185
|
+
params.set(localeRouting.param, locale);
|
|
186
|
+
const qs = params.toString();
|
|
187
|
+
return qs ? `${base}?${qs}` : base;
|
|
188
|
+
}
|
|
189
|
+
return `/${locale}${pathname}`;
|
|
190
|
+
}
|
|
191
|
+
function resolvePath(template, slug, locale) {
|
|
192
|
+
const relative = template.replace(SLUG_PLACEHOLDER, slug);
|
|
193
|
+
return applyLocaleToPath(relative, locale);
|
|
194
|
+
}
|
|
195
|
+
function extractSlugFromResolvedPath(template, resolvedPath) {
|
|
196
|
+
let pathname = resolvedPath;
|
|
197
|
+
if (localeRouting.strategy === "search-param") {
|
|
198
|
+
pathname = splitPathAndSearch(resolvedPath).pathname;
|
|
199
|
+
} else if (localeRouting.strategy === "path-prefix") {
|
|
200
|
+
for (const locale of config.locales) {
|
|
201
|
+
if (locale === defaultLocale && !localeRouting.prefixDefaultLocale) continue;
|
|
202
|
+
const prefix2 = `/${locale}`;
|
|
203
|
+
if (pathname === prefix2 || pathname.startsWith(`${prefix2}/`)) {
|
|
204
|
+
pathname = pathname.slice(prefix2.length) || "/";
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const prefix = pathPrefix(template);
|
|
210
|
+
const suffix = pathSuffix(template);
|
|
211
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
212
|
+
if (suffix && !pathname.endsWith(suffix)) return null;
|
|
213
|
+
const slugEnd = suffix ? pathname.length - suffix.length : pathname.length;
|
|
214
|
+
const slug = pathname.slice(prefix.length, slugEnd);
|
|
215
|
+
return slug.length > 0 ? slug : null;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
defaultLocale,
|
|
219
|
+
locales: config.locales,
|
|
220
|
+
localeRouting,
|
|
221
|
+
prefixedLocales,
|
|
222
|
+
resolvePath,
|
|
223
|
+
extractSlugFromResolvedPath,
|
|
224
|
+
applyLocaleToPath
|
|
225
|
+
};
|
|
176
226
|
}
|
|
177
227
|
|
|
178
228
|
// src/config/resolve-config.ts
|
|
@@ -195,6 +245,13 @@ function resolveConfig(input, baseDir) {
|
|
|
195
245
|
`scribe config: defaultLocale "${defaultLocale}" is not in locales [${raw.locales.join(", ")}]`
|
|
196
246
|
);
|
|
197
247
|
}
|
|
248
|
+
const localeRouting = raw.localeRouting ?? {
|
|
249
|
+
strategy: "path-prefix",
|
|
250
|
+
prefixDefaultLocale: false
|
|
251
|
+
};
|
|
252
|
+
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
253
|
+
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
254
|
+
}
|
|
198
255
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
199
256
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
200
257
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -222,6 +279,7 @@ function resolveConfig(input, baseDir) {
|
|
|
222
279
|
assetsPath,
|
|
223
280
|
locales: [...raw.locales],
|
|
224
281
|
defaultLocale,
|
|
282
|
+
localeRouting,
|
|
225
283
|
localePresets: raw.localePresets,
|
|
226
284
|
translate: raw.translate,
|
|
227
285
|
types
|
|
@@ -272,10 +330,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
272
330
|
}
|
|
273
331
|
function extractByPaths(data, paths) {
|
|
274
332
|
const out = {};
|
|
275
|
-
for (const
|
|
276
|
-
const value = getAtPath(data,
|
|
333
|
+
for (const path13 of paths) {
|
|
334
|
+
const value = getAtPath(data, path13);
|
|
277
335
|
if (value !== void 0) {
|
|
278
|
-
setAtPath(out,
|
|
336
|
+
setAtPath(out, path13.filter((p) => p !== "*"), value);
|
|
279
337
|
}
|
|
280
338
|
}
|
|
281
339
|
return out;
|
|
@@ -293,13 +351,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
293
351
|
const translatable = pickTranslatable(localeData, schema);
|
|
294
352
|
return deepMerge(structural, translatable);
|
|
295
353
|
}
|
|
296
|
-
function getAtPath(obj,
|
|
354
|
+
function getAtPath(obj, path13) {
|
|
297
355
|
let current = obj;
|
|
298
|
-
for (const segment of
|
|
356
|
+
for (const segment of path13) {
|
|
299
357
|
if (segment === "*") {
|
|
300
358
|
if (!Array.isArray(current)) return void 0;
|
|
301
359
|
return current.map(
|
|
302
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
360
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path13.slice(path13.indexOf("*") + 1)) : void 0
|
|
303
361
|
);
|
|
304
362
|
}
|
|
305
363
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -307,13 +365,13 @@ function getAtPath(obj, path11) {
|
|
|
307
365
|
}
|
|
308
366
|
return current;
|
|
309
367
|
}
|
|
310
|
-
function setAtPath(obj,
|
|
311
|
-
if (
|
|
312
|
-
if (
|
|
313
|
-
obj[
|
|
368
|
+
function setAtPath(obj, path13, value) {
|
|
369
|
+
if (path13.length === 0) return;
|
|
370
|
+
if (path13.length === 1) {
|
|
371
|
+
obj[path13[0]] = value;
|
|
314
372
|
return;
|
|
315
373
|
}
|
|
316
|
-
const [head, ...rest] =
|
|
374
|
+
const [head, ...rest] = path13;
|
|
317
375
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
318
376
|
obj[head] = {};
|
|
319
377
|
}
|
|
@@ -358,45 +416,23 @@ var LOCALE_BUILTIN_KEYS = [
|
|
|
358
416
|
["translationOf", "translationOf is internal; remove from locale translation"],
|
|
359
417
|
["enSlug", "enSlug is internal; remove from locale translation"]
|
|
360
418
|
];
|
|
361
|
-
|
|
419
|
+
var DEPRECATED_REDIRECT_FIELDS = [
|
|
420
|
+
[
|
|
421
|
+
"aliases",
|
|
422
|
+
"aliases frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
423
|
+
],
|
|
424
|
+
[
|
|
425
|
+
"redirect_to",
|
|
426
|
+
"redirect_to frontmatter is removed \u2014 add redirects to content/<type>/_redirects.json instead"
|
|
427
|
+
]
|
|
428
|
+
];
|
|
429
|
+
function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
|
|
362
430
|
const issues = [];
|
|
363
431
|
const rest = { ...data };
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
let redirectTo;
|
|
369
|
-
if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
|
|
370
|
-
if (typeof redirectRaw !== "string") {
|
|
371
|
-
issues.push({
|
|
372
|
-
field: "redirect_to",
|
|
373
|
-
message: "redirect_to must be a string path",
|
|
374
|
-
level: "error"
|
|
375
|
-
});
|
|
376
|
-
} else if (!pathTemplate) {
|
|
377
|
-
issues.push({
|
|
378
|
-
field: "redirect_to",
|
|
379
|
-
message: "redirect_to is not allowed on reference-only content types",
|
|
380
|
-
level: "error"
|
|
381
|
-
});
|
|
382
|
-
} else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
|
|
383
|
-
issues.push({
|
|
384
|
-
field: "redirect_to",
|
|
385
|
-
message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
|
|
386
|
-
level: "error"
|
|
387
|
-
});
|
|
388
|
-
} else {
|
|
389
|
-
redirectTo = redirectRaw;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const aliases = aliasesResult.success ? aliasesResult.data : [];
|
|
393
|
-
if (!aliasesResult.success) {
|
|
394
|
-
for (const issue of aliasesResult.error.issues) {
|
|
395
|
-
issues.push({
|
|
396
|
-
field: `aliases.${issue.path.join(".")}`,
|
|
397
|
-
message: issue.message,
|
|
398
|
-
level: "error"
|
|
399
|
-
});
|
|
432
|
+
for (const [field2, message] of DEPRECATED_REDIRECT_FIELDS) {
|
|
433
|
+
if (rest[field2] !== void 0) {
|
|
434
|
+
issues.push({ field: field2, message, level: "error" });
|
|
435
|
+
delete rest[field2];
|
|
400
436
|
}
|
|
401
437
|
}
|
|
402
438
|
let publishedAt;
|
|
@@ -458,8 +494,6 @@ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
|
|
|
458
494
|
}
|
|
459
495
|
return {
|
|
460
496
|
builtin: {
|
|
461
|
-
aliases,
|
|
462
|
-
redirectTo,
|
|
463
497
|
publishedAt,
|
|
464
498
|
updatedAt,
|
|
465
499
|
noindex,
|
|
@@ -484,20 +518,24 @@ function documentLastModified(doc) {
|
|
|
484
518
|
const date = new Date(raw);
|
|
485
519
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
486
520
|
}
|
|
487
|
-
function resolveCanonicalPathname(type, doc, defaultLocale) {
|
|
521
|
+
function resolveCanonicalPathname(type, doc, defaultLocale, localeRouting) {
|
|
522
|
+
const urlBuilder = createUrlBuilder({
|
|
523
|
+
locales: [defaultLocale, ...doc.locale !== defaultLocale ? [doc.locale] : []],
|
|
524
|
+
defaultLocale,
|
|
525
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
526
|
+
});
|
|
488
527
|
if (doc.canonicalPathOverride) {
|
|
489
|
-
|
|
490
|
-
return `/${doc.locale}${doc.canonicalPathOverride}`;
|
|
528
|
+
return urlBuilder.applyLocaleToPath(doc.canonicalPathOverride, doc.locale);
|
|
491
529
|
}
|
|
492
530
|
if (!type.path) return `/${doc.slug}`;
|
|
493
|
-
return resolvePath(type.path, doc.slug, doc.locale
|
|
531
|
+
return urlBuilder.resolvePath(type.path, doc.slug, doc.locale);
|
|
494
532
|
}
|
|
495
|
-
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
|
|
533
|
+
function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, localeRouting) {
|
|
496
534
|
const out = { ...frontmatter };
|
|
497
535
|
if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
|
|
498
536
|
if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
|
|
499
537
|
out.noindex = doc.noindex;
|
|
500
|
-
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
|
|
538
|
+
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
|
|
501
539
|
return out;
|
|
502
540
|
}
|
|
503
541
|
function seoFieldsFromEn(enDoc) {
|
|
@@ -505,8 +543,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
505
543
|
publishedAt: enDoc.publishedAt,
|
|
506
544
|
updatedAt: enDoc.updatedAt,
|
|
507
545
|
noindex: enDoc.noindex,
|
|
508
|
-
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
509
|
-
redirectTo: enDoc.redirectTo
|
|
546
|
+
canonicalPathOverride: enDoc.canonicalPathOverride
|
|
510
547
|
};
|
|
511
548
|
}
|
|
512
549
|
var SCHEMA_VERSION = 4;
|
|
@@ -736,7 +773,8 @@ function parseEnMdx(filePath, config, type) {
|
|
|
736
773
|
locale: config.defaultLocale
|
|
737
774
|
},
|
|
738
775
|
type,
|
|
739
|
-
config.defaultLocale
|
|
776
|
+
config.defaultLocale,
|
|
777
|
+
config.localeRouting
|
|
740
778
|
);
|
|
741
779
|
const crossIssues = type.crossValidate?.(result.data, {
|
|
742
780
|
locale: config.defaultLocale,
|
|
@@ -750,8 +788,6 @@ function parseEnMdx(filePath, config, type) {
|
|
|
750
788
|
slug,
|
|
751
789
|
enSlug: slug,
|
|
752
790
|
locale: config.defaultLocale,
|
|
753
|
-
aliases: builtin.aliases,
|
|
754
|
-
redirectTo: builtin.redirectTo,
|
|
755
791
|
publishedAt: builtin.publishedAt,
|
|
756
792
|
updatedAt: builtin.updatedAt,
|
|
757
793
|
noindex: builtin.noindex,
|
|
@@ -762,7 +798,7 @@ function parseEnMdx(filePath, config, type) {
|
|
|
762
798
|
};
|
|
763
799
|
return { document: document2, issues };
|
|
764
800
|
}
|
|
765
|
-
function buildDocumentFromTranslation(row, enDoc, type) {
|
|
801
|
+
function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
766
802
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
767
803
|
const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
|
|
768
804
|
const seo = seoFieldsFromEn(enDoc);
|
|
@@ -770,14 +806,13 @@ function buildDocumentFromTranslation(row, enDoc, type) {
|
|
|
770
806
|
merged,
|
|
771
807
|
{ ...seo, slug: row.slug, locale: row.locale },
|
|
772
808
|
type,
|
|
773
|
-
|
|
809
|
+
config.defaultLocale,
|
|
810
|
+
config.localeRouting
|
|
774
811
|
);
|
|
775
812
|
return {
|
|
776
813
|
slug: row.slug,
|
|
777
814
|
enSlug: row.en_slug,
|
|
778
815
|
locale: row.locale,
|
|
779
|
-
aliases: [],
|
|
780
|
-
redirectTo: seo.redirectTo,
|
|
781
816
|
publishedAt: seo.publishedAt,
|
|
782
817
|
updatedAt: seo.updatedAt,
|
|
783
818
|
noindex: seo.noindex,
|
|
@@ -855,7 +890,7 @@ function createContentLoader(config, type) {
|
|
|
855
890
|
for (const row of rowsByLocale.get(locale) ?? []) {
|
|
856
891
|
const enDoc = englishBySlug.get(row.en_slug);
|
|
857
892
|
if (!enDoc) continue;
|
|
858
|
-
const doc = buildDocumentFromTranslation(row, enDoc, type);
|
|
893
|
+
const doc = buildDocumentFromTranslation(row, enDoc, type, config);
|
|
859
894
|
bySlug.set(doc.slug, doc);
|
|
860
895
|
byEnSlug.set(row.en_slug, doc);
|
|
861
896
|
}
|
|
@@ -903,56 +938,16 @@ function getTranslatablePayload(doc, type) {
|
|
|
903
938
|
}
|
|
904
939
|
|
|
905
940
|
// src/i18n/resolve-document.ts
|
|
906
|
-
function
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
return null;
|
|
913
|
-
}
|
|
914
|
-
function enCanonicalDoc(doc, allDocs, defaultLocale) {
|
|
915
|
-
if (doc.locale === defaultLocale) return doc;
|
|
916
|
-
return allDocs.get(defaultLocale)?.bySlug.get(doc.enSlug) ?? doc;
|
|
917
|
-
}
|
|
918
|
-
function redirectPathForDocument(doc, locale, defaultLocale, allDocs, type) {
|
|
919
|
-
if (!type.path) return void 0;
|
|
920
|
-
const enDoc = enCanonicalDoc(doc, allDocs, defaultLocale);
|
|
921
|
-
if (!enDoc.redirectTo) return void 0;
|
|
922
|
-
const targetEnSlug = extractSlugFromResolvedPath(type.path, enDoc.redirectTo);
|
|
923
|
-
if (!targetEnSlug) return void 0;
|
|
924
|
-
const targetEnDoc = allDocs.get(defaultLocale)?.bySlug.get(targetEnSlug);
|
|
925
|
-
const targetSlug = targetEnDoc ? getSlugForLocale(targetEnDoc, defaultLocale, locale, allDocs, defaultLocale) ?? targetEnSlug : targetEnSlug;
|
|
926
|
-
return resolvePath(type.path, targetSlug, locale, defaultLocale);
|
|
927
|
-
}
|
|
928
|
-
function withRedirectTo(result, locale, defaultLocale, allDocs, type) {
|
|
929
|
-
if (result.shouldRedirectTo || !result.document) return result;
|
|
930
|
-
const redirect = redirectPathForDocument(result.document, locale, defaultLocale, allDocs, type);
|
|
931
|
-
if (!redirect) return result;
|
|
932
|
-
return {
|
|
933
|
-
document: null,
|
|
934
|
-
actualLocale: result.actualLocale,
|
|
935
|
-
shouldRedirectTo: redirect
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
941
|
+
function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
|
|
942
|
+
const urlBuilder = createUrlBuilder({
|
|
943
|
+
locales: [defaultLocale, locale],
|
|
944
|
+
defaultLocale,
|
|
945
|
+
localeRouting: localeRouting ?? { strategy: "path-prefix", prefixDefaultLocale: false }
|
|
946
|
+
});
|
|
939
947
|
const idx = allDocs.get(locale);
|
|
940
948
|
const direct = idx?.bySlug.get(slug);
|
|
941
949
|
if (direct) {
|
|
942
|
-
return
|
|
943
|
-
}
|
|
944
|
-
const aliasDoc = findEnDocByAlias(slug, allDocs, defaultLocale);
|
|
945
|
-
if (aliasDoc && type.path) {
|
|
946
|
-
const canonicalSlug = aliasDoc.slug;
|
|
947
|
-
const localizedSlug = getSlugForLocale(aliasDoc, defaultLocale, locale, allDocs, defaultLocale);
|
|
948
|
-
const targetSlug = localizedSlug ?? canonicalSlug;
|
|
949
|
-
if (targetSlug !== slug) {
|
|
950
|
-
return {
|
|
951
|
-
document: null,
|
|
952
|
-
actualLocale: locale,
|
|
953
|
-
shouldRedirectTo: resolvePath(type.path, targetSlug, locale, defaultLocale)
|
|
954
|
-
};
|
|
955
|
-
}
|
|
950
|
+
return { document: direct, actualLocale: locale };
|
|
956
951
|
}
|
|
957
952
|
for (const [docLocale, docIdx] of allDocs) {
|
|
958
953
|
const found = docIdx.bySlug.get(slug);
|
|
@@ -965,17 +960,11 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
965
960
|
return {
|
|
966
961
|
document: null,
|
|
967
962
|
actualLocale: locale,
|
|
968
|
-
shouldRedirectTo: resolvePath(type.path, correctSlug, locale
|
|
963
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
|
|
969
964
|
};
|
|
970
965
|
}
|
|
971
966
|
if (docLocale === defaultLocale) {
|
|
972
|
-
return
|
|
973
|
-
{ document: found, actualLocale: defaultLocale },
|
|
974
|
-
locale,
|
|
975
|
-
defaultLocale,
|
|
976
|
-
allDocs,
|
|
977
|
-
type
|
|
978
|
-
);
|
|
967
|
+
return { document: found, actualLocale: defaultLocale };
|
|
979
968
|
}
|
|
980
969
|
if (!type.path) {
|
|
981
970
|
return { document: null, actualLocale: locale };
|
|
@@ -983,39 +972,21 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type) {
|
|
|
983
972
|
return {
|
|
984
973
|
document: null,
|
|
985
974
|
actualLocale: locale,
|
|
986
|
-
shouldRedirectTo: resolvePath(type.path, found.enSlug, locale
|
|
975
|
+
shouldRedirectTo: urlBuilder.resolvePath(type.path, found.enSlug, locale)
|
|
987
976
|
};
|
|
988
977
|
}
|
|
989
978
|
if (locale !== defaultLocale) {
|
|
990
979
|
const enDoc = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
991
980
|
if (enDoc && type.indexFallback === "en") {
|
|
992
|
-
return
|
|
993
|
-
{ document: enDoc, actualLocale: defaultLocale },
|
|
994
|
-
locale,
|
|
995
|
-
defaultLocale,
|
|
996
|
-
allDocs,
|
|
997
|
-
type
|
|
998
|
-
);
|
|
981
|
+
return { document: enDoc, actualLocale: defaultLocale };
|
|
999
982
|
}
|
|
1000
983
|
const byEn = idx?.byEnSlug.get(slug);
|
|
1001
984
|
if (byEn) {
|
|
1002
|
-
return
|
|
1003
|
-
{ document: byEn, actualLocale: locale },
|
|
1004
|
-
locale,
|
|
1005
|
-
defaultLocale,
|
|
1006
|
-
allDocs,
|
|
1007
|
-
type
|
|
1008
|
-
);
|
|
985
|
+
return { document: byEn, actualLocale: locale };
|
|
1009
986
|
}
|
|
1010
987
|
const fallback = allDocs.get(defaultLocale)?.bySlug.get(slug);
|
|
1011
988
|
if (fallback && type.indexFallback === "en") {
|
|
1012
|
-
return
|
|
1013
|
-
{ document: fallback, actualLocale: defaultLocale },
|
|
1014
|
-
locale,
|
|
1015
|
-
defaultLocale,
|
|
1016
|
-
allDocs,
|
|
1017
|
-
type
|
|
1018
|
-
);
|
|
989
|
+
return { document: fallback, actualLocale: defaultLocale };
|
|
1019
990
|
}
|
|
1020
991
|
}
|
|
1021
992
|
return { document: null, actualLocale: locale };
|
|
@@ -1057,6 +1028,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1057
1028
|
}
|
|
1058
1029
|
return type.path;
|
|
1059
1030
|
}
|
|
1031
|
+
const urlBuilder = createUrlBuilder(config);
|
|
1060
1032
|
const runtime = {
|
|
1061
1033
|
id: type.id,
|
|
1062
1034
|
config: type,
|
|
@@ -1072,15 +1044,21 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1072
1044
|
return load().get(locale)?.bySlug.get(slug) ?? null;
|
|
1073
1045
|
},
|
|
1074
1046
|
resolve(slug, locale) {
|
|
1075
|
-
const result = resolveLocalizedDocument(
|
|
1047
|
+
const result = resolveLocalizedDocument(
|
|
1048
|
+
slug,
|
|
1049
|
+
locale,
|
|
1050
|
+
config.defaultLocale,
|
|
1051
|
+
load(),
|
|
1052
|
+
type,
|
|
1053
|
+
config.localeRouting
|
|
1054
|
+
);
|
|
1076
1055
|
if (result.document && type.path) {
|
|
1077
1056
|
return {
|
|
1078
1057
|
...result,
|
|
1079
|
-
canonicalPath: resolvePath(
|
|
1058
|
+
canonicalPath: urlBuilder.resolvePath(
|
|
1080
1059
|
type.path,
|
|
1081
1060
|
result.document.slug,
|
|
1082
|
-
result.actualLocale
|
|
1083
|
-
config.defaultLocale
|
|
1061
|
+
result.actualLocale
|
|
1084
1062
|
)
|
|
1085
1063
|
};
|
|
1086
1064
|
}
|
|
@@ -1104,10 +1082,9 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1104
1082
|
alternates(doc) {
|
|
1105
1083
|
const pathTemplate = assertRoutable("alternates");
|
|
1106
1084
|
const out = {
|
|
1107
|
-
[config.defaultLocale]: resolvePath(
|
|
1085
|
+
[config.defaultLocale]: urlBuilder.resolvePath(
|
|
1108
1086
|
pathTemplate,
|
|
1109
1087
|
doc.enSlug,
|
|
1110
|
-
config.defaultLocale,
|
|
1111
1088
|
config.defaultLocale
|
|
1112
1089
|
)
|
|
1113
1090
|
};
|
|
@@ -1116,7 +1093,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1116
1093
|
if (locale === config.defaultLocale) continue;
|
|
1117
1094
|
const translated = all.get(locale)?.byEnSlug.get(doc.enSlug);
|
|
1118
1095
|
if (translated) {
|
|
1119
|
-
out[locale] = resolvePath(pathTemplate, translated.slug, locale
|
|
1096
|
+
out[locale] = urlBuilder.resolvePath(pathTemplate, translated.slug, locale);
|
|
1120
1097
|
}
|
|
1121
1098
|
}
|
|
1122
1099
|
return out;
|
|
@@ -1131,7 +1108,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1131
1108
|
},
|
|
1132
1109
|
url(slug, locale) {
|
|
1133
1110
|
const pathTemplate = assertRoutable("url");
|
|
1134
|
-
return resolvePath(pathTemplate, slug, locale
|
|
1111
|
+
return urlBuilder.resolvePath(pathTemplate, slug, locale);
|
|
1135
1112
|
},
|
|
1136
1113
|
related(doc, fieldName, locale) {
|
|
1137
1114
|
const meta = relationFields.get(fieldName);
|
|
@@ -1187,6 +1164,9 @@ function createProject(config) {
|
|
|
1187
1164
|
getType: getRuntime,
|
|
1188
1165
|
listTypes() {
|
|
1189
1166
|
return Array.from(runtimes.values());
|
|
1167
|
+
},
|
|
1168
|
+
listRoutableTypes() {
|
|
1169
|
+
return Array.from(runtimes.values()).filter((runtime) => isRoutableType(runtime.config));
|
|
1190
1170
|
}
|
|
1191
1171
|
};
|
|
1192
1172
|
}
|
|
@@ -1194,207 +1174,329 @@ function enFileExists(config, type, enSlug) {
|
|
|
1194
1174
|
const dir = path3__default.default.join(config.rootDir, type.contentDir);
|
|
1195
1175
|
return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
|
|
1196
1176
|
}
|
|
1197
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1198
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1199
|
-
}
|
|
1200
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1201
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1202
|
-
}
|
|
1203
1177
|
function listEnSlugs(rootDir, contentDir) {
|
|
1204
1178
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1205
1179
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1206
1180
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1207
1181
|
}
|
|
1208
1182
|
|
|
1209
|
-
// src/
|
|
1210
|
-
function
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1183
|
+
// src/i18n/translation-index.ts
|
|
1184
|
+
function buildTranslationIndex(allDocs, locales, defaultLocale) {
|
|
1185
|
+
const index = /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const locale of locales) {
|
|
1187
|
+
if (locale === defaultLocale) continue;
|
|
1188
|
+
const idx = allDocs.get(locale);
|
|
1189
|
+
if (!idx) continue;
|
|
1190
|
+
const localeMap = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const [enSlug, doc] of idx.byEnSlug) {
|
|
1192
|
+
localeMap.set(enSlug, doc.slug);
|
|
1193
|
+
}
|
|
1194
|
+
index.set(locale, localeMap);
|
|
1219
1195
|
}
|
|
1196
|
+
return index;
|
|
1197
|
+
}
|
|
1198
|
+
var redirectFromSchema = zod.z.union([
|
|
1199
|
+
slugPatternSchema,
|
|
1200
|
+
zod.z.array(slugPatternSchema).min(1).max(20)
|
|
1201
|
+
]);
|
|
1202
|
+
var typeRedirectEntrySchema = zod.z.object({
|
|
1203
|
+
from: redirectFromSchema,
|
|
1204
|
+
toSlug: slugPatternSchema.optional(),
|
|
1205
|
+
toType: zod.z.string().min(1).optional(),
|
|
1206
|
+
toUrl: zod.z.string().min(1).optional(),
|
|
1207
|
+
permanent: zod.z.boolean().optional()
|
|
1208
|
+
}).superRefine((entry, ctx) => {
|
|
1209
|
+
const hasToSlug = entry.toSlug !== void 0;
|
|
1210
|
+
const hasToUrl = entry.toUrl !== void 0;
|
|
1211
|
+
const hasToType = entry.toType !== void 0;
|
|
1212
|
+
const targetCount = Number(hasToSlug) + Number(hasToUrl);
|
|
1213
|
+
if (targetCount !== 1) {
|
|
1214
|
+
ctx.addIssue({
|
|
1215
|
+
code: "custom",
|
|
1216
|
+
message: "Each redirect must specify exactly one of toSlug/toType+toSlug or toUrl",
|
|
1217
|
+
path: ["toSlug"]
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (hasToUrl && hasToType) {
|
|
1222
|
+
ctx.addIssue({
|
|
1223
|
+
code: "custom",
|
|
1224
|
+
message: "toUrl cannot be combined with toType",
|
|
1225
|
+
path: ["toUrl"]
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (hasToType && !hasToSlug) {
|
|
1229
|
+
ctx.addIssue({
|
|
1230
|
+
code: "custom",
|
|
1231
|
+
message: "Cross-type redirects require both toType and toSlug",
|
|
1232
|
+
path: ["toSlug"]
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
var typeRedirectsFileSchema = zod.z.object({
|
|
1237
|
+
redirects: zod.z.array(typeRedirectEntrySchema).max(500)
|
|
1238
|
+
});
|
|
1239
|
+
function normalizeRedirectFrom(from) {
|
|
1240
|
+
return Array.isArray(from) ? from : [from];
|
|
1241
|
+
}
|
|
1242
|
+
function parseRedirectEntry(entry) {
|
|
1243
|
+
const fromSlugs = normalizeRedirectFrom(entry.from);
|
|
1244
|
+
if (entry.toUrl !== void 0) {
|
|
1245
|
+
return {
|
|
1246
|
+
fromSlugs,
|
|
1247
|
+
kind: "anywhere",
|
|
1248
|
+
toUrl: entry.toUrl,
|
|
1249
|
+
permanent: entry.permanent ?? true
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (entry.toType !== void 0) {
|
|
1253
|
+
return {
|
|
1254
|
+
fromSlugs,
|
|
1255
|
+
kind: "cross-type",
|
|
1256
|
+
toType: entry.toType,
|
|
1257
|
+
toSlug: entry.toSlug,
|
|
1258
|
+
permanent: entry.permanent ?? true
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
fromSlugs,
|
|
1263
|
+
kind: "same-type",
|
|
1264
|
+
toSlug: entry.toSlug,
|
|
1265
|
+
permanent: entry.permanent ?? true
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/redirects/load-type-redirects.ts
|
|
1270
|
+
var TYPE_REDIRECTS_FILENAME = "_redirects.json";
|
|
1271
|
+
function redirectsFilePath(config, type) {
|
|
1272
|
+
return path3__default.default.join(config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1273
|
+
}
|
|
1274
|
+
function loadTypeRedirectsFile(config, type) {
|
|
1275
|
+
const filePath = redirectsFilePath(config, type);
|
|
1276
|
+
if (!fs2__default.default.existsSync(filePath)) return null;
|
|
1277
|
+
let raw;
|
|
1278
|
+
try {
|
|
1279
|
+
raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const parsed = typeRedirectsFileSchema.safeParse(raw);
|
|
1286
|
+
if (!parsed.success) {
|
|
1287
|
+
const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
|
|
1288
|
+
throw new Error(`${type.id}: invalid ${TYPE_REDIRECTS_FILENAME}: ${details}`);
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
contentTypeId: type.id,
|
|
1292
|
+
contentDir: type.contentDir,
|
|
1293
|
+
filePath,
|
|
1294
|
+
entries: parsed.data.redirects.map(parseRedirectEntry)
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function loadAllTypeRedirects(config) {
|
|
1298
|
+
const out = [];
|
|
1220
1299
|
for (const type of config.types) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
message: `Alias "${alias}" must not equal the canonical slug`,
|
|
1233
|
-
level: "error"
|
|
1234
|
-
});
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
if (canonicalSlugs.has(alias)) {
|
|
1238
|
-
issues.push({
|
|
1239
|
-
contentTypeId: type.id,
|
|
1240
|
-
enSlug,
|
|
1241
|
-
field: "aliases",
|
|
1242
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1243
|
-
level: "error"
|
|
1244
|
-
});
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
const existing = aliasToTarget.get(alias);
|
|
1248
|
-
if (existing) {
|
|
1249
|
-
issues.push({
|
|
1250
|
-
contentTypeId: type.id,
|
|
1251
|
-
enSlug,
|
|
1252
|
-
field: "aliases",
|
|
1253
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1254
|
-
level: "error"
|
|
1255
|
-
});
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
aliasToTarget.set(alias, {
|
|
1259
|
-
contentTypeId: type.id,
|
|
1260
|
-
canonicalSlug: enSlug,
|
|
1261
|
-
path: type.path
|
|
1262
|
-
});
|
|
1263
|
-
allAliasSlugs.add(alias);
|
|
1300
|
+
const loaded = loadTypeRedirectsFile(config, type);
|
|
1301
|
+
if (loaded) out.push(loaded);
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
function collectRedirectSourceSlugs(loaded) {
|
|
1306
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1307
|
+
for (const file of loaded) {
|
|
1308
|
+
for (const entry of file.entries) {
|
|
1309
|
+
for (const from of entry.fromSlugs) {
|
|
1310
|
+
slugs.add(from);
|
|
1264
1311
|
}
|
|
1265
1312
|
}
|
|
1266
1313
|
}
|
|
1267
|
-
return
|
|
1314
|
+
return slugs;
|
|
1268
1315
|
}
|
|
1269
|
-
function
|
|
1270
|
-
const
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const type = config.types.find((t) => t.id === target.contentTypeId);
|
|
1277
|
-
if (!type) continue;
|
|
1278
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1279
|
-
if (!known) {
|
|
1280
|
-
issues.push({
|
|
1281
|
-
contentTypeId: type.id,
|
|
1282
|
-
enSlug: target.canonicalSlug,
|
|
1283
|
-
field: "aliases",
|
|
1284
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1285
|
-
level: "warning"
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
if (enFileExists(config, type, alias)) {
|
|
1289
|
-
issues.push({
|
|
1290
|
-
contentTypeId: type.id,
|
|
1291
|
-
enSlug: target.canonicalSlug,
|
|
1292
|
-
field: "aliases",
|
|
1293
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1294
|
-
level: "warning"
|
|
1295
|
-
});
|
|
1316
|
+
function collectOutboundRedirectSourcesByType(loaded) {
|
|
1317
|
+
const out = /* @__PURE__ */ new Map();
|
|
1318
|
+
for (const file of loaded) {
|
|
1319
|
+
const set = out.get(file.contentTypeId) ?? /* @__PURE__ */ new Set();
|
|
1320
|
+
for (const entry of file.entries) {
|
|
1321
|
+
for (const from of entry.fromSlugs) {
|
|
1322
|
+
set.add(from);
|
|
1296
1323
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1324
|
+
}
|
|
1325
|
+
out.set(file.contentTypeId, set);
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/redirects/build-json-redirects.ts
|
|
1331
|
+
function localizedSlug(db, contentTypeId, enSlug, locale, defaultLocale) {
|
|
1332
|
+
if (locale === defaultLocale) return enSlug;
|
|
1333
|
+
const row = listTranslationsForEnSlug(db, contentTypeId, enSlug).find(
|
|
1334
|
+
(translation) => translation.locale === locale
|
|
1335
|
+
);
|
|
1336
|
+
return row?.slug ?? enSlug;
|
|
1337
|
+
}
|
|
1338
|
+
function resolveSlugTargetDestination(project, db, targetType, toSlug, locale, urlBuilder) {
|
|
1339
|
+
const targetSlug = localizedSlug(
|
|
1340
|
+
db,
|
|
1341
|
+
targetType.id,
|
|
1342
|
+
toSlug,
|
|
1343
|
+
locale,
|
|
1344
|
+
project.config.defaultLocale
|
|
1345
|
+
);
|
|
1346
|
+
return urlBuilder.resolvePath(targetType.path, targetSlug, locale);
|
|
1347
|
+
}
|
|
1348
|
+
function buildEntryRules(project, sourceType, entry, db, urlBuilder) {
|
|
1349
|
+
if (!isRoutableType(sourceType)) return [];
|
|
1350
|
+
const out = [];
|
|
1351
|
+
const { defaultLocale } = project.config;
|
|
1352
|
+
let targetType = sourceType;
|
|
1353
|
+
if (entry.kind === "cross-type") {
|
|
1354
|
+
const resolvedTarget = project.config.types.find((type) => type.id === entry.toType);
|
|
1355
|
+
if (!resolvedTarget?.path) return out;
|
|
1356
|
+
targetType = resolvedTarget;
|
|
1357
|
+
}
|
|
1358
|
+
for (const fromEnSlug of entry.fromSlugs) {
|
|
1359
|
+
const locales = project.config.locales;
|
|
1360
|
+
for (const locale of locales) {
|
|
1361
|
+
const fromSlug = localizedSlug(db, sourceType.id, fromEnSlug, locale, defaultLocale);
|
|
1362
|
+
const source = urlBuilder.resolvePath(sourceType.path, fromSlug, locale);
|
|
1363
|
+
let destination;
|
|
1364
|
+
if (entry.kind === "anywhere") {
|
|
1365
|
+
destination = entry.toUrl;
|
|
1366
|
+
} else {
|
|
1367
|
+
destination = resolveSlugTargetDestination(
|
|
1368
|
+
project,
|
|
1369
|
+
db,
|
|
1370
|
+
entry.kind === "cross-type" ? targetType : sourceType,
|
|
1371
|
+
entry.toSlug,
|
|
1372
|
+
locale,
|
|
1373
|
+
urlBuilder
|
|
1374
|
+
);
|
|
1306
1375
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1376
|
+
if (source === destination) continue;
|
|
1377
|
+
out.push({
|
|
1378
|
+
source,
|
|
1379
|
+
destination,
|
|
1380
|
+
permanent: entry.permanent
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (sourceType.id === "blog") {
|
|
1384
|
+
let destinationEn;
|
|
1385
|
+
if (entry.kind === "anywhere") {
|
|
1386
|
+
destinationEn = entry.toUrl;
|
|
1387
|
+
} else {
|
|
1388
|
+
destinationEn = resolveSlugTargetDestination(
|
|
1389
|
+
project,
|
|
1390
|
+
db,
|
|
1391
|
+
entry.kind === "cross-type" ? targetType : sourceType,
|
|
1392
|
+
entry.toSlug,
|
|
1393
|
+
defaultLocale,
|
|
1394
|
+
urlBuilder
|
|
1311
1395
|
);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1396
|
+
}
|
|
1397
|
+
out.push({
|
|
1398
|
+
source: `/${fromEnSlug}`,
|
|
1399
|
+
destination: destinationEn,
|
|
1400
|
+
permanent: entry.permanent
|
|
1401
|
+
});
|
|
1402
|
+
for (const locale of urlBuilder.prefixedLocales) {
|
|
1403
|
+
const fromSlug = localizedSlug(db, sourceType.id, fromEnSlug, locale, defaultLocale);
|
|
1404
|
+
let destination;
|
|
1405
|
+
if (entry.kind === "anywhere") {
|
|
1406
|
+
destination = entry.toUrl;
|
|
1407
|
+
} else {
|
|
1408
|
+
destination = resolveSlugTargetDestination(
|
|
1409
|
+
project,
|
|
1410
|
+
db,
|
|
1411
|
+
entry.kind === "cross-type" ? targetType : sourceType,
|
|
1412
|
+
entry.toSlug,
|
|
1413
|
+
locale,
|
|
1414
|
+
urlBuilder
|
|
1415
|
+
);
|
|
1322
1416
|
}
|
|
1417
|
+
out.push({
|
|
1418
|
+
source: `/${locale}/${fromSlug}`,
|
|
1419
|
+
destination,
|
|
1420
|
+
permanent: entry.permanent
|
|
1421
|
+
});
|
|
1422
|
+
out.push({
|
|
1423
|
+
source: `/${locale}/${fromEnSlug}`,
|
|
1424
|
+
destination,
|
|
1425
|
+
permanent: entry.permanent
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return out;
|
|
1431
|
+
}
|
|
1432
|
+
function buildJsonRedirects(project) {
|
|
1433
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1434
|
+
const db = openStore(project.config, "readonly");
|
|
1435
|
+
const out = [];
|
|
1436
|
+
try {
|
|
1437
|
+
for (const type of project.config.types) {
|
|
1438
|
+
if (!isRoutableType(type)) continue;
|
|
1439
|
+
const loaded = loadTypeRedirectsFile(project.config, type);
|
|
1440
|
+
if (!loaded) continue;
|
|
1441
|
+
for (const entry of loaded.entries) {
|
|
1442
|
+
out.push(...buildEntryRules(project, type, entry, db, urlBuilder));
|
|
1323
1443
|
}
|
|
1324
1444
|
}
|
|
1325
1445
|
} finally {
|
|
1326
1446
|
db.close();
|
|
1327
1447
|
}
|
|
1328
|
-
return
|
|
1448
|
+
return out;
|
|
1329
1449
|
}
|
|
1330
|
-
function
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
);
|
|
1340
|
-
if (doc?.redirectTo) {
|
|
1341
|
-
issues.push({
|
|
1342
|
-
contentTypeId: target.contentTypeId,
|
|
1343
|
-
enSlug: target.canonicalSlug,
|
|
1344
|
-
field: "aliases",
|
|
1345
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1346
|
-
level: "error"
|
|
1347
|
-
});
|
|
1450
|
+
function buildRedirectSourceSlugSet(project) {
|
|
1451
|
+
const out = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (const type of project.config.types) {
|
|
1453
|
+
const loaded = loadTypeRedirectsFile(project.config, type);
|
|
1454
|
+
if (!loaded) continue;
|
|
1455
|
+
for (const entry of loaded.entries) {
|
|
1456
|
+
for (const from of entry.fromSlugs) {
|
|
1457
|
+
out.add(`${type.id}:${from}`);
|
|
1458
|
+
}
|
|
1348
1459
|
}
|
|
1349
1460
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1461
|
+
return out;
|
|
1462
|
+
}
|
|
1463
|
+
function buildLegacyNoBlogPathRedirects(project, redirectSourceKeys) {
|
|
1464
|
+
const blogType = project.config.types.find((type) => type.id === "blog" && type.path);
|
|
1465
|
+
if (!blogType?.path) return [];
|
|
1466
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1467
|
+
const db = openStore(project.config, "readonly");
|
|
1468
|
+
const out = [];
|
|
1469
|
+
try {
|
|
1470
|
+
for (const enSlug of listEnSlugs(project.config.rootDir, blogType.contentDir)) {
|
|
1471
|
+
if (redirectSourceKeys.has(`blog:${enSlug}`)) continue;
|
|
1472
|
+
out.push({
|
|
1473
|
+
source: `/${enSlug}`,
|
|
1474
|
+
destination: urlBuilder.resolvePath(blogType.path, enSlug, project.config.defaultLocale),
|
|
1475
|
+
permanent: true
|
|
1476
|
+
});
|
|
1477
|
+
for (const locale of urlBuilder.prefixedLocales) {
|
|
1478
|
+
const translatedSlug = localizedSlug(
|
|
1479
|
+
db,
|
|
1480
|
+
blogType.id,
|
|
1369
1481
|
enSlug,
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1482
|
+
locale,
|
|
1483
|
+
project.config.defaultLocale
|
|
1484
|
+
);
|
|
1485
|
+
out.push({
|
|
1486
|
+
source: `/${locale}/${enSlug}`,
|
|
1487
|
+
destination: urlBuilder.resolvePath(blogType.path, translatedSlug, locale),
|
|
1488
|
+
permanent: true
|
|
1373
1489
|
});
|
|
1374
1490
|
}
|
|
1375
1491
|
}
|
|
1492
|
+
} finally {
|
|
1493
|
+
db.close();
|
|
1376
1494
|
}
|
|
1377
|
-
return
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
// src/i18n/translation-index.ts
|
|
1381
|
-
function buildTranslationIndex(allDocs, locales, defaultLocale) {
|
|
1382
|
-
const index = /* @__PURE__ */ new Map();
|
|
1383
|
-
for (const locale of locales) {
|
|
1384
|
-
if (locale === defaultLocale) continue;
|
|
1385
|
-
const idx = allDocs.get(locale);
|
|
1386
|
-
if (!idx) continue;
|
|
1387
|
-
const localeMap = /* @__PURE__ */ new Map();
|
|
1388
|
-
for (const [enSlug, doc] of idx.byEnSlug) {
|
|
1389
|
-
localeMap.set(enSlug, doc.slug);
|
|
1390
|
-
}
|
|
1391
|
-
index.set(locale, localeMap);
|
|
1392
|
-
}
|
|
1393
|
-
return index;
|
|
1495
|
+
return out;
|
|
1394
1496
|
}
|
|
1395
1497
|
|
|
1396
|
-
// src/redirects/
|
|
1397
|
-
function buildRedirectTranslationIndex(project, typeId) {
|
|
1498
|
+
// src/redirects/build-redirects.ts
|
|
1499
|
+
function buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs) {
|
|
1398
1500
|
const runtime = project.getType(typeId);
|
|
1399
1501
|
const { config } = project;
|
|
1400
1502
|
const merged = buildTranslationIndex(
|
|
@@ -1402,11 +1504,9 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1402
1504
|
config.locales,
|
|
1403
1505
|
config.defaultLocale
|
|
1404
1506
|
);
|
|
1405
|
-
const { aliasToTarget } = buildGlobalAliasIndex(project);
|
|
1406
1507
|
const db = openStore(project.config, "readonly");
|
|
1407
1508
|
try {
|
|
1408
|
-
for (const
|
|
1409
|
-
if (target.contentTypeId !== typeId) continue;
|
|
1509
|
+
for (const alias of redirectSourceSlugs) {
|
|
1410
1510
|
for (const row of listTranslationsForEnSlug(db, typeId, alias)) {
|
|
1411
1511
|
const localeMap = merged.get(row.locale) ?? /* @__PURE__ */ new Map();
|
|
1412
1512
|
if (!localeMap.has(alias)) {
|
|
@@ -1420,168 +1520,63 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1420
1520
|
}
|
|
1421
1521
|
return merged;
|
|
1422
1522
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
const pathTemplate = type.path;
|
|
1429
|
-
for (const [alias, target] of aliasToTarget) {
|
|
1430
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1431
|
-
const canonical = target.canonicalSlug;
|
|
1432
|
-
out.push({
|
|
1433
|
-
source: resolvePath(pathTemplate, alias, defaultLocale, defaultLocale),
|
|
1434
|
-
destination: resolvePath(pathTemplate, canonical, defaultLocale, defaultLocale),
|
|
1435
|
-
permanent: true
|
|
1436
|
-
});
|
|
1437
|
-
for (const locale of prefixedLocales) {
|
|
1438
|
-
const localeMap = translationIndex.get(locale);
|
|
1439
|
-
const toLocale = localeMap?.get(canonical) ?? canonical;
|
|
1440
|
-
const fromLocale = localeMap?.get(alias);
|
|
1441
|
-
if (fromLocale) {
|
|
1442
|
-
out.push({
|
|
1443
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1444
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1445
|
-
permanent: true
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
|
-
out.push({
|
|
1449
|
-
source: resolvePath(pathTemplate, alias, locale, defaultLocale),
|
|
1450
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1451
|
-
permanent: true
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
return out;
|
|
1456
|
-
}
|
|
1457
|
-
function buildAliasLegacyNoBlogPathRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
|
|
1458
|
-
if (type.id !== "blog") return [];
|
|
1459
|
-
const out = [];
|
|
1460
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1461
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1462
|
-
for (const locale of prefixedLocales) {
|
|
1463
|
-
const localeMap = translationIndex.get(locale);
|
|
1464
|
-
const toLocale = localeMap?.get(target.canonicalSlug) ?? target.canonicalSlug;
|
|
1465
|
-
out.push({
|
|
1466
|
-
source: `/${locale}/${alias}`,
|
|
1467
|
-
destination: resolvePath(type.path, toLocale, locale, defaultLocale),
|
|
1468
|
-
permanent: true
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
return out;
|
|
1473
|
-
}
|
|
1474
|
-
function buildRedirectToRules(type, config, translationIndex, prefixedLocales, defaultLocale) {
|
|
1475
|
-
const out = [];
|
|
1476
|
-
const pathTemplate = type.path;
|
|
1477
|
-
for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
|
|
1478
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1479
|
-
if (!doc?.redirectTo) continue;
|
|
1480
|
-
out.push({
|
|
1481
|
-
source: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1482
|
-
destination: doc.redirectTo,
|
|
1483
|
-
permanent: true
|
|
1484
|
-
});
|
|
1485
|
-
for (const locale of prefixedLocales) {
|
|
1486
|
-
const localeMap = translationIndex.get(locale);
|
|
1487
|
-
const fromLocale = localeMap?.get(enSlug);
|
|
1488
|
-
if (!fromLocale) continue;
|
|
1489
|
-
const targetEnSlug = extractSlugFromResolvedPath(pathTemplate, doc.redirectTo);
|
|
1490
|
-
if (!targetEnSlug) continue;
|
|
1491
|
-
const toLocale = localeMap?.get(targetEnSlug) ?? targetEnSlug;
|
|
1492
|
-
out.push({
|
|
1493
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1494
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1495
|
-
permanent: true
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
return out;
|
|
1500
|
-
}
|
|
1501
|
-
function buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale) {
|
|
1523
|
+
function buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs) {
|
|
1524
|
+
const type = project.getType(typeId).config;
|
|
1525
|
+
if (!isRoutableType(type)) return [];
|
|
1526
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1527
|
+
const translationIndex = buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs);
|
|
1502
1528
|
const out = [];
|
|
1503
1529
|
const pathTemplate = type.path;
|
|
1504
1530
|
for (const [locale, localeMap] of translationIndex) {
|
|
1505
1531
|
for (const [enSlug, translatedSlug] of localeMap) {
|
|
1506
1532
|
if (enSlug === translatedSlug) continue;
|
|
1507
|
-
if (
|
|
1533
|
+
if (redirectSourceSlugs.has(enSlug)) continue;
|
|
1508
1534
|
out.push({
|
|
1509
|
-
source: resolvePath(pathTemplate, enSlug, locale
|
|
1510
|
-
destination: resolvePath(pathTemplate, translatedSlug, locale
|
|
1535
|
+
source: urlBuilder.resolvePath(pathTemplate, enSlug, locale),
|
|
1536
|
+
destination: urlBuilder.resolvePath(pathTemplate, translatedSlug, locale),
|
|
1511
1537
|
permanent: true
|
|
1512
1538
|
});
|
|
1513
1539
|
}
|
|
1514
1540
|
}
|
|
1515
1541
|
return out;
|
|
1516
1542
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
destination: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1526
|
-
permanent: true
|
|
1527
|
-
});
|
|
1528
|
-
for (const locale of prefixedLocales) {
|
|
1529
|
-
const translatedSlug = translationIndex.get(locale)?.get(enSlug);
|
|
1530
|
-
const canonicalSlug = translatedSlug ?? enSlug;
|
|
1531
|
-
out.push({
|
|
1532
|
-
source: `/${locale}/${enSlug}`,
|
|
1533
|
-
destination: resolvePath(pathTemplate, canonicalSlug, locale, defaultLocale),
|
|
1534
|
-
permanent: true
|
|
1535
|
-
});
|
|
1543
|
+
function buildTypeRedirects(project, typeId) {
|
|
1544
|
+
const loaded = loadAllTypeRedirects(project.config).find((file) => file.contentTypeId === typeId);
|
|
1545
|
+
const redirectSourceSlugs = /* @__PURE__ */ new Set();
|
|
1546
|
+
if (loaded) {
|
|
1547
|
+
for (const entry of loaded.entries) {
|
|
1548
|
+
for (const from of entry.fromSlugs) {
|
|
1549
|
+
redirectSourceSlugs.add(from);
|
|
1550
|
+
}
|
|
1536
1551
|
}
|
|
1537
1552
|
}
|
|
1538
|
-
return out;
|
|
1539
|
-
}
|
|
1540
|
-
function buildTypeRedirects(project, typeId, aliasIndex, options) {
|
|
1541
|
-
const type = project.getType(typeId).config;
|
|
1542
|
-
if (!isRoutableType(type)) return [];
|
|
1543
|
-
const { defaultLocale } = project.config;
|
|
1544
|
-
const translationIndex = buildRedirectTranslationIndex(project, typeId);
|
|
1545
|
-
const aliasSourceSlugs = /* @__PURE__ */ new Set();
|
|
1546
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1547
|
-
if (target.contentTypeId === typeId) aliasSourceSlugs.add(alias);
|
|
1548
|
-
}
|
|
1549
1553
|
return [
|
|
1550
|
-
...
|
|
1551
|
-
...buildAliasLegacyNoBlogPathRedirects(
|
|
1552
|
-
type,
|
|
1553
|
-
aliasIndex,
|
|
1554
|
-
translationIndex,
|
|
1555
|
-
options.prefixedLocales,
|
|
1556
|
-
defaultLocale
|
|
1557
|
-
),
|
|
1558
|
-
...buildRedirectToRules(type, project.config, translationIndex, options.prefixedLocales, defaultLocale),
|
|
1559
|
-
...buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale),
|
|
1560
|
-
...buildLegacyNoBlogPathRedirects(type, project.config, translationIndex, options.prefixedLocales, defaultLocale)
|
|
1554
|
+
...buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs)
|
|
1561
1555
|
];
|
|
1562
1556
|
}
|
|
1563
|
-
function buildAllContentRedirects(project,
|
|
1564
|
-
const
|
|
1565
|
-
const out = [
|
|
1557
|
+
function buildAllContentRedirects(project, _options = {}) {
|
|
1558
|
+
const redirectSourceKeys = buildRedirectSourceSlugSet(project);
|
|
1559
|
+
const out = [
|
|
1560
|
+
...buildJsonRedirects(project),
|
|
1561
|
+
...buildLegacyNoBlogPathRedirects(project, redirectSourceKeys)
|
|
1562
|
+
];
|
|
1566
1563
|
for (const type of project.config.types) {
|
|
1567
1564
|
if (!isRoutableType(type)) continue;
|
|
1568
|
-
out.push(...buildTypeRedirects(project, type.id
|
|
1565
|
+
out.push(...buildTypeRedirects(project, type.id));
|
|
1569
1566
|
}
|
|
1570
1567
|
return out;
|
|
1571
1568
|
}
|
|
1572
1569
|
function getRedirectSourceSlugs(project) {
|
|
1573
|
-
const
|
|
1574
|
-
const
|
|
1570
|
+
const loaded = loadAllTypeRedirects(project.config);
|
|
1571
|
+
const aliasSlugs = collectRedirectSourceSlugs(loaded);
|
|
1572
|
+
const outboundByType = collectOutboundRedirectSourcesByType(loaded);
|
|
1575
1573
|
for (const type of project.config.types) {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const doc = readEnDocument(project.config, type, enSlug);
|
|
1579
|
-
if (doc?.redirectTo) outbound.add(enSlug);
|
|
1574
|
+
if (!outboundByType.has(type.id)) {
|
|
1575
|
+
outboundByType.set(type.id, /* @__PURE__ */ new Set());
|
|
1580
1576
|
}
|
|
1581
|
-
outboundByType.set(type.id, outbound);
|
|
1582
1577
|
}
|
|
1583
1578
|
return {
|
|
1584
|
-
aliasSlugs
|
|
1579
|
+
aliasSlugs,
|
|
1585
1580
|
outboundByType
|
|
1586
1581
|
};
|
|
1587
1582
|
}
|
|
@@ -1589,13 +1584,12 @@ function getRedirectSourceSlugs(project) {
|
|
|
1589
1584
|
// src/sitemap/join-base-url.ts
|
|
1590
1585
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1591
1586
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1592
|
-
const
|
|
1593
|
-
return `${origin}${
|
|
1587
|
+
const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1588
|
+
return `${origin}${path13}`;
|
|
1594
1589
|
}
|
|
1595
1590
|
|
|
1596
1591
|
// src/sitemap/generate-sitemap.ts
|
|
1597
1592
|
function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
|
|
1598
|
-
if (enDoc.redirectTo) return false;
|
|
1599
1593
|
if (redirectSources.aliasSlugs.has(enSlug)) return false;
|
|
1600
1594
|
const outbound = redirectSources.outboundByType.get(contentTypeId);
|
|
1601
1595
|
if (outbound?.has(enSlug)) return false;
|
|
@@ -1682,6 +1676,7 @@ function createScribe(input) {
|
|
|
1682
1676
|
project,
|
|
1683
1677
|
getType: project.getType,
|
|
1684
1678
|
listTypes: project.listTypes,
|
|
1679
|
+
listRoutableTypes: project.listRoutableTypes,
|
|
1685
1680
|
sitemap(options) {
|
|
1686
1681
|
return generateSitemap(project, options);
|
|
1687
1682
|
}
|
|
@@ -1734,6 +1729,168 @@ function loadConfigSync(options = {}) {
|
|
|
1734
1729
|
}
|
|
1735
1730
|
return resolveConfig(config, path3__default.default.dirname(configPath));
|
|
1736
1731
|
}
|
|
1732
|
+
function liveDocExists(config, typeId, enSlug) {
|
|
1733
|
+
const type = config.types.find((entry) => entry.id === typeId);
|
|
1734
|
+
if (!type) return false;
|
|
1735
|
+
return enFileExists(config, type, enSlug);
|
|
1736
|
+
}
|
|
1737
|
+
function resolveRedirectTarget(project, sourceTypeId, entry) {
|
|
1738
|
+
if (entry.kind === "anywhere") return null;
|
|
1739
|
+
if (entry.kind === "cross-type") {
|
|
1740
|
+
return { typeId: entry.toType, enSlug: entry.toSlug };
|
|
1741
|
+
}
|
|
1742
|
+
return { typeId: sourceTypeId, enSlug: entry.toSlug };
|
|
1743
|
+
}
|
|
1744
|
+
function matchRoutableTarget(project, toUrl) {
|
|
1745
|
+
const urlBuilder = createUrlBuilder(project.config);
|
|
1746
|
+
for (const type of project.config.types) {
|
|
1747
|
+
if (!type.path) continue;
|
|
1748
|
+
const enSlug = urlBuilder.extractSlugFromResolvedPath(type.path, toUrl);
|
|
1749
|
+
if (enSlug && liveDocExists(project.config, type.id, enSlug)) {
|
|
1750
|
+
return { typeId: type.id, enSlug };
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
function validateTypeRedirects(project) {
|
|
1756
|
+
const issues = [];
|
|
1757
|
+
const globalFrom = /* @__PURE__ */ new Map();
|
|
1758
|
+
for (const type of project.config.types) {
|
|
1759
|
+
const filePath = path3__default.default.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
|
|
1760
|
+
if (!fs2__default.default.existsSync(filePath)) continue;
|
|
1761
|
+
let loaded;
|
|
1762
|
+
try {
|
|
1763
|
+
loaded = loadTypeRedirectsFile(project.config, type);
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
issues.push({
|
|
1766
|
+
level: "error",
|
|
1767
|
+
contentType: type.id,
|
|
1768
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1769
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1770
|
+
});
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (!loaded) continue;
|
|
1774
|
+
if (!isRoutableType(type)) {
|
|
1775
|
+
issues.push({
|
|
1776
|
+
level: "error",
|
|
1777
|
+
contentType: type.id,
|
|
1778
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1779
|
+
message: `Redirects are only supported on routable content types (missing path)`
|
|
1780
|
+
});
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
for (const entry of loaded.entries) {
|
|
1784
|
+
for (const from of entry.fromSlugs) {
|
|
1785
|
+
if (from === entry.toSlug) {
|
|
1786
|
+
issues.push({
|
|
1787
|
+
level: "error",
|
|
1788
|
+
contentType: type.id,
|
|
1789
|
+
enSlug: from,
|
|
1790
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1791
|
+
message: `Redirect source "${from}" must not equal its target slug`
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
if (liveDocExists(project.config, type.id, from)) {
|
|
1795
|
+
issues.push({
|
|
1796
|
+
level: "error",
|
|
1797
|
+
contentType: type.id,
|
|
1798
|
+
enSlug: from,
|
|
1799
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1800
|
+
message: `Redirect source "${from}" still has a live EN file \u2014 delete or rename the MDX first`
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
const existing = globalFrom.get(from);
|
|
1804
|
+
if (existing && existing.typeId !== type.id) {
|
|
1805
|
+
issues.push({
|
|
1806
|
+
level: "error",
|
|
1807
|
+
contentType: type.id,
|
|
1808
|
+
enSlug: from,
|
|
1809
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1810
|
+
message: `Redirect source "${from}" is already claimed by ${existing.typeId}`
|
|
1811
|
+
});
|
|
1812
|
+
} else if (existing) {
|
|
1813
|
+
issues.push({
|
|
1814
|
+
level: "error",
|
|
1815
|
+
contentType: type.id,
|
|
1816
|
+
enSlug: from,
|
|
1817
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1818
|
+
message: `Duplicate redirect source "${from}" in ${TYPE_REDIRECTS_FILENAME}`
|
|
1819
|
+
});
|
|
1820
|
+
} else {
|
|
1821
|
+
globalFrom.set(from, { typeId: type.id, filePath: loaded.filePath });
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (entry.kind === "cross-type") {
|
|
1825
|
+
const targetType = project.config.types.find((candidate) => candidate.id === entry.toType);
|
|
1826
|
+
if (!targetType) {
|
|
1827
|
+
issues.push({
|
|
1828
|
+
level: "error",
|
|
1829
|
+
contentType: type.id,
|
|
1830
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1831
|
+
message: `Unknown redirect target type "${entry.toType}"`
|
|
1832
|
+
});
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (!isRoutableType(targetType)) {
|
|
1836
|
+
issues.push({
|
|
1837
|
+
level: "error",
|
|
1838
|
+
contentType: type.id,
|
|
1839
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1840
|
+
message: `Redirect target type "${entry.toType}" is not routable (missing path)`
|
|
1841
|
+
});
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const target = resolveRedirectTarget(project, type.id, entry);
|
|
1846
|
+
if (target) {
|
|
1847
|
+
if (!liveDocExists(project.config, target.typeId, target.enSlug)) {
|
|
1848
|
+
issues.push({
|
|
1849
|
+
level: "error",
|
|
1850
|
+
contentType: type.id,
|
|
1851
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1852
|
+
message: `Redirect target ${target.typeId}/${target.enSlug} does not exist`
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
if (entry.kind === "anywhere" && entry.toUrl?.startsWith("/")) {
|
|
1858
|
+
const matched = matchRoutableTarget(project, entry.toUrl);
|
|
1859
|
+
if (matched && !liveDocExists(project.config, matched.typeId, matched.enSlug)) {
|
|
1860
|
+
issues.push({
|
|
1861
|
+
level: "error",
|
|
1862
|
+
contentType: type.id,
|
|
1863
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1864
|
+
message: `Redirect toUrl "${entry.toUrl}" does not resolve to a live document`
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const allLoaded = loadAllTypeRedirects(project.config);
|
|
1871
|
+
for (const file of allLoaded) {
|
|
1872
|
+
for (const entry of file.entries) {
|
|
1873
|
+
if (entry.kind === "anywhere" || !entry.toSlug) continue;
|
|
1874
|
+
const targetTypeId = entry.kind === "cross-type" ? entry.toType : file.contentTypeId;
|
|
1875
|
+
const targetType = project.config.types.find((type) => type.id === targetTypeId);
|
|
1876
|
+
if (!targetType?.path) continue;
|
|
1877
|
+
for (const from of entry.fromSlugs) {
|
|
1878
|
+
if (globalFrom.get(from)?.typeId !== file.contentTypeId) continue;
|
|
1879
|
+
const chainTarget = globalFrom.get(entry.toSlug);
|
|
1880
|
+
if (chainTarget) {
|
|
1881
|
+
issues.push({
|
|
1882
|
+
level: "error",
|
|
1883
|
+
contentType: file.contentTypeId,
|
|
1884
|
+
enSlug: from,
|
|
1885
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1886
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return issues;
|
|
1893
|
+
}
|
|
1737
1894
|
function getAtPath2(obj, fieldPath) {
|
|
1738
1895
|
let current = obj;
|
|
1739
1896
|
for (const segment of fieldPath) {
|
|
@@ -1923,7 +2080,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1923
2080
|
}
|
|
1924
2081
|
|
|
1925
2082
|
// src/validate/validate-project.ts
|
|
1926
|
-
function
|
|
2083
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1927
2084
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1928
2085
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1929
2086
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1941,7 +2098,7 @@ function validateProject(config) {
|
|
|
1941
2098
|
}
|
|
1942
2099
|
const db = openStore(config, "readonly");
|
|
1943
2100
|
for (const type of config.types) {
|
|
1944
|
-
const enSlugs =
|
|
2101
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1945
2102
|
const englishSlugs = new Set(enSlugs);
|
|
1946
2103
|
if (!type.translate && isRoutableType(type)) {
|
|
1947
2104
|
issues.push({
|
|
@@ -2023,33 +2180,8 @@ function validateProject(config) {
|
|
|
2023
2180
|
}
|
|
2024
2181
|
}
|
|
2025
2182
|
db.close();
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
issues.push({
|
|
2029
|
-
level: issue.level,
|
|
2030
|
-
contentType: issue.contentTypeId,
|
|
2031
|
-
enSlug: issue.enSlug,
|
|
2032
|
-
field: issue.field,
|
|
2033
|
-
message: issue.message
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
2037
|
-
issues.push({
|
|
2038
|
-
level: issue.level,
|
|
2039
|
-
contentType: issue.contentTypeId,
|
|
2040
|
-
enSlug: issue.enSlug,
|
|
2041
|
-
field: issue.field,
|
|
2042
|
-
message: issue.message
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
2046
|
-
issues.push({
|
|
2047
|
-
level: issue.level,
|
|
2048
|
-
contentType: issue.contentTypeId,
|
|
2049
|
-
enSlug: issue.enSlug,
|
|
2050
|
-
field: issue.field,
|
|
2051
|
-
message: issue.message
|
|
2052
|
-
});
|
|
2183
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
2184
|
+
issues.push(issue);
|
|
2053
2185
|
}
|
|
2054
2186
|
for (const issue of validateRelations(project)) {
|
|
2055
2187
|
issues.push({
|
|
@@ -2086,7 +2218,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
2086
2218
|
}
|
|
2087
2219
|
|
|
2088
2220
|
// src/translate/worklist.ts
|
|
2089
|
-
function
|
|
2221
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
2090
2222
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
2091
2223
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
2092
2224
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -2097,7 +2229,7 @@ function buildWorklist(config, options = {}) {
|
|
|
2097
2229
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
2098
2230
|
for (const type of config.types) {
|
|
2099
2231
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
2100
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
2232
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
2101
2233
|
for (const enSlug of enSlugs) {
|
|
2102
2234
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
2103
2235
|
if (!enDoc) continue;
|
|
@@ -2369,11 +2501,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
|
2369
2501
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
2370
2502
|
if (slugStrategy === "localized") {
|
|
2371
2503
|
const rules = [
|
|
2372
|
-
"Provide a URL slug in JSON field `slug` that is
|
|
2504
|
+
"Provide a URL slug in JSON field `slug` that is Localized into the target language.",
|
|
2373
2505
|
"Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
|
|
2374
2506
|
"Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
|
|
2375
2507
|
"For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
|
|
2376
|
-
"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."
|
|
2508
|
+
"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.",
|
|
2509
|
+
'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 \\".'
|
|
2377
2510
|
];
|
|
2378
2511
|
if (preserveTerms?.length) {
|
|
2379
2512
|
rules.push(
|
|
@@ -2406,6 +2539,95 @@ function resolveTranslateConfig(config, type) {
|
|
|
2406
2539
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2407
2540
|
}
|
|
2408
2541
|
|
|
2542
|
+
// src/translate/sanitize-mdx-jsx.ts
|
|
2543
|
+
function sanitizeMdxJsxAttributeQuotes(body) {
|
|
2544
|
+
let adjusted = false;
|
|
2545
|
+
let out = "";
|
|
2546
|
+
let i = 0;
|
|
2547
|
+
while (i < body.length) {
|
|
2548
|
+
if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
|
|
2549
|
+
out += body[i];
|
|
2550
|
+
i += 1;
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
const tagStart = i;
|
|
2554
|
+
i += 1;
|
|
2555
|
+
while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
|
|
2556
|
+
const tagName = body.slice(tagStart + 1, i);
|
|
2557
|
+
let tagOut = `<${tagName}`;
|
|
2558
|
+
let tagAdjusted = false;
|
|
2559
|
+
while (i < body.length && body[i] !== ">" && body[i] !== "/") {
|
|
2560
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) {
|
|
2561
|
+
tagOut += body[i];
|
|
2562
|
+
i += 1;
|
|
2563
|
+
}
|
|
2564
|
+
if (i >= body.length || body[i] === ">" || body[i] === "/") break;
|
|
2565
|
+
const attrStart = i;
|
|
2566
|
+
while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
|
|
2567
|
+
body.slice(attrStart, i);
|
|
2568
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2569
|
+
if (body[i] !== "=") {
|
|
2570
|
+
tagOut += body.slice(attrStart, i);
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
tagOut += body.slice(attrStart, i);
|
|
2574
|
+
tagOut += "=";
|
|
2575
|
+
i += 1;
|
|
2576
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
|
|
2577
|
+
const quote = body[i];
|
|
2578
|
+
if (quote !== '"' && quote !== "'") {
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (quote === "'") {
|
|
2582
|
+
const valStart2 = i + 1;
|
|
2583
|
+
i += 1;
|
|
2584
|
+
while (i < body.length) {
|
|
2585
|
+
if (body[i] === "\\") {
|
|
2586
|
+
i += 2;
|
|
2587
|
+
continue;
|
|
2588
|
+
}
|
|
2589
|
+
if (body[i] === "'") break;
|
|
2590
|
+
i += 1;
|
|
2591
|
+
}
|
|
2592
|
+
tagOut += body.slice(valStart2 - 1, i + 1);
|
|
2593
|
+
i += 1;
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
const valStart = i + 1;
|
|
2597
|
+
i += 1;
|
|
2598
|
+
let closeIdx = -1;
|
|
2599
|
+
for (let scan = valStart; scan < body.length; scan += 1) {
|
|
2600
|
+
if (body[scan] !== '"') continue;
|
|
2601
|
+
let j = scan + 1;
|
|
2602
|
+
while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
|
|
2603
|
+
if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
|
|
2604
|
+
closeIdx = scan;
|
|
2605
|
+
break;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
if (closeIdx === -1) {
|
|
2609
|
+
tagOut += body.slice(valStart - 1, i);
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
const value = body.slice(valStart, closeIdx);
|
|
2613
|
+
const hasInternalQuote = value.includes('"');
|
|
2614
|
+
if (hasInternalQuote) {
|
|
2615
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2616
|
+
tagOut += `'${escaped}'`;
|
|
2617
|
+
tagAdjusted = true;
|
|
2618
|
+
} else {
|
|
2619
|
+
tagOut += `"${value}"`;
|
|
2620
|
+
}
|
|
2621
|
+
i = closeIdx + 1;
|
|
2622
|
+
}
|
|
2623
|
+
if (tagAdjusted) adjusted = true;
|
|
2624
|
+
tagOut += body[i] ?? "";
|
|
2625
|
+
out += tagOut;
|
|
2626
|
+
i += 1;
|
|
2627
|
+
}
|
|
2628
|
+
return { body: out, adjusted };
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2409
2631
|
// src/translate/validate-translation.ts
|
|
2410
2632
|
function formatZodIssues(error) {
|
|
2411
2633
|
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -2539,6 +2761,9 @@ async function translatePage(config, item, options = {}) {
|
|
|
2539
2761
|
if (!validated.ok) {
|
|
2540
2762
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2541
2763
|
}
|
|
2764
|
+
const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
|
|
2765
|
+
result.parsed.body
|
|
2766
|
+
);
|
|
2542
2767
|
const writeDb = openStore(config, "readwrite");
|
|
2543
2768
|
const snapshotId = recordEnSnapshot(
|
|
2544
2769
|
config,
|
|
@@ -2557,7 +2782,7 @@ async function translatePage(config, item, options = {}) {
|
|
|
2557
2782
|
locale: item.locale,
|
|
2558
2783
|
slug,
|
|
2559
2784
|
frontmatter: validated.frontmatter,
|
|
2560
|
-
body:
|
|
2785
|
+
body: translatedBody,
|
|
2561
2786
|
enHash: currentEnHash,
|
|
2562
2787
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2563
2788
|
model: result.model,
|
|
@@ -2576,7 +2801,8 @@ async function translatePage(config, item, options = {}) {
|
|
|
2576
2801
|
usage: result.usage,
|
|
2577
2802
|
estimatedCostUsd,
|
|
2578
2803
|
durationMs: Date.now() - startedAt,
|
|
2579
|
-
slugAdjusted
|
|
2804
|
+
slugAdjusted,
|
|
2805
|
+
mdxAdjusted: mdxAdjusted || void 0
|
|
2580
2806
|
};
|
|
2581
2807
|
} catch (error) {
|
|
2582
2808
|
return {
|
|
@@ -2653,6 +2879,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2653
2879
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2654
2880
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2655
2881
|
const out = [];
|
|
2882
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2656
2883
|
for (const type of project.listTypes()) {
|
|
2657
2884
|
if (!isRoutableType(type.config)) continue;
|
|
2658
2885
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2668,7 +2895,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2668
2895
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2669
2896
|
const doc = resolved.document;
|
|
2670
2897
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2671
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2898
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2672
2899
|
out.push({
|
|
2673
2900
|
relativePath: urlPath.slice(1),
|
|
2674
2901
|
urlPath,
|
|
@@ -2716,6 +2943,7 @@ exports.buildStaticRawExports = buildStaticRawExports;
|
|
|
2716
2943
|
exports.buildWorklist = buildWorklist;
|
|
2717
2944
|
exports.createProject = createProject;
|
|
2718
2945
|
exports.createScribe = createScribe;
|
|
2946
|
+
exports.createUrlBuilder = createUrlBuilder;
|
|
2719
2947
|
exports.defineConfig = defineConfig;
|
|
2720
2948
|
exports.defineContentType = defineContentType;
|
|
2721
2949
|
exports.exportDirSegment = exportDirSegment;
|
|
@@ -2727,6 +2955,7 @@ exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
|
|
|
2727
2955
|
exports.getRelationTarget = getRelationTarget;
|
|
2728
2956
|
exports.getStaticExportRoots = getStaticExportRoots;
|
|
2729
2957
|
exports.isResolvedConfig = isResolvedConfig;
|
|
2958
|
+
exports.isRoutableType = isRoutableType;
|
|
2730
2959
|
exports.loadConfigSync = loadConfigSync;
|
|
2731
2960
|
exports.resolveConfig = resolveConfig;
|
|
2732
2961
|
exports.resolveLocalesFromPreset = resolveLocalesFromPreset;
|