@moku-labs/web 1.5.3 → 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 +104 -29
- package/dist/index.d.cts +12 -3
- package/dist/index.d.mts +12 -3
- package/dist/index.mjs +104 -29
- 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
|
|
@@ -5117,7 +5158,24 @@ function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
|
5117
5158
|
* ```
|
|
5118
5159
|
*/
|
|
5119
5160
|
async function writeDocument(outDir, entry, params, html) {
|
|
5120
|
-
|
|
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);
|
|
5121
5179
|
await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), { recursive: true });
|
|
5122
5180
|
await (0, node_fs_promises.writeFile)(filePath, html, "utf8");
|
|
5123
5181
|
}
|
|
@@ -5127,14 +5185,19 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
5127
5185
|
* `<head>`/body → assemble the document (template fill or in-code shell) → write.
|
|
5128
5186
|
* Uses the configured shell `template` when supplied, otherwise the in-code shell.
|
|
5129
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
|
+
*
|
|
5130
5193
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
5131
5194
|
* @param instance - The concrete page instance to render.
|
|
5132
|
-
* @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).
|
|
5133
5196
|
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
5134
|
-
* @returns The
|
|
5197
|
+
* @returns The rendered page(s): the canonical page, plus the `/{defaultLocale}/` alias when emitted.
|
|
5135
5198
|
* @example
|
|
5136
5199
|
* ```ts
|
|
5137
|
-
* await renderInstance(ctx, instance,
|
|
5200
|
+
* await renderInstance(ctx, instance, shell, false);
|
|
5138
5201
|
* ```
|
|
5139
5202
|
*/
|
|
5140
5203
|
async function renderInstance(ctx, instance, shell, reuse) {
|
|
@@ -5156,20 +5219,31 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
5156
5219
|
};
|
|
5157
5220
|
const html = shell.template === null ? renderDocument(parts) : fillTemplate(shell.template, parts);
|
|
5158
5221
|
await writeDocument(ctx.config.outDir, entry, params, html);
|
|
5159
|
-
|
|
5222
|
+
const clientNavigable = definition._handlers.render !== void 0;
|
|
5223
|
+
const pages = [{
|
|
5160
5224
|
url,
|
|
5161
5225
|
html,
|
|
5162
5226
|
data,
|
|
5163
|
-
clientNavigable
|
|
5164
|
-
};
|
|
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;
|
|
5165
5239
|
}
|
|
5166
5240
|
/**
|
|
5167
5241
|
* Prepare the per-build {@link RenderShell} ONCE (O(1) per page): read the optional
|
|
5168
5242
|
* shell `template` from disk when configured + present, and precompute the injected
|
|
5169
5243
|
* asset tags. `template` is `null` when unset/missing (use the in-code shell).
|
|
5170
5244
|
*
|
|
5171
|
-
* @param ctx - Plugin context (provides `config`, `state`).
|
|
5172
|
-
* @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.
|
|
5173
5247
|
* @example
|
|
5174
5248
|
* ```ts
|
|
5175
5249
|
* const shell = await prepareShell(ctx);
|
|
@@ -5180,7 +5254,8 @@ async function prepareShell(ctx) {
|
|
|
5180
5254
|
const template = typeof templatePath === "string" && (0, node_fs.existsSync)(templatePath) ? await (0, node_fs_promises.readFile)(templatePath, "utf8") : null;
|
|
5181
5255
|
return {
|
|
5182
5256
|
assets: buildAssetTags(ctx),
|
|
5183
|
-
template
|
|
5257
|
+
template,
|
|
5258
|
+
defaultLocale: ctx.require(i18nPlugin).defaultLocale()
|
|
5184
5259
|
};
|
|
5185
5260
|
}
|
|
5186
5261
|
/**
|
|
@@ -5314,7 +5389,7 @@ async function renderPages(ctx, options) {
|
|
|
5314
5389
|
const byPattern = makeEntryMap(router);
|
|
5315
5390
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5316
5391
|
const shell = await prepareShell(ctx);
|
|
5317
|
-
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();
|
|
5318
5393
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
5319
5394
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5320
5395
|
return {
|
package/dist/index.d.cts
CHANGED
|
@@ -3326,15 +3326,24 @@ declare function defineRoutes<T extends RouteMap>(routes: T): T;
|
|
|
3326
3326
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
3327
3327
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
3328
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
|
+
*
|
|
3329
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).
|
|
3330
3338
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
3331
3339
|
* @example
|
|
3332
3340
|
* ```ts
|
|
3333
|
-
* const url = createUrls(routes);
|
|
3334
|
-
* 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/"
|
|
3335
3344
|
* ```
|
|
3336
3345
|
*/
|
|
3337
|
-
declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
|
|
3346
|
+
declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: string): Urls<T>;
|
|
3338
3347
|
//#endregion
|
|
3339
3348
|
//#region src/plugins/router/index.d.ts
|
|
3340
3349
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -3326,15 +3326,24 @@ declare function defineRoutes<T extends RouteMap>(routes: T): T;
|
|
|
3326
3326
|
* directly: no `app.router` reference, no manual "bind", no module global, no
|
|
3327
3327
|
* "not bound" guard, and no createApp ↔ routes cycle.
|
|
3328
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
|
+
*
|
|
3329
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).
|
|
3330
3338
|
* @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
|
|
3331
3339
|
* @example
|
|
3332
3340
|
* ```ts
|
|
3333
|
-
* const url = createUrls(routes);
|
|
3334
|
-
* 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/"
|
|
3335
3344
|
* ```
|
|
3336
3345
|
*/
|
|
3337
|
-
declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
|
|
3346
|
+
declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: string): Urls<T>;
|
|
3338
3347
|
//#endregion
|
|
3339
3348
|
//#region src/plugins/router/index.d.ts
|
|
3340
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
|
|
@@ -5104,7 +5145,24 @@ function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
|
5104
5145
|
* ```
|
|
5105
5146
|
*/
|
|
5106
5147
|
async function writeDocument(outDir, entry, params, html) {
|
|
5107
|
-
|
|
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);
|
|
5108
5166
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
5109
5167
|
await writeFile(filePath, html, "utf8");
|
|
5110
5168
|
}
|
|
@@ -5114,14 +5172,19 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
5114
5172
|
* `<head>`/body → assemble the document (template fill or in-code shell) → write.
|
|
5115
5173
|
* Uses the configured shell `template` when supplied, otherwise the in-code shell.
|
|
5116
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
|
+
*
|
|
5117
5180
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
5118
5181
|
* @param instance - The concrete page instance to render.
|
|
5119
|
-
* @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).
|
|
5120
5183
|
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
5121
|
-
* @returns The
|
|
5184
|
+
* @returns The rendered page(s): the canonical page, plus the `/{defaultLocale}/` alias when emitted.
|
|
5122
5185
|
* @example
|
|
5123
5186
|
* ```ts
|
|
5124
|
-
* await renderInstance(ctx, instance,
|
|
5187
|
+
* await renderInstance(ctx, instance, shell, false);
|
|
5125
5188
|
* ```
|
|
5126
5189
|
*/
|
|
5127
5190
|
async function renderInstance(ctx, instance, shell, reuse) {
|
|
@@ -5143,20 +5206,31 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
5143
5206
|
};
|
|
5144
5207
|
const html = shell.template === null ? renderDocument(parts) : fillTemplate(shell.template, parts);
|
|
5145
5208
|
await writeDocument(ctx.config.outDir, entry, params, html);
|
|
5146
|
-
|
|
5209
|
+
const clientNavigable = definition._handlers.render !== void 0;
|
|
5210
|
+
const pages = [{
|
|
5147
5211
|
url,
|
|
5148
5212
|
html,
|
|
5149
5213
|
data,
|
|
5150
|
-
clientNavigable
|
|
5151
|
-
};
|
|
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;
|
|
5152
5226
|
}
|
|
5153
5227
|
/**
|
|
5154
5228
|
* Prepare the per-build {@link RenderShell} ONCE (O(1) per page): read the optional
|
|
5155
5229
|
* shell `template` from disk when configured + present, and precompute the injected
|
|
5156
5230
|
* asset tags. `template` is `null` when unset/missing (use the in-code shell).
|
|
5157
5231
|
*
|
|
5158
|
-
* @param ctx - Plugin context (provides `config`, `state`).
|
|
5159
|
-
* @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.
|
|
5160
5234
|
* @example
|
|
5161
5235
|
* ```ts
|
|
5162
5236
|
* const shell = await prepareShell(ctx);
|
|
@@ -5167,7 +5241,8 @@ async function prepareShell(ctx) {
|
|
|
5167
5241
|
const template = typeof templatePath === "string" && existsSync(templatePath) ? await readFile(templatePath, "utf8") : null;
|
|
5168
5242
|
return {
|
|
5169
5243
|
assets: buildAssetTags(ctx),
|
|
5170
|
-
template
|
|
5244
|
+
template,
|
|
5245
|
+
defaultLocale: ctx.require(i18nPlugin).defaultLocale()
|
|
5171
5246
|
};
|
|
5172
5247
|
}
|
|
5173
5248
|
/**
|
|
@@ -5301,7 +5376,7 @@ async function renderPages(ctx, options) {
|
|
|
5301
5376
|
const byPattern = makeEntryMap(router);
|
|
5302
5377
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5303
5378
|
const shell = await prepareShell(ctx);
|
|
5304
|
-
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();
|
|
5305
5380
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
5306
5381
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5307
5382
|
return {
|
package/package.json
CHANGED