@moku-labs/web 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1138,6 +1138,25 @@ type Api$1 = {
1138
1138
  * ```
1139
1139
  */
1140
1140
  render(route: ResolvedRoute, data: unknown): string;
1141
+ /**
1142
+ * Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
1143
+ * landing page that has no route identity (e.g. the apex-domain `/` redirect a
1144
+ * `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
1145
+ * apps that opt out keep a bare redirect. Pulled synchronously by `build`.
1146
+ *
1147
+ * @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
1148
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
1149
+ * @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
1150
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
1151
+ * @example
1152
+ * ```ts
1153
+ * api.siteHead({ url: "/en/", locale: "en" });
1154
+ * ```
1155
+ */
1156
+ siteHead(input: {
1157
+ url: string;
1158
+ locale?: string;
1159
+ }): string;
1141
1160
  };
1142
1161
  declare namespace types_d_exports$6 {
1143
1162
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/browser.mjs CHANGED
@@ -2514,6 +2514,42 @@ function composeHead(input) {
2514
2514
  }), ...head.elements ?? []]);
2515
2515
  }
2516
2516
  /**
2517
+ * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
2518
+ * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
2519
+ * configured — so apps that opt out keep a bare redirect (no behavior change). The site
2520
+ * name + description become the card's title/description (`og:type=website`); `url` is the
2521
+ * canonical the page points at. A bare article/tag alias gets this site card as a fallback;
2522
+ * crawlers that honor the page's `rel=canonical` still resolve the per-route card.
2523
+ *
2524
+ * @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
2525
+ * @returns The ordered site-level head element set, or `[]` when no default image is set.
2526
+ * @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
2527
+ */
2528
+ function composeSiteHead(input) {
2529
+ const { site, defaults, url, ogLocale } = input;
2530
+ const image = defaults.defaultOgImage;
2531
+ if (image === void 0) return [];
2532
+ const absoluteImage = resolveImage(image, site);
2533
+ const name = site.name();
2534
+ const description = site.description();
2535
+ const elements = [
2536
+ meta("description", description),
2537
+ og("og:type", "website"),
2538
+ og("og:site_name", name),
2539
+ og("og:title", name),
2540
+ og("og:description", description),
2541
+ og("og:url", url),
2542
+ og("og:image", absoluteImage),
2543
+ twitter("twitter:card", defaults.twitterCard),
2544
+ twitter("twitter:title", name),
2545
+ twitter("twitter:description", description),
2546
+ twitter("twitter:image", absoluteImage)
2547
+ ];
2548
+ if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
2549
+ if (ogLocale) elements.push(og("og:locale", ogLocale));
2550
+ return elements;
2551
+ }
2552
+ /**
2517
2553
  * HTML-escape a value for safe insertion into an attribute or text node. `&` is
2518
2554
  * escaped first so already-escaped entities are not double-escaped.
2519
2555
  *
@@ -2602,28 +2638,53 @@ function readDefaults(state) {
2602
2638
  * ```
2603
2639
  */
2604
2640
  function createApi$1(ctx) {
2605
- return {
2606
- /**
2607
- * Compose the final `<head>` inner HTML for a route (pulled by `build`).
2608
- *
2609
- * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
2610
- * @param data - The page data object passed to the route's loader/render.
2611
- * @returns The serialized inner HTML of `<head>`.
2612
- * @example
2613
- * ```ts
2614
- * api.render(route, { title: "Post" });
2615
- * ```
2616
- */
2617
- render(route, data) {
2618
- return serializeHead(composeHead({
2619
- route,
2620
- data,
2621
- defaults: readDefaults(ctx.state),
2622
- site: ctx.require(sitePlugin),
2623
- i18n: ctx.require(i18nPlugin),
2624
- router: ctx.require(routerPlugin)
2625
- }));
2626
- } };
2641
+ return {
2642
+ /**
2643
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
2644
+ *
2645
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
2646
+ * @param data - The page data object passed to the route's loader/render.
2647
+ * @returns The serialized inner HTML of `<head>`.
2648
+ * @example
2649
+ * ```ts
2650
+ * api.render(route, { title: "Post" });
2651
+ * ```
2652
+ */
2653
+ render(route, data) {
2654
+ return serializeHead(composeHead({
2655
+ route,
2656
+ data,
2657
+ defaults: readDefaults(ctx.state),
2658
+ site: ctx.require(sitePlugin),
2659
+ i18n: ctx.require(i18nPlugin),
2660
+ router: ctx.require(routerPlugin)
2661
+ }));
2662
+ },
2663
+ /**
2664
+ * Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
2665
+ * `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
2666
+ * `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
2667
+ *
2668
+ * @param input - The landing URL/path plus an optional locale (for `og:locale`).
2669
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
2670
+ * @param input.locale - Optional locale whose `og:locale` is emitted.
2671
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
2672
+ * @example
2673
+ * ```ts
2674
+ * api.siteHead({ url: "/en/", locale: "en" });
2675
+ * ```
2676
+ */
2677
+ siteHead(input) {
2678
+ const site = ctx.require(sitePlugin);
2679
+ const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
2680
+ return serializeHead(composeSiteHead({
2681
+ site,
2682
+ defaults: readDefaults(ctx.state),
2683
+ url: site.canonical(input.url),
2684
+ ...ogLocale === void 0 ? {} : { ogLocale }
2685
+ }));
2686
+ }
2687
+ };
2627
2688
  }
2628
2689
  //#endregion
2629
2690
  //#region src/plugins/head/config.ts
package/dist/index.cjs CHANGED
@@ -3072,6 +3072,42 @@ function composeHead(input) {
3072
3072
  }), ...head.elements ?? []]);
3073
3073
  }
3074
3074
  /**
3075
+ * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
3076
+ * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
3077
+ * configured — so apps that opt out keep a bare redirect (no behavior change). The site
3078
+ * name + description become the card's title/description (`og:type=website`); `url` is the
3079
+ * canonical the page points at. A bare article/tag alias gets this site card as a fallback;
3080
+ * crawlers that honor the page's `rel=canonical` still resolve the per-route card.
3081
+ *
3082
+ * @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
3083
+ * @returns The ordered site-level head element set, or `[]` when no default image is set.
3084
+ * @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
3085
+ */
3086
+ function composeSiteHead(input) {
3087
+ const { site, defaults, url, ogLocale } = input;
3088
+ const image = defaults.defaultOgImage;
3089
+ if (image === void 0) return [];
3090
+ const absoluteImage = resolveImage(image, site);
3091
+ const name = site.name();
3092
+ const description = site.description();
3093
+ const elements = [
3094
+ meta("description", description),
3095
+ og("og:type", "website"),
3096
+ og("og:site_name", name),
3097
+ og("og:title", name),
3098
+ og("og:description", description),
3099
+ og("og:url", url),
3100
+ og("og:image", absoluteImage),
3101
+ twitter("twitter:card", defaults.twitterCard),
3102
+ twitter("twitter:title", name),
3103
+ twitter("twitter:description", description),
3104
+ twitter("twitter:image", absoluteImage)
3105
+ ];
3106
+ if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
3107
+ if (ogLocale) elements.push(og("og:locale", ogLocale));
3108
+ return elements;
3109
+ }
3110
+ /**
3075
3111
  * HTML-escape a value for safe insertion into an attribute or text node. `&` is
3076
3112
  * escaped first so already-escaped entities are not double-escaped.
3077
3113
  *
@@ -3160,28 +3196,53 @@ function readDefaults(state) {
3160
3196
  * ```
3161
3197
  */
3162
3198
  function createApi$4(ctx) {
3163
- return {
3164
- /**
3165
- * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3166
- *
3167
- * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3168
- * @param data - The page data object passed to the route's loader/render.
3169
- * @returns The serialized inner HTML of `<head>`.
3170
- * @example
3171
- * ```ts
3172
- * api.render(route, { title: "Post" });
3173
- * ```
3174
- */
3175
- render(route, data) {
3176
- return serializeHead(composeHead({
3177
- route,
3178
- data,
3179
- defaults: readDefaults(ctx.state),
3180
- site: ctx.require(sitePlugin),
3181
- i18n: ctx.require(i18nPlugin),
3182
- router: ctx.require(routerPlugin)
3183
- }));
3184
- } };
3199
+ return {
3200
+ /**
3201
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3202
+ *
3203
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3204
+ * @param data - The page data object passed to the route's loader/render.
3205
+ * @returns The serialized inner HTML of `<head>`.
3206
+ * @example
3207
+ * ```ts
3208
+ * api.render(route, { title: "Post" });
3209
+ * ```
3210
+ */
3211
+ render(route, data) {
3212
+ return serializeHead(composeHead({
3213
+ route,
3214
+ data,
3215
+ defaults: readDefaults(ctx.state),
3216
+ site: ctx.require(sitePlugin),
3217
+ i18n: ctx.require(i18nPlugin),
3218
+ router: ctx.require(routerPlugin)
3219
+ }));
3220
+ },
3221
+ /**
3222
+ * Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
3223
+ * `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
3224
+ * `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
3225
+ *
3226
+ * @param input - The landing URL/path plus an optional locale (for `og:locale`).
3227
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
3228
+ * @param input.locale - Optional locale whose `og:locale` is emitted.
3229
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
3230
+ * @example
3231
+ * ```ts
3232
+ * api.siteHead({ url: "/en/", locale: "en" });
3233
+ * ```
3234
+ */
3235
+ siteHead(input) {
3236
+ const site = ctx.require(sitePlugin);
3237
+ const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
3238
+ return serializeHead(composeSiteHead({
3239
+ site,
3240
+ defaults: readDefaults(ctx.state),
3241
+ url: site.canonical(input.url),
3242
+ ...ogLocale === void 0 ? {} : { ogLocale }
3243
+ }));
3244
+ }
3245
+ };
3185
3246
  }
3186
3247
  //#endregion
3187
3248
  //#region src/plugins/head/config.ts
@@ -3754,19 +3815,26 @@ async function processImages(ctx, options = {}) {
3754
3815
  * bare path that points at the default-locale-prefixed URL. Deliberately does NOT
3755
3816
  * emit a Cloudflare `_redirects` catch-all (an SSG infinite-loop trap). Gated by
3756
3817
  * `config.localeRedirects` (false/unset disables).
3818
+ *
3819
+ * When `head.defaultOgImage` is configured, each redirect page ALSO carries the
3820
+ * site-level Open Graph / Twitter block (`head.siteHead`) so a social crawler that
3821
+ * fetches the apex domain (or any locale-less alias) — and does not follow the
3822
+ * meta-refresh — still gets a branded preview card. No image configured ⇒ bare redirect.
3757
3823
  */
3758
3824
  /**
3759
- * Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`.
3825
+ * Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`,
3826
+ * with an optional site-level OG/Twitter block injected at the end of `<head>`.
3760
3827
  *
3761
3828
  * @param target - The default-locale-prefixed URL to redirect to.
3829
+ * @param headExtra - Extra `<head>` inner HTML (the site-level OG block), or `""` for none.
3762
3830
  * @returns The complete redirect HTML document string.
3763
3831
  * @example
3764
3832
  * ```ts
3765
- * redirectHtml("/en/about/");
3833
+ * redirectHtml("/en/about/", '<meta property="og:image" content="…">');
3766
3834
  * ```
3767
3835
  */
3768
- function redirectHtml(target) {
3769
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"></head><body><a href="${target}">Redirecting…</a></body></html>`;
3836
+ function redirectHtml(target, headExtra = "") {
3837
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}">${headExtra}</head><body><a href="${target}">Redirecting…</a></body></html>`;
3770
3838
  }
3771
3839
  /**
3772
3840
  * Correlate manifest definitions to compiled `TypedRoute` entries by pattern (the
@@ -3858,16 +3926,17 @@ async function expandRedirects(definition, entry, defaultLocale, ctx) {
3858
3926
  * @param job.file - The redirect page's output path, relative to `outDir`.
3859
3927
  * @param job.target - The absolute default-locale URL the page redirects to.
3860
3928
  * @param outDir - The build output directory the file is resolved against.
3929
+ * @param headExtra - The site-level OG block to inject into `<head>`, or `""` for none.
3861
3930
  * @returns Resolves once the redirect HTML page is written.
3862
3931
  * @example
3863
3932
  * ```ts
3864
- * await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist");
3933
+ * await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist", "");
3865
3934
  * ```
3866
3935
  */
3867
- async function writeRedirectFile(job, outDir) {
3936
+ async function writeRedirectFile(job, outDir, headExtra = "") {
3868
3937
  const filePath = node_path$1.default.join(outDir, job.file);
3869
3938
  await (0, node_fs_promises.mkdir)(node_path$1.default.dirname(filePath), { recursive: true });
3870
- await (0, node_fs_promises.writeFile)(filePath, redirectHtml(job.target), "utf8");
3939
+ await (0, node_fs_promises.writeFile)(filePath, redirectHtml(job.target, headExtra), "utf8");
3871
3940
  }
3872
3941
  /**
3873
3942
  * Emits one bare-path redirect HTML page per locale-prefixed route path, each a
@@ -3890,7 +3959,14 @@ async function generateLocaleRedirects(ctx) {
3890
3959
  const router = ctx.require(routerPlugin);
3891
3960
  const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
3892
3961
  const jobs = (await Promise.all(pairRoutes(router).map(([definition, entry]) => expandRedirects(definition, entry, defaultLocale, ctx)))).flat();
3893
- await Promise.all(jobs.map((job) => writeRedirectFile(job, ctx.config.outDir)));
3962
+ const head = ctx.has("head") ? ctx.require(headPlugin) : void 0;
3963
+ await Promise.all(jobs.map((job) => {
3964
+ const headExtra = head ? head.siteHead({
3965
+ url: job.target,
3966
+ locale: defaultLocale
3967
+ }) : "";
3968
+ return writeRedirectFile(job, ctx.config.outDir, headExtra);
3969
+ }));
3894
3970
  ctx.log.debug("build:locale-redirects", { written: jobs.length });
3895
3971
  return { written: jobs.length };
3896
3972
  }
package/dist/index.d.cts CHANGED
@@ -1138,6 +1138,25 @@ type Api$4 = {
1138
1138
  * ```
1139
1139
  */
1140
1140
  render(route: ResolvedRoute, data: unknown): string;
1141
+ /**
1142
+ * Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
1143
+ * landing page that has no route identity (e.g. the apex-domain `/` redirect a
1144
+ * `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
1145
+ * apps that opt out keep a bare redirect. Pulled synchronously by `build`.
1146
+ *
1147
+ * @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
1148
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
1149
+ * @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
1150
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
1151
+ * @example
1152
+ * ```ts
1153
+ * api.siteHead({ url: "/en/", locale: "en" });
1154
+ * ```
1155
+ */
1156
+ siteHead(input: {
1157
+ url: string;
1158
+ locale?: string;
1159
+ }): string;
1141
1160
  };
1142
1161
  declare namespace types_d_exports$9 {
1143
1162
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/index.d.mts CHANGED
@@ -1138,6 +1138,25 @@ type Api$4 = {
1138
1138
  * ```
1139
1139
  */
1140
1140
  render(route: ResolvedRoute, data: unknown): string;
1141
+ /**
1142
+ * Compose the SITE-LEVEL `<head>` Open Graph / Twitter block for a bare-path redirect or
1143
+ * landing page that has no route identity (e.g. the apex-domain `/` redirect a
1144
+ * `localeRedirects` build emits). Returns `""` UNLESS `defaultOgImage` is configured, so
1145
+ * apps that opt out keep a bare redirect. Pulled synchronously by `build`.
1146
+ *
1147
+ * @param input - The landing URL (resolved to an absolute canonical) plus an optional locale.
1148
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
1149
+ * @param input.locale - Optional locale whose `og:locale` is emitted (e.g. the default locale).
1150
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
1151
+ * @example
1152
+ * ```ts
1153
+ * api.siteHead({ url: "/en/", locale: "en" });
1154
+ * ```
1155
+ */
1156
+ siteHead(input: {
1157
+ url: string;
1158
+ locale?: string;
1159
+ }): string;
1141
1160
  };
1142
1161
  declare namespace types_d_exports$9 {
1143
1162
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/index.mjs CHANGED
@@ -3059,6 +3059,42 @@ function composeHead(input) {
3059
3059
  }), ...head.elements ?? []]);
3060
3060
  }
3061
3061
  /**
3062
+ * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
3063
+ * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
3064
+ * configured — so apps that opt out keep a bare redirect (no behavior change). The site
3065
+ * name + description become the card's title/description (`og:type=website`); `url` is the
3066
+ * canonical the page points at. A bare article/tag alias gets this site card as a fallback;
3067
+ * crawlers that honor the page's `rel=canonical` still resolve the per-route card.
3068
+ *
3069
+ * @param input - The site slice, head defaults, landing URL, and optional `og:locale`.
3070
+ * @returns The ordered site-level head element set, or `[]` when no default image is set.
3071
+ * @example composeSiteHead({ site, defaults, url: "https://blog.dev/en/", ogLocale: "en_US" })
3072
+ */
3073
+ function composeSiteHead(input) {
3074
+ const { site, defaults, url, ogLocale } = input;
3075
+ const image = defaults.defaultOgImage;
3076
+ if (image === void 0) return [];
3077
+ const absoluteImage = resolveImage(image, site);
3078
+ const name = site.name();
3079
+ const description = site.description();
3080
+ const elements = [
3081
+ meta("description", description),
3082
+ og("og:type", "website"),
3083
+ og("og:site_name", name),
3084
+ og("og:title", name),
3085
+ og("og:description", description),
3086
+ og("og:url", url),
3087
+ og("og:image", absoluteImage),
3088
+ twitter("twitter:card", defaults.twitterCard),
3089
+ twitter("twitter:title", name),
3090
+ twitter("twitter:description", description),
3091
+ twitter("twitter:image", absoluteImage)
3092
+ ];
3093
+ if (defaults.twitterHandle) elements.push(twitter("twitter:site", defaults.twitterHandle));
3094
+ if (ogLocale) elements.push(og("og:locale", ogLocale));
3095
+ return elements;
3096
+ }
3097
+ /**
3062
3098
  * HTML-escape a value for safe insertion into an attribute or text node. `&` is
3063
3099
  * escaped first so already-escaped entities are not double-escaped.
3064
3100
  *
@@ -3147,28 +3183,53 @@ function readDefaults(state) {
3147
3183
  * ```
3148
3184
  */
3149
3185
  function createApi$4(ctx) {
3150
- return {
3151
- /**
3152
- * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3153
- *
3154
- * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3155
- * @param data - The page data object passed to the route's loader/render.
3156
- * @returns The serialized inner HTML of `<head>`.
3157
- * @example
3158
- * ```ts
3159
- * api.render(route, { title: "Post" });
3160
- * ```
3161
- */
3162
- render(route, data) {
3163
- return serializeHead(composeHead({
3164
- route,
3165
- data,
3166
- defaults: readDefaults(ctx.state),
3167
- site: ctx.require(sitePlugin),
3168
- i18n: ctx.require(i18nPlugin),
3169
- router: ctx.require(routerPlugin)
3170
- }));
3171
- } };
3186
+ return {
3187
+ /**
3188
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3189
+ *
3190
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3191
+ * @param data - The page data object passed to the route's loader/render.
3192
+ * @returns The serialized inner HTML of `<head>`.
3193
+ * @example
3194
+ * ```ts
3195
+ * api.render(route, { title: "Post" });
3196
+ * ```
3197
+ */
3198
+ render(route, data) {
3199
+ return serializeHead(composeHead({
3200
+ route,
3201
+ data,
3202
+ defaults: readDefaults(ctx.state),
3203
+ site: ctx.require(sitePlugin),
3204
+ i18n: ctx.require(i18nPlugin),
3205
+ router: ctx.require(routerPlugin)
3206
+ }));
3207
+ },
3208
+ /**
3209
+ * Compose the site-level OG/Twitter block for a bare-path redirect/landing page. Resolves
3210
+ * `site`/`i18n` via `ctx.require`, absolutizes `url` against the site base, and emits an
3211
+ * `og:locale` for `locale` when supplied. Returns `""` when no `defaultOgImage` is configured.
3212
+ *
3213
+ * @param input - The landing URL/path plus an optional locale (for `og:locale`).
3214
+ * @param input.url - The page's URL or path (absolutized via `site.canonical`) → `og:url`.
3215
+ * @param input.locale - Optional locale whose `og:locale` is emitted.
3216
+ * @returns The serialized inner HTML of the site-level head block, or `""` when disabled.
3217
+ * @example
3218
+ * ```ts
3219
+ * api.siteHead({ url: "/en/", locale: "en" });
3220
+ * ```
3221
+ */
3222
+ siteHead(input) {
3223
+ const site = ctx.require(sitePlugin);
3224
+ const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
3225
+ return serializeHead(composeSiteHead({
3226
+ site,
3227
+ defaults: readDefaults(ctx.state),
3228
+ url: site.canonical(input.url),
3229
+ ...ogLocale === void 0 ? {} : { ogLocale }
3230
+ }));
3231
+ }
3232
+ };
3172
3233
  }
3173
3234
  //#endregion
3174
3235
  //#region src/plugins/head/config.ts
@@ -3741,19 +3802,26 @@ async function processImages(ctx, options = {}) {
3741
3802
  * bare path that points at the default-locale-prefixed URL. Deliberately does NOT
3742
3803
  * emit a Cloudflare `_redirects` catch-all (an SSG infinite-loop trap). Gated by
3743
3804
  * `config.localeRedirects` (false/unset disables).
3805
+ *
3806
+ * When `head.defaultOgImage` is configured, each redirect page ALSO carries the
3807
+ * site-level Open Graph / Twitter block (`head.siteHead`) so a social crawler that
3808
+ * fetches the apex domain (or any locale-less alias) — and does not follow the
3809
+ * meta-refresh — still gets a branded preview card. No image configured ⇒ bare redirect.
3744
3810
  */
3745
3811
  /**
3746
- * Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`.
3812
+ * Render a redirect HTML page: a `0;url` refresh meta + a canonical link to `target`,
3813
+ * with an optional site-level OG/Twitter block injected at the end of `<head>`.
3747
3814
  *
3748
3815
  * @param target - The default-locale-prefixed URL to redirect to.
3816
+ * @param headExtra - Extra `<head>` inner HTML (the site-level OG block), or `""` for none.
3749
3817
  * @returns The complete redirect HTML document string.
3750
3818
  * @example
3751
3819
  * ```ts
3752
- * redirectHtml("/en/about/");
3820
+ * redirectHtml("/en/about/", '<meta property="og:image" content="…">');
3753
3821
  * ```
3754
3822
  */
3755
- function redirectHtml(target) {
3756
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"></head><body><a href="${target}">Redirecting…</a></body></html>`;
3823
+ function redirectHtml(target, headExtra = "") {
3824
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}">${headExtra}</head><body><a href="${target}">Redirecting…</a></body></html>`;
3757
3825
  }
3758
3826
  /**
3759
3827
  * Correlate manifest definitions to compiled `TypedRoute` entries by pattern (the
@@ -3845,16 +3913,17 @@ async function expandRedirects(definition, entry, defaultLocale, ctx) {
3845
3913
  * @param job.file - The redirect page's output path, relative to `outDir`.
3846
3914
  * @param job.target - The absolute default-locale URL the page redirects to.
3847
3915
  * @param outDir - The build output directory the file is resolved against.
3916
+ * @param headExtra - The site-level OG block to inject into `<head>`, or `""` for none.
3848
3917
  * @returns Resolves once the redirect HTML page is written.
3849
3918
  * @example
3850
3919
  * ```ts
3851
- * await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist");
3920
+ * await writeRedirectFile({ file: "about/index.html", target: "/en/about/" }, "dist", "");
3852
3921
  * ```
3853
3922
  */
3854
- async function writeRedirectFile(job, outDir) {
3923
+ async function writeRedirectFile(job, outDir, headExtra = "") {
3855
3924
  const filePath = path.join(outDir, job.file);
3856
3925
  await mkdir(path.dirname(filePath), { recursive: true });
3857
- await writeFile(filePath, redirectHtml(job.target), "utf8");
3926
+ await writeFile(filePath, redirectHtml(job.target, headExtra), "utf8");
3858
3927
  }
3859
3928
  /**
3860
3929
  * Emits one bare-path redirect HTML page per locale-prefixed route path, each a
@@ -3877,7 +3946,14 @@ async function generateLocaleRedirects(ctx) {
3877
3946
  const router = ctx.require(routerPlugin);
3878
3947
  const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
3879
3948
  const jobs = (await Promise.all(pairRoutes(router).map(([definition, entry]) => expandRedirects(definition, entry, defaultLocale, ctx)))).flat();
3880
- await Promise.all(jobs.map((job) => writeRedirectFile(job, ctx.config.outDir)));
3949
+ const head = ctx.has("head") ? ctx.require(headPlugin) : void 0;
3950
+ await Promise.all(jobs.map((job) => {
3951
+ const headExtra = head ? head.siteHead({
3952
+ url: job.target,
3953
+ locale: defaultLocale
3954
+ }) : "";
3955
+ return writeRedirectFile(job, ctx.config.outDir, headExtra);
3956
+ }));
3881
3957
  ctx.log.debug("build:locale-redirects", { written: jobs.length });
3882
3958
  return { written: jobs.length };
3883
3959
  }
package/package.json CHANGED
@@ -113,5 +113,5 @@
113
113
  "test:cli-e2e": "bun test src/plugins/cli/__tests__/e2e/",
114
114
  "test:coverage": "vitest run --project unit --project integration --coverage"
115
115
  },
116
- "version": "1.4.1"
116
+ "version": "1.5.0"
117
117
  }