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