scribe-cms 0.0.8 → 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 +432 -384
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +432 -384
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +642 -512
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +642 -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.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);
|
|
@@ -1186,207 +1163,329 @@ function enFileExists(config, type, enSlug) {
|
|
|
1186
1163
|
const dir = path3.join(config.rootDir, type.contentDir);
|
|
1187
1164
|
return fs2.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs2.existsSync(path3.join(dir, `${enSlug}.md`));
|
|
1188
1165
|
}
|
|
1189
|
-
function sqliteHasTranslations(db, contentTypeId, enSlug) {
|
|
1190
|
-
return listTranslationsForEnSlug(db, contentTypeId, enSlug).length > 0;
|
|
1191
|
-
}
|
|
1192
|
-
function isAliasKnown(config, type, aliasEnSlug, db) {
|
|
1193
|
-
return enFileExists(config, type, aliasEnSlug) || sqliteHasTranslations(db, type.id, aliasEnSlug);
|
|
1194
|
-
}
|
|
1195
1166
|
function listEnSlugs(rootDir, contentDir) {
|
|
1196
1167
|
const dir = path3.join(rootDir, contentDir);
|
|
1197
1168
|
if (!fs2.existsSync(dir)) return [];
|
|
1198
1169
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
1199
1170
|
}
|
|
1200
1171
|
|
|
1201
|
-
// src/
|
|
1202
|
-
function
|
|
1203
|
-
const
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
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);
|
|
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}`);
|
|
1211
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 = [];
|
|
1212
1288
|
for (const type of config.types) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
message: `Alias "${alias}" must not equal the canonical slug`,
|
|
1225
|
-
level: "error"
|
|
1226
|
-
});
|
|
1227
|
-
continue;
|
|
1228
|
-
}
|
|
1229
|
-
if (canonicalSlugs.has(alias)) {
|
|
1230
|
-
issues.push({
|
|
1231
|
-
contentTypeId: type.id,
|
|
1232
|
-
enSlug,
|
|
1233
|
-
field: "aliases",
|
|
1234
|
-
message: `Alias "${alias}" collides with another document's canonical slug`,
|
|
1235
|
-
level: "error"
|
|
1236
|
-
});
|
|
1237
|
-
continue;
|
|
1238
|
-
}
|
|
1239
|
-
const existing = aliasToTarget.get(alias);
|
|
1240
|
-
if (existing) {
|
|
1241
|
-
issues.push({
|
|
1242
|
-
contentTypeId: type.id,
|
|
1243
|
-
enSlug,
|
|
1244
|
-
field: "aliases",
|
|
1245
|
-
message: `Alias "${alias}" is already claimed by ${existing.contentTypeId}/${existing.canonicalSlug}`,
|
|
1246
|
-
level: "error"
|
|
1247
|
-
});
|
|
1248
|
-
continue;
|
|
1249
|
-
}
|
|
1250
|
-
aliasToTarget.set(alias, {
|
|
1251
|
-
contentTypeId: type.id,
|
|
1252
|
-
canonicalSlug: enSlug,
|
|
1253
|
-
path: type.path
|
|
1254
|
-
});
|
|
1255
|
-
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);
|
|
1256
1300
|
}
|
|
1257
1301
|
}
|
|
1258
1302
|
}
|
|
1259
|
-
return
|
|
1303
|
+
return slugs;
|
|
1260
1304
|
}
|
|
1261
|
-
function
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const type = config.types.find((t) => t.id === target.contentTypeId);
|
|
1269
|
-
if (!type) continue;
|
|
1270
|
-
const known = isAliasKnown(config, type, alias, db);
|
|
1271
|
-
if (!known) {
|
|
1272
|
-
issues.push({
|
|
1273
|
-
contentTypeId: type.id,
|
|
1274
|
-
enSlug: target.canonicalSlug,
|
|
1275
|
-
field: "aliases",
|
|
1276
|
-
message: `Alias "${alias}" is unknown (no EN file or sqlite translations) \u2014 only EN redirect will work`,
|
|
1277
|
-
level: "warning"
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
if (enFileExists(config, type, alias)) {
|
|
1281
|
-
issues.push({
|
|
1282
|
-
contentTypeId: type.id,
|
|
1283
|
-
enSlug: target.canonicalSlug,
|
|
1284
|
-
field: "aliases",
|
|
1285
|
-
message: `Alias "${alias}" still has an EN file on disk \u2014 delete it manually`,
|
|
1286
|
-
level: "warning"
|
|
1287
|
-
});
|
|
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);
|
|
1288
1312
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
+
);
|
|
1298
1364
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1303
1384
|
);
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
+
);
|
|
1314
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));
|
|
1315
1432
|
}
|
|
1316
1433
|
}
|
|
1317
1434
|
} finally {
|
|
1318
1435
|
db.close();
|
|
1319
1436
|
}
|
|
1320
|
-
return
|
|
1437
|
+
return out;
|
|
1321
1438
|
}
|
|
1322
|
-
function
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
);
|
|
1332
|
-
if (doc?.redirectTo) {
|
|
1333
|
-
issues.push({
|
|
1334
|
-
contentTypeId: target.contentTypeId,
|
|
1335
|
-
enSlug: target.canonicalSlug,
|
|
1336
|
-
field: "aliases",
|
|
1337
|
-
message: `Alias "${alias}" points at "${target.canonicalSlug}" which has redirect_to \u2014 resolve to the final canonical slug`,
|
|
1338
|
-
level: "error"
|
|
1339
|
-
});
|
|
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
|
+
}
|
|
1340
1448
|
}
|
|
1341
1449
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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,
|
|
1361
1470
|
enSlug,
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
|
1365
1478
|
});
|
|
1366
1479
|
}
|
|
1367
1480
|
}
|
|
1481
|
+
} finally {
|
|
1482
|
+
db.close();
|
|
1368
1483
|
}
|
|
1369
|
-
return
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// src/i18n/translation-index.ts
|
|
1373
|
-
function buildTranslationIndex(allDocs, locales, defaultLocale) {
|
|
1374
|
-
const index = /* @__PURE__ */ new Map();
|
|
1375
|
-
for (const locale of locales) {
|
|
1376
|
-
if (locale === defaultLocale) continue;
|
|
1377
|
-
const idx = allDocs.get(locale);
|
|
1378
|
-
if (!idx) continue;
|
|
1379
|
-
const localeMap = /* @__PURE__ */ new Map();
|
|
1380
|
-
for (const [enSlug, doc] of idx.byEnSlug) {
|
|
1381
|
-
localeMap.set(enSlug, doc.slug);
|
|
1382
|
-
}
|
|
1383
|
-
index.set(locale, localeMap);
|
|
1384
|
-
}
|
|
1385
|
-
return index;
|
|
1484
|
+
return out;
|
|
1386
1485
|
}
|
|
1387
1486
|
|
|
1388
|
-
// src/redirects/
|
|
1389
|
-
function buildRedirectTranslationIndex(project, typeId) {
|
|
1487
|
+
// src/redirects/build-redirects.ts
|
|
1488
|
+
function buildRedirectTranslationIndex(project, typeId, redirectSourceSlugs) {
|
|
1390
1489
|
const runtime = project.getType(typeId);
|
|
1391
1490
|
const { config } = project;
|
|
1392
1491
|
const merged = buildTranslationIndex(
|
|
@@ -1394,11 +1493,9 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1394
1493
|
config.locales,
|
|
1395
1494
|
config.defaultLocale
|
|
1396
1495
|
);
|
|
1397
|
-
const { aliasToTarget } = buildGlobalAliasIndex(project);
|
|
1398
1496
|
const db = openStore(project.config, "readonly");
|
|
1399
1497
|
try {
|
|
1400
|
-
for (const
|
|
1401
|
-
if (target.contentTypeId !== typeId) continue;
|
|
1498
|
+
for (const alias of redirectSourceSlugs) {
|
|
1402
1499
|
for (const row of listTranslationsForEnSlug(db, typeId, alias)) {
|
|
1403
1500
|
const localeMap = merged.get(row.locale) ?? /* @__PURE__ */ new Map();
|
|
1404
1501
|
if (!localeMap.has(alias)) {
|
|
@@ -1412,168 +1509,63 @@ function buildRedirectTranslationIndex(project, typeId) {
|
|
|
1412
1509
|
}
|
|
1413
1510
|
return merged;
|
|
1414
1511
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
const
|
|
1419
|
-
const
|
|
1420
|
-
const pathTemplate = type.path;
|
|
1421
|
-
for (const [alias, target] of aliasToTarget) {
|
|
1422
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1423
|
-
const canonical = target.canonicalSlug;
|
|
1424
|
-
out.push({
|
|
1425
|
-
source: resolvePath(pathTemplate, alias, defaultLocale, defaultLocale),
|
|
1426
|
-
destination: resolvePath(pathTemplate, canonical, defaultLocale, defaultLocale),
|
|
1427
|
-
permanent: true
|
|
1428
|
-
});
|
|
1429
|
-
for (const locale of prefixedLocales) {
|
|
1430
|
-
const localeMap = translationIndex.get(locale);
|
|
1431
|
-
const toLocale = localeMap?.get(canonical) ?? canonical;
|
|
1432
|
-
const fromLocale = localeMap?.get(alias);
|
|
1433
|
-
if (fromLocale) {
|
|
1434
|
-
out.push({
|
|
1435
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1436
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1437
|
-
permanent: true
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
out.push({
|
|
1441
|
-
source: resolvePath(pathTemplate, alias, locale, defaultLocale),
|
|
1442
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1443
|
-
permanent: true
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return out;
|
|
1448
|
-
}
|
|
1449
|
-
function buildAliasLegacyNoBlogPathRedirects(type, aliasIndex, translationIndex, prefixedLocales, defaultLocale) {
|
|
1450
|
-
if (type.id !== "blog") return [];
|
|
1451
|
-
const out = [];
|
|
1452
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1453
|
-
if (target.contentTypeId !== type.id) continue;
|
|
1454
|
-
for (const locale of prefixedLocales) {
|
|
1455
|
-
const localeMap = translationIndex.get(locale);
|
|
1456
|
-
const toLocale = localeMap?.get(target.canonicalSlug) ?? target.canonicalSlug;
|
|
1457
|
-
out.push({
|
|
1458
|
-
source: `/${locale}/${alias}`,
|
|
1459
|
-
destination: resolvePath(type.path, toLocale, locale, defaultLocale),
|
|
1460
|
-
permanent: true
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
return out;
|
|
1465
|
-
}
|
|
1466
|
-
function buildRedirectToRules(type, config, translationIndex, prefixedLocales, defaultLocale) {
|
|
1467
|
-
const out = [];
|
|
1468
|
-
const pathTemplate = type.path;
|
|
1469
|
-
for (const enSlug of listEnSlugs(config.rootDir, type.contentDir)) {
|
|
1470
|
-
const doc = readEnDocument(config, type, enSlug);
|
|
1471
|
-
if (!doc?.redirectTo) continue;
|
|
1472
|
-
out.push({
|
|
1473
|
-
source: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1474
|
-
destination: doc.redirectTo,
|
|
1475
|
-
permanent: true
|
|
1476
|
-
});
|
|
1477
|
-
for (const locale of prefixedLocales) {
|
|
1478
|
-
const localeMap = translationIndex.get(locale);
|
|
1479
|
-
const fromLocale = localeMap?.get(enSlug);
|
|
1480
|
-
if (!fromLocale) continue;
|
|
1481
|
-
const targetEnSlug = extractSlugFromResolvedPath(pathTemplate, doc.redirectTo);
|
|
1482
|
-
if (!targetEnSlug) continue;
|
|
1483
|
-
const toLocale = localeMap?.get(targetEnSlug) ?? targetEnSlug;
|
|
1484
|
-
out.push({
|
|
1485
|
-
source: resolvePath(pathTemplate, fromLocale, locale, defaultLocale),
|
|
1486
|
-
destination: resolvePath(pathTemplate, toLocale, locale, defaultLocale),
|
|
1487
|
-
permanent: true
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
return out;
|
|
1492
|
-
}
|
|
1493
|
-
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);
|
|
1494
1517
|
const out = [];
|
|
1495
1518
|
const pathTemplate = type.path;
|
|
1496
1519
|
for (const [locale, localeMap] of translationIndex) {
|
|
1497
1520
|
for (const [enSlug, translatedSlug] of localeMap) {
|
|
1498
1521
|
if (enSlug === translatedSlug) continue;
|
|
1499
|
-
if (
|
|
1522
|
+
if (redirectSourceSlugs.has(enSlug)) continue;
|
|
1500
1523
|
out.push({
|
|
1501
|
-
source: resolvePath(pathTemplate, enSlug, locale
|
|
1502
|
-
destination: resolvePath(pathTemplate, translatedSlug, locale
|
|
1524
|
+
source: urlBuilder.resolvePath(pathTemplate, enSlug, locale),
|
|
1525
|
+
destination: urlBuilder.resolvePath(pathTemplate, translatedSlug, locale),
|
|
1503
1526
|
permanent: true
|
|
1504
1527
|
});
|
|
1505
1528
|
}
|
|
1506
1529
|
}
|
|
1507
1530
|
return out;
|
|
1508
1531
|
}
|
|
1509
|
-
function
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
destination: resolvePath(pathTemplate, enSlug, defaultLocale, defaultLocale),
|
|
1518
|
-
permanent: true
|
|
1519
|
-
});
|
|
1520
|
-
for (const locale of prefixedLocales) {
|
|
1521
|
-
const translatedSlug = translationIndex.get(locale)?.get(enSlug);
|
|
1522
|
-
const canonicalSlug = translatedSlug ?? enSlug;
|
|
1523
|
-
out.push({
|
|
1524
|
-
source: `/${locale}/${enSlug}`,
|
|
1525
|
-
destination: resolvePath(pathTemplate, canonicalSlug, locale, defaultLocale),
|
|
1526
|
-
permanent: true
|
|
1527
|
-
});
|
|
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
|
+
}
|
|
1528
1540
|
}
|
|
1529
1541
|
}
|
|
1530
|
-
return out;
|
|
1531
|
-
}
|
|
1532
|
-
function buildTypeRedirects(project, typeId, aliasIndex, options) {
|
|
1533
|
-
const type = project.getType(typeId).config;
|
|
1534
|
-
if (!isRoutableType(type)) return [];
|
|
1535
|
-
const { defaultLocale } = project.config;
|
|
1536
|
-
const translationIndex = buildRedirectTranslationIndex(project, typeId);
|
|
1537
|
-
const aliasSourceSlugs = /* @__PURE__ */ new Set();
|
|
1538
|
-
for (const [alias, target] of aliasIndex.aliasToTarget) {
|
|
1539
|
-
if (target.contentTypeId === typeId) aliasSourceSlugs.add(alias);
|
|
1540
|
-
}
|
|
1541
1542
|
return [
|
|
1542
|
-
...
|
|
1543
|
-
...buildAliasLegacyNoBlogPathRedirects(
|
|
1544
|
-
type,
|
|
1545
|
-
aliasIndex,
|
|
1546
|
-
translationIndex,
|
|
1547
|
-
options.prefixedLocales,
|
|
1548
|
-
defaultLocale
|
|
1549
|
-
),
|
|
1550
|
-
...buildRedirectToRules(type, project.config, translationIndex, options.prefixedLocales, defaultLocale),
|
|
1551
|
-
...buildCrossLocaleSlugRedirects(type, translationIndex, aliasSourceSlugs, defaultLocale),
|
|
1552
|
-
...buildLegacyNoBlogPathRedirects(type, project.config, translationIndex, options.prefixedLocales, defaultLocale)
|
|
1543
|
+
...buildCrossLocaleSlugRedirects(project, typeId, redirectSourceSlugs)
|
|
1553
1544
|
];
|
|
1554
1545
|
}
|
|
1555
|
-
function buildAllContentRedirects(project,
|
|
1556
|
-
const
|
|
1557
|
-
const out = [
|
|
1546
|
+
function buildAllContentRedirects(project, _options = {}) {
|
|
1547
|
+
const redirectSourceKeys = buildRedirectSourceSlugSet(project);
|
|
1548
|
+
const out = [
|
|
1549
|
+
...buildJsonRedirects(project),
|
|
1550
|
+
...buildLegacyNoBlogPathRedirects(project, redirectSourceKeys)
|
|
1551
|
+
];
|
|
1558
1552
|
for (const type of project.config.types) {
|
|
1559
1553
|
if (!isRoutableType(type)) continue;
|
|
1560
|
-
out.push(...buildTypeRedirects(project, type.id
|
|
1554
|
+
out.push(...buildTypeRedirects(project, type.id));
|
|
1561
1555
|
}
|
|
1562
1556
|
return out;
|
|
1563
1557
|
}
|
|
1564
1558
|
function getRedirectSourceSlugs(project) {
|
|
1565
|
-
const
|
|
1566
|
-
const
|
|
1559
|
+
const loaded = loadAllTypeRedirects(project.config);
|
|
1560
|
+
const aliasSlugs = collectRedirectSourceSlugs(loaded);
|
|
1561
|
+
const outboundByType = collectOutboundRedirectSourcesByType(loaded);
|
|
1567
1562
|
for (const type of project.config.types) {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const doc = readEnDocument(project.config, type, enSlug);
|
|
1571
|
-
if (doc?.redirectTo) outbound.add(enSlug);
|
|
1563
|
+
if (!outboundByType.has(type.id)) {
|
|
1564
|
+
outboundByType.set(type.id, /* @__PURE__ */ new Set());
|
|
1572
1565
|
}
|
|
1573
|
-
outboundByType.set(type.id, outbound);
|
|
1574
1566
|
}
|
|
1575
1567
|
return {
|
|
1576
|
-
aliasSlugs
|
|
1568
|
+
aliasSlugs,
|
|
1577
1569
|
outboundByType
|
|
1578
1570
|
};
|
|
1579
1571
|
}
|
|
@@ -1581,13 +1573,12 @@ function getRedirectSourceSlugs(project) {
|
|
|
1581
1573
|
// src/sitemap/join-base-url.ts
|
|
1582
1574
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1583
1575
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1584
|
-
const
|
|
1585
|
-
return `${origin}${
|
|
1576
|
+
const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1577
|
+
return `${origin}${path13}`;
|
|
1586
1578
|
}
|
|
1587
1579
|
|
|
1588
1580
|
// src/sitemap/generate-sitemap.ts
|
|
1589
1581
|
function shouldIncludeEnDoc(enDoc, enSlug, redirectSources, contentTypeId, excludeNoindex) {
|
|
1590
|
-
if (enDoc.redirectTo) return false;
|
|
1591
1582
|
if (redirectSources.aliasSlugs.has(enSlug)) return false;
|
|
1592
1583
|
const outbound = redirectSources.outboundByType.get(contentTypeId);
|
|
1593
1584
|
if (outbound?.has(enSlug)) return false;
|
|
@@ -1727,6 +1718,168 @@ function loadConfigSync(options = {}) {
|
|
|
1727
1718
|
}
|
|
1728
1719
|
return resolveConfig(config, path3.dirname(configPath));
|
|
1729
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
|
+
}
|
|
1730
1883
|
function getAtPath2(obj, fieldPath) {
|
|
1731
1884
|
let current = obj;
|
|
1732
1885
|
for (const segment of fieldPath) {
|
|
@@ -1916,7 +2069,7 @@ function validateTranslationSlugSuffixes(config, db) {
|
|
|
1916
2069
|
}
|
|
1917
2070
|
|
|
1918
2071
|
// src/validate/validate-project.ts
|
|
1919
|
-
function
|
|
2072
|
+
function listEnSlugs3(rootDir, contentDir) {
|
|
1920
2073
|
const dir = path3.join(rootDir, contentDir);
|
|
1921
2074
|
if (!fs2.existsSync(dir)) return [];
|
|
1922
2075
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -1934,7 +2087,7 @@ function validateProject(config) {
|
|
|
1934
2087
|
}
|
|
1935
2088
|
const db = openStore(config, "readonly");
|
|
1936
2089
|
for (const type of config.types) {
|
|
1937
|
-
const enSlugs =
|
|
2090
|
+
const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
|
|
1938
2091
|
const englishSlugs = new Set(enSlugs);
|
|
1939
2092
|
if (!type.translate && isRoutableType(type)) {
|
|
1940
2093
|
issues.push({
|
|
@@ -2016,33 +2169,8 @@ function validateProject(config) {
|
|
|
2016
2169
|
}
|
|
2017
2170
|
}
|
|
2018
2171
|
db.close();
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
issues.push({
|
|
2022
|
-
level: issue.level,
|
|
2023
|
-
contentType: issue.contentTypeId,
|
|
2024
|
-
enSlug: issue.enSlug,
|
|
2025
|
-
field: issue.field,
|
|
2026
|
-
message: issue.message
|
|
2027
|
-
});
|
|
2028
|
-
}
|
|
2029
|
-
for (const issue of validateAliasRedirectChains(project)) {
|
|
2030
|
-
issues.push({
|
|
2031
|
-
level: issue.level,
|
|
2032
|
-
contentType: issue.contentTypeId,
|
|
2033
|
-
enSlug: issue.enSlug,
|
|
2034
|
-
field: issue.field,
|
|
2035
|
-
message: issue.message
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
for (const issue of validateAliasCoverage(project)) {
|
|
2039
|
-
issues.push({
|
|
2040
|
-
level: issue.level,
|
|
2041
|
-
contentType: issue.contentTypeId,
|
|
2042
|
-
enSlug: issue.enSlug,
|
|
2043
|
-
field: issue.field,
|
|
2044
|
-
message: issue.message
|
|
2045
|
-
});
|
|
2172
|
+
for (const issue of validateTypeRedirects(project)) {
|
|
2173
|
+
issues.push(issue);
|
|
2046
2174
|
}
|
|
2047
2175
|
for (const issue of validateRelations(project)) {
|
|
2048
2176
|
issues.push({
|
|
@@ -2079,7 +2207,7 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
2079
2207
|
}
|
|
2080
2208
|
|
|
2081
2209
|
// src/translate/worklist.ts
|
|
2082
|
-
function
|
|
2210
|
+
function listEnSlugs4(rootDir, contentDir) {
|
|
2083
2211
|
const dir = path3.join(rootDir, contentDir);
|
|
2084
2212
|
if (!fs2.existsSync(dir)) return [];
|
|
2085
2213
|
return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
|
|
@@ -2090,7 +2218,7 @@ function buildWorklist(config, options = {}) {
|
|
|
2090
2218
|
const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
2091
2219
|
for (const type of config.types) {
|
|
2092
2220
|
if (options.contentType && type.id !== options.contentType) continue;
|
|
2093
|
-
const enSlugs = options.enSlug ? [options.enSlug] :
|
|
2221
|
+
const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
|
|
2094
2222
|
for (const enSlug of enSlugs) {
|
|
2095
2223
|
const enDoc = readEnDocument(config, type, enSlug);
|
|
2096
2224
|
if (!enDoc) continue;
|
|
@@ -2740,6 +2868,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2740
2868
|
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2741
2869
|
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2742
2870
|
const out = [];
|
|
2871
|
+
const urlBuilder = createUrlBuilder(config);
|
|
2743
2872
|
for (const type of project.listTypes()) {
|
|
2744
2873
|
if (!isRoutableType(type.config)) continue;
|
|
2745
2874
|
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
@@ -2755,7 +2884,7 @@ function buildStaticRawExports(project, options = {}) {
|
|
|
2755
2884
|
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2756
2885
|
const doc = resolved.document;
|
|
2757
2886
|
const slugWithExt = `${doc.slug}${extension}`;
|
|
2758
|
-
const urlPath = resolvePath(pathTemplate, slugWithExt, locale
|
|
2887
|
+
const urlPath = urlBuilder.resolvePath(pathTemplate, slugWithExt, locale);
|
|
2759
2888
|
out.push({
|
|
2760
2889
|
relativePath: urlPath.slice(1),
|
|
2761
2890
|
urlPath,
|
|
@@ -2798,6 +2927,6 @@ function writeStaticRawExports(project, options = {}) {
|
|
|
2798
2927
|
return { exports, written: exports.length };
|
|
2799
2928
|
}
|
|
2800
2929
|
|
|
2801
|
-
export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, isRoutableType, 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 };
|
|
2802
2931
|
//# sourceMappingURL=index.js.map
|
|
2803
2932
|
//# sourceMappingURL=index.js.map
|