nuxt-og-image 6.3.2 → 6.3.3

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.
Files changed (58) hide show
  1. package/dist/chunks/tw4.cjs +1 -1
  2. package/dist/chunks/tw4.mjs +1 -1
  3. package/dist/chunks/uno.cjs +1 -1
  4. package/dist/chunks/uno.mjs +1 -1
  5. package/dist/devtools/200.html +1 -1
  6. package/dist/devtools/404.html +1 -1
  7. package/dist/devtools/_nuxt/25sBfX0g.js +6 -0
  8. package/dist/devtools/_nuxt/BfLfC02J.js +3 -0
  9. package/dist/devtools/_nuxt/CXdvjlAh.js +4 -0
  10. package/dist/devtools/_nuxt/CfJ_B4Jj.js +3 -0
  11. package/dist/devtools/_nuxt/Cg59iSrl.js +3 -0
  12. package/dist/devtools/_nuxt/{CoSxBJd8.js → DEOhyhjE.js} +1 -1
  13. package/dist/devtools/_nuxt/{C9JKABtj.js → DSwQLvvb.js} +1 -1
  14. package/dist/devtools/_nuxt/DevtoolsSnippet.QjsBSjre.css +1 -0
  15. package/dist/devtools/_nuxt/QN5ZUyx0.js +174 -0
  16. package/dist/devtools/_nuxt/TTqexA7S.js +1 -0
  17. package/dist/devtools/_nuxt/XgRlUfhy.js +2 -0
  18. package/dist/devtools/_nuxt/builds/latest.json +1 -1
  19. package/dist/devtools/_nuxt/builds/meta/2a1b436d-89c1-4c47-b621-d8bb140754c5.json +1 -0
  20. package/dist/devtools/_nuxt/entry.lR7NqsKt.css +2 -0
  21. package/dist/devtools/_nuxt/{pages.CPczdJu3.css → pages.DpBIgUOF.css} +1 -1
  22. package/dist/devtools/debug/index.html +1 -1
  23. package/dist/devtools/docs/index.html +1 -1
  24. package/dist/devtools/index.html +1 -1
  25. package/dist/devtools/templates/index.html +1 -1
  26. package/dist/module.cjs +1 -1
  27. package/dist/module.d.cts +3 -2
  28. package/dist/module.d.mts +3 -2
  29. package/dist/module.d.ts +3 -2
  30. package/dist/module.json +1 -1
  31. package/dist/module.mjs +1 -1
  32. package/dist/runtime/app/composables/_defineOgImageRaw.js +13 -7
  33. package/dist/runtime/app/composables/defineOgImageScreenshot.js +3 -1
  34. package/dist/runtime/app/utils.d.ts +8 -3
  35. package/dist/runtime/app/utils.js +79 -10
  36. package/dist/runtime/server/og-image/bindings/takumi/wasm.d.ts +1 -1
  37. package/dist/runtime/server/og-image/bindings/takumi/wasm.js +1 -1
  38. package/dist/runtime/server/og-image/font-subsets.d.ts +24 -0
  39. package/dist/runtime/server/og-image/font-subsets.js +53 -0
  40. package/dist/runtime/server/og-image/fonts.d.ts +7 -0
  41. package/dist/runtime/server/og-image/fonts.js +5 -4
  42. package/dist/runtime/server/og-image/satori/renderer.js +38 -4
  43. package/dist/runtime/server/og-image/takumi/renderer.js +28 -12
  44. package/dist/runtime/types.d.ts +8 -1
  45. package/dist/shared/{nuxt-og-image.PUNoqZDW.cjs → nuxt-og-image.CW0bAY2T.cjs} +38 -14
  46. package/dist/shared/{nuxt-og-image.CYm-mAcA.mjs → nuxt-og-image.JSCwEibR.mjs} +37 -13
  47. package/package.json +20 -19
  48. package/dist/devtools/_nuxt/BNA9K40e.js +0 -3
  49. package/dist/devtools/_nuxt/C-b6hTTf.js +0 -6
  50. package/dist/devtools/_nuxt/CD0R49mQ.js +0 -2
  51. package/dist/devtools/_nuxt/CaQt7uvw.js +0 -4
  52. package/dist/devtools/_nuxt/D0q6HvYk.js +0 -174
  53. package/dist/devtools/_nuxt/D2zWjF09.js +0 -3
  54. package/dist/devtools/_nuxt/DA9abGfd.js +0 -3
  55. package/dist/devtools/_nuxt/DGG_4uof.js +0 -1
  56. package/dist/devtools/_nuxt/DevtoolsSnippet.CHln_zRX.css +0 -1
  57. package/dist/devtools/_nuxt/builds/meta/9d408b39-cc9a-4751-be86-2f2ab3ba2395.json +0 -1
  58. package/dist/devtools/_nuxt/entry.BPMZ_Jol.css +0 -2
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ import 'nuxt-site-config/kit';
7
7
  import 'ohash';
8
8
  import 'pathe';
9
9
  import 'pkg-types';
10
- export { m as default } from './shared/nuxt-og-image.CYm-mAcA.mjs';
10
+ export { m as default } from './shared/nuxt-og-image.JSCwEibR.mjs';
11
11
  import 'nuxtseo-shared/kit';
12
12
  import '../dist/runtime/logger.js';
13
13
  import 'node:crypto';
@@ -1,7 +1,6 @@
1
- import { createError, useError, useNuxtApp, useRequestEvent, useRoute, useState } from "nuxt/app";
1
+ import { createError, injectHead, useError, useNuxtApp, useRequestEvent, useRoute, useState } from "nuxt/app";
2
2
  import { toValue } from "vue";
3
3
  import { createOgImageMeta, getOgImagePath, setHeadOgImagePrebuilt, useOgImageRuntimeConfig } from "../utils.js";
4
- const RE_COMMA = /,/g;
5
4
  export function defineOgImageRaw(_options = {}) {
6
5
  const nuxtApp = useNuxtApp();
7
6
  const route = useRoute();
@@ -69,6 +68,15 @@ function useProcessOgImageOptions(_options, nuxtApp, route, basePath) {
69
68
  if (validOptions[key] === void 0)
70
69
  validOptions[key] = defaults[key];
71
70
  }
71
+ let head;
72
+ if (import.meta.server) {
73
+ try {
74
+ head = injectHead();
75
+ } catch (e) {
76
+ if (import.meta.dev)
77
+ console.warn("[nuxt-og-image] Failed to inject head for OG image metadata generation.", e);
78
+ }
79
+ }
72
80
  if (route.query)
73
81
  validOptions._query = route.query;
74
82
  if (validOptions.url) {
@@ -79,13 +87,11 @@ function useProcessOgImageOptions(_options, nuxtApp, route, basePath) {
79
87
  if (hash) {
80
88
  validOptions._hash = hash;
81
89
  }
82
- createOgImageMeta(path, validOptions, nuxtApp.ssrContext);
90
+ createOgImageMeta(path, validOptions, nuxtApp.ssrContext, basePath, head);
83
91
  if (import.meta.prerender) {
84
- const ogKey = validOptions.key || "og";
85
- const prerenderPath = (path.split("?")[0] || path).replace(RE_COMMA, "%2C");
86
92
  const event = useRequestEvent(nuxtApp);
87
- const prerenderPaths = event.context._ogImagePrerenderPaths || (event.context._ogImagePrerenderPaths = /* @__PURE__ */ new Map());
88
- prerenderPaths.set(ogKey, prerenderPath);
93
+ if (!event.context._ogImagePrerenderPaths)
94
+ event.context._ogImagePrerenderPaths = /* @__PURE__ */ new Map();
89
95
  }
90
96
  return path;
91
97
  }
@@ -3,12 +3,14 @@ import { defineOgImageRaw } from "./_defineOgImageRaw.js";
3
3
  export function defineOgImageScreenshot(options = {}) {
4
4
  const router = useRouter();
5
5
  const route = router.currentRoute.value?.path || "/";
6
+ const { delay, mask, selector, colorScheme, ...rest } = options;
6
7
  return defineOgImageRaw({
7
8
  alt: `Web page screenshot${route ? ` of ${route}` : ""}.`,
8
9
  renderer: "browser",
9
10
  extension: "jpeg",
10
11
  component: "PageScreenshot",
11
12
  // this is an alias
12
- ...options
13
+ screenshot: { delay, mask, selector, colorScheme },
14
+ ...rest
13
15
  });
14
16
  }
@@ -1,7 +1,12 @@
1
- import type { ActiveHeadEntry, Head } from '@unhead/vue';
1
+ import type { ActiveHeadEntry, Head, VueHeadClient } from '@unhead/vue';
2
2
  import type { NuxtSSRContext } from 'nuxt/app';
3
3
  import type { OgImageOptions, OgImageOptionsInternal, OgImagePrebuilt, OgImageRuntimeConfig } from '../types.js';
4
- type OgImagePayload = [string, OgImageOptionsInternal, Required<Head>['meta']];
4
+ /**
5
+ * Payload format: [ogKey, options, basePath]
6
+ * basePath is stored so the lazy meta() callback can rebuild URLs
7
+ * after injecting head-derived title/description.
8
+ */
9
+ type OgImagePayload = [string, OgImageOptionsInternal, string];
5
10
  declare module 'nuxt/app' {
6
11
  interface NuxtSSRContext {
7
12
  _ogImagePayloads?: OgImagePayload[];
@@ -10,7 +15,7 @@ declare module 'nuxt/app' {
10
15
  }
11
16
  }
12
17
  export declare function setHeadOgImagePrebuilt(input: OgImagePrebuilt): void;
13
- export declare function createOgImageMeta(src: string, input: OgImageOptions | OgImagePrebuilt, ssrContext: NuxtSSRContext): void;
18
+ export declare function createOgImageMeta(src: string, input: OgImageOptions | OgImagePrebuilt, ssrContext: NuxtSSRContext, pagePath?: string, head?: VueHeadClient): void;
14
19
  export declare function resolveComponentName(component: OgImageOptionsInternal['component']): OgImageOptionsInternal['component'];
15
20
  export interface GetOgImagePathResult {
16
21
  path: string;
@@ -4,9 +4,46 @@ import { defu } from "defu";
4
4
  import { stringify } from "devalue";
5
5
  import { useHead, useRuntimeConfig } from "nuxt/app";
6
6
  import { joinURL, withQuery } from "ufo";
7
+ import { toValue } from "vue";
7
8
  import { logger } from "../logger.js";
8
9
  import { buildOgImageUrl, generateMeta, separateProps } from "../shared.js";
9
10
  const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/;
11
+ function extractHeadSeoProps(head) {
12
+ const result = {};
13
+ try {
14
+ for (const entry of head.entries.values()) {
15
+ const input = toValue(entry.input);
16
+ if (!input || typeof input !== "object")
17
+ continue;
18
+ if ("title" in input) {
19
+ const t = toValue(input.title);
20
+ if (typeof t === "string")
21
+ result.title = t;
22
+ }
23
+ if (input._flatMeta && typeof input._flatMeta === "object") {
24
+ const d = toValue(input._flatMeta.description) || toValue(input._flatMeta.ogDescription);
25
+ if (typeof d === "string")
26
+ result.description = d;
27
+ }
28
+ if (Array.isArray(input.meta)) {
29
+ for (const meta of input.meta) {
30
+ const m = toValue(meta);
31
+ if (!m || typeof m !== "object")
32
+ continue;
33
+ if (m.name === "description" || m.property === "og:description") {
34
+ const c = toValue(m.content);
35
+ if (typeof c === "string")
36
+ result.description = c;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ } catch (e) {
42
+ if (import.meta.dev)
43
+ logger.warn("Failed to extract SEO props from head entries", e);
44
+ }
45
+ return result;
46
+ }
10
47
  export function setHeadOgImagePrebuilt(input) {
11
48
  if (import.meta.client) {
12
49
  return;
@@ -17,32 +54,61 @@ export function setHeadOgImagePrebuilt(input) {
17
54
  const meta = generateMeta(url, input);
18
55
  useHead({ meta }, { tagPriority: "high" });
19
56
  }
20
- export function createOgImageMeta(src, input, ssrContext) {
57
+ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
21
58
  if (import.meta.client) {
22
59
  return;
23
60
  }
24
- const { defaults } = useOgImageRuntimeConfig();
61
+ const ogImageConfig = useOgImageRuntimeConfig();
62
+ const { defaults } = ogImageConfig;
25
63
  const resolvedOptions = separateProps(defu(input, defaults));
26
64
  resolvedOptions.key = resolvedOptions.key || "og";
27
65
  const payloads = ssrContext._ogImagePayloads || [];
28
66
  const currentPayloadIdx = payloads.findIndex(([k]) => k === resolvedOptions.key);
29
67
  const _input = separateProps(defu(input, currentPayloadIdx >= 0 ? payloads[currentPayloadIdx][1] : {}));
30
- let url = src || input.url || resolvedOptions.url;
31
- if (!url)
68
+ if (!src && !input.url && !resolvedOptions.url)
32
69
  return;
33
- if (input._query && Object.keys(input._query).length && url)
34
- url = withQuery(url, { _query: input._query });
35
- const meta = generateMeta(url, resolvedOptions);
70
+ const basePath = pagePath || "/";
36
71
  if (currentPayloadIdx === -1) {
37
- payloads.push([resolvedOptions.key, _input, meta]);
72
+ payloads.push([resolvedOptions.key, _input, basePath]);
38
73
  } else {
39
- payloads[currentPayloadIdx] = [resolvedOptions.key, _input, meta];
74
+ payloads[currentPayloadIdx] = [resolvedOptions.key, _input, basePath];
40
75
  }
76
+ const baseURL = useRuntimeConfig().app.baseURL;
41
77
  ssrContext._ogImageInstance?.dispose();
42
78
  ssrContext._ogImageInstance = useHead({
79
+ // Meta is generated lazily so that title/description from useSeoMeta / useHead
80
+ // are available regardless of call ordering (all component setups have completed
81
+ // by the time Unhead resolves tags).
43
82
  meta() {
44
83
  const finalPayload = ssrContext._ogImagePayloads || [];
45
- return finalPayload.flatMap(([_, __, meta2]) => meta2);
84
+ const seo = head ? extractHeadSeoProps(head) : void 0;
85
+ return finalPayload.flatMap(([_, options, payloadBasePath]) => {
86
+ const opts = { ...options, props: { ...options.props } };
87
+ if (seo) {
88
+ if (seo.title && typeof opts.props.title === "undefined")
89
+ opts.props.title = seo.title;
90
+ if (seo.description && typeof opts.props.description === "undefined")
91
+ opts.props.description = seo.description;
92
+ }
93
+ const extension = opts.extension || defaults?.extension || "png";
94
+ const isStatic = import.meta.prerender;
95
+ const urlOpts = { ...opts, _path: payloadBasePath };
96
+ const componentName = opts.component || componentNames?.[0]?.pascalName;
97
+ const component = componentNames?.find((c) => c.pascalName === componentName || c.kebabName === componentName);
98
+ if (component?.hash)
99
+ urlOpts._componentHash = component.hash;
100
+ const result = buildOgImageUrl(urlOpts, extension, isStatic, defaults, ogImageConfig.security?.secret || void 0);
101
+ const resolvedUrl = joinURL("/", baseURL, result.url);
102
+ const finalUrl = opts._query && Object.keys(opts._query).length ? withQuery(resolvedUrl, { _query: opts._query }) : resolvedUrl;
103
+ if (import.meta.prerender && ssrContext.event) {
104
+ const prerenderPaths = ssrContext.event.context._ogImagePrerenderPaths;
105
+ if (prerenderPaths) {
106
+ const ogKey = opts.key || "og";
107
+ prerenderPaths.set(ogKey, (finalUrl.split("?")[0] || finalUrl).replace(/,/g, "%2C"));
108
+ }
109
+ }
110
+ return generateMeta(finalUrl, opts);
111
+ });
46
112
  }
47
113
  }, {
48
114
  processTemplateParams: true,
@@ -56,10 +122,13 @@ export function createOgImageMeta(src, input, ssrContext) {
56
122
  type: "application/json",
57
123
  processTemplateParams: true,
58
124
  innerHTML: () => {
125
+ const seo = head ? extractHeadSeoProps(head) : void 0;
59
126
  const devtoolsPayload = (ssrContext._ogImagePayloads || []).map(([key, options]) => {
60
127
  const payload = resolveUnrefHeadInput(options);
61
128
  if (payload.props && typeof payload.props.title === "undefined")
62
129
  payload.props.title = "%s";
130
+ if (seo?.description && payload.props && typeof payload.props.description === "undefined")
131
+ payload.props.description = seo.description;
63
132
  if (typeof payload.component === "string") {
64
133
  payload.component = resolveComponentName(payload.component);
65
134
  } else if (payload.component) {
@@ -1,4 +1,4 @@
1
- import { extractResourceUrls, Renderer } from '@takumi-rs/wasm';
1
+ import { extractResourceUrls, Renderer } from '@takumi-rs/wasm/no-bundler';
2
2
  declare const _default: {
3
3
  initWasmPromise: Promise<import("@takumi-rs/wasm").InitOutput>;
4
4
  Renderer: typeof Renderer;
@@ -1,4 +1,4 @@
1
- import init, { extractResourceUrls, Renderer } from "@takumi-rs/wasm";
1
+ import init, { extractResourceUrls, Renderer } from "@takumi-rs/wasm/no-bundler";
2
2
  const wasmBinary = import("@takumi-rs/wasm/takumi_wasm_bg.wasm?module").then((m) => m.default || m);
3
3
  export default {
4
4
  initWasmPromise: wasmBinary.then((wasm) => init({ module_or_path: wasm })),
@@ -0,0 +1,24 @@
1
+ import type { RuntimeFontConfig } from '../../types.js';
2
+ /**
3
+ * Rename unicode-range subset fonts so renderers can fall back between them.
4
+ *
5
+ * Both Satori and Takumi pick the first loaded font file for a given family
6
+ * name and don't fall back to other font files for missing glyphs. When CJK
7
+ * fonts like "Noto Sans SC" are split into 100+ unicode-range subsets by
8
+ * fontsource, each covering ~200 characters, this means only one subset's
9
+ * glyphs render while the rest show as .notdef boxes.
10
+ *
11
+ * Fix: rename each subset to "Family__N" and use font-family fallback chains
12
+ * so renderers try each subset in order per character.
13
+ */
14
+ export declare function renameSubsetFonts(fonts: RuntimeFontConfig[]): RuntimeFontConfig[];
15
+ /**
16
+ * Build a mapping from original family names to their renamed subset chain.
17
+ * E.g., "Noto Sans SC" → ["Noto Sans SC__0", "Noto Sans SC__1", ...]
18
+ */
19
+ export declare function buildSubsetFamilyChain(fonts: RuntimeFontConfig[]): Map<string, string[]>;
20
+ /**
21
+ * Resolve a font family name through subset chains (case-insensitive).
22
+ * Returns the chain of renamed subset names, or undefined if not a subset font.
23
+ */
24
+ export declare function resolveSubsetChain(family: string, chains: Map<string, string[]>): string[] | undefined;
@@ -0,0 +1,53 @@
1
+ export function renameSubsetFonts(fonts) {
2
+ const groups = /* @__PURE__ */ new Map();
3
+ for (const f of fonts) {
4
+ const key = `${f.family}\0${f.weight}\0${f.style}`;
5
+ const arr = groups.get(key);
6
+ if (arr)
7
+ arr.push(f);
8
+ else
9
+ groups.set(key, [f]);
10
+ }
11
+ const result = [];
12
+ let changed = false;
13
+ for (const members of groups.values()) {
14
+ const needsRename = members.length > 1 && new Set(members.map((f) => f.cacheKey)).size > 1;
15
+ if (!needsRename) {
16
+ result.push(...members);
17
+ continue;
18
+ }
19
+ changed = true;
20
+ for (let i = 0; i < members.length; i++) {
21
+ const f = members[i];
22
+ result.push({
23
+ ...f,
24
+ originalFamily: f.originalFamily || f.family,
25
+ family: `${f.family}__${i}`
26
+ });
27
+ }
28
+ }
29
+ return changed ? result : fonts;
30
+ }
31
+ export function buildSubsetFamilyChain(fonts) {
32
+ const chains = /* @__PURE__ */ new Map();
33
+ for (const f of fonts) {
34
+ if (!f.originalFamily)
35
+ continue;
36
+ const arr = chains.get(f.originalFamily);
37
+ if (arr)
38
+ arr.push(f.family);
39
+ else
40
+ chains.set(f.originalFamily, [f.family]);
41
+ }
42
+ return chains;
43
+ }
44
+ export function resolveSubsetChain(family, chains) {
45
+ const direct = chains.get(family);
46
+ if (direct)
47
+ return direct;
48
+ const lower = family.toLowerCase();
49
+ for (const [key, value] of chains) {
50
+ if (key.toLowerCase() === lower)
51
+ return value;
52
+ }
53
+ }
@@ -1,5 +1,6 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { OgImageRenderEventContext, RuntimeFontConfig } from '../../types.js';
3
+ export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from './font-subsets.js';
3
4
  export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from './unicode-range.js';
4
5
  type FontFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
5
6
  export interface LoadFontsOptions {
@@ -43,6 +44,12 @@ export interface LoadFontsForRendererOptions extends LoadFontsOptions {
43
44
  /**
44
45
  * Shared font loading sequence used by both renderers.
45
46
  * Loads base fonts via loadAllFonts, then appends any custom font definitions.
47
+ *
48
+ * When a font family has multiple unicode-range subsets loaded (e.g., CJK fonts
49
+ * split into many small chunks), renames each subset to a unique family name
50
+ * (e.g., "Noto Sans SC__0", "Noto Sans SC__1") so renderers use font-family
51
+ * fallback chains for per-character glyph coverage. Without this, both Satori
52
+ * and Takumi pick the first font file and show .notdef for characters in other subsets.
46
53
  */
47
54
  export declare function loadFontsForRenderer(event: OgImageRenderEventContext, options: LoadFontsForRendererOptions): Promise<RuntimeFontConfig[]>;
48
55
  /** Get the default font family override from options or first resolved font */
@@ -4,7 +4,9 @@ import resolvedFonts from "#og-image/fonts";
4
4
  import availableFonts from "#og-image/fonts-available";
5
5
  import { logger } from "../../logger.js";
6
6
  import { fontArrayCache, fontCache } from "./cache/lru.js";
7
+ import { renameSubsetFonts } from "./font-subsets.js";
7
8
  import { codepointsIntersectRanges, parseUnicodeRange } from "./unicode-range.js";
9
+ export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from "./font-subsets.js";
8
10
  export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from "./unicode-range.js";
9
11
  function fontFormat(src) {
10
12
  if (src.endsWith(".woff2"))
@@ -203,10 +205,9 @@ export async function loadDefinedFonts(event, fontDefs) {
203
205
  }
204
206
  export async function loadFontsForRenderer(event, options) {
205
207
  const baseFonts = await loadAllFonts(event.e, options);
206
- if (!Array.isArray(options.fontDefs) || options.fontDefs.length === 0)
207
- return baseFonts;
208
- const customFonts = await loadDefinedFonts(event, options.fontDefs);
209
- return [...baseFonts, ...customFonts];
208
+ const customFonts = Array.isArray(options.fontDefs) && options.fontDefs.length > 0 ? await loadDefinedFonts(event, options.fontDefs) : [];
209
+ const allFonts = customFonts.length > 0 ? [...baseFonts, ...customFonts] : baseFonts;
210
+ return renameSubsetFonts(allFonts);
210
211
  }
211
212
  export function getDefaultFontFamily(options) {
212
213
  const fontFamilyOverride = options.props?.fontFamily;
@@ -2,7 +2,7 @@ import { tw4FontVars } from "#og-image-virtual/tw4-theme.mjs";
2
2
  import compatibility from "#og-image/compatibility";
3
3
  import { defu } from "defu";
4
4
  import { useOgImageRuntimeConfig } from "../../utils.js";
5
- import { extractCodepoints, getDefaultFontFamily, loadAllFontsDebug, loadFontsForRenderer } from "../fonts.js";
5
+ import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadAllFontsDebug, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
6
6
  import { getResvg, getSatori, getSharp } from "./instances.js";
7
7
  import { createVNodes } from "./vnodes.js";
8
8
  const _satoriFontCache = /* @__PURE__ */ new WeakMap();
@@ -43,13 +43,23 @@ export async function createSvg(event) {
43
43
  const satoriFonts = !hasCustomFonts && _satoriFontCache.get(fonts) || fonts.map((f) => ({ ...f, name: f.family }));
44
44
  if (!hasCustomFonts)
45
45
  _satoriFontCache.set(fonts, satoriFonts);
46
+ const subsetChains = buildSubsetFamilyChain(fonts);
46
47
  const loadedFamilies = new Set(satoriFonts.map((f) => f.name));
47
48
  const defaultFamily = satoriFonts[0]?.name;
48
49
  function resolveAvailableFamily(cssValue) {
49
50
  const families = cssValue.split(",").map((f) => f.trim().replace(RE_FONT_QUOTES, ""));
50
- const available = families.filter((f) => loadedFamilies.has(f));
51
- if (available.length > 0)
52
- return available.join(", ");
51
+ const resolved = [];
52
+ for (const f of families) {
53
+ if (loadedFamilies.has(f)) {
54
+ resolved.push(f);
55
+ continue;
56
+ }
57
+ const chain = resolveSubsetChain(f, subsetChains);
58
+ if (chain)
59
+ resolved.push(...chain);
60
+ }
61
+ if (resolved.length > 0)
62
+ return resolved.join(", ");
53
63
  return defaultFamily;
54
64
  }
55
65
  const fontFamily = {};
@@ -61,6 +71,8 @@ export async function createSvg(event) {
61
71
  if (resolved)
62
72
  fontFamily[slot] = resolved;
63
73
  }
74
+ if (subsetChains.size > 0)
75
+ rewriteVNodeFontFamilies(vnodes, subsetChains);
64
76
  const satoriOptions = defu(options.satori, _satoriOptions, {
65
77
  fonts: satoriFonts,
66
78
  tailwindConfig: Object.keys(fontFamily).length ? { theme: { fontFamily } } : void 0,
@@ -105,6 +117,28 @@ async function createJpeg(event) {
105
117
  const options = defu(event.options.sharp, sharpOptions);
106
118
  return sharp(svgBuffer, options).jpeg(options).toBuffer();
107
119
  }
120
+ function rewriteVNodeFontFamilies(node, subsetChains) {
121
+ const style = node.props?.style;
122
+ if (style?.fontFamily && typeof style.fontFamily === "string") {
123
+ const families = style.fontFamily.split(",").map((f) => f.trim().replace(RE_FONT_QUOTES, ""));
124
+ const resolved = [];
125
+ for (const f of families) {
126
+ const chain = resolveSubsetChain(f, subsetChains);
127
+ if (chain)
128
+ resolved.push(...chain);
129
+ else
130
+ resolved.push(f);
131
+ }
132
+ style.fontFamily = resolved.join(", ");
133
+ }
134
+ const children = node.props?.children;
135
+ if (Array.isArray(children)) {
136
+ for (const child of children) {
137
+ if (child && typeof child === "object")
138
+ rewriteVNodeFontFamilies(child, subsetChains);
139
+ }
140
+ }
141
+ }
108
142
  const SatoriRenderer = {
109
143
  name: "satori",
110
144
  supportedFormats: ["png", "jpeg", "jpg", "json"],
@@ -2,7 +2,7 @@ import { getNitroOrigin } from "#site-config/server/composables";
2
2
  import { defu } from "defu";
3
3
  import { withBase } from "ufo";
4
4
  import { logger } from "../../../logger.js";
5
- import { extractCodepoints, getDefaultFontFamily, loadFontsForRenderer } from "../fonts.js";
5
+ import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
6
6
  import { getExtractResourceUrls, getTakumi } from "./instances.js";
7
7
  import { createTakumiNodes } from "./nodes.js";
8
8
  const RE_QUOTES = /['"]/g;
@@ -48,22 +48,32 @@ function lookupFontFamily(family, loadedFamilies) {
48
48
  return loaded;
49
49
  }
50
50
  }
51
- function rewriteFontFamilies(node, loadedFamilies) {
51
+ function rewriteFontFamilies(node, loadedFamilies, subsetChains) {
52
52
  if (node.style?.fontFamily) {
53
53
  const families = node.style.fontFamily.split(",").map((f) => f.trim().replace(RE_QUOTES, ""));
54
- const resolved = families.map((f) => lookupFontFamily(f, loadedFamilies) || f);
55
- const seen = new Set(resolved.map((f) => f.toLowerCase()));
56
- for (const family of loadedFamilies) {
57
- if (!seen.has(family.toLowerCase())) {
58
- resolved.push(family);
59
- seen.add(family.toLowerCase());
54
+ const resolved = [];
55
+ const seen = /* @__PURE__ */ new Set();
56
+ const addUnique = (name) => {
57
+ if (!seen.has(name.toLowerCase())) {
58
+ resolved.push(name);
59
+ seen.add(name.toLowerCase());
60
60
  }
61
+ };
62
+ for (const f of families) {
63
+ const chain = resolveSubsetChain(f, subsetChains);
64
+ if (chain) {
65
+ chain.forEach(addUnique);
66
+ continue;
67
+ }
68
+ addUnique(lookupFontFamily(f, loadedFamilies) || f);
61
69
  }
70
+ for (const family of loadedFamilies)
71
+ addUnique(family);
62
72
  node.style.fontFamily = resolved.map((f) => `"${f}"`).join(", ");
63
73
  }
64
74
  if ("children" in node && node.children) {
65
75
  for (const child of node.children)
66
- rewriteFontFamilies(child, loadedFamilies);
76
+ rewriteFontFamilies(child, loadedFamilies, subsetChains);
67
77
  }
68
78
  }
69
79
  async function createImage(event, format) {
@@ -73,14 +83,20 @@ async function createImage(event, format) {
73
83
  const codepoints = extractCodepoints(nodes);
74
84
  const fonts = await loadFontsForRenderer(event, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints });
75
85
  await event._nitro.hooks.callHook("nuxt-og-image:takumi:nodes", nodes, event);
86
+ const subsetChains = buildSubsetFamilyChain(fonts);
76
87
  const state = await getTakumiState(event);
77
88
  await loadFontsIntoRenderer(state, fonts);
78
89
  const rootStyle = nodes.style ?? {};
79
- if (fontFamilyOverride && state.loadedFamilies.has(fontFamilyOverride)) {
80
- rootStyle.fontFamily = fontFamilyOverride;
90
+ if (fontFamilyOverride) {
91
+ const chain = subsetChains.get(fontFamilyOverride);
92
+ if (chain) {
93
+ rootStyle.fontFamily = chain.map((f) => `"${f}"`).join(", ");
94
+ } else if (state.loadedFamilies.has(fontFamilyOverride)) {
95
+ rootStyle.fontFamily = fontFamilyOverride;
96
+ }
81
97
  }
82
98
  nodes.style = rootStyle;
83
- rewriteFontFamilies(nodes, state.loadedFamilies);
99
+ rewriteFontFamilies(nodes, state.loadedFamilies, subsetChains);
84
100
  const extractResourceUrls = await getExtractResourceUrls();
85
101
  const resourceUrls = await extractResourceUrls(nodes);
86
102
  const origin = getNitroOrigin(event.e);
@@ -213,6 +213,8 @@ export interface FontConfig {
213
213
  export interface RuntimeFontConfig extends FontConfig {
214
214
  cacheKey: string;
215
215
  data: BufferSource;
216
+ /** Original family name before subset renaming (e.g., "Noto Sans SC" when family is "Noto Sans SC__0") */
217
+ originalFamily?: string;
216
218
  }
217
219
  export interface RuntimeCompatibilitySchema {
218
220
  browser: 'chrome-launcher' | 'on-demand' | 'playwright' | 'cloudflare' | false;
@@ -242,7 +244,12 @@ export type ExtractComponentProps<T extends Component> = T extends new (...args:
242
244
  export type ReactiveComponentProps<T extends Component> = {
243
245
  [K in keyof ExtractComponentProps<T>]?: MaybeRefOrGetter<ExtractComponentProps<T>[K]>;
244
246
  };
245
- export type OgImagePageScreenshotOptions = Omit<OgImageOptions, 'html' | 'component' | 'satori' | 'resvg' | 'sharp'>;
247
+ export type OgImagePageScreenshotOptions = Omit<OgImageOptions, 'html' | 'component' | 'satori' | 'resvg' | 'sharp' | 'screenshot'> & Omit<ScreenshotOptions, 'width' | 'height' | 'delay'> & {
248
+ /**
249
+ * How long to wait before taking the screenshot. Useful for waiting for animations.
250
+ */
251
+ delay?: number;
252
+ };
246
253
  export interface VNode {
247
254
  type: string;
248
255
  props: {