@moku-labs/web 1.4.0 → 1.5.0
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/dist/browser.d.mts +48 -3
- package/dist/browser.mjs +175 -43
- package/dist/index.cjs +198 -51
- package/dist/index.d.cts +48 -3
- package/dist/index.d.mts +48 -3
- package/dist/index.mjs +198 -51
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$3 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$5 {
|
|
561
587
|
export { Api$2 as Api, ClientRoute, CompileInput, CompiledRoute, Config$2 as Config, ExtractApi$1 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$2 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
|
@@ -1112,6 +1138,25 @@ type Api$1 = {
|
|
|
1112
1138
|
* ```
|
|
1113
1139
|
*/
|
|
1114
1140
|
render(route: ResolvedRoute, data: unknown): string;
|
|
1141
|
+
/**
|
|
1142
|
+
* Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
|
|
1143
|
+
* landing page that has no route identity (e.g. the apex-domain `/` redirect a
|
|
1144
|
+
* `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
|
|
1145
|
+
* apps that opt out keep a bare redirect. Pulled synchronously by `build`.
|
|
1146
|
+
*
|
|
1147
|
+
* @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
|
|
1148
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
1149
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
|
|
1150
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
1151
|
+
* @example
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
siteHead(input: {
|
|
1157
|
+
url: string;
|
|
1158
|
+
locale?: string;
|
|
1159
|
+
}): string;
|
|
1115
1160
|
};
|
|
1116
1161
|
declare namespace types_d_exports$6 {
|
|
1117
1162
|
export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
|
package/dist/browser.mjs
CHANGED
|
@@ -1331,22 +1331,101 @@ function extractGroups(groups) {
|
|
|
1331
1331
|
}
|
|
1332
1332
|
return params;
|
|
1333
1333
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1334
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1335
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1336
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1337
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1338
|
+
/**
|
|
1339
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1340
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1341
|
+
*
|
|
1342
|
+
* @param text - The static segment text.
|
|
1343
|
+
* @returns The regex-escaped segment.
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* escapeStaticSegment("about"); // "about"
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
function escapeStaticSegment(text) {
|
|
1350
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1351
|
+
}
|
|
1336
1352
|
/**
|
|
1337
|
-
*
|
|
1353
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1354
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1355
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1338
1356
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1357
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1358
|
+
* @returns The regex fragment for that segment.
|
|
1359
|
+
* @example
|
|
1360
|
+
* ```ts
|
|
1361
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1362
|
+
* ```
|
|
1342
1363
|
*/
|
|
1364
|
+
function segmentToRegex(segment) {
|
|
1365
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1366
|
+
if (named) {
|
|
1367
|
+
const [, name, constraint] = named;
|
|
1368
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1369
|
+
}
|
|
1370
|
+
return escapeStaticSegment(segment);
|
|
1371
|
+
}
|
|
1343
1372
|
/**
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1373
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1374
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1375
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1376
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1377
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1346
1378
|
*
|
|
1347
|
-
* @param
|
|
1348
|
-
* @
|
|
1349
|
-
* @
|
|
1379
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1380
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```ts
|
|
1383
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1384
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1385
|
+
* ```
|
|
1386
|
+
*/
|
|
1387
|
+
function createPathMatcher(source) {
|
|
1388
|
+
const segments = source.split("/");
|
|
1389
|
+
let pattern = "^";
|
|
1390
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1391
|
+
const segment = segments[index] ?? "";
|
|
1392
|
+
if (segment === "") {
|
|
1393
|
+
pattern += "/";
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const optional = segment.endsWith("?");
|
|
1397
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1398
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1399
|
+
}
|
|
1400
|
+
pattern += "$";
|
|
1401
|
+
const regexp = new RegExp(pattern);
|
|
1402
|
+
return {
|
|
1403
|
+
/**
|
|
1404
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1405
|
+
*
|
|
1406
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1407
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1408
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1409
|
+
* @example
|
|
1410
|
+
* ```ts
|
|
1411
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
exec(input) {
|
|
1415
|
+
const result = regexp.exec(input.pathname);
|
|
1416
|
+
if (!result) return null;
|
|
1417
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1418
|
+
} };
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/plugins/router/builders/match.ts
|
|
1422
|
+
/**
|
|
1423
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1424
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1425
|
+
*
|
|
1426
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1427
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1428
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1350
1429
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1351
1430
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1352
1431
|
* @example
|
|
@@ -1390,14 +1469,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1390
1469
|
}
|
|
1391
1470
|
//#endregion
|
|
1392
1471
|
//#region src/plugins/router/builders/compile.ts
|
|
1393
|
-
/**
|
|
1394
|
-
* @file router plugin — compilation + validation domain.
|
|
1395
|
-
*
|
|
1396
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1397
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1398
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1399
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1400
|
-
*/
|
|
1401
1472
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1402
1473
|
const ERROR_PREFIX$6 = "[web] router";
|
|
1403
1474
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -1569,8 +1640,8 @@ function buildFilePath(pattern, params) {
|
|
|
1569
1640
|
function buildMatchers(pattern, locales) {
|
|
1570
1641
|
const langRegex = `(${locales.join("|")})`;
|
|
1571
1642
|
return {
|
|
1572
|
-
withLang:
|
|
1573
|
-
bare:
|
|
1643
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
1644
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
1574
1645
|
};
|
|
1575
1646
|
}
|
|
1576
1647
|
/**
|
|
@@ -2443,6 +2514,42 @@ function composeHead(input) {
|
|
|
2443
2514
|
}), ...head.elements ?? []]);
|
|
2444
2515
|
}
|
|
2445
2516
|
/**
|
|
2517
|
+
* Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
|
|
2518
|
+
* page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
|
|
2519
|
+
* configured — so apps that opt out keep a bare redirect (no behavior change). The site
|
|
2520
|
+
* name + description become the card's title/description (`og:type=website`); `url` is the
|
|
2521
|
+
* canonical the page points at. A bare article/tag alias gets this site card as a fallback;
|
|
2522
|
+
* crawlers that honor the page's `rel=canonical` still resolve the per-route card.
|
|
2523
|
+
*
|
|
2524
|
+
* @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
|
|
2525
|
+
* @returns The ordered site-level head element set, or `[]` when no default image is set.
|
|
2526
|
+
* @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
|
|
2527
|
+
*/
|
|
2528
|
+
function composeSiteHead(input) {
|
|
2529
|
+
const { site, defaults, url, ogLocale } = input;
|
|
2530
|
+
const image = defaults.defaultOgImage;
|
|
2531
|
+
if (image === void 0) return [];
|
|
2532
|
+
const absoluteImage = resolveImage(image, site);
|
|
2533
|
+
const name = site.name();
|
|
2534
|
+
const description = site.description();
|
|
2535
|
+
const elements = [
|
|
2536
|
+
meta("description", description),
|
|
2537
|
+
og("og:type", "website"),
|
|
2538
|
+
og("og:site_name", name),
|
|
2539
|
+
og("og:title", name),
|
|
2540
|
+
og("og:description", description),
|
|
2541
|
+
og("og:url", url),
|
|
2542
|
+
og("og:image", absoluteImage),
|
|
2543
|
+
twitter("twitter:card", defaults.twitterCard),
|
|
2544
|
+
twitter("twitter:title", name),
|
|
2545
|
+
twitter("twitter:description", description),
|
|
2546
|
+
twitter("twitter:image", absoluteImage)
|
|
2547
|
+
];
|
|
2548
|
+
if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
|
|
2549
|
+
if (ogLocale) elements.push(og("og:locale", ogLocale));
|
|
2550
|
+
return elements;
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2446
2553
|
* HTML-escape a value for safe insertion into an attribute or text node. `&` is
|
|
2447
2554
|
* escaped first so already-escaped entities are not double-escaped.
|
|
2448
2555
|
*
|
|
@@ -2531,28 +2638,53 @@ function readDefaults(state) {
|
|
|
2531
2638
|
* ```
|
|
2532
2639
|
*/
|
|
2533
2640
|
function createApi$1(ctx) {
|
|
2534
|
-
return {
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
render(route, data) {
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2641
|
+
return {
|
|
2642
|
+
/**
|
|
2643
|
+
* Compose the final `<head>` inner HTML for a route (pulled by `build`).
|
|
2644
|
+
*
|
|
2645
|
+
* @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
|
|
2646
|
+
* @param data - The page data object passed to the route's loader/render.
|
|
2647
|
+
* @returns The serialized inner HTML of `<head>`.
|
|
2648
|
+
* @example
|
|
2649
|
+
* ```ts
|
|
2650
|
+
* api.render(route, { title: "Post" });
|
|
2651
|
+
* ```
|
|
2652
|
+
*/
|
|
2653
|
+
render(route, data) {
|
|
2654
|
+
return serializeHead(composeHead({
|
|
2655
|
+
route,
|
|
2656
|
+
data,
|
|
2657
|
+
defaults: readDefaults(ctx.state),
|
|
2658
|
+
site: ctx.require(sitePlugin),
|
|
2659
|
+
i18n: ctx.require(i18nPlugin),
|
|
2660
|
+
router: ctx.require(routerPlugin)
|
|
2661
|
+
}));
|
|
2662
|
+
},
|
|
2663
|
+
/**
|
|
2664
|
+
* Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
|
|
2665
|
+
* `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
|
|
2666
|
+
* `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
|
|
2667
|
+
*
|
|
2668
|
+
* @param input - The landing URL/path plus an optional locale (for `og:locale`).
|
|
2669
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
2670
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted.
|
|
2671
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
2672
|
+
* @example
|
|
2673
|
+
* ```ts
|
|
2674
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
2675
|
+
* ```
|
|
2676
|
+
*/
|
|
2677
|
+
siteHead(input) {
|
|
2678
|
+
const site = ctx.require(sitePlugin);
|
|
2679
|
+
const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
|
|
2680
|
+
return serializeHead(composeSiteHead({
|
|
2681
|
+
site,
|
|
2682
|
+
defaults: readDefaults(ctx.state),
|
|
2683
|
+
url: site.canonical(input.url),
|
|
2684
|
+
...ogLocale === void 0 ? {} : { ogLocale }
|
|
2685
|
+
}));
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2556
2688
|
}
|
|
2557
2689
|
//#endregion
|
|
2558
2690
|
//#region src/plugins/head/config.ts
|
package/dist/index.cjs
CHANGED
|
@@ -1889,22 +1889,101 @@ function extractGroups(groups) {
|
|
|
1889
1889
|
}
|
|
1890
1890
|
return params;
|
|
1891
1891
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1892
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1893
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1894
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1895
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1894
1896
|
/**
|
|
1895
|
-
*
|
|
1897
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1898
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1896
1899
|
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
+
* @param text - The static segment text.
|
|
1901
|
+
* @returns The regex-escaped segment.
|
|
1902
|
+
* @example
|
|
1903
|
+
* ```ts
|
|
1904
|
+
* escapeStaticSegment("about"); // "about"
|
|
1905
|
+
* ```
|
|
1900
1906
|
*/
|
|
1907
|
+
function escapeStaticSegment(text) {
|
|
1908
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1909
|
+
}
|
|
1901
1910
|
/**
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1911
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1912
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1913
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1904
1914
|
*
|
|
1905
|
-
* @param
|
|
1906
|
-
* @
|
|
1907
|
-
* @
|
|
1915
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1916
|
+
* @returns The regex fragment for that segment.
|
|
1917
|
+
* @example
|
|
1918
|
+
* ```ts
|
|
1919
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1920
|
+
* ```
|
|
1921
|
+
*/
|
|
1922
|
+
function segmentToRegex(segment) {
|
|
1923
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1924
|
+
if (named) {
|
|
1925
|
+
const [, name, constraint] = named;
|
|
1926
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1927
|
+
}
|
|
1928
|
+
return escapeStaticSegment(segment);
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1932
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1933
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1934
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1935
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1936
|
+
*
|
|
1937
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1938
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```ts
|
|
1941
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1942
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1943
|
+
* ```
|
|
1944
|
+
*/
|
|
1945
|
+
function createPathMatcher(source) {
|
|
1946
|
+
const segments = source.split("/");
|
|
1947
|
+
let pattern = "^";
|
|
1948
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1949
|
+
const segment = segments[index] ?? "";
|
|
1950
|
+
if (segment === "") {
|
|
1951
|
+
pattern += "/";
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const optional = segment.endsWith("?");
|
|
1955
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1956
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1957
|
+
}
|
|
1958
|
+
pattern += "$";
|
|
1959
|
+
const regexp = new RegExp(pattern);
|
|
1960
|
+
return {
|
|
1961
|
+
/**
|
|
1962
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1963
|
+
*
|
|
1964
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1965
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1966
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1970
|
+
* ```
|
|
1971
|
+
*/
|
|
1972
|
+
exec(input) {
|
|
1973
|
+
const result = regexp.exec(input.pathname);
|
|
1974
|
+
if (!result) return null;
|
|
1975
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1976
|
+
} };
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region src/plugins/router/builders/match.ts
|
|
1980
|
+
/**
|
|
1981
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1982
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1983
|
+
*
|
|
1984
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1985
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1986
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1908
1987
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1909
1988
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1910
1989
|
* @example
|
|
@@ -1948,14 +2027,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1948
2027
|
}
|
|
1949
2028
|
//#endregion
|
|
1950
2029
|
//#region src/plugins/router/builders/compile.ts
|
|
1951
|
-
/**
|
|
1952
|
-
* @file router plugin — compilation + validation domain.
|
|
1953
|
-
*
|
|
1954
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1955
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1956
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1957
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1958
|
-
*/
|
|
1959
2030
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1960
2031
|
const ERROR_PREFIX$11 = "[web] router";
|
|
1961
2032
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -2127,8 +2198,8 @@ function buildFilePath(pattern, params) {
|
|
|
2127
2198
|
function buildMatchers(pattern, locales) {
|
|
2128
2199
|
const langRegex = `(${locales.join("|")})`;
|
|
2129
2200
|
return {
|
|
2130
|
-
withLang:
|
|
2131
|
-
bare:
|
|
2201
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
2202
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
2132
2203
|
};
|
|
2133
2204
|
}
|
|
2134
2205
|
/**
|
|
@@ -3001,6 +3072,42 @@ function composeHead(input) {
|
|
|
3001
3072
|
}), ...head.elements ?? []]);
|
|
3002
3073
|
}
|
|
3003
3074
|
/**
|
|
3075
|
+
* Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
|
|
3076
|
+
* page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
|
|
3077
|
+
* configured — so apps that opt out keep a bare redirect (no behavior change). The site
|
|
3078
|
+
* name + description become the card's title/description (`og:type=website`); `url` is the
|
|
3079
|
+
* canonical the page points at. A bare article/tag alias gets this site card as a fallback;
|
|
3080
|
+
* crawlers that honor the page's `rel=canonical` still resolve the per-route card.
|
|
3081
|
+
*
|
|
3082
|
+
* @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
|
|
3083
|
+
* @returns The ordered site-level head element set, or `[]` when no default image is set.
|
|
3084
|
+
* @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
|
|
3085
|
+
*/
|
|
3086
|
+
function composeSiteHead(input) {
|
|
3087
|
+
const { site, defaults, url, ogLocale } = input;
|
|
3088
|
+
const image = defaults.defaultOgImage;
|
|
3089
|
+
if (image === void 0) return [];
|
|
3090
|
+
const absoluteImage = resolveImage(image, site);
|
|
3091
|
+
const name = site.name();
|
|
3092
|
+
const description = site.description();
|
|
3093
|
+
const elements = [
|
|
3094
|
+
meta("description", description),
|
|
3095
|
+
og("og:type", "website"),
|
|
3096
|
+
og("og:site_name", name),
|
|
3097
|
+
og("og:title", name),
|
|
3098
|
+
og("og:description", description),
|
|
3099
|
+
og("og:url", url),
|
|
3100
|
+
og("og:image", absoluteImage),
|
|
3101
|
+
twitter("twitter:card", defaults.twitterCard),
|
|
3102
|
+
twitter("twitter:title", name),
|
|
3103
|
+
twitter("twitter:description", description),
|
|
3104
|
+
twitter("twitter:image", absoluteImage)
|
|
3105
|
+
];
|
|
3106
|
+
if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
|
|
3107
|
+
if (ogLocale) elements.push(og("og:locale", ogLocale));
|
|
3108
|
+
return elements;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3004
3111
|
* HTML-escape a value for safe insertion into an attribute or text node. `&` is
|
|
3005
3112
|
* escaped first so already-escaped entities are not double-escaped.
|
|
3006
3113
|
*
|
|
@@ -3089,28 +3196,53 @@ function readDefaults(state) {
|
|
|
3089
3196
|
* ```
|
|
3090
3197
|
*/
|
|
3091
3198
|
function createApi$4(ctx) {
|
|
3092
|
-
return {
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
render(route, data) {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3199
|
+
return {
|
|
3200
|
+
/**
|
|
3201
|
+
* Compose the final `<head>` inner HTML for a route (pulled by `build`).
|
|
3202
|
+
*
|
|
3203
|
+
* @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
|
|
3204
|
+
* @param data - The page data object passed to the route's loader/render.
|
|
3205
|
+
* @returns The serialized inner HTML of `<head>`.
|
|
3206
|
+
* @example
|
|
3207
|
+
* ```ts
|
|
3208
|
+
* api.render(route, { title: "Post" });
|
|
3209
|
+
* ```
|
|
3210
|
+
*/
|
|
3211
|
+
render(route, data) {
|
|
3212
|
+
return serializeHead(composeHead({
|
|
3213
|
+
route,
|
|
3214
|
+
data,
|
|
3215
|
+
defaults: readDefaults(ctx.state),
|
|
3216
|
+
site: ctx.require(sitePlugin),
|
|
3217
|
+
i18n: ctx.require(i18nPlugin),
|
|
3218
|
+
router: ctx.require(routerPlugin)
|
|
3219
|
+
}));
|
|
3220
|
+
},
|
|
3221
|
+
/**
|
|
3222
|
+
* Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
|
|
3223
|
+
* `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
|
|
3224
|
+
* `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
|
|
3225
|
+
*
|
|
3226
|
+
* @param input - The landing URL/path plus an optional locale (for `og:locale`).
|
|
3227
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
3228
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted.
|
|
3229
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
3230
|
+
* @example
|
|
3231
|
+
* ```ts
|
|
3232
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
3233
|
+
* ```
|
|
3234
|
+
*/
|
|
3235
|
+
siteHead(input) {
|
|
3236
|
+
const site = ctx.require(sitePlugin);
|
|
3237
|
+
const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
|
|
3238
|
+
return serializeHead(composeSiteHead({
|
|
3239
|
+
site,
|
|
3240
|
+
defaults: readDefaults(ctx.state),
|
|
3241
|
+
url: site.canonical(input.url),
|
|
3242
|
+
...ogLocale === void 0 ? {} : { ogLocale }
|
|
3243
|
+
}));
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3114
3246
|
}
|
|
3115
3247
|
//#endregion
|
|
3116
3248
|
//#region src/plugins/head/config.ts
|
|
@@ -3683,19 +3815,26 @@ async function processImages(ctx, options = {}) {
|
|
|
3683
3815
|
* bare path that points at the default-locale-prefixed URL. Deliberately does NOT
|
|
3684
3816
|
* emit a Cloudflare `_redirects` catch-all (an SSG infinite-loop trap). Gated by
|
|
3685
3817
|
* `config.localeRedirects` (false/unset disables).
|
|
3818
|
+
*
|
|
3819
|
+
* When `head.defaultOgImage` is configured, each redirect page ALSO carries the
|
|
3820
|
+
* site-level Open Graph / Twitter block (`head.siteHead`) so a social crawler that
|
|
3821
|
+
* fetches the apex domain (or any locale-less alias) — and does not follow the
|
|
3822
|
+
* meta-refresh — still gets a branded preview card. No image configured ⇒ bare redirect.
|
|
3686
3823
|
*/
|
|
3687
3824
|
/**
|
|
3688
|
-
* Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target
|
|
3825
|
+
* Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`,
|
|
3826
|
+
* with an optional site-level OG/Twitter block injected at the end of `<head>`.
|
|
3689
3827
|
*
|
|
3690
3828
|
* @param target - The default-locale-prefixed URL to redirect to.
|
|
3829
|
+
* @param headExtra - Extra `<head>` inner HTML (the site-level OG block), or `""` for none.
|
|
3691
3830
|
* @returns The complete redirect HTML document string.
|
|
3692
3831
|
* @example
|
|
3693
3832
|
* ```ts
|
|
3694
|
-
* redirectHtml("/en/about/");
|
|
3833
|
+
* redirectHtml("/en/about/", '<meta property="og:image" content="…">');
|
|
3695
3834
|
* ```
|
|
3696
3835
|
*/
|
|
3697
|
-
function redirectHtml(target) {
|
|
3698
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"
|
|
3836
|
+
function redirectHtml(target, headExtra = "") {
|
|
3837
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}">${headExtra}</head><body><a href="${target}">Redirecting…</a></body></html>`;
|
|
3699
3838
|
}
|
|
3700
3839
|
/**
|
|
3701
3840
|
* Correlate manifest definitions to compiled `TypedRoute` entries by pattern (the
|
|
@@ -3787,16 +3926,17 @@ async function expandRedirects(definition, entry, defaultLocale, ctx) {
|
|
|
3787
3926
|
* @param job.file - The redirect page's output path, relative to `outDir`.
|
|
3788
3927
|
* @param job.target - The absolute default-locale URL the page redirects to.
|
|
3789
3928
|
* @param outDir - The build output directory the file is resolved against.
|
|
3929
|
+
* @param headExtra - The site-level OG block to inject into `<head>`, or `""` for none.
|
|
3790
3930
|
* @returns Resolves once the redirect HTML page is written.
|
|
3791
3931
|
* @example
|
|
3792
3932
|
* ```ts
|
|
3793
|
-
* await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist");
|
|
3933
|
+
* await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist", "");
|
|
3794
3934
|
* ```
|
|
3795
3935
|
*/
|
|
3796
|
-
async function writeRedirectFile(job, outDir) {
|
|
3936
|
+
async function writeRedirectFile(job, outDir, headExtra = "") {
|
|
3797
3937
|
const filePath = node_path$1.default.join(outDir, job.file);
|
|
3798
3938
|
await (0, node_fs_promises.mkdir)(node_path$1.default.dirname(filePath), { recursive: true });
|
|
3799
|
-
await (0, node_fs_promises.writeFile)(filePath, redirectHtml(job.target), "utf8");
|
|
3939
|
+
await (0, node_fs_promises.writeFile)(filePath, redirectHtml(job.target, headExtra), "utf8");
|
|
3800
3940
|
}
|
|
3801
3941
|
/**
|
|
3802
3942
|
* Emits one bare-path redirect HTML page per locale-prefixed route path, each a
|
|
@@ -3819,7 +3959,14 @@ async function generateLocaleRedirects(ctx) {
|
|
|
3819
3959
|
const router = ctx.require(routerPlugin);
|
|
3820
3960
|
const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
|
|
3821
3961
|
const jobs = (await Promise.all(pairRoutes(router).map(([definition, entry]) => expandRedirects(definition, entry, defaultLocale, ctx)))).flat();
|
|
3822
|
-
|
|
3962
|
+
const head = ctx.has("head") ? ctx.require(headPlugin) : void 0;
|
|
3963
|
+
await Promise.all(jobs.map((job) => {
|
|
3964
|
+
const headExtra = head ? head.siteHead({
|
|
3965
|
+
url: job.target,
|
|
3966
|
+
locale: defaultLocale
|
|
3967
|
+
}) : "";
|
|
3968
|
+
return writeRedirectFile(job, ctx.config.outDir, headExtra);
|
|
3969
|
+
}));
|
|
3823
3970
|
ctx.log.debug("build:locale-redirects", { written: jobs.length });
|
|
3824
3971
|
return { written: jobs.length };
|
|
3825
3972
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$6 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$8 {
|
|
561
587
|
export { Api$5 as Api, ClientRoute, CompileInput, CompiledRoute, Config$5 as Config, ExtractApi$2 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$5 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
|
@@ -1112,6 +1138,25 @@ type Api$4 = {
|
|
|
1112
1138
|
* ```
|
|
1113
1139
|
*/
|
|
1114
1140
|
render(route: ResolvedRoute, data: unknown): string;
|
|
1141
|
+
/**
|
|
1142
|
+
* Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
|
|
1143
|
+
* landing page that has no route identity (e.g. the apex-domain `/` redirect a
|
|
1144
|
+
* `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
|
|
1145
|
+
* apps that opt out keep a bare redirect. Pulled synchronously by `build`.
|
|
1146
|
+
*
|
|
1147
|
+
* @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
|
|
1148
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
1149
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
|
|
1150
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
1151
|
+
* @example
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
siteHead(input: {
|
|
1157
|
+
url: string;
|
|
1158
|
+
locale?: string;
|
|
1159
|
+
}): string;
|
|
1115
1160
|
};
|
|
1116
1161
|
declare namespace types_d_exports$9 {
|
|
1117
1162
|
export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
|
package/dist/index.d.mts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$6 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$8 {
|
|
561
587
|
export { Api$5 as Api, ClientRoute, CompileInput, CompiledRoute, Config$5 as Config, ExtractApi$2 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$5 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
|
@@ -1112,6 +1138,25 @@ type Api$4 = {
|
|
|
1112
1138
|
* ```
|
|
1113
1139
|
*/
|
|
1114
1140
|
render(route: ResolvedRoute, data: unknown): string;
|
|
1141
|
+
/**
|
|
1142
|
+
* Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
|
|
1143
|
+
* landing page that has no route identity (e.g. the apex-domain `/` redirect a
|
|
1144
|
+
* `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
|
|
1145
|
+
* apps that opt out keep a bare redirect. Pulled synchronously by `build`.
|
|
1146
|
+
*
|
|
1147
|
+
* @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
|
|
1148
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
1149
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
|
|
1150
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
1151
|
+
* @example
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
siteHead(input: {
|
|
1157
|
+
url: string;
|
|
1158
|
+
locale?: string;
|
|
1159
|
+
}): string;
|
|
1115
1160
|
};
|
|
1116
1161
|
declare namespace types_d_exports$9 {
|
|
1117
1162
|
export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
|
package/dist/index.mjs
CHANGED
|
@@ -1876,22 +1876,101 @@ function extractGroups(groups) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
return params;
|
|
1878
1878
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1879
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1880
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1881
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1882
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1881
1883
|
/**
|
|
1882
|
-
*
|
|
1884
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1885
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1883
1886
|
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
+
* @param text - The static segment text.
|
|
1888
|
+
* @returns The regex-escaped segment.
|
|
1889
|
+
* @example
|
|
1890
|
+
* ```ts
|
|
1891
|
+
* escapeStaticSegment("about"); // "about"
|
|
1892
|
+
* ```
|
|
1887
1893
|
*/
|
|
1894
|
+
function escapeStaticSegment(text) {
|
|
1895
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1896
|
+
}
|
|
1888
1897
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1898
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1899
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1900
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1891
1901
|
*
|
|
1892
|
-
* @param
|
|
1893
|
-
* @
|
|
1894
|
-
* @
|
|
1902
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1903
|
+
* @returns The regex fragment for that segment.
|
|
1904
|
+
* @example
|
|
1905
|
+
* ```ts
|
|
1906
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1907
|
+
* ```
|
|
1908
|
+
*/
|
|
1909
|
+
function segmentToRegex(segment) {
|
|
1910
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1911
|
+
if (named) {
|
|
1912
|
+
const [, name, constraint] = named;
|
|
1913
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1914
|
+
}
|
|
1915
|
+
return escapeStaticSegment(segment);
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1919
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1920
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1921
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1922
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1923
|
+
*
|
|
1924
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1925
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1926
|
+
* @example
|
|
1927
|
+
* ```ts
|
|
1928
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1929
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1930
|
+
* ```
|
|
1931
|
+
*/
|
|
1932
|
+
function createPathMatcher(source) {
|
|
1933
|
+
const segments = source.split("/");
|
|
1934
|
+
let pattern = "^";
|
|
1935
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1936
|
+
const segment = segments[index] ?? "";
|
|
1937
|
+
if (segment === "") {
|
|
1938
|
+
pattern += "/";
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const optional = segment.endsWith("?");
|
|
1942
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1943
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1944
|
+
}
|
|
1945
|
+
pattern += "$";
|
|
1946
|
+
const regexp = new RegExp(pattern);
|
|
1947
|
+
return {
|
|
1948
|
+
/**
|
|
1949
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1950
|
+
*
|
|
1951
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1952
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1953
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1954
|
+
* @example
|
|
1955
|
+
* ```ts
|
|
1956
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
exec(input) {
|
|
1960
|
+
const result = regexp.exec(input.pathname);
|
|
1961
|
+
if (!result) return null;
|
|
1962
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1963
|
+
} };
|
|
1964
|
+
}
|
|
1965
|
+
//#endregion
|
|
1966
|
+
//#region src/plugins/router/builders/match.ts
|
|
1967
|
+
/**
|
|
1968
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1969
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1970
|
+
*
|
|
1971
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1972
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1973
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1895
1974
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1896
1975
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1897
1976
|
* @example
|
|
@@ -1935,14 +2014,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1935
2014
|
}
|
|
1936
2015
|
//#endregion
|
|
1937
2016
|
//#region src/plugins/router/builders/compile.ts
|
|
1938
|
-
/**
|
|
1939
|
-
* @file router plugin — compilation + validation domain.
|
|
1940
|
-
*
|
|
1941
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1942
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1943
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1944
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1945
|
-
*/
|
|
1946
2017
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1947
2018
|
const ERROR_PREFIX$11 = "[web] router";
|
|
1948
2019
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -2114,8 +2185,8 @@ function buildFilePath(pattern, params) {
|
|
|
2114
2185
|
function buildMatchers(pattern, locales) {
|
|
2115
2186
|
const langRegex = `(${locales.join("|")})`;
|
|
2116
2187
|
return {
|
|
2117
|
-
withLang:
|
|
2118
|
-
bare:
|
|
2188
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
2189
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
2119
2190
|
};
|
|
2120
2191
|
}
|
|
2121
2192
|
/**
|
|
@@ -2988,6 +3059,42 @@ function composeHead(input) {
|
|
|
2988
3059
|
}), ...head.elements ?? []]);
|
|
2989
3060
|
}
|
|
2990
3061
|
/**
|
|
3062
|
+
* Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
|
|
3063
|
+
* page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
|
|
3064
|
+
* configured — so apps that opt out keep a bare redirect (no behavior change). The site
|
|
3065
|
+
* name + description become the card's title/description (`og:type=website`); `url` is the
|
|
3066
|
+
* canonical the page points at. A bare article/tag alias gets this site card as a fallback;
|
|
3067
|
+
* crawlers that honor the page's `rel=canonical` still resolve the per-route card.
|
|
3068
|
+
*
|
|
3069
|
+
* @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
|
|
3070
|
+
* @returns The ordered site-level head element set, or `[]` when no default image is set.
|
|
3071
|
+
* @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
|
|
3072
|
+
*/
|
|
3073
|
+
function composeSiteHead(input) {
|
|
3074
|
+
const { site, defaults, url, ogLocale } = input;
|
|
3075
|
+
const image = defaults.defaultOgImage;
|
|
3076
|
+
if (image === void 0) return [];
|
|
3077
|
+
const absoluteImage = resolveImage(image, site);
|
|
3078
|
+
const name = site.name();
|
|
3079
|
+
const description = site.description();
|
|
3080
|
+
const elements = [
|
|
3081
|
+
meta("description", description),
|
|
3082
|
+
og("og:type", "website"),
|
|
3083
|
+
og("og:site_name", name),
|
|
3084
|
+
og("og:title", name),
|
|
3085
|
+
og("og:description", description),
|
|
3086
|
+
og("og:url", url),
|
|
3087
|
+
og("og:image", absoluteImage),
|
|
3088
|
+
twitter("twitter:card", defaults.twitterCard),
|
|
3089
|
+
twitter("twitter:title", name),
|
|
3090
|
+
twitter("twitter:description", description),
|
|
3091
|
+
twitter("twitter:image", absoluteImage)
|
|
3092
|
+
];
|
|
3093
|
+
if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
|
|
3094
|
+
if (ogLocale) elements.push(og("og:locale", ogLocale));
|
|
3095
|
+
return elements;
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
2991
3098
|
* HTML-escape a value for safe insertion into an attribute or text node. `&` is
|
|
2992
3099
|
* escaped first so already-escaped entities are not double-escaped.
|
|
2993
3100
|
*
|
|
@@ -3076,28 +3183,53 @@ function readDefaults(state) {
|
|
|
3076
3183
|
* ```
|
|
3077
3184
|
*/
|
|
3078
3185
|
function createApi$4(ctx) {
|
|
3079
|
-
return {
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
render(route, data) {
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3186
|
+
return {
|
|
3187
|
+
/**
|
|
3188
|
+
* Compose the final `<head>` inner HTML for a route (pulled by `build`).
|
|
3189
|
+
*
|
|
3190
|
+
* @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
|
|
3191
|
+
* @param data - The page data object passed to the route's loader/render.
|
|
3192
|
+
* @returns The serialized inner HTML of `<head>`.
|
|
3193
|
+
* @example
|
|
3194
|
+
* ```ts
|
|
3195
|
+
* api.render(route, { title: "Post" });
|
|
3196
|
+
* ```
|
|
3197
|
+
*/
|
|
3198
|
+
render(route, data) {
|
|
3199
|
+
return serializeHead(composeHead({
|
|
3200
|
+
route,
|
|
3201
|
+
data,
|
|
3202
|
+
defaults: readDefaults(ctx.state),
|
|
3203
|
+
site: ctx.require(sitePlugin),
|
|
3204
|
+
i18n: ctx.require(i18nPlugin),
|
|
3205
|
+
router: ctx.require(routerPlugin)
|
|
3206
|
+
}));
|
|
3207
|
+
},
|
|
3208
|
+
/**
|
|
3209
|
+
* Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
|
|
3210
|
+
* `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
|
|
3211
|
+
* `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
|
|
3212
|
+
*
|
|
3213
|
+
* @param input - The landing URL/path plus an optional locale (for `og:locale`).
|
|
3214
|
+
* @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
|
|
3215
|
+
* @param input.locale - Optional locale whose `og:locale` is emitted.
|
|
3216
|
+
* @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
|
|
3217
|
+
* @example
|
|
3218
|
+
* ```ts
|
|
3219
|
+
* api.siteHead({ url: "/en/", locale: "en" });
|
|
3220
|
+
* ```
|
|
3221
|
+
*/
|
|
3222
|
+
siteHead(input) {
|
|
3223
|
+
const site = ctx.require(sitePlugin);
|
|
3224
|
+
const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
|
|
3225
|
+
return serializeHead(composeSiteHead({
|
|
3226
|
+
site,
|
|
3227
|
+
defaults: readDefaults(ctx.state),
|
|
3228
|
+
url: site.canonical(input.url),
|
|
3229
|
+
...ogLocale === void 0 ? {} : { ogLocale }
|
|
3230
|
+
}));
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3101
3233
|
}
|
|
3102
3234
|
//#endregion
|
|
3103
3235
|
//#region src/plugins/head/config.ts
|
|
@@ -3670,19 +3802,26 @@ async function processImages(ctx, options = {}) {
|
|
|
3670
3802
|
* bare path that points at the default-locale-prefixed URL. Deliberately does NOT
|
|
3671
3803
|
* emit a Cloudflare `_redirects` catch-all (an SSG infinite-loop trap). Gated by
|
|
3672
3804
|
* `config.localeRedirects` (false/unset disables).
|
|
3805
|
+
*
|
|
3806
|
+
* When `head.defaultOgImage` is configured, each redirect page ALSO carries the
|
|
3807
|
+
* site-level Open Graph / Twitter block (`head.siteHead`) so a social crawler that
|
|
3808
|
+
* fetches the apex domain (or any locale-less alias) — and does not follow the
|
|
3809
|
+
* meta-refresh — still gets a branded preview card. No image configured ⇒ bare redirect.
|
|
3673
3810
|
*/
|
|
3674
3811
|
/**
|
|
3675
|
-
* Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target
|
|
3812
|
+
* Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`,
|
|
3813
|
+
* with an optional site-level OG/Twitter block injected at the end of `<head>`.
|
|
3676
3814
|
*
|
|
3677
3815
|
* @param target - The default-locale-prefixed URL to redirect to.
|
|
3816
|
+
* @param headExtra - Extra `<head>` inner HTML (the site-level OG block), or `""` for none.
|
|
3678
3817
|
* @returns The complete redirect HTML document string.
|
|
3679
3818
|
* @example
|
|
3680
3819
|
* ```ts
|
|
3681
|
-
* redirectHtml("/en/about/");
|
|
3820
|
+
* redirectHtml("/en/about/", '<meta property="og:image" content="…">');
|
|
3682
3821
|
* ```
|
|
3683
3822
|
*/
|
|
3684
|
-
function redirectHtml(target) {
|
|
3685
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"
|
|
3823
|
+
function redirectHtml(target, headExtra = "") {
|
|
3824
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}">${headExtra}</head><body><a href="${target}">Redirecting…</a></body></html>`;
|
|
3686
3825
|
}
|
|
3687
3826
|
/**
|
|
3688
3827
|
* Correlate manifest definitions to compiled `TypedRoute` entries by pattern (the
|
|
@@ -3774,16 +3913,17 @@ async function expandRedirects(definition, entry, defaultLocale, ctx) {
|
|
|
3774
3913
|
* @param job.file - The redirect page's output path, relative to `outDir`.
|
|
3775
3914
|
* @param job.target - The absolute default-locale URL the page redirects to.
|
|
3776
3915
|
* @param outDir - The build output directory the file is resolved against.
|
|
3916
|
+
* @param headExtra - The site-level OG block to inject into `<head>`, or `""` for none.
|
|
3777
3917
|
* @returns Resolves once the redirect HTML page is written.
|
|
3778
3918
|
* @example
|
|
3779
3919
|
* ```ts
|
|
3780
|
-
* await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist");
|
|
3920
|
+
* await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist", "");
|
|
3781
3921
|
* ```
|
|
3782
3922
|
*/
|
|
3783
|
-
async function writeRedirectFile(job, outDir) {
|
|
3923
|
+
async function writeRedirectFile(job, outDir, headExtra = "") {
|
|
3784
3924
|
const filePath = path.join(outDir, job.file);
|
|
3785
3925
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
3786
|
-
await writeFile(filePath, redirectHtml(job.target), "utf8");
|
|
3926
|
+
await writeFile(filePath, redirectHtml(job.target, headExtra), "utf8");
|
|
3787
3927
|
}
|
|
3788
3928
|
/**
|
|
3789
3929
|
* Emits one bare-path redirect HTML page per locale-prefixed route path, each a
|
|
@@ -3806,7 +3946,14 @@ async function generateLocaleRedirects(ctx) {
|
|
|
3806
3946
|
const router = ctx.require(routerPlugin);
|
|
3807
3947
|
const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
|
|
3808
3948
|
const jobs = (await Promise.all(pairRoutes(router).map(([definition, entry]) => expandRedirects(definition, entry, defaultLocale, ctx)))).flat();
|
|
3809
|
-
|
|
3949
|
+
const head = ctx.has("head") ? ctx.require(headPlugin) : void 0;
|
|
3950
|
+
await Promise.all(jobs.map((job) => {
|
|
3951
|
+
const headExtra = head ? head.siteHead({
|
|
3952
|
+
url: job.target,
|
|
3953
|
+
locale: defaultLocale
|
|
3954
|
+
}) : "";
|
|
3955
|
+
return writeRedirectFile(job, ctx.config.outDir, headExtra);
|
|
3956
|
+
}));
|
|
3810
3957
|
ctx.log.debug("build:locale-redirects", { written: jobs.length });
|
|
3811
3958
|
return { written: jobs.length };
|
|
3812
3959
|
}
|
package/package.json
CHANGED