scribe-cms 0.0.8 → 0.0.10
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 +433 -384
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +433 -384
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +643 -512
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +643 -513
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +287 -261
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +286 -261
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/builtin-fields.d.ts +8 -8
- package/dist/src/core/builtin-fields.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +11 -4
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/create-project.d.ts.map +1 -1
- package/dist/src/export/build-static-raw-exports.d.ts.map +1 -1
- package/dist/src/i18n/build-url.d.ts +16 -3
- package/dist/src/i18n/build-url.d.ts.map +1 -1
- package/dist/src/i18n/resolve-document.d.ts +3 -2
- package/dist/src/i18n/resolve-document.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/loader/create-loader.d.ts.map +1 -1
- package/dist/src/redirects/build-json-redirects.d.ts +6 -0
- package/dist/src/redirects/build-json-redirects.d.ts.map +1 -0
- package/dist/src/redirects/build-redirects.d.ts +9 -7
- package/dist/src/redirects/build-redirects.d.ts.map +1 -1
- package/dist/src/redirects/load-type-redirects.d.ts +14 -0
- package/dist/src/redirects/load-type-redirects.d.ts.map +1 -0
- package/dist/src/redirects/redirect-schema.d.ts +31 -0
- package/dist/src/redirects/redirect-schema.d.ts.map +1 -0
- package/dist/src/runtime.d.ts +3 -1
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -1
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/src/validate/validate-redirects.d.ts +4 -0
- package/dist/src/validate/validate-redirects.d.ts.map +1 -0
- package/dist/studio/server.cjs +90 -61
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js +90 -61
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/core/slug-aliases.d.ts +0 -27
- package/dist/src/core/slug-aliases.d.ts.map +0 -1
- package/dist/src/redirects/translation-index.d.ts +0 -4
- package/dist/src/redirects/translation-index.d.ts.map +0 -1
package/dist/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);
|
|
@@ -1197,207 +1174,329 @@ function enFileExists(config, type, enSlug) {
|
|
|
1197
1174
|
const dir = path3__default.default.join(config.rootDir, type.contentDir);
|
|
1198
1175
|
return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
|
|
1199
1176
|
}
|
|
1200
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1201
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1202
|
-
}
|
|
1203
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1204
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1205
|
-
}
|
|
1206
1177
|
function listEnSlugs(rootDir, contentDir) {
|
|
1207
1178
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1208
1179
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1209
1180
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1210
1181
|
}
|
|
1211
1182
|
|
|
1212
|
-
// src/
|
|
1213
|
-
function
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
|
|
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);
|
|
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}`);
|
|
1222
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 = [];
|
|
1223
1299
|
for (const type of config.types) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
message: `Alias "${alias}" must not equal the canonical slug`,
|
|
1236
|
-
level: "error"
|
|
1237
|
-
});
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
if (canonicalSlugs.has(alias)) {
|
|
1241
|
-
issues.push({
|
|
1242
|
-
contentTypeId: type.id,
|
|
1243
|
-
enSlug,
|
|
1244
|
-
field: "aliases",
|
|
1245
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1246
|
-
level: "error"
|
|
1247
|
-
});
|
|
1248
|
-
continue;
|
|
1249
|
-
}
|
|
1250
|
-
const existing = aliasToTarget.get(alias);
|
|
1251
|
-
if (existing) {
|
|
1252
|
-
issues.push({
|
|
1253
|
-
contentTypeId: type.id,
|
|
1254
|
-
enSlug,
|
|
1255
|
-
field: "aliases",
|
|
1256
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1257
|
-
level: "error"
|
|
1258
|
-
});
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
|
-
aliasToTarget.set(alias, {
|
|
1262
|
-
contentTypeId: type.id,
|
|
1263
|
-
canonicalSlug: enSlug,
|
|
1264
|
-
path: type.path
|
|
1265
|
-
});
|
|
1266
|
-
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);
|
|
1267
1311
|
}
|
|
1268
1312
|
}
|
|
1269
1313
|
}
|
|
1270
|
-
return
|
|
1314
|
+
return slugs;
|
|
1271
1315
|
}
|
|
1272
|
-
function
|
|
1273
|
-
const
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
const type = config.types.find((t) => t.id === target.contentTypeId);
|
|
1280
|
-
if (!type) continue;
|
|
1281
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1282
|
-
if (!known) {
|
|
1283
|
-
issues.push({
|
|
1284
|
-
contentTypeId: type.id,
|
|
1285
|
-
enSlug: target.canonicalSlug,
|
|
1286
|
-
field: "aliases",
|
|
1287
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1288
|
-
level: "warning"
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
if (enFileExists(config, type, alias)) {
|
|
1292
|
-
issues.push({
|
|
1293
|
-
contentTypeId: type.id,
|
|
1294
|
-
enSlug: target.canonicalSlug,
|
|
1295
|
-
field: "aliases",
|
|
1296
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1297
|
-
level: "warning"
|
|
1298
|
-
});
|
|
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);
|
|
1299
1323
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
+
);
|
|
1309
1375
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
|
1314
1395
|
);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
+
);
|
|
1325
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));
|
|
1326
1443
|
}
|
|
1327
1444
|
}
|
|
1328
1445
|
} finally {
|
|
1329
1446
|
db.close();
|
|
1330
1447
|
}
|
|
1331
|
-
return
|
|
1448
|
+
return out;
|
|
1332
1449
|
}
|
|
1333
|
-
function
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
);
|
|
1343
|
-
if (doc?.redirectTo) {
|
|
1344
|
-
issues.push({
|
|
1345
|
-
contentTypeId: target.contentTypeId,
|
|
1346
|
-
enSlug: target.canonicalSlug,
|
|
1347
|
-
field: "aliases",
|
|
1348
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1349
|
-
level: "error"
|
|
1350
|
-
});
|
|
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
|
+
}
|
|
1351
1459
|
}
|
|
1352
1460
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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,
|
|
1372
1481
|
enSlug,
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
|
1376
1489
|
});
|
|
1377
1490
|
}
|
|
1378
1491
|
}
|
|
1492
|
+
} finally {
|
|
1493
|
+
db.close();
|
|
1379
1494
|
}
|
|
1380
|
-
return
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// src/i18n/translation-index.ts
|
|
1384
|
-
function buildTranslationIndex(allDocs, locales, defaultLocale) {
|
|
1385
|
-
const index = /* @__PURE__ */ new Map();
|
|
1386
|
-
for (const locale of locales) {
|
|
1387
|
-
if (locale === defaultLocale) continue;
|
|
1388
|
-
const idx = allDocs.get(locale);
|
|
1389
|
-
if (!idx) continue;
|
|
1390
|
-
const localeMap = /* @__PURE__ */ new Map();
|
|
1391
|
-
for (const [enSlug, doc] of idx.byEnSlug) {
|
|
1392
|
-
localeMap.set(enSlug, doc.slug);
|
|
1393
|
-
}
|
|
1394
|
-
index.set(locale, localeMap);
|
|
1395
|
-
}
|
|
1396
|
-
return index;
|
|
1495
|
+
return out;
|
|
1397
1496
|
}
|
|
1398
1497
|
|
|
1399
|
-
// src/redirects/
|
|
1400
|
-
function buildRedirectTranslationIndex(project, typeId) {
|
|
1498
|
+
// src/redirects/build-redirects.ts
|
|
1499
|
+
function buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs) {
|
|
1401
1500
|
const runtime = project.getType(typeId);
|
|
1402
1501
|
const { config } = project;
|
|
1403
1502
|
const merged = buildTranslationIndex(
|
|
@@ -1405,11 +1504,9 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1405
1504
|
config.locales,
|
|
1406
1505
|
config.defaultLocale
|
|
1407
1506
|
);
|
|
1408
|
-
const { aliasToTarget } = buildGlobalAliasIndex(project);
|
|
1409
1507
|
const db = openStore(project.config, "readonly");
|
|
1410
1508
|
try {
|
|
1411
|
-
for (const
|
|
1412
|
-
if (target.contentTypeId !== typeId) continue;
|
|
1509
|
+
for (const alias of redirectSourceSlugs) {
|
|
1413
1510
|
for (const row of listTranslationsForEnSlug(db, typeId, alias)) {
|
|
1414
1511
|
const localeMap = merged.get(row.locale) ?? /* @__PURE__ */ new Map();
|
|
1415
1512
|
if (!localeMap.has(alias)) {
|
|
@@ -1423,168 +1520,63 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1423
1520
|
}
|
|
1424
1521
|
return merged;
|
|
1425
1522
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
const
|
|
1431
|
-
const pathTemplate = type.path;
|
|
1432
|
-
for (const [alias, target] of aliasToTarget) {
|
|
1433
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1434
|
-
const canonical = target.canonicalSlug;
|
|
1435
|
-
out.push({
|
|
1436
|
-
source: resolvePath(pathTemplate, alias, defaultLocale, defaultLocale),
|
|
1437
|
-
destination: resolvePath(pathTemplate, canonical, defaultLocale, defaultLocale),
|
|
1438
|
-
permanent: true
|
|
1439
|
-
});
|
|
1440
|
-
for (const locale of prefixedLocales) {
|
|
1441
|
-
const localeMap = translationIndex.get(locale);
|
|
1442
|
-
const toLocale = localeMap?.get(canonical) ?? canonical;
|
|
1443
|
-
const fromLocale = localeMap?.get(alias);
|
|
1444
|
-
if (fromLocale) {
|
|
1445
|
-
out.push({
|
|
1446
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1447
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1448
|
-
permanent: true
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
out.push({
|
|
1452
|
-
source: resolvePath(pathTemplate, alias, locale, defaultLocale),
|
|
1453
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1454
|
-
permanent: true
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
return out;
|
|
1459
|
-
}
|
|
1460
|
-
function buildAliasLegacyNoBlogPathRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
|
|
1461
|
-
if (type.id !== "blog") return [];
|
|
1462
|
-
const out = [];
|
|
1463
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1464
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1465
|
-
for (const locale of prefixedLocales) {
|
|
1466
|
-
const localeMap = translationIndex.get(locale);
|
|
1467
|
-
const toLocale = localeMap?.get(target.canonicalSlug) ?? target.canonicalSlug;
|
|
1468
|
-
out.push({
|
|
1469
|
-
source: `/${locale}/${alias}`,
|
|
1470
|
-
destination: resolvePath(type.path, toLocale, locale, defaultLocale),
|
|
1471
|
-
permanent: true
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
return out;
|
|
1476
|
-
}
|
|
1477
|
-
function buildRedirectToRules(type, config, translationIndex, prefixedLocales, defaultLocale) {
|
|
1478
|
-
const out = [];
|
|
1479
|
-
const pathTemplate = type.path;
|
|
1480
|
-
for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
|
|
1481
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1482
|
-
if (!doc?.redirectTo) continue;
|
|
1483
|
-
out.push({
|
|
1484
|
-
source: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1485
|
-
destination: doc.redirectTo,
|
|
1486
|
-
permanent: true
|
|
1487
|
-
});
|
|
1488
|
-
for (const locale of prefixedLocales) {
|
|
1489
|
-
const localeMap = translationIndex.get(locale);
|
|
1490
|
-
const fromLocale = localeMap?.get(enSlug);
|
|
1491
|
-
if (!fromLocale) continue;
|
|
1492
|
-
const targetEnSlug = extractSlugFromResolvedPath(pathTemplate, doc.redirectTo);
|
|
1493
|
-
if (!targetEnSlug) continue;
|
|
1494
|
-
const toLocale = localeMap?.get(targetEnSlug) ?? targetEnSlug;
|
|
1495
|
-
out.push({
|
|
1496
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1497
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1498
|
-
permanent: true
|
|
1499
|
-
});
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
return out;
|
|
1503
|
-
}
|
|
1504
|
-
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);
|
|
1505
1528
|
const out = [];
|
|
1506
1529
|
const pathTemplate = type.path;
|
|
1507
1530
|
for (const [locale, localeMap] of translationIndex) {
|
|
1508
1531
|
for (const [enSlug, translatedSlug] of localeMap) {
|
|
1509
1532
|
if (enSlug === translatedSlug) continue;
|
|
1510
|
-
if (
|
|
1533
|
+
if (redirectSourceSlugs.has(enSlug)) continue;
|
|
1511
1534
|
out.push({
|
|
1512
|
-
source: resolvePath(pathTemplate, enSlug, locale
|
|
1513
|
-
destination: resolvePath(pathTemplate, translatedSlug, locale
|
|
1535
|
+
source: urlBuilder.resolvePath(pathTemplate, enSlug, locale),
|
|
1536
|
+
destination: urlBuilder.resolvePath(pathTemplate, translatedSlug, locale),
|
|
1514
1537
|
permanent: true
|
|
1515
1538
|
});
|
|
1516
1539
|
}
|
|
1517
1540
|
}
|
|
1518
1541
|
return out;
|
|
1519
1542
|
}
|
|
1520
|
-
function
|
|
1521
|
-
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
destination: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1529
|
-
permanent: true
|
|
1530
|
-
});
|
|
1531
|
-
for (const locale of prefixedLocales) {
|
|
1532
|
-
const translatedSlug = translationIndex.get(locale)?.get(enSlug);
|
|
1533
|
-
const canonicalSlug = translatedSlug ?? enSlug;
|
|
1534
|
-
out.push({
|
|
1535
|
-
source: `/${locale}/${enSlug}`,
|
|
1536
|
-
destination: resolvePath(pathTemplate, canonicalSlug, locale, defaultLocale),
|
|
1537
|
-
permanent: true
|
|
1538
|
-
});
|
|
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
|
+
}
|
|
1539
1551
|
}
|
|
1540
1552
|
}
|
|
1541
|
-
return out;
|
|
1542
|
-
}
|
|
1543
|
-
function buildTypeRedirects(project, typeId, aliasIndex, options) {
|
|
1544
|
-
const type = project.getType(typeId).config;
|
|
1545
|
-
if (!isRoutableType(type)) return [];
|
|
1546
|
-
const { defaultLocale } = project.config;
|
|
1547
|
-
const translationIndex = buildRedirectTranslationIndex(project, typeId);
|
|
1548
|
-
const aliasSourceSlugs = /* @__PURE__ */ new Set();
|
|
1549
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1550
|
-
if (target.contentTypeId === typeId) aliasSourceSlugs.add(alias);
|
|
1551
|
-
}
|
|
1552
1553
|
return [
|
|
1553
|
-
...
|
|
1554
|
-
...buildAliasLegacyNoBlogPathRedirects(
|
|
1555
|
-
type,
|
|
1556
|
-
aliasIndex,
|
|
1557
|
-
translationIndex,
|
|
1558
|
-
options.prefixedLocales,
|
|
1559
|
-
defaultLocale
|
|
1560
|
-
),
|
|
1561
|
-
...buildRedirectToRules(type, project.config, translationIndex, options.prefixedLocales, defaultLocale),
|
|
1562
|
-
...buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale),
|
|
1563
|
-
...buildLegacyNoBlogPathRedirects(type, project.config, translationIndex, options.prefixedLocales, defaultLocale)
|
|
1554
|
+
...buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs)
|
|
1564
1555
|
];
|
|
1565
1556
|
}
|
|
1566
|
-
function buildAllContentRedirects(project,
|
|
1567
|
-
const
|
|
1568
|
-
const out = [
|
|
1557
|
+
function buildAllContentRedirects(project, _options = {}) {
|
|
1558
|
+
const redirectSourceKeys = buildRedirectSourceSlugSet(project);
|
|
1559
|
+
const out = [
|
|
1560
|
+
...buildJsonRedirects(project),
|
|
1561
|
+
...buildLegacyNoBlogPathRedirects(project, redirectSourceKeys)
|
|
1562
|
+
];
|
|
1569
1563
|
for (const type of project.config.types) {
|
|
1570
1564
|
if (!isRoutableType(type)) continue;
|
|
1571
|
-
out.push(...buildTypeRedirects(project, type.id
|
|
1565
|
+
out.push(...buildTypeRedirects(project, type.id));
|
|
1572
1566
|
}
|
|
1573
1567
|
return out;
|
|
1574
1568
|
}
|
|
1575
1569
|
function getRedirectSourceSlugs(project) {
|
|
1576
|
-
const
|
|
1577
|
-
const
|
|
1570
|
+
const loaded = loadAllTypeRedirects(project.config);
|
|
1571
|
+
const aliasSlugs = collectRedirectSourceSlugs(loaded);
|
|
1572
|
+
const outboundByType = collectOutboundRedirectSourcesByType(loaded);
|
|
1578
1573
|
for (const type of project.config.types) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
const doc = readEnDocument(project.config, type, enSlug);
|
|
1582
|
-
if (doc?.redirectTo) outbound.add(enSlug);
|
|
1574
|
+
if (!outboundByType.has(type.id)) {
|
|
1575
|
+
outboundByType.set(type.id, /* @__PURE__ */ new Set());
|
|
1583
1576
|
}
|
|
1584
|
-
outboundByType.set(type.id, outbound);
|
|
1585
1577
|
}
|
|
1586
1578
|
return {
|
|
1587
|
-
aliasSlugs
|
|
1579
|
+
aliasSlugs,
|
|
1588
1580
|
outboundByType
|
|
1589
1581
|
};
|
|
1590
1582
|
}
|
|
@@ -1592,13 +1584,12 @@ function getRedirectSourceSlugs(project) {
|
|
|
1592
1584
|
// src/sitemap/join-base-url.ts
|
|
1593
1585
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1594
1586
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1595
|
-
const
|
|
1596
|
-
return `${origin}${
|
|
1587
|
+
const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1588
|
+
return `${origin}${path13}`;
|
|
1597
1589
|
}
|
|
1598
1590
|
|
|
1599
1591
|
// src/sitemap/generate-sitemap.ts
|
|
1600
1592
|
function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
|
|
1601
|
-
if (enDoc.redirectTo) return false;
|
|
1602
1593
|
if (redirectSources.aliasSlugs.has(enSlug)) return false;
|
|
1603
1594
|
const outbound = redirectSources.outboundByType.get(contentTypeId);
|
|
1604
1595
|
if (outbound?.has(enSlug)) return false;
|
|
@@ -1738,6 +1729,169 @@ function loadConfigSync(options = {}) {
|
|
|
1738
1729
|
}
|
|
1739
1730
|
return resolveConfig(config, path3__default.default.dirname(configPath));
|
|
1740
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 (entry.kind === "same-type" && 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 targetAsSource = globalFrom.get(entry.toSlug);
|
|
1880
|
+
if (!targetAsSource) continue;
|
|
1881
|
+
const sameSlugCrossType = entry.kind === "cross-type" && entry.fromSlugs.includes(entry.toSlug) && targetAsSource.typeId === file.contentTypeId;
|
|
1882
|
+
if (sameSlugCrossType) continue;
|
|
1883
|
+
issues.push({
|
|
1884
|
+
level: "error",
|
|
1885
|
+
contentType: file.contentTypeId,
|
|
1886
|
+
enSlug: from,
|
|
1887
|
+
field: TYPE_REDIRECTS_FILENAME,
|
|
1888
|
+
message: `Redirect chain detected: "${from}" \u2192 "${entry.toSlug}" \u2192 existing redirect source`
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return issues;
|
|
1894
|
+
}
|
|
1741
1895
|
function getAtPath2(obj, fieldPath) {
|
|
1742
1896
|
let current = obj;
|
|
1743
1897
|
for (const segment of fieldPath) {
|
|
@@ -1927,7 +2081,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1927
2081
|
}
|
|
1928
2082
|
|
|
1929
2083
|
// src/validate/validate-project.ts
|
|
1930
|
-
function
|
|
2084
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1931
2085
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
1932
2086
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
1933
2087
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1945,7 +2099,7 @@ function validateProject(config) {
|
|
|
1945
2099
|
}
|
|
1946
2100
|
const db = openStore(config, "readonly");
|
|
1947
2101
|
for (const type of config.types) {
|
|
1948
|
-
const enSlugs =
|
|
2102
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1949
2103
|
const englishSlugs = new Set(enSlugs);
|
|
1950
2104
|
if (!type.translate && isRoutableType(type)) {
|
|
1951
2105
|
issues.push({
|
|
@@ -2027,33 +2181,8 @@ function validateProject(config) {
|
|
|
2027
2181
|
}
|
|
2028
2182
|
}
|
|
2029
2183
|
db.close();
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
issues.push({
|
|
2033
|
-
level: issue.level,
|
|
2034
|
-
contentType: issue.contentTypeId,
|
|
2035
|
-
enSlug: issue.enSlug,
|
|
2036
|
-
field: issue.field,
|
|
2037
|
-
message: issue.message
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
2041
|
-
issues.push({
|
|
2042
|
-
level: issue.level,
|
|
2043
|
-
contentType: issue.contentTypeId,
|
|
2044
|
-
enSlug: issue.enSlug,
|
|
2045
|
-
field: issue.field,
|
|
2046
|
-
message: issue.message
|
|
2047
|
-
});
|
|
2048
|
-
}
|
|
2049
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
2050
|
-
issues.push({
|
|
2051
|
-
level: issue.level,
|
|
2052
|
-
contentType: issue.contentTypeId,
|
|
2053
|
-
enSlug: issue.enSlug,
|
|
2054
|
-
field: issue.field,
|
|
2055
|
-
message: issue.message
|
|
2056
|
-
});
|
|
2184
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
2185
|
+
issues.push(issue);
|
|
2057
2186
|
}
|
|
2058
2187
|
for (const issue of validateRelations(project)) {
|
|
2059
2188
|
issues.push({
|
|
@@ -2090,7 +2219,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
2090
2219
|
}
|
|
2091
2220
|
|
|
2092
2221
|
// src/translate/worklist.ts
|
|
2093
|
-
function
|
|
2222
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
2094
2223
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
2095
2224
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
2096
2225
|
return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -2101,7 +2230,7 @@ function buildWorklist(config, options = {}) {
|
|
|
2101
2230
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
2102
2231
|
for (const type of config.types) {
|
|
2103
2232
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
2104
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
2233
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
2105
2234
|
for (const enSlug of enSlugs) {
|
|
2106
2235
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
2107
2236
|
if (!enDoc) continue;
|
|
@@ -2751,6 +2880,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2751
2880
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2752
2881
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2753
2882
|
const out = [];
|
|
2883
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2754
2884
|
for (const type of project.listTypes()) {
|
|
2755
2885
|
if (!isRoutableType(type.config)) continue;
|
|
2756
2886
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2766,7 +2896,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2766
2896
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2767
2897
|
const doc = resolved.document;
|
|
2768
2898
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2769
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2899
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2770
2900
|
out.push({
|
|
2771
2901
|
relativePath: urlPath.slice(1),
|
|
2772
2902
|
urlPath,
|
|
@@ -2814,6 +2944,7 @@ exports.buildStaticRawExports = buildStaticRawExports;
|
|
|
2814
2944
|
exports.buildWorklist = buildWorklist;
|
|
2815
2945
|
exports.createProject = createProject;
|
|
2816
2946
|
exports.createScribe = createScribe;
|
|
2947
|
+
exports.createUrlBuilder = createUrlBuilder;
|
|
2817
2948
|
exports.defineConfig = defineConfig;
|
|
2818
2949
|
exports.defineContentType = defineContentType;
|
|
2819
2950
|
exports.exportDirSegment = exportDirSegment;
|