@moku-labs/web 1.5.2 → 1.6.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 +12 -3
- package/dist/browser.mjs +59 -18
- package/dist/index.cjs +130 -34
- package/dist/index.d.cts +21 -6
- package/dist/index.d.mts +21 -6
- package/dist/index.mjs +130 -34
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1550,15 +1550,24 @@ declare function defineRoutes<T extends RouteMap>(routes: T): T;
|
|
|
1550
1550
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
1551
1551
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
1552
1552
|
*
|
|
1553
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
1554
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
1555
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
1556
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
1557
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
1558
|
+
* prefixed.
|
|
1559
|
+
*
|
|
1553
1560
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
1561
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
1554
1562
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
1555
1563
|
* @example
|
|
1556
1564
|
* ```ts
|
|
1557
|
-
* const url = createUrls(routes);
|
|
1558
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
1565
|
+
* const url = createUrls(routes, "en");
|
|
1566
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
1567
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
1559
1568
|
* ```
|
|
1560
1569
|
*/
|
|
1561
|
-
declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
|
|
1570
|
+
declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: string): Urls<T>;
|
|
1562
1571
|
//#endregion
|
|
1563
1572
|
//#region src/plugins/router/index.d.ts
|
|
1564
1573
|
/**
|
package/dist/browser.mjs
CHANGED
|
@@ -1587,15 +1587,22 @@ function patternToUrlPattern(pattern, variant, langRegex) {
|
|
|
1587
1587
|
* param is absent has its segment skipped entirely (no empty segment), so a missing
|
|
1588
1588
|
* `{lang:?}` collapses cleanly instead of leaving a double slash.
|
|
1589
1589
|
*
|
|
1590
|
+
* The default locale is served at BARE paths: when `defaultLocale` is given, the
|
|
1591
|
+
* optional `{lang:?}` segment is also skipped for it (so `{ lang: defaultLocale }`
|
|
1592
|
+
* resolves to `/…` while every other locale keeps its `/{locale}/…` prefix).
|
|
1593
|
+
*
|
|
1590
1594
|
* @param pattern - The route pattern.
|
|
1591
1595
|
* @param params - Param values to substitute.
|
|
1596
|
+
* @param defaultLocale - The locale served bare (its `{lang:?}` segment is omitted).
|
|
1592
1597
|
* @returns The resolved relative URL string.
|
|
1593
1598
|
* @example
|
|
1594
1599
|
* ```ts
|
|
1595
1600
|
* buildUrl("/{slug}/", { slug: "hello" }); // "/hello/"
|
|
1601
|
+
* buildUrl("/{lang:?}/", { lang: "en" }, "en"); // "/"
|
|
1602
|
+
* buildUrl("/{lang:?}/", { lang: "ru" }, "en"); // "/ru/"
|
|
1596
1603
|
* ```
|
|
1597
1604
|
*/
|
|
1598
|
-
function buildUrl(pattern, params) {
|
|
1605
|
+
function buildUrl(pattern, params, defaultLocale) {
|
|
1599
1606
|
const out = [];
|
|
1600
1607
|
for (const segment of pattern.split("/")) {
|
|
1601
1608
|
const placeholder = parsePlaceholder(segment);
|
|
@@ -1605,6 +1612,7 @@ function buildUrl(pattern, params) {
|
|
|
1605
1612
|
}
|
|
1606
1613
|
const value = params[placeholder.name] ?? "";
|
|
1607
1614
|
if (placeholder.optional && value === "") continue;
|
|
1615
|
+
if (placeholder.name === "lang" && placeholder.optional && value === defaultLocale) continue;
|
|
1608
1616
|
out.push(value);
|
|
1609
1617
|
}
|
|
1610
1618
|
return out.join("/");
|
|
@@ -1614,14 +1622,15 @@ function buildUrl(pattern, params) {
|
|
|
1614
1622
|
*
|
|
1615
1623
|
* @param pattern - The route pattern.
|
|
1616
1624
|
* @param params - Param values to substitute.
|
|
1625
|
+
* @param defaultLocale - The locale served bare (forwarded to {@link buildUrl}).
|
|
1617
1626
|
* @returns The output file path, e.g. `hello/index.html`.
|
|
1618
1627
|
* @example
|
|
1619
1628
|
* ```ts
|
|
1620
1629
|
* buildFilePath("/{slug}/", { slug: "hello" });
|
|
1621
1630
|
* ```
|
|
1622
1631
|
*/
|
|
1623
|
-
function buildFilePath(pattern, params) {
|
|
1624
|
-
const cleanPath = buildUrl(pattern, params).replace(/^\//, "").replace(/\/$/, "");
|
|
1632
|
+
function buildFilePath(pattern, params, defaultLocale) {
|
|
1633
|
+
const cleanPath = buildUrl(pattern, params, defaultLocale).replace(/^\//, "").replace(/\/$/, "");
|
|
1625
1634
|
return cleanPath === "" ? "index.html" : `${cleanPath}/index.html`;
|
|
1626
1635
|
}
|
|
1627
1636
|
/**
|
|
@@ -1650,15 +1659,16 @@ function buildMatchers(pattern, locales) {
|
|
|
1650
1659
|
* pattern.
|
|
1651
1660
|
*
|
|
1652
1661
|
* @param pattern - The route pattern bound into the closure.
|
|
1662
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
1653
1663
|
* @returns A function mapping params to the resolved relative URL.
|
|
1654
1664
|
* @example
|
|
1655
1665
|
* ```ts
|
|
1656
|
-
* const toUrl = createToUrlFn("/{slug}/");
|
|
1666
|
+
* const toUrl = createToUrlFn("/{slug}/", "en");
|
|
1657
1667
|
* toUrl({ slug: "x" }); // "/x/"
|
|
1658
1668
|
* ```
|
|
1659
1669
|
*/
|
|
1660
|
-
function createToUrlFunction(pattern) {
|
|
1661
|
-
return (params) => buildUrl(pattern, params);
|
|
1670
|
+
function createToUrlFunction(pattern, defaultLocale) {
|
|
1671
|
+
return (params) => buildUrl(pattern, params, defaultLocale);
|
|
1662
1672
|
}
|
|
1663
1673
|
/**
|
|
1664
1674
|
* Build the `toFile` closure for a route — resolves the output file path from
|
|
@@ -1667,15 +1677,16 @@ function createToUrlFunction(pattern) {
|
|
|
1667
1677
|
*
|
|
1668
1678
|
* @param pattern - The route pattern bound into the closure.
|
|
1669
1679
|
* @param definition - The route definition carrying any `toFile` override.
|
|
1680
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
1670
1681
|
* @returns A function mapping params to the output file path.
|
|
1671
1682
|
* @example
|
|
1672
1683
|
* ```ts
|
|
1673
|
-
* const toFile = createToFileFn("/{slug}/", definition);
|
|
1684
|
+
* const toFile = createToFileFn("/{slug}/", definition, "en");
|
|
1674
1685
|
* toFile({ slug: "x" }); // "x/index.html"
|
|
1675
1686
|
* ```
|
|
1676
1687
|
*/
|
|
1677
|
-
function createToFileFunction(pattern, definition) {
|
|
1678
|
-
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
|
|
1688
|
+
function createToFileFunction(pattern, definition, defaultLocale) {
|
|
1689
|
+
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params, defaultLocale);
|
|
1679
1690
|
}
|
|
1680
1691
|
/**
|
|
1681
1692
|
* Compile a single route definition into its `CompiledRoute` entry.
|
|
@@ -1692,8 +1703,8 @@ function createToFileFunction(pattern, definition) {
|
|
|
1692
1703
|
function compileRoute(name, definition, input) {
|
|
1693
1704
|
const { pattern } = definition;
|
|
1694
1705
|
const matchers = buildMatchers(pattern, input.locales);
|
|
1695
|
-
const toUrl = createToUrlFunction(pattern);
|
|
1696
|
-
const toFile = createToFileFunction(pattern, definition);
|
|
1706
|
+
const toUrl = createToUrlFunction(pattern, input.defaultLocale);
|
|
1707
|
+
const toFile = createToFileFunction(pattern, definition, input.defaultLocale);
|
|
1697
1708
|
return {
|
|
1698
1709
|
name,
|
|
1699
1710
|
pattern,
|
|
@@ -2124,15 +2135,24 @@ function defineRoutes(routes) {
|
|
|
2124
2135
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
2125
2136
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
2126
2137
|
*
|
|
2138
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
2139
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
2140
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
2141
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
2142
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
2143
|
+
* prefixed.
|
|
2144
|
+
*
|
|
2127
2145
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
2146
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
2128
2147
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
2129
2148
|
* @example
|
|
2130
2149
|
* ```ts
|
|
2131
|
-
* const url = createUrls(routes);
|
|
2132
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
2150
|
+
* const url = createUrls(routes, "en");
|
|
2151
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
2152
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
2133
2153
|
* ```
|
|
2134
2154
|
*/
|
|
2135
|
-
function createUrls(routes) {
|
|
2155
|
+
function createUrls(routes, defaultLocale) {
|
|
2136
2156
|
return {
|
|
2137
2157
|
/**
|
|
2138
2158
|
* Build a route's URL path from its name and params.
|
|
@@ -2143,13 +2163,13 @@ function createUrls(routes) {
|
|
|
2143
2163
|
* @throws {Error} If `name` is not present in the route map.
|
|
2144
2164
|
* @example
|
|
2145
2165
|
* ```ts
|
|
2146
|
-
* url.toUrl("home", { lang: "
|
|
2166
|
+
* url.toUrl("home", { lang: "ru" }); // "/ru/"
|
|
2147
2167
|
* ```
|
|
2148
2168
|
*/
|
|
2149
2169
|
toUrl(name, params = {}) {
|
|
2150
2170
|
const definition = routes[name];
|
|
2151
2171
|
if (!definition) throw new Error(`[web] router: unknown route name "${String(name)}".\n Check the name matches a key in the route map passed to createUrls.`);
|
|
2152
|
-
return buildUrl(definition.pattern, params);
|
|
2172
|
+
return buildUrl(definition.pattern, params, defaultLocale);
|
|
2153
2173
|
} };
|
|
2154
2174
|
}
|
|
2155
2175
|
//#endregion
|
|
@@ -4424,10 +4444,31 @@ function contentApi(ctx) {
|
|
|
4424
4444
|
*/
|
|
4425
4445
|
async function resolveArticle(ctx, slug, locale) {
|
|
4426
4446
|
const native = await ctx.provider.readArticle(slug, locale, locale, false);
|
|
4427
|
-
if (native !== null) return native;
|
|
4447
|
+
if (native !== null) return bareDefaultLocaleUrl(ctx, native);
|
|
4428
4448
|
const fallbackLocale = ctx.defaultLocale();
|
|
4429
4449
|
if (fallbackLocale === locale) return null;
|
|
4430
|
-
|
|
4450
|
+
const fallback = await ctx.provider.readArticle(slug, fallbackLocale, locale, true);
|
|
4451
|
+
return fallback === null ? fallback : bareDefaultLocaleUrl(ctx, fallback);
|
|
4452
|
+
}
|
|
4453
|
+
/**
|
|
4454
|
+
* The default locale is served at BARE paths, so strip the leading `/{defaultLocale}`
|
|
4455
|
+
* from a default-locale article's `url` (`/en/hello/` → `/hello/`). Providers build
|
|
4456
|
+
* always-prefixed URLs (they have no i18n access); this is the single place the bare
|
|
4457
|
+
* default is applied, so article canonicals + feed GUIDs match the served URL.
|
|
4458
|
+
* Idempotent (a bare URL has no prefix to strip) and a no-op for non-default locales.
|
|
4459
|
+
*
|
|
4460
|
+
* @param ctx - Kernel-free domain context (i18n helpers).
|
|
4461
|
+
* @param article - The resolved article (mutated in place).
|
|
4462
|
+
* @returns The same article, with a bare `url` when it is the default locale.
|
|
4463
|
+
* @example
|
|
4464
|
+
* ```ts
|
|
4465
|
+
* bareDefaultLocaleUrl(ctx, { ...article, locale: "en", url: "/en/hello/" }).url; // "/hello/"
|
|
4466
|
+
* ```
|
|
4467
|
+
*/
|
|
4468
|
+
function bareDefaultLocaleUrl(ctx, article) {
|
|
4469
|
+
const prefix = `/${ctx.defaultLocale()}`;
|
|
4470
|
+
if (article.locale === ctx.defaultLocale() && article.url.startsWith(`${prefix}/`)) article.url = article.url.slice(prefix.length);
|
|
4471
|
+
return article;
|
|
4431
4472
|
}
|
|
4432
4473
|
/**
|
|
4433
4474
|
* Comparator sorting articles by frontmatter date descending (newest first),
|
package/dist/index.cjs
CHANGED
|
@@ -1232,10 +1232,31 @@ function contentApi(ctx) {
|
|
|
1232
1232
|
*/
|
|
1233
1233
|
async function resolveArticle(ctx, slug, locale) {
|
|
1234
1234
|
const native = await ctx.provider.readArticle(slug, locale, locale, false);
|
|
1235
|
-
if (native !== null) return native;
|
|
1235
|
+
if (native !== null) return bareDefaultLocaleUrl(ctx, native);
|
|
1236
1236
|
const fallbackLocale = ctx.defaultLocale();
|
|
1237
1237
|
if (fallbackLocale === locale) return null;
|
|
1238
|
-
|
|
1238
|
+
const fallback = await ctx.provider.readArticle(slug, fallbackLocale, locale, true);
|
|
1239
|
+
return fallback === null ? fallback : bareDefaultLocaleUrl(ctx, fallback);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* The default locale is served at BARE paths, so strip the leading `/{defaultLocale}`
|
|
1243
|
+
* from a default-locale article's `url` (`/en/hello/` → `/hello/`). Providers build
|
|
1244
|
+
* always-prefixed URLs (they have no i18n access); this is the single place the bare
|
|
1245
|
+
* default is applied, so article canonicals + feed GUIDs match the served URL.
|
|
1246
|
+
* Idempotent (a bare URL has no prefix to strip) and a no-op for non-default locales.
|
|
1247
|
+
*
|
|
1248
|
+
* @param ctx - Kernel-free domain context (i18n helpers).
|
|
1249
|
+
* @param article - The resolved article (mutated in place).
|
|
1250
|
+
* @returns The same article, with a bare `url` when it is the default locale.
|
|
1251
|
+
* @example
|
|
1252
|
+
* ```ts
|
|
1253
|
+
* bareDefaultLocaleUrl(ctx, { ...article, locale: "en", url: "/en/hello/" }).url; // "/hello/"
|
|
1254
|
+
* ```
|
|
1255
|
+
*/
|
|
1256
|
+
function bareDefaultLocaleUrl(ctx, article) {
|
|
1257
|
+
const prefix = `/${ctx.defaultLocale()}`;
|
|
1258
|
+
if (article.locale === ctx.defaultLocale() && article.url.startsWith(`${prefix}/`)) article.url = article.url.slice(prefix.length);
|
|
1259
|
+
return article;
|
|
1239
1260
|
}
|
|
1240
1261
|
/**
|
|
1241
1262
|
* Comparator sorting articles by frontmatter date descending (newest first),
|
|
@@ -2145,15 +2166,22 @@ function patternToUrlPattern(pattern, variant, langRegex) {
|
|
|
2145
2166
|
* param is absent has its segment skipped entirely (no empty segment), so a missing
|
|
2146
2167
|
* `{lang:?}` collapses cleanly instead of leaving a double slash.
|
|
2147
2168
|
*
|
|
2169
|
+
* The default locale is served at BARE paths: when `defaultLocale` is given, the
|
|
2170
|
+
* optional `{lang:?}` segment is also skipped for it (so `{ lang: defaultLocale }`
|
|
2171
|
+
* resolves to `/…` while every other locale keeps its `/{locale}/…` prefix).
|
|
2172
|
+
*
|
|
2148
2173
|
* @param pattern - The route pattern.
|
|
2149
2174
|
* @param params - Param values to substitute.
|
|
2175
|
+
* @param defaultLocale - The locale served bare (its `{lang:?}` segment is omitted).
|
|
2150
2176
|
* @returns The resolved relative URL string.
|
|
2151
2177
|
* @example
|
|
2152
2178
|
* ```ts
|
|
2153
2179
|
* buildUrl("/{slug}/", { slug: "hello" }); // "/hello/"
|
|
2180
|
+
* buildUrl("/{lang:?}/", { lang: "en" }, "en"); // "/"
|
|
2181
|
+
* buildUrl("/{lang:?}/", { lang: "ru" }, "en"); // "/ru/"
|
|
2154
2182
|
* ```
|
|
2155
2183
|
*/
|
|
2156
|
-
function buildUrl(pattern, params) {
|
|
2184
|
+
function buildUrl(pattern, params, defaultLocale) {
|
|
2157
2185
|
const out = [];
|
|
2158
2186
|
for (const segment of pattern.split("/")) {
|
|
2159
2187
|
const placeholder = parsePlaceholder(segment);
|
|
@@ -2163,6 +2191,7 @@ function buildUrl(pattern, params) {
|
|
|
2163
2191
|
}
|
|
2164
2192
|
const value = params[placeholder.name] ?? "";
|
|
2165
2193
|
if (placeholder.optional && value === "") continue;
|
|
2194
|
+
if (placeholder.name === "lang" && placeholder.optional && value === defaultLocale) continue;
|
|
2166
2195
|
out.push(value);
|
|
2167
2196
|
}
|
|
2168
2197
|
return out.join("/");
|
|
@@ -2172,14 +2201,15 @@ function buildUrl(pattern, params) {
|
|
|
2172
2201
|
*
|
|
2173
2202
|
* @param pattern - The route pattern.
|
|
2174
2203
|
* @param params - Param values to substitute.
|
|
2204
|
+
* @param defaultLocale - The locale served bare (forwarded to {@link buildUrl}).
|
|
2175
2205
|
* @returns The output file path, e.g. `hello/index.html`.
|
|
2176
2206
|
* @example
|
|
2177
2207
|
* ```ts
|
|
2178
2208
|
* buildFilePath("/{slug}/", { slug: "hello" });
|
|
2179
2209
|
* ```
|
|
2180
2210
|
*/
|
|
2181
|
-
function buildFilePath(pattern, params) {
|
|
2182
|
-
const cleanPath = buildUrl(pattern, params).replace(/^\//, "").replace(/\/$/, "");
|
|
2211
|
+
function buildFilePath(pattern, params, defaultLocale) {
|
|
2212
|
+
const cleanPath = buildUrl(pattern, params, defaultLocale).replace(/^\//, "").replace(/\/$/, "");
|
|
2183
2213
|
return cleanPath === "" ? "index.html" : `${cleanPath}/index.html`;
|
|
2184
2214
|
}
|
|
2185
2215
|
/**
|
|
@@ -2208,15 +2238,16 @@ function buildMatchers(pattern, locales) {
|
|
|
2208
2238
|
* pattern.
|
|
2209
2239
|
*
|
|
2210
2240
|
* @param pattern - The route pattern bound into the closure.
|
|
2241
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
2211
2242
|
* @returns A function mapping params to the resolved relative URL.
|
|
2212
2243
|
* @example
|
|
2213
2244
|
* ```ts
|
|
2214
|
-
* const toUrl = createToUrlFn("/{slug}/");
|
|
2245
|
+
* const toUrl = createToUrlFn("/{slug}/", "en");
|
|
2215
2246
|
* toUrl({ slug: "x" }); // "/x/"
|
|
2216
2247
|
* ```
|
|
2217
2248
|
*/
|
|
2218
|
-
function createToUrlFunction(pattern) {
|
|
2219
|
-
return (params) => buildUrl(pattern, params);
|
|
2249
|
+
function createToUrlFunction(pattern, defaultLocale) {
|
|
2250
|
+
return (params) => buildUrl(pattern, params, defaultLocale);
|
|
2220
2251
|
}
|
|
2221
2252
|
/**
|
|
2222
2253
|
* Build the `toFile` closure for a route — resolves the output file path from
|
|
@@ -2225,15 +2256,16 @@ function createToUrlFunction(pattern) {
|
|
|
2225
2256
|
*
|
|
2226
2257
|
* @param pattern - The route pattern bound into the closure.
|
|
2227
2258
|
* @param definition - The route definition carrying any `toFile` override.
|
|
2259
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
2228
2260
|
* @returns A function mapping params to the output file path.
|
|
2229
2261
|
* @example
|
|
2230
2262
|
* ```ts
|
|
2231
|
-
* const toFile = createToFileFn("/{slug}/", definition);
|
|
2263
|
+
* const toFile = createToFileFn("/{slug}/", definition, "en");
|
|
2232
2264
|
* toFile({ slug: "x" }); // "x/index.html"
|
|
2233
2265
|
* ```
|
|
2234
2266
|
*/
|
|
2235
|
-
function createToFileFunction(pattern, definition) {
|
|
2236
|
-
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
|
|
2267
|
+
function createToFileFunction(pattern, definition, defaultLocale) {
|
|
2268
|
+
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params, defaultLocale);
|
|
2237
2269
|
}
|
|
2238
2270
|
/**
|
|
2239
2271
|
* Compile a single route definition into its `CompiledRoute` entry.
|
|
@@ -2250,8 +2282,8 @@ function createToFileFunction(pattern, definition) {
|
|
|
2250
2282
|
function compileRoute(name, definition, input) {
|
|
2251
2283
|
const { pattern } = definition;
|
|
2252
2284
|
const matchers = buildMatchers(pattern, input.locales);
|
|
2253
|
-
const toUrl = createToUrlFunction(pattern);
|
|
2254
|
-
const toFile = createToFileFunction(pattern, definition);
|
|
2285
|
+
const toUrl = createToUrlFunction(pattern, input.defaultLocale);
|
|
2286
|
+
const toFile = createToFileFunction(pattern, definition, input.defaultLocale);
|
|
2255
2287
|
return {
|
|
2256
2288
|
name,
|
|
2257
2289
|
pattern,
|
|
@@ -2682,15 +2714,24 @@ function defineRoutes(routes) {
|
|
|
2682
2714
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
2683
2715
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
2684
2716
|
*
|
|
2717
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
2718
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
2719
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
2720
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
2721
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
2722
|
+
* prefixed.
|
|
2723
|
+
*
|
|
2685
2724
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
2725
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
2686
2726
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
2687
2727
|
* @example
|
|
2688
2728
|
* ```ts
|
|
2689
|
-
* const url = createUrls(routes);
|
|
2690
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
2729
|
+
* const url = createUrls(routes, "en");
|
|
2730
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
2731
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
2691
2732
|
* ```
|
|
2692
2733
|
*/
|
|
2693
|
-
function createUrls(routes) {
|
|
2734
|
+
function createUrls(routes, defaultLocale) {
|
|
2694
2735
|
return {
|
|
2695
2736
|
/**
|
|
2696
2737
|
* Build a route's URL path from its name and params.
|
|
@@ -2701,13 +2742,13 @@ function createUrls(routes) {
|
|
|
2701
2742
|
* @throws {Error} If `name` is not present in the route map.
|
|
2702
2743
|
* @example
|
|
2703
2744
|
* ```ts
|
|
2704
|
-
* url.toUrl("home", { lang: "
|
|
2745
|
+
* url.toUrl("home", { lang: "ru" }); // "/ru/"
|
|
2705
2746
|
* ```
|
|
2706
2747
|
*/
|
|
2707
2748
|
toUrl(name, params = {}) {
|
|
2708
2749
|
const definition = routes[name];
|
|
2709
2750
|
if (!definition) throw new Error(`[web] router: unknown route name "${String(name)}".\n Check the name matches a key in the route map passed to createUrls.`);
|
|
2710
|
-
return buildUrl(definition.pattern, params);
|
|
2751
|
+
return buildUrl(definition.pattern, params, defaultLocale);
|
|
2711
2752
|
} };
|
|
2712
2753
|
}
|
|
2713
2754
|
//#endregion
|
|
@@ -3992,10 +4033,31 @@ function wrap(body) {
|
|
|
3992
4033
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3993
4034
|
}
|
|
3994
4035
|
/**
|
|
4036
|
+
* Resolve the 404 page HTML from `config.notFound`. Precedence: `path` (a
|
|
4037
|
+
* complete page file, read verbatim) > `body` (a fragment, wrapped in the
|
|
4038
|
+
* minimal shell) > the built-in default.
|
|
4039
|
+
*
|
|
4040
|
+
* @param notFound - The `config.notFound` value (already known to be truthy).
|
|
4041
|
+
* @returns The complete HTML document to write.
|
|
4042
|
+
* @example
|
|
4043
|
+
* ```ts
|
|
4044
|
+
* const html = await resolveHtml({ path: "src/404.html" });
|
|
4045
|
+
* ```
|
|
4046
|
+
*/
|
|
4047
|
+
async function resolveHtml(notFound) {
|
|
4048
|
+
if (typeof notFound === "object" && notFound.path) try {
|
|
4049
|
+
return await (0, node_fs_promises.readFile)(notFound.path, "utf8");
|
|
4050
|
+
} catch (error) {
|
|
4051
|
+
throw new Error(`build:not-found — could not read notFound.path "${notFound.path}"`, { cause: error });
|
|
4052
|
+
}
|
|
4053
|
+
return wrap(typeof notFound === "object" && notFound.body ? notFound.body : DEFAULT_BODY);
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
3995
4056
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
3996
|
-
* default page;
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
4057
|
+
* default page; `{ body }` writes the supplied HTML body content inside the
|
|
4058
|
+
* minimal document shell; `{ path }` writes the referenced HTML page file
|
|
4059
|
+
* verbatim (the app owns the whole document). No-op (returns `null`) when
|
|
4060
|
+
* `notFound` is false/unset.
|
|
3999
4061
|
*
|
|
4000
4062
|
* @param ctx - Plugin context (provides `config`, `log`).
|
|
4001
4063
|
* @returns The written file path, or `null` when disabled.
|
|
@@ -4010,10 +4072,10 @@ async function generateNotFound(ctx) {
|
|
|
4010
4072
|
ctx.log.debug("build:not-found", { skipped: true });
|
|
4011
4073
|
return null;
|
|
4012
4074
|
}
|
|
4013
|
-
const
|
|
4075
|
+
const html = await resolveHtml(notFound);
|
|
4014
4076
|
await (0, node_fs_promises.mkdir)(outDir, { recursive: true });
|
|
4015
4077
|
const file = node_path$1.default.join(outDir, "404.html");
|
|
4016
|
-
await (0, node_fs_promises.writeFile)(file,
|
|
4078
|
+
await (0, node_fs_promises.writeFile)(file, html, "utf8");
|
|
4017
4079
|
ctx.log.debug("build:not-found", { path: file });
|
|
4018
4080
|
return { path: file };
|
|
4019
4081
|
}
|
|
@@ -5096,7 +5158,24 @@ function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
|
5096
5158
|
* ```
|
|
5097
5159
|
*/
|
|
5098
5160
|
async function writeDocument(outDir, entry, params, html) {
|
|
5099
|
-
|
|
5161
|
+
await writeDocumentAt(outDir, entry.toFile(params), html);
|
|
5162
|
+
}
|
|
5163
|
+
/**
|
|
5164
|
+
* Write an HTML document to an explicit relative path under `outDir`, creating parent
|
|
5165
|
+
* directories first. Backs both the canonical page path ({@link writeDocument}) and the
|
|
5166
|
+
* default-locale `/{defaultLocale}/…` alias copy ({@link renderInstance}).
|
|
5167
|
+
*
|
|
5168
|
+
* @param outDir - The build output directory.
|
|
5169
|
+
* @param relativeFile - The output file path relative to `outDir` (e.g. `en/index.html`).
|
|
5170
|
+
* @param html - The complete HTML document to write.
|
|
5171
|
+
* @returns A promise resolved once the file is written.
|
|
5172
|
+
* @example
|
|
5173
|
+
* ```ts
|
|
5174
|
+
* await writeDocumentAt("dist", "en/about/index.html", "<!DOCTYPE html>…");
|
|
5175
|
+
* ```
|
|
5176
|
+
*/
|
|
5177
|
+
async function writeDocumentAt(outDir, relativeFile, html) {
|
|
5178
|
+
const filePath = node_path.default.join(outDir, relativeFile);
|
|
5100
5179
|
await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), { recursive: true });
|
|
5101
5180
|
await (0, node_fs_promises.writeFile)(filePath, html, "utf8");
|
|
5102
5181
|
}
|
|
@@ -5106,14 +5185,19 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
5106
5185
|
* `<head>`/body → assemble the document (template fill or in-code shell) → write.
|
|
5107
5186
|
* Uses the configured shell `template` when supplied, otherwise the in-code shell.
|
|
5108
5187
|
*
|
|
5188
|
+
* The default locale is served at BARE paths, so each default-locale page on a
|
|
5189
|
+
* `{lang:?}` route is ALSO written to `/{defaultLocale}/…` — the SAME rendered HTML (its
|
|
5190
|
+
* canonical already points at the bare URL) — so an explicit `/{defaultLocale}/…` link
|
|
5191
|
+
* serves the page directly with no redirect. Both pages are returned so each gets a sidecar.
|
|
5192
|
+
*
|
|
5109
5193
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
5110
5194
|
* @param instance - The concrete page instance to render.
|
|
5111
|
-
* @param shell - Per-build wiring shared across instances (asset tags + template).
|
|
5195
|
+
* @param shell - Per-build wiring shared across instances (asset tags + template + default locale).
|
|
5112
5196
|
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
5113
|
-
* @returns The
|
|
5197
|
+
* @returns The rendered page(s): the canonical page, plus the `/{defaultLocale}/` alias when emitted.
|
|
5114
5198
|
* @example
|
|
5115
5199
|
* ```ts
|
|
5116
|
-
* await renderInstance(ctx, instance,
|
|
5200
|
+
* await renderInstance(ctx, instance, shell, false);
|
|
5117
5201
|
* ```
|
|
5118
5202
|
*/
|
|
5119
5203
|
async function renderInstance(ctx, instance, shell, reuse) {
|
|
@@ -5135,20 +5219,31 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
5135
5219
|
};
|
|
5136
5220
|
const html = shell.template === null ? renderDocument(parts) : fillTemplate(shell.template, parts);
|
|
5137
5221
|
await writeDocument(ctx.config.outDir, entry, params, html);
|
|
5138
|
-
|
|
5222
|
+
const clientNavigable = definition._handlers.render !== void 0;
|
|
5223
|
+
const pages = [{
|
|
5139
5224
|
url,
|
|
5140
5225
|
html,
|
|
5141
5226
|
data,
|
|
5142
|
-
clientNavigable
|
|
5143
|
-
};
|
|
5227
|
+
clientNavigable
|
|
5228
|
+
}];
|
|
5229
|
+
if (locale === shell.defaultLocale && entry.pattern.includes("{lang:?}")) {
|
|
5230
|
+
await writeDocumentAt(ctx.config.outDir, `${shell.defaultLocale}/${entry.toFile(params)}`, html);
|
|
5231
|
+
pages.push({
|
|
5232
|
+
url: `/${shell.defaultLocale}${url}`,
|
|
5233
|
+
html,
|
|
5234
|
+
data,
|
|
5235
|
+
clientNavigable
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
return pages;
|
|
5144
5239
|
}
|
|
5145
5240
|
/**
|
|
5146
5241
|
* Prepare the per-build {@link RenderShell} ONCE (O(1) per page): read the optional
|
|
5147
5242
|
* shell `template` from disk when configured + present, and precompute the injected
|
|
5148
5243
|
* asset tags. `template` is `null` when unset/missing (use the in-code shell).
|
|
5149
5244
|
*
|
|
5150
|
-
* @param ctx - Plugin context (provides `config`, `state`).
|
|
5151
|
-
* @returns The shared shell wiring (asset tags + template-or-null) for every page.
|
|
5245
|
+
* @param ctx - Plugin context (provides `config`, `state`, `require`).
|
|
5246
|
+
* @returns The shared shell wiring (asset tags + template-or-null + default locale) for every page.
|
|
5152
5247
|
* @example
|
|
5153
5248
|
* ```ts
|
|
5154
5249
|
* const shell = await prepareShell(ctx);
|
|
@@ -5159,7 +5254,8 @@ async function prepareShell(ctx) {
|
|
|
5159
5254
|
const template = typeof templatePath === "string" && (0, node_fs.existsSync)(templatePath) ? await (0, node_fs_promises.readFile)(templatePath, "utf8") : null;
|
|
5160
5255
|
return {
|
|
5161
5256
|
assets: buildAssetTags(ctx),
|
|
5162
|
-
template
|
|
5257
|
+
template,
|
|
5258
|
+
defaultLocale: ctx.require(i18nPlugin).defaultLocale()
|
|
5163
5259
|
};
|
|
5164
5260
|
}
|
|
5165
5261
|
/**
|
|
@@ -5293,7 +5389,7 @@ async function renderPages(ctx, options) {
|
|
|
5293
5389
|
const byPattern = makeEntryMap(router);
|
|
5294
5390
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5295
5391
|
const shell = await prepareShell(ctx);
|
|
5296
|
-
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse));
|
|
5392
|
+
const rendered = (await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse))).flat();
|
|
5297
5393
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
5298
5394
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5299
5395
|
return {
|
package/dist/index.d.cts
CHANGED
|
@@ -1663,12 +1663,18 @@ type Config$3 = {
|
|
|
1663
1663
|
injectAssets?: boolean; /** Directory copied verbatim into `outDir` (skipped silently if absent). Default `"public"`. */
|
|
1664
1664
|
publicDir?: string;
|
|
1665
1665
|
/**
|
|
1666
|
-
* Emit `outDir/404.html`.
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1666
|
+
* Emit `outDir/404.html`. One of:
|
|
1667
|
+
* - `true` — the built-in default page.
|
|
1668
|
+
* - `{ body }` — literal HTML body content, wrapped in a minimal document shell.
|
|
1669
|
+
* - `{ path }` — path to a complete HTML page file (resolved from the project
|
|
1670
|
+
* root), written out VERBATIM so the app owns the whole document (its own
|
|
1671
|
+
* `<head>`, asset links, and body).
|
|
1672
|
+
*
|
|
1673
|
+
* `path` takes precedence over `body` when both are set. Default `false`.
|
|
1669
1674
|
*/
|
|
1670
1675
|
notFound?: boolean | {
|
|
1671
1676
|
body?: string;
|
|
1677
|
+
path?: string;
|
|
1672
1678
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1673
1679
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1674
1680
|
clientEntry?: string;
|
|
@@ -3320,15 +3326,24 @@ declare function defineRoutes<T extends RouteMap>(routes: T): T;
|
|
|
3320
3326
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
3321
3327
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
3322
3328
|
*
|
|
3329
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
3330
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
3331
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
3332
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
3333
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
3334
|
+
* prefixed.
|
|
3335
|
+
*
|
|
3323
3336
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
3337
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
3324
3338
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
3325
3339
|
* @example
|
|
3326
3340
|
* ```ts
|
|
3327
|
-
* const url = createUrls(routes);
|
|
3328
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
3341
|
+
* const url = createUrls(routes, "en");
|
|
3342
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
3343
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
3329
3344
|
* ```
|
|
3330
3345
|
*/
|
|
3331
|
-
declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
|
|
3346
|
+
declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: string): Urls<T>;
|
|
3332
3347
|
//#endregion
|
|
3333
3348
|
//#region src/plugins/router/index.d.ts
|
|
3334
3349
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1663,12 +1663,18 @@ type Config$3 = {
|
|
|
1663
1663
|
injectAssets?: boolean; /** Directory copied verbatim into `outDir` (skipped silently if absent). Default `"public"`. */
|
|
1664
1664
|
publicDir?: string;
|
|
1665
1665
|
/**
|
|
1666
|
-
* Emit `outDir/404.html`.
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1666
|
+
* Emit `outDir/404.html`. One of:
|
|
1667
|
+
* - `true` — the built-in default page.
|
|
1668
|
+
* - `{ body }` — literal HTML body content, wrapped in a minimal document shell.
|
|
1669
|
+
* - `{ path }` — path to a complete HTML page file (resolved from the project
|
|
1670
|
+
* root), written out VERBATIM so the app owns the whole document (its own
|
|
1671
|
+
* `<head>`, asset links, and body).
|
|
1672
|
+
*
|
|
1673
|
+
* `path` takes precedence over `body` when both are set. Default `false`.
|
|
1669
1674
|
*/
|
|
1670
1675
|
notFound?: boolean | {
|
|
1671
1676
|
body?: string;
|
|
1677
|
+
path?: string;
|
|
1672
1678
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1673
1679
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1674
1680
|
clientEntry?: string;
|
|
@@ -3320,15 +3326,24 @@ declare function defineRoutes<T extends RouteMap>(routes: T): T;
|
|
|
3320
3326
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
3321
3327
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
3322
3328
|
*
|
|
3329
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
3330
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
3331
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
3332
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
3333
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
3334
|
+
* prefixed.
|
|
3335
|
+
*
|
|
3323
3336
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
3337
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
3324
3338
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
3325
3339
|
* @example
|
|
3326
3340
|
* ```ts
|
|
3327
|
-
* const url = createUrls(routes);
|
|
3328
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
3341
|
+
* const url = createUrls(routes, "en");
|
|
3342
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
3343
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
3329
3344
|
* ```
|
|
3330
3345
|
*/
|
|
3331
|
-
declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
|
|
3346
|
+
declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: string): Urls<T>;
|
|
3332
3347
|
//#endregion
|
|
3333
3348
|
//#region src/plugins/router/index.d.ts
|
|
3334
3349
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1219,10 +1219,31 @@ function contentApi(ctx) {
|
|
|
1219
1219
|
*/
|
|
1220
1220
|
async function resolveArticle(ctx, slug, locale) {
|
|
1221
1221
|
const native = await ctx.provider.readArticle(slug, locale, locale, false);
|
|
1222
|
-
if (native !== null) return native;
|
|
1222
|
+
if (native !== null) return bareDefaultLocaleUrl(ctx, native);
|
|
1223
1223
|
const fallbackLocale = ctx.defaultLocale();
|
|
1224
1224
|
if (fallbackLocale === locale) return null;
|
|
1225
|
-
|
|
1225
|
+
const fallback = await ctx.provider.readArticle(slug, fallbackLocale, locale, true);
|
|
1226
|
+
return fallback === null ? fallback : bareDefaultLocaleUrl(ctx, fallback);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* The default locale is served at BARE paths, so strip the leading `/{defaultLocale}`
|
|
1230
|
+
* from a default-locale article's `url` (`/en/hello/` → `/hello/`). Providers build
|
|
1231
|
+
* always-prefixed URLs (they have no i18n access); this is the single place the bare
|
|
1232
|
+
* default is applied, so article canonicals + feed GUIDs match the served URL.
|
|
1233
|
+
* Idempotent (a bare URL has no prefix to strip) and a no-op for non-default locales.
|
|
1234
|
+
*
|
|
1235
|
+
* @param ctx - Kernel-free domain context (i18n helpers).
|
|
1236
|
+
* @param article - The resolved article (mutated in place).
|
|
1237
|
+
* @returns The same article, with a bare `url` when it is the default locale.
|
|
1238
|
+
* @example
|
|
1239
|
+
* ```ts
|
|
1240
|
+
* bareDefaultLocaleUrl(ctx, { ...article, locale: "en", url: "/en/hello/" }).url; // "/hello/"
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
function bareDefaultLocaleUrl(ctx, article) {
|
|
1244
|
+
const prefix = `/${ctx.defaultLocale()}`;
|
|
1245
|
+
if (article.locale === ctx.defaultLocale() && article.url.startsWith(`${prefix}/`)) article.url = article.url.slice(prefix.length);
|
|
1246
|
+
return article;
|
|
1226
1247
|
}
|
|
1227
1248
|
/**
|
|
1228
1249
|
* Comparator sorting articles by frontmatter date descending (newest first),
|
|
@@ -2132,15 +2153,22 @@ function patternToUrlPattern(pattern, variant, langRegex) {
|
|
|
2132
2153
|
* param is absent has its segment skipped entirely (no empty segment), so a missing
|
|
2133
2154
|
* `{lang:?}` collapses cleanly instead of leaving a double slash.
|
|
2134
2155
|
*
|
|
2156
|
+
* The default locale is served at BARE paths: when `defaultLocale` is given, the
|
|
2157
|
+
* optional `{lang:?}` segment is also skipped for it (so `{ lang: defaultLocale }`
|
|
2158
|
+
* resolves to `/…` while every other locale keeps its `/{locale}/…` prefix).
|
|
2159
|
+
*
|
|
2135
2160
|
* @param pattern - The route pattern.
|
|
2136
2161
|
* @param params - Param values to substitute.
|
|
2162
|
+
* @param defaultLocale - The locale served bare (its `{lang:?}` segment is omitted).
|
|
2137
2163
|
* @returns The resolved relative URL string.
|
|
2138
2164
|
* @example
|
|
2139
2165
|
* ```ts
|
|
2140
2166
|
* buildUrl("/{slug}/", { slug: "hello" }); // "/hello/"
|
|
2167
|
+
* buildUrl("/{lang:?}/", { lang: "en" }, "en"); // "/"
|
|
2168
|
+
* buildUrl("/{lang:?}/", { lang: "ru" }, "en"); // "/ru/"
|
|
2141
2169
|
* ```
|
|
2142
2170
|
*/
|
|
2143
|
-
function buildUrl(pattern, params) {
|
|
2171
|
+
function buildUrl(pattern, params, defaultLocale) {
|
|
2144
2172
|
const out = [];
|
|
2145
2173
|
for (const segment of pattern.split("/")) {
|
|
2146
2174
|
const placeholder = parsePlaceholder(segment);
|
|
@@ -2150,6 +2178,7 @@ function buildUrl(pattern, params) {
|
|
|
2150
2178
|
}
|
|
2151
2179
|
const value = params[placeholder.name] ?? "";
|
|
2152
2180
|
if (placeholder.optional && value === "") continue;
|
|
2181
|
+
if (placeholder.name === "lang" && placeholder.optional && value === defaultLocale) continue;
|
|
2153
2182
|
out.push(value);
|
|
2154
2183
|
}
|
|
2155
2184
|
return out.join("/");
|
|
@@ -2159,14 +2188,15 @@ function buildUrl(pattern, params) {
|
|
|
2159
2188
|
*
|
|
2160
2189
|
* @param pattern - The route pattern.
|
|
2161
2190
|
* @param params - Param values to substitute.
|
|
2191
|
+
* @param defaultLocale - The locale served bare (forwarded to {@link buildUrl}).
|
|
2162
2192
|
* @returns The output file path, e.g. `hello/index.html`.
|
|
2163
2193
|
* @example
|
|
2164
2194
|
* ```ts
|
|
2165
2195
|
* buildFilePath("/{slug}/", { slug: "hello" });
|
|
2166
2196
|
* ```
|
|
2167
2197
|
*/
|
|
2168
|
-
function buildFilePath(pattern, params) {
|
|
2169
|
-
const cleanPath = buildUrl(pattern, params).replace(/^\//, "").replace(/\/$/, "");
|
|
2198
|
+
function buildFilePath(pattern, params, defaultLocale) {
|
|
2199
|
+
const cleanPath = buildUrl(pattern, params, defaultLocale).replace(/^\//, "").replace(/\/$/, "");
|
|
2170
2200
|
return cleanPath === "" ? "index.html" : `${cleanPath}/index.html`;
|
|
2171
2201
|
}
|
|
2172
2202
|
/**
|
|
@@ -2195,15 +2225,16 @@ function buildMatchers(pattern, locales) {
|
|
|
2195
2225
|
* pattern.
|
|
2196
2226
|
*
|
|
2197
2227
|
* @param pattern - The route pattern bound into the closure.
|
|
2228
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
2198
2229
|
* @returns A function mapping params to the resolved relative URL.
|
|
2199
2230
|
* @example
|
|
2200
2231
|
* ```ts
|
|
2201
|
-
* const toUrl = createToUrlFn("/{slug}/");
|
|
2232
|
+
* const toUrl = createToUrlFn("/{slug}/", "en");
|
|
2202
2233
|
* toUrl({ slug: "x" }); // "/x/"
|
|
2203
2234
|
* ```
|
|
2204
2235
|
*/
|
|
2205
|
-
function createToUrlFunction(pattern) {
|
|
2206
|
-
return (params) => buildUrl(pattern, params);
|
|
2236
|
+
function createToUrlFunction(pattern, defaultLocale) {
|
|
2237
|
+
return (params) => buildUrl(pattern, params, defaultLocale);
|
|
2207
2238
|
}
|
|
2208
2239
|
/**
|
|
2209
2240
|
* Build the `toFile` closure for a route — resolves the output file path from
|
|
@@ -2212,15 +2243,16 @@ function createToUrlFunction(pattern) {
|
|
|
2212
2243
|
*
|
|
2213
2244
|
* @param pattern - The route pattern bound into the closure.
|
|
2214
2245
|
* @param definition - The route definition carrying any `toFile` override.
|
|
2246
|
+
* @param defaultLocale - The locale served bare (bound into the closure).
|
|
2215
2247
|
* @returns A function mapping params to the output file path.
|
|
2216
2248
|
* @example
|
|
2217
2249
|
* ```ts
|
|
2218
|
-
* const toFile = createToFileFn("/{slug}/", definition);
|
|
2250
|
+
* const toFile = createToFileFn("/{slug}/", definition, "en");
|
|
2219
2251
|
* toFile({ slug: "x" }); // "x/index.html"
|
|
2220
2252
|
* ```
|
|
2221
2253
|
*/
|
|
2222
|
-
function createToFileFunction(pattern, definition) {
|
|
2223
|
-
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
|
|
2254
|
+
function createToFileFunction(pattern, definition, defaultLocale) {
|
|
2255
|
+
return (params) => definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params, defaultLocale);
|
|
2224
2256
|
}
|
|
2225
2257
|
/**
|
|
2226
2258
|
* Compile a single route definition into its `CompiledRoute` entry.
|
|
@@ -2237,8 +2269,8 @@ function createToFileFunction(pattern, definition) {
|
|
|
2237
2269
|
function compileRoute(name, definition, input) {
|
|
2238
2270
|
const { pattern } = definition;
|
|
2239
2271
|
const matchers = buildMatchers(pattern, input.locales);
|
|
2240
|
-
const toUrl = createToUrlFunction(pattern);
|
|
2241
|
-
const toFile = createToFileFunction(pattern, definition);
|
|
2272
|
+
const toUrl = createToUrlFunction(pattern, input.defaultLocale);
|
|
2273
|
+
const toFile = createToFileFunction(pattern, definition, input.defaultLocale);
|
|
2242
2274
|
return {
|
|
2243
2275
|
name,
|
|
2244
2276
|
pattern,
|
|
@@ -2669,15 +2701,24 @@ function defineRoutes(routes) {
|
|
|
2669
2701
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
2670
2702
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
2671
2703
|
*
|
|
2704
|
+
* Pass `defaultLocale` so the builder serves that locale at BARE paths, matching the
|
|
2705
|
+
* runtime `router.toUrl` (which reads the default locale from the i18n plugin):
|
|
2706
|
+
* `toUrl("home", { lang: defaultLocale })` then resolves to `/` instead of
|
|
2707
|
+
* `/{defaultLocale}/`. It is app-free, so the default locale cannot be inferred — the
|
|
2708
|
+
* consumer supplies it (e.g. `createUrls(routes, "en")`). Omit it to keep every locale
|
|
2709
|
+
* prefixed.
|
|
2710
|
+
*
|
|
2672
2711
|
* @param routes - The route map (typically the value returned by {@link defineRoutes}).
|
|
2712
|
+
* @param defaultLocale - The locale to serve bare (its `{lang:?}` prefix is omitted).
|
|
2673
2713
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
2674
2714
|
* @example
|
|
2675
2715
|
* ```ts
|
|
2676
|
-
* const url = createUrls(routes);
|
|
2677
|
-
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/
|
|
2716
|
+
* const url = createUrls(routes, "en");
|
|
2717
|
+
* url.toUrl("article", { lang: "en", slug: "hello" }); // "/hello/"
|
|
2718
|
+
* url.toUrl("article", { lang: "ru", slug: "hello" }); // "/ru/hello/"
|
|
2678
2719
|
* ```
|
|
2679
2720
|
*/
|
|
2680
|
-
function createUrls(routes) {
|
|
2721
|
+
function createUrls(routes, defaultLocale) {
|
|
2681
2722
|
return {
|
|
2682
2723
|
/**
|
|
2683
2724
|
* Build a route's URL path from its name and params.
|
|
@@ -2688,13 +2729,13 @@ function createUrls(routes) {
|
|
|
2688
2729
|
* @throws {Error} If `name` is not present in the route map.
|
|
2689
2730
|
* @example
|
|
2690
2731
|
* ```ts
|
|
2691
|
-
* url.toUrl("home", { lang: "
|
|
2732
|
+
* url.toUrl("home", { lang: "ru" }); // "/ru/"
|
|
2692
2733
|
* ```
|
|
2693
2734
|
*/
|
|
2694
2735
|
toUrl(name, params = {}) {
|
|
2695
2736
|
const definition = routes[name];
|
|
2696
2737
|
if (!definition) throw new Error(`[web] router: unknown route name "${String(name)}".\n Check the name matches a key in the route map passed to createUrls.`);
|
|
2697
|
-
return buildUrl(definition.pattern, params);
|
|
2738
|
+
return buildUrl(definition.pattern, params, defaultLocale);
|
|
2698
2739
|
} };
|
|
2699
2740
|
}
|
|
2700
2741
|
//#endregion
|
|
@@ -3979,10 +4020,31 @@ function wrap(body) {
|
|
|
3979
4020
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3980
4021
|
}
|
|
3981
4022
|
/**
|
|
4023
|
+
* Resolve the 404 page HTML from `config.notFound`. Precedence: `path` (a
|
|
4024
|
+
* complete page file, read verbatim) > `body` (a fragment, wrapped in the
|
|
4025
|
+
* minimal shell) > the built-in default.
|
|
4026
|
+
*
|
|
4027
|
+
* @param notFound - The `config.notFound` value (already known to be truthy).
|
|
4028
|
+
* @returns The complete HTML document to write.
|
|
4029
|
+
* @example
|
|
4030
|
+
* ```ts
|
|
4031
|
+
* const html = await resolveHtml({ path: "src/404.html" });
|
|
4032
|
+
* ```
|
|
4033
|
+
*/
|
|
4034
|
+
async function resolveHtml(notFound) {
|
|
4035
|
+
if (typeof notFound === "object" && notFound.path) try {
|
|
4036
|
+
return await readFile(notFound.path, "utf8");
|
|
4037
|
+
} catch (error) {
|
|
4038
|
+
throw new Error(`build:not-found — could not read notFound.path "${notFound.path}"`, { cause: error });
|
|
4039
|
+
}
|
|
4040
|
+
return wrap(typeof notFound === "object" && notFound.body ? notFound.body : DEFAULT_BODY);
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
3982
4043
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
3983
|
-
* default page;
|
|
3984
|
-
*
|
|
3985
|
-
*
|
|
4044
|
+
* default page; `{ body }` writes the supplied HTML body content inside the
|
|
4045
|
+
* minimal document shell; `{ path }` writes the referenced HTML page file
|
|
4046
|
+
* verbatim (the app owns the whole document). No-op (returns `null`) when
|
|
4047
|
+
* `notFound` is false/unset.
|
|
3986
4048
|
*
|
|
3987
4049
|
* @param ctx - Plugin context (provides `config`, `log`).
|
|
3988
4050
|
* @returns The written file path, or `null` when disabled.
|
|
@@ -3997,10 +4059,10 @@ async function generateNotFound(ctx) {
|
|
|
3997
4059
|
ctx.log.debug("build:not-found", { skipped: true });
|
|
3998
4060
|
return null;
|
|
3999
4061
|
}
|
|
4000
|
-
const
|
|
4062
|
+
const html = await resolveHtml(notFound);
|
|
4001
4063
|
await mkdir(outDir, { recursive: true });
|
|
4002
4064
|
const file = path.join(outDir, "404.html");
|
|
4003
|
-
await writeFile(file,
|
|
4065
|
+
await writeFile(file, html, "utf8");
|
|
4004
4066
|
ctx.log.debug("build:not-found", { path: file });
|
|
4005
4067
|
return { path: file };
|
|
4006
4068
|
}
|
|
@@ -5083,7 +5145,24 @@ function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
|
5083
5145
|
* ```
|
|
5084
5146
|
*/
|
|
5085
5147
|
async function writeDocument(outDir, entry, params, html) {
|
|
5086
|
-
|
|
5148
|
+
await writeDocumentAt(outDir, entry.toFile(params), html);
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Write an HTML document to an explicit relative path under `outDir`, creating parent
|
|
5152
|
+
* directories first. Backs both the canonical page path ({@link writeDocument}) and the
|
|
5153
|
+
* default-locale `/{defaultLocale}/…` alias copy ({@link renderInstance}).
|
|
5154
|
+
*
|
|
5155
|
+
* @param outDir - The build output directory.
|
|
5156
|
+
* @param relativeFile - The output file path relative to `outDir` (e.g. `en/index.html`).
|
|
5157
|
+
* @param html - The complete HTML document to write.
|
|
5158
|
+
* @returns A promise resolved once the file is written.
|
|
5159
|
+
* @example
|
|
5160
|
+
* ```ts
|
|
5161
|
+
* await writeDocumentAt("dist", "en/about/index.html", "<!DOCTYPE html>…");
|
|
5162
|
+
* ```
|
|
5163
|
+
*/
|
|
5164
|
+
async function writeDocumentAt(outDir, relativeFile, html) {
|
|
5165
|
+
const filePath = path.join(outDir, relativeFile);
|
|
5087
5166
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
5088
5167
|
await writeFile(filePath, html, "utf8");
|
|
5089
5168
|
}
|
|
@@ -5093,14 +5172,19 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
5093
5172
|
* `<head>`/body → assemble the document (template fill or in-code shell) → write.
|
|
5094
5173
|
* Uses the configured shell `template` when supplied, otherwise the in-code shell.
|
|
5095
5174
|
*
|
|
5175
|
+
* The default locale is served at BARE paths, so each default-locale page on a
|
|
5176
|
+
* `{lang:?}` route is ALSO written to `/{defaultLocale}/…` — the SAME rendered HTML (its
|
|
5177
|
+
* canonical already points at the bare URL) — so an explicit `/{defaultLocale}/…` link
|
|
5178
|
+
* serves the page directly with no redirect. Both pages are returned so each gets a sidecar.
|
|
5179
|
+
*
|
|
5096
5180
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
5097
5181
|
* @param instance - The concrete page instance to render.
|
|
5098
|
-
* @param shell - Per-build wiring shared across instances (asset tags + template).
|
|
5182
|
+
* @param shell - Per-build wiring shared across instances (asset tags + template + default locale).
|
|
5099
5183
|
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
5100
|
-
* @returns The
|
|
5184
|
+
* @returns The rendered page(s): the canonical page, plus the `/{defaultLocale}/` alias when emitted.
|
|
5101
5185
|
* @example
|
|
5102
5186
|
* ```ts
|
|
5103
|
-
* await renderInstance(ctx, instance,
|
|
5187
|
+
* await renderInstance(ctx, instance, shell, false);
|
|
5104
5188
|
* ```
|
|
5105
5189
|
*/
|
|
5106
5190
|
async function renderInstance(ctx, instance, shell, reuse) {
|
|
@@ -5122,20 +5206,31 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
5122
5206
|
};
|
|
5123
5207
|
const html = shell.template === null ? renderDocument(parts) : fillTemplate(shell.template, parts);
|
|
5124
5208
|
await writeDocument(ctx.config.outDir, entry, params, html);
|
|
5125
|
-
|
|
5209
|
+
const clientNavigable = definition._handlers.render !== void 0;
|
|
5210
|
+
const pages = [{
|
|
5126
5211
|
url,
|
|
5127
5212
|
html,
|
|
5128
5213
|
data,
|
|
5129
|
-
clientNavigable
|
|
5130
|
-
};
|
|
5214
|
+
clientNavigable
|
|
5215
|
+
}];
|
|
5216
|
+
if (locale === shell.defaultLocale && entry.pattern.includes("{lang:?}")) {
|
|
5217
|
+
await writeDocumentAt(ctx.config.outDir, `${shell.defaultLocale}/${entry.toFile(params)}`, html);
|
|
5218
|
+
pages.push({
|
|
5219
|
+
url: `/${shell.defaultLocale}${url}`,
|
|
5220
|
+
html,
|
|
5221
|
+
data,
|
|
5222
|
+
clientNavigable
|
|
5223
|
+
});
|
|
5224
|
+
}
|
|
5225
|
+
return pages;
|
|
5131
5226
|
}
|
|
5132
5227
|
/**
|
|
5133
5228
|
* Prepare the per-build {@link RenderShell} ONCE (O(1) per page): read the optional
|
|
5134
5229
|
* shell `template` from disk when configured + present, and precompute the injected
|
|
5135
5230
|
* asset tags. `template` is `null` when unset/missing (use the in-code shell).
|
|
5136
5231
|
*
|
|
5137
|
-
* @param ctx - Plugin context (provides `config`, `state`).
|
|
5138
|
-
* @returns The shared shell wiring (asset tags + template-or-null) for every page.
|
|
5232
|
+
* @param ctx - Plugin context (provides `config`, `state`, `require`).
|
|
5233
|
+
* @returns The shared shell wiring (asset tags + template-or-null + default locale) for every page.
|
|
5139
5234
|
* @example
|
|
5140
5235
|
* ```ts
|
|
5141
5236
|
* const shell = await prepareShell(ctx);
|
|
@@ -5146,7 +5241,8 @@ async function prepareShell(ctx) {
|
|
|
5146
5241
|
const template = typeof templatePath === "string" && existsSync(templatePath) ? await readFile(templatePath, "utf8") : null;
|
|
5147
5242
|
return {
|
|
5148
5243
|
assets: buildAssetTags(ctx),
|
|
5149
|
-
template
|
|
5244
|
+
template,
|
|
5245
|
+
defaultLocale: ctx.require(i18nPlugin).defaultLocale()
|
|
5150
5246
|
};
|
|
5151
5247
|
}
|
|
5152
5248
|
/**
|
|
@@ -5280,7 +5376,7 @@ async function renderPages(ctx, options) {
|
|
|
5280
5376
|
const byPattern = makeEntryMap(router);
|
|
5281
5377
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5282
5378
|
const shell = await prepareShell(ctx);
|
|
5283
|
-
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse));
|
|
5379
|
+
const rendered = (await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse))).flat();
|
|
5284
5380
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
5285
5381
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5286
5382
|
return {
|
package/package.json
CHANGED