@moku-labs/web 1.4.1 → 1.5.1
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 +19 -0
- package/dist/browser.mjs +83 -22
- package/dist/index.cjs +185 -32
- package/dist/index.d.cts +26 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.mjs +185 -32
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -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
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
render(route, data) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
render(route, data) {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
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}"
|
|
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
|
-
|
|
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
|
}
|
|
@@ -4098,6 +4174,45 @@ function defaultCard(input) {
|
|
|
4098
4174
|
} }, input.title);
|
|
4099
4175
|
}
|
|
4100
4176
|
/**
|
|
4177
|
+
* The built-in SITE-LEVEL default card — the site name over its description on a dark
|
|
4178
|
+
* background, centered. Rendered once (when `ogImage.defaultCard` is true) to `og-default.png`
|
|
4179
|
+
* and used as the `head.defaultOgImage` fallback. Reads `siteName` + `description` from the
|
|
4180
|
+
* {@link RichOgInput}; the per-article `render` hook is deliberately NOT applied here.
|
|
4181
|
+
*
|
|
4182
|
+
* @param input - The rich OG input (only `siteName` + `description` are used).
|
|
4183
|
+
* @returns The Preact `VNode` for the site default card.
|
|
4184
|
+
* @example
|
|
4185
|
+
* ```ts
|
|
4186
|
+
* defaultSiteCard({ ...input, siteName: "My Blog", description: "A dev blog" });
|
|
4187
|
+
* ```
|
|
4188
|
+
*/
|
|
4189
|
+
function defaultSiteCard(input) {
|
|
4190
|
+
const children = [(0, preact.h)("div", { style: {
|
|
4191
|
+
display: "flex",
|
|
4192
|
+
fontSize: 72,
|
|
4193
|
+
fontWeight: 700,
|
|
4194
|
+
color: "#ffffff"
|
|
4195
|
+
} }, input.siteName)];
|
|
4196
|
+
if (input.description) children.push((0, preact.h)("div", { style: {
|
|
4197
|
+
display: "flex",
|
|
4198
|
+
marginTop: 28,
|
|
4199
|
+
maxWidth: 900,
|
|
4200
|
+
fontSize: 32,
|
|
4201
|
+
color: "#a1a1aa",
|
|
4202
|
+
textAlign: "center"
|
|
4203
|
+
} }, input.description));
|
|
4204
|
+
return (0, preact.h)("div", { style: {
|
|
4205
|
+
display: "flex",
|
|
4206
|
+
flexDirection: "column",
|
|
4207
|
+
width: "100%",
|
|
4208
|
+
height: "100%",
|
|
4209
|
+
alignItems: "center",
|
|
4210
|
+
justifyContent: "center",
|
|
4211
|
+
padding: 80,
|
|
4212
|
+
background: "#0b0b0c"
|
|
4213
|
+
} }, ...children);
|
|
4214
|
+
}
|
|
4215
|
+
/**
|
|
4101
4216
|
* The default PNG renderer: a Preact `VNode` (custom `render` hook or the built-in
|
|
4102
4217
|
* card) is rendered to SVG by Satori, then rasterized to PNG by resvg. Both native
|
|
4103
4218
|
* deps are imported LAZILY (browser-safe goal); the VNode→Satori-input cast happens
|
|
@@ -4187,6 +4302,24 @@ function resolveSiteName(ctx) {
|
|
|
4187
4302
|
}
|
|
4188
4303
|
}
|
|
4189
4304
|
/**
|
|
4305
|
+
* Resolve the site description via `ctx.require(sitePlugin)`, falling back to `""` when the
|
|
4306
|
+
* site API is unavailable (e.g. unit mocks that omit it). Used by the site default card.
|
|
4307
|
+
*
|
|
4308
|
+
* @param ctx - Plugin context (provides `require`).
|
|
4309
|
+
* @returns The site description, or `""` when the site plugin is not wired.
|
|
4310
|
+
* @example
|
|
4311
|
+
* ```ts
|
|
4312
|
+
* resolveSiteDescription(ctx);
|
|
4313
|
+
* ```
|
|
4314
|
+
*/
|
|
4315
|
+
function resolveSiteDescription(ctx) {
|
|
4316
|
+
try {
|
|
4317
|
+
return ctx.require(sitePlugin).description();
|
|
4318
|
+
} catch {
|
|
4319
|
+
return "";
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
/**
|
|
4190
4323
|
* Render (or cache-skip) one article's OG image, mutating {@link RenderTally} in
|
|
4191
4324
|
* place. A matching cached hash bumps `skipped` and returns early; otherwise the
|
|
4192
4325
|
* PNG is rasterized to `<outDir>/og/<slug>.png`, the cache entry is updated, and
|
|
@@ -4257,6 +4390,10 @@ async function generateOgImages(ctx, options = {}) {
|
|
|
4257
4390
|
fonts,
|
|
4258
4391
|
...renderHook
|
|
4259
4392
|
});
|
|
4393
|
+
const renderSitePng = options.renderPng ?? makeDefaultRenderer({
|
|
4394
|
+
fonts,
|
|
4395
|
+
render: defaultSiteCard
|
|
4396
|
+
});
|
|
4260
4397
|
const siteName = resolveSiteName(ctx);
|
|
4261
4398
|
const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
|
|
4262
4399
|
const articles = selectArticles(readCachedContent(ctx), defaultLocale);
|
|
@@ -4281,15 +4418,31 @@ async function generateOgImages(ctx, options = {}) {
|
|
|
4281
4418
|
outDir
|
|
4282
4419
|
}, tally);
|
|
4283
4420
|
})));
|
|
4421
|
+
const defaultCard = config.defaultCard === true;
|
|
4422
|
+
if (defaultCard) {
|
|
4423
|
+
const png = await renderSitePng({
|
|
4424
|
+
title: siteName,
|
|
4425
|
+
description: resolveSiteDescription(ctx),
|
|
4426
|
+
date: "",
|
|
4427
|
+
tags: [],
|
|
4428
|
+
locale: defaultLocale,
|
|
4429
|
+
siteName,
|
|
4430
|
+
size
|
|
4431
|
+
});
|
|
4432
|
+
await (0, node_fs_promises.mkdir)(ctx.config.outDir, { recursive: true });
|
|
4433
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "og-default.png"), png);
|
|
4434
|
+
}
|
|
4284
4435
|
await persistDiskCache(ctx.config.outDir, cache);
|
|
4285
4436
|
ctx.log.debug("build:og-images", {
|
|
4286
4437
|
rendered: tally.rendered,
|
|
4287
|
-
skipped: tally.skipped
|
|
4438
|
+
skipped: tally.skipped,
|
|
4439
|
+
defaultCard
|
|
4288
4440
|
});
|
|
4289
4441
|
return {
|
|
4290
4442
|
rendered: tally.rendered,
|
|
4291
4443
|
skipped: tally.skipped,
|
|
4292
|
-
peakConcurrency: tally.peakConcurrency
|
|
4444
|
+
peakConcurrency: tally.peakConcurrency,
|
|
4445
|
+
defaultCard
|
|
4293
4446
|
};
|
|
4294
4447
|
}
|
|
4295
4448
|
/**
|
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 };
|
|
@@ -1613,6 +1632,13 @@ interface OgImageConfig {
|
|
|
1613
1632
|
render?(input: RichOgInput): import("preact").VNode;
|
|
1614
1633
|
/** Explicit named fonts loaded once per build (overrides the first-file scan). */
|
|
1615
1634
|
fonts?: OgFont[];
|
|
1635
|
+
/**
|
|
1636
|
+
* When `true`, also render a single SITE-LEVEL default card to `<outDir>/og-default.png`
|
|
1637
|
+
* — a generic site name + description on a dark background, using the same loaded fonts (the
|
|
1638
|
+
* per-article `render` hook is NOT applied). Point `head.defaultOgImage` at `"/og-default.png"`
|
|
1639
|
+
* to use it as the og:image fallback for non-article pages. Default `false`.
|
|
1640
|
+
*/
|
|
1641
|
+
defaultCard?: boolean;
|
|
1616
1642
|
}
|
|
1617
1643
|
/**
|
|
1618
1644
|
* Public configuration for the `build` plugin. Flags give opt-in granularity over
|
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 };
|
|
@@ -1613,6 +1632,13 @@ interface OgImageConfig {
|
|
|
1613
1632
|
render?(input: RichOgInput): import("preact").VNode;
|
|
1614
1633
|
/** Explicit named fonts loaded once per build (overrides the first-file scan). */
|
|
1615
1634
|
fonts?: OgFont[];
|
|
1635
|
+
/**
|
|
1636
|
+
* When `true`, also render a single SITE-LEVEL default card to `<outDir>/og-default.png`
|
|
1637
|
+
* — a generic site name + description on a dark background, using the same loaded fonts (the
|
|
1638
|
+
* per-article `render` hook is NOT applied). Point `head.defaultOgImage` at `"/og-default.png"`
|
|
1639
|
+
* to use it as the og:image fallback for non-article pages. Default `false`.
|
|
1640
|
+
*/
|
|
1641
|
+
defaultCard?: boolean;
|
|
1616
1642
|
}
|
|
1617
1643
|
/**
|
|
1618
1644
|
* Public configuration for the `build` plugin. Flags give opt-in granularity over
|
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
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
render(route, data) {
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
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}"
|
|
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
|
-
|
|
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
|
}
|
|
@@ -4085,6 +4161,45 @@ function defaultCard(input) {
|
|
|
4085
4161
|
} }, input.title);
|
|
4086
4162
|
}
|
|
4087
4163
|
/**
|
|
4164
|
+
* The built-in SITE-LEVEL default card — the site name over its description on a dark
|
|
4165
|
+
* background, centered. Rendered once (when `ogImage.defaultCard` is true) to `og-default.png`
|
|
4166
|
+
* and used as the `head.defaultOgImage` fallback. Reads `siteName` + `description` from the
|
|
4167
|
+
* {@link RichOgInput}; the per-article `render` hook is deliberately NOT applied here.
|
|
4168
|
+
*
|
|
4169
|
+
* @param input - The rich OG input (only `siteName` + `description` are used).
|
|
4170
|
+
* @returns The Preact `VNode` for the site default card.
|
|
4171
|
+
* @example
|
|
4172
|
+
* ```ts
|
|
4173
|
+
* defaultSiteCard({ ...input, siteName: "My Blog", description: "A dev blog" });
|
|
4174
|
+
* ```
|
|
4175
|
+
*/
|
|
4176
|
+
function defaultSiteCard(input) {
|
|
4177
|
+
const children = [h("div", { style: {
|
|
4178
|
+
display: "flex",
|
|
4179
|
+
fontSize: 72,
|
|
4180
|
+
fontWeight: 700,
|
|
4181
|
+
color: "#ffffff"
|
|
4182
|
+
} }, input.siteName)];
|
|
4183
|
+
if (input.description) children.push(h("div", { style: {
|
|
4184
|
+
display: "flex",
|
|
4185
|
+
marginTop: 28,
|
|
4186
|
+
maxWidth: 900,
|
|
4187
|
+
fontSize: 32,
|
|
4188
|
+
color: "#a1a1aa",
|
|
4189
|
+
textAlign: "center"
|
|
4190
|
+
} }, input.description));
|
|
4191
|
+
return h("div", { style: {
|
|
4192
|
+
display: "flex",
|
|
4193
|
+
flexDirection: "column",
|
|
4194
|
+
width: "100%",
|
|
4195
|
+
height: "100%",
|
|
4196
|
+
alignItems: "center",
|
|
4197
|
+
justifyContent: "center",
|
|
4198
|
+
padding: 80,
|
|
4199
|
+
background: "#0b0b0c"
|
|
4200
|
+
} }, ...children);
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4088
4203
|
* The default PNG renderer: a Preact `VNode` (custom `render` hook or the built-in
|
|
4089
4204
|
* card) is rendered to SVG by Satori, then rasterized to PNG by resvg. Both native
|
|
4090
4205
|
* deps are imported LAZILY (browser-safe goal); the VNode→Satori-input cast happens
|
|
@@ -4174,6 +4289,24 @@ function resolveSiteName(ctx) {
|
|
|
4174
4289
|
}
|
|
4175
4290
|
}
|
|
4176
4291
|
/**
|
|
4292
|
+
* Resolve the site description via `ctx.require(sitePlugin)`, falling back to `""` when the
|
|
4293
|
+
* site API is unavailable (e.g. unit mocks that omit it). Used by the site default card.
|
|
4294
|
+
*
|
|
4295
|
+
* @param ctx - Plugin context (provides `require`).
|
|
4296
|
+
* @returns The site description, or `""` when the site plugin is not wired.
|
|
4297
|
+
* @example
|
|
4298
|
+
* ```ts
|
|
4299
|
+
* resolveSiteDescription(ctx);
|
|
4300
|
+
* ```
|
|
4301
|
+
*/
|
|
4302
|
+
function resolveSiteDescription(ctx) {
|
|
4303
|
+
try {
|
|
4304
|
+
return ctx.require(sitePlugin).description();
|
|
4305
|
+
} catch {
|
|
4306
|
+
return "";
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
/**
|
|
4177
4310
|
* Render (or cache-skip) one article's OG image, mutating {@link RenderTally} in
|
|
4178
4311
|
* place. A matching cached hash bumps `skipped` and returns early; otherwise the
|
|
4179
4312
|
* PNG is rasterized to `<outDir>/og/<slug>.png`, the cache entry is updated, and
|
|
@@ -4244,6 +4377,10 @@ async function generateOgImages(ctx, options = {}) {
|
|
|
4244
4377
|
fonts,
|
|
4245
4378
|
...renderHook
|
|
4246
4379
|
});
|
|
4380
|
+
const renderSitePng = options.renderPng ?? makeDefaultRenderer({
|
|
4381
|
+
fonts,
|
|
4382
|
+
render: defaultSiteCard
|
|
4383
|
+
});
|
|
4247
4384
|
const siteName = resolveSiteName(ctx);
|
|
4248
4385
|
const defaultLocale = ctx.require(i18nPlugin).defaultLocale();
|
|
4249
4386
|
const articles = selectArticles(readCachedContent(ctx), defaultLocale);
|
|
@@ -4268,15 +4405,31 @@ async function generateOgImages(ctx, options = {}) {
|
|
|
4268
4405
|
outDir
|
|
4269
4406
|
}, tally);
|
|
4270
4407
|
})));
|
|
4408
|
+
const defaultCard = config.defaultCard === true;
|
|
4409
|
+
if (defaultCard) {
|
|
4410
|
+
const png = await renderSitePng({
|
|
4411
|
+
title: siteName,
|
|
4412
|
+
description: resolveSiteDescription(ctx),
|
|
4413
|
+
date: "",
|
|
4414
|
+
tags: [],
|
|
4415
|
+
locale: defaultLocale,
|
|
4416
|
+
siteName,
|
|
4417
|
+
size
|
|
4418
|
+
});
|
|
4419
|
+
await mkdir(ctx.config.outDir, { recursive: true });
|
|
4420
|
+
await writeFile(path.join(ctx.config.outDir, "og-default.png"), png);
|
|
4421
|
+
}
|
|
4271
4422
|
await persistDiskCache(ctx.config.outDir, cache);
|
|
4272
4423
|
ctx.log.debug("build:og-images", {
|
|
4273
4424
|
rendered: tally.rendered,
|
|
4274
|
-
skipped: tally.skipped
|
|
4425
|
+
skipped: tally.skipped,
|
|
4426
|
+
defaultCard
|
|
4275
4427
|
});
|
|
4276
4428
|
return {
|
|
4277
4429
|
rendered: tally.rendered,
|
|
4278
4430
|
skipped: tally.skipped,
|
|
4279
|
-
peakConcurrency: tally.peakConcurrency
|
|
4431
|
+
peakConcurrency: tally.peakConcurrency,
|
|
4432
|
+
defaultCard
|
|
4280
4433
|
};
|
|
4281
4434
|
}
|
|
4282
4435
|
/**
|
package/package.json
CHANGED