nuxt-og-image 6.3.2 → 6.3.4
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/chunks/tw4.cjs +1 -1
- package/dist/chunks/tw4.mjs +1 -1
- package/dist/chunks/uno.cjs +1 -1
- package/dist/chunks/uno.mjs +1 -1
- package/dist/content.d.cts +3 -1
- package/dist/content.d.mts +3 -1
- package/dist/content.d.ts +3 -1
- package/dist/devtools/200.html +1 -1
- package/dist/devtools/404.html +1 -1
- package/dist/devtools/_nuxt/{C9JKABtj.js → 05VU1kAP.js} +1 -1
- package/dist/devtools/_nuxt/BWm573-p.js +3 -0
- package/dist/devtools/_nuxt/C4ssBbxk.js +23 -0
- package/dist/devtools/_nuxt/Cy0omYQh.js +152 -0
- package/dist/devtools/_nuxt/D6HqPJ2P.js +1 -0
- package/dist/devtools/_nuxt/{C-b6hTTf.js → DDRo8-tD.js} +1 -1
- package/dist/devtools/_nuxt/DJOUfd_s.js +2 -0
- package/dist/devtools/_nuxt/DO62csGn.js +1 -0
- package/dist/devtools/_nuxt/DevtoolsSection.BA22BKWd.css +1 -0
- package/dist/devtools/_nuxt/DevtoolsSnippet.5f9DiVqQ.css +1 -0
- package/dist/devtools/_nuxt/DggwDRkJ.js +4 -0
- package/dist/devtools/_nuxt/Dgwhb1xi.js +1 -0
- package/dist/devtools/_nuxt/DmsY5seq.js +3 -0
- package/dist/devtools/_nuxt/Dn8E1mDk.js +1 -0
- package/dist/devtools/_nuxt/{DA9abGfd.js → DziLl24l.js} +1 -1
- package/dist/devtools/_nuxt/builds/latest.json +1 -1
- package/dist/devtools/_nuxt/builds/meta/13f85b83-b220-4283-8efd-9b1adfb63ca9.json +1 -0
- package/dist/devtools/_nuxt/entry.D0N1PjT9.css +2 -0
- package/dist/devtools/_nuxt/{pages.CPczdJu3.css → pages.DjPHX4aO.css} +1 -1
- package/dist/devtools/_nuxt/renderer-select.Dd3SlzOB.css +1 -0
- package/dist/devtools/debug/index.html +1 -1
- package/dist/devtools/docs/index.html +1 -1
- package/dist/devtools/index.html +1 -1
- package/dist/devtools/templates/index.html +1 -1
- package/dist/module.cjs +1 -1
- package/dist/module.d.cts +3 -2
- package/dist/module.d.mts +3 -2
- package/dist/module.d.ts +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/app/composables/_defineOgImageRaw.js +13 -7
- package/dist/runtime/app/composables/defineOgImageScreenshot.js +3 -1
- package/dist/runtime/app/utils.d.ts +8 -3
- package/dist/runtime/app/utils.js +79 -10
- package/dist/runtime/server/og-image/bindings/takumi/wasm.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/takumi/wasm.js +1 -1
- package/dist/runtime/server/og-image/font-subsets.d.ts +24 -0
- package/dist/runtime/server/og-image/font-subsets.js +53 -0
- package/dist/runtime/server/og-image/fonts.d.ts +7 -0
- package/dist/runtime/server/og-image/fonts.js +5 -4
- package/dist/runtime/server/og-image/satori/renderer.js +38 -4
- package/dist/runtime/server/og-image/takumi/renderer.js +28 -12
- package/dist/runtime/types.d.ts +8 -1
- package/dist/shared/{nuxt-og-image.CYm-mAcA.mjs → nuxt-og-image.DW7_z3y5.mjs} +54 -13
- package/dist/shared/{nuxt-og-image.PUNoqZDW.cjs → nuxt-og-image.as-lQ2yV.cjs} +55 -14
- package/package.json +26 -25
- package/dist/devtools/_nuxt/BNA9K40e.js +0 -3
- package/dist/devtools/_nuxt/CD0R49mQ.js +0 -2
- package/dist/devtools/_nuxt/CaQt7uvw.js +0 -4
- package/dist/devtools/_nuxt/CoSxBJd8.js +0 -1
- package/dist/devtools/_nuxt/D0q6HvYk.js +0 -174
- package/dist/devtools/_nuxt/D2zWjF09.js +0 -3
- package/dist/devtools/_nuxt/DGG_4uof.js +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.CHln_zRX.css +0 -1
- package/dist/devtools/_nuxt/builds/meta/9d408b39-cc9a-4751-be86-2f2ab3ba2395.json +0 -1
- package/dist/devtools/_nuxt/entry.BPMZ_Jol.css +0 -2
- /package/dist/devtools/_nuxt/{Bx_VzvYa.js → BsRKQH5X.js} +0 -0
- /package/dist/devtools/_nuxt/{PnYIYlhI.js → CDaXKRRu.js} +0 -0
- /package/dist/devtools/_nuxt/{z1-BJQbH.js → CM-t-6ZT.js} +0 -0
- /package/dist/devtools/_nuxt/{CHHO6nw6.js → CP0tQR2M.js} +0 -0
- /package/dist/devtools/_nuxt/{BwW5PI4u.js → HWyD49jH.js} +0 -0
- /package/dist/devtools/_nuxt/{CDUvHTjX.js → PONEy9N-.js} +0 -0
package/dist/module.json
CHANGED
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.
|
|
10
|
+
export { m as default } from './shared/nuxt-og-image.DW7_z3y5.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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
if (!url)
|
|
68
|
+
if (!src && !input.url && !resolvedOptions.url)
|
|
32
69
|
return;
|
|
33
|
-
|
|
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,
|
|
72
|
+
payloads.push([resolvedOptions.key, _input, basePath]);
|
|
38
73
|
} else {
|
|
39
|
-
payloads[currentPayloadIdx] = [resolvedOptions.key, _input,
|
|
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
|
-
|
|
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 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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
55
|
-
const seen = new Set(
|
|
56
|
-
|
|
57
|
-
if (!seen.has(
|
|
58
|
-
resolved.push(
|
|
59
|
-
seen.add(
|
|
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
|
|
80
|
-
|
|
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);
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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: {
|