nuxt-og-image 7.0.2 → 7.0.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.
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { resolveModulePath } from 'exsolve';
4
4
  import { dirname, join } from 'pathe';
5
- import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
5
+ import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.C4hc520M.mjs';
6
6
  import 'node:fs';
7
7
  import 'node:module';
8
8
  import '@nuxt/kit';
@@ -1,8 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { defu } from 'defu';
3
3
  import { logger } from '../../dist/runtime/logger.js';
4
- import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
5
- export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
4
+ import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.C4hc520M.mjs';
5
+ export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.C4hc520M.mjs';
6
6
  import 'node:fs';
7
7
  import 'node:module';
8
8
  import 'node:url';
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.1 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "7.0.2",
7
+ "version": "7.0.4",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ import 'defu';
7
7
  import 'fnv1a-64';
8
8
  import 'nuxtseo-shared/kit';
9
9
  import 'pathe';
10
- export { n as default } from './shared/nuxt-og-image.CSPRgfu6.mjs';
10
+ export { n as default } from './shared/nuxt-og-image.C4hc520M.mjs';
11
11
  import '../dist/runtime/logger.js';
12
12
  import '../dist/runtime/shared/publish.js';
13
13
  import 'node:crypto';
@@ -83,13 +83,13 @@ export function clientProcessOgImageOptions(input, route, basePath) {
83
83
  if (validOptions.url) {
84
84
  const url = validOptions.url;
85
85
  const absoluteUrl = /^https?:\/\//.test(url) ? url : toValue(resolveImageUrl(url));
86
- registerClientOgHead(ogKey, { meta: generateMeta(absoluteUrl, validOptions, metaOptions) }, { tagPriority: "high" });
86
+ registerClientOgHead(ogKey, { meta: generateMeta(absoluteUrl, validOptions, metaOptions) });
87
87
  paths.push(url);
88
88
  continue;
89
89
  }
90
90
  if (hasServerRuntime) {
91
91
  const finalUrl2 = buildResolverUrl(baseURL, basePath, ogKey, route.query);
92
- registerClientOgHead(ogKey, { meta: generateMeta(toValue(resolveImageUrl(finalUrl2)), validOptions, metaOptions) }, { tagPriority: "high" });
92
+ registerClientOgHead(ogKey, { meta: generateMeta(toValue(resolveImageUrl(finalUrl2)), validOptions, metaOptions) });
93
93
  paths.push(finalUrl2);
94
94
  continue;
95
95
  }
@@ -105,7 +105,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
105
105
  const query = opts._query && Object.keys(opts._query).length ? { _query: opts._query } : {};
106
106
  const finalUrl = withQuery(resolvedUrl, query);
107
107
  const imageUrl = rc.app.cdnURL ? withQuery(joinURL(rc.app.cdnURL, result.url), query) : toValue(resolveImageUrl(finalUrl));
108
- registerClientOgHead(ogKey, { meta: generateMeta(imageUrl, opts, metaOptions) }, { processTemplateParams: true, tagPriority: "high" });
108
+ registerClientOgHead(ogKey, { meta: generateMeta(imageUrl, opts, metaOptions) }, { processTemplateParams: true });
109
109
  paths.push(finalUrl);
110
110
  }
111
111
  return paths;
@@ -58,7 +58,7 @@ export function setHeadOgImagePrebuilt(input) {
58
58
  const urlValue = toValue(url);
59
59
  const absoluteUrl = /^https?:\/\//.test(urlValue) ? urlValue : toValue(withSiteUrl(urlValue, { withBase: true, canonical: !import.meta.dev }));
60
60
  const meta = generateMeta(absoluteUrl, input, { includeTwitter });
61
- useHead({ meta }, { tagPriority: "high" });
61
+ useHead({ meta });
62
62
  }
63
63
  export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
64
64
  if (import.meta.client) {
@@ -126,8 +126,7 @@ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
126
126
  });
127
127
  }
128
128
  }, {
129
- processTemplateParams: true,
130
- tagPriority: "high"
129
+ processTemplateParams: true
131
130
  });
132
131
  if (import.meta.dev || import.meta.prerender) {
133
132
  ssrContext._ogImageDevtoolsInstance?.dispose();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Font binaries as renderers receive them.
3
+ *
4
+ * Takumi's WASM build rejects WOFF, and one bad font fails the whole render.
5
+ * The loader inflates WOFF to the TTF or OTF it wraps and rejects bytes that are no font.
6
+ * The decoder only needs `DecompressionStream`, so it runs on Node and edge runtimes.
7
+ */
8
+ export type FontBinaryResult = {
9
+ _tag: 'Ok';
10
+ data: Uint8Array<ArrayBuffer>;
11
+ } | {
12
+ _tag: 'Err';
13
+ reason: string;
14
+ };
15
+ /** Whether the bytes are WOFF2, which Satori cannot read. WOFF2 needs Brotli, so the loader never inflates it. */
16
+ export declare function isWoff2Binary(source: BufferSource): boolean;
17
+ /**
18
+ * Parse loaded font bytes into a binary that every Takumi build accepts.
19
+ * TTF, OTF, font collections and WOFF2 pass through; WOFF inflates to TTF or OTF.
20
+ */
21
+ export declare function parseFontBinary(source: BufferSource): Promise<FontBinaryResult>;
@@ -0,0 +1,123 @@
1
+ const WOFF_HEADER_SIZE = 44;
2
+ const WOFF_TABLE_ENTRY_SIZE = 20;
3
+ const SFNT_TABLE_RECORD_SIZE = 16;
4
+ const MAX_SFNT_SIZE = 64 * 1024 * 1024;
5
+ const SIGNATURES = {
6
+ truetype: 65536,
7
+ appleTrueType: 1953658213,
8
+ // 'true'
9
+ openType: 1330926671,
10
+ // 'OTTO'
11
+ collection: 1953784678,
12
+ // 'ttcf'
13
+ woff: 2001684038,
14
+ // 'wOFF'
15
+ woff2: 2001684018
16
+ // 'wOF2'
17
+ };
18
+ function toBytes(source) {
19
+ if (ArrayBuffer.isView(source))
20
+ return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
21
+ return new Uint8Array(source);
22
+ }
23
+ function readSignature(data) {
24
+ if (data.byteLength < 4)
25
+ return void 0;
26
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0);
27
+ }
28
+ async function inflate(data, size) {
29
+ const output = new Uint8Array(size);
30
+ const reader = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate")).getReader();
31
+ let written = 0;
32
+ while (true) {
33
+ const { done, value } = await reader.read();
34
+ if (done)
35
+ break;
36
+ if (written + value.byteLength > size) {
37
+ await reader.cancel();
38
+ throw new Error("a table inflates past its declared size");
39
+ }
40
+ output.set(value, written);
41
+ written += value.byteLength;
42
+ }
43
+ if (written !== size)
44
+ throw new Error("a table inflates short of its declared size");
45
+ return output;
46
+ }
47
+ async function decodeWoff(woff) {
48
+ if (woff.byteLength < WOFF_HEADER_SIZE)
49
+ throw new Error("the WOFF header is truncated");
50
+ const view = new DataView(woff.buffer, woff.byteOffset, woff.byteLength);
51
+ const flavor = view.getUint32(4);
52
+ const numTables = view.getUint16(12);
53
+ if (numTables === 0)
54
+ throw new Error("the WOFF file has no tables");
55
+ if (WOFF_HEADER_SIZE + numTables * WOFF_TABLE_ENTRY_SIZE > woff.byteLength)
56
+ throw new Error("the WOFF table directory is truncated");
57
+ const tables = Array.from({ length: numTables }, (_, index) => {
58
+ const entry = WOFF_HEADER_SIZE + index * WOFF_TABLE_ENTRY_SIZE;
59
+ return {
60
+ tag: view.getUint32(entry),
61
+ offset: view.getUint32(entry + 4),
62
+ compLength: view.getUint32(entry + 8),
63
+ origLength: view.getUint32(entry + 12),
64
+ checksum: view.getUint32(entry + 16)
65
+ };
66
+ });
67
+ const headerSize = 12 + numTables * SFNT_TABLE_RECORD_SIZE;
68
+ let sfntSize = headerSize;
69
+ for (const table of tables) {
70
+ if (table.offset + table.compLength > woff.byteLength)
71
+ throw new Error("a WOFF table runs past the end of the file");
72
+ if (table.compLength > table.origLength)
73
+ throw new Error("a WOFF table is larger than its decoded size");
74
+ sfntSize += table.origLength + 3 & ~3;
75
+ }
76
+ if (sfntSize > MAX_SFNT_SIZE)
77
+ throw new Error(`the decoded font exceeds ${MAX_SFNT_SIZE} bytes`);
78
+ const tableData = await Promise.all(tables.map((table) => {
79
+ const stored = woff.subarray(table.offset, table.offset + table.compLength);
80
+ return table.compLength < table.origLength ? inflate(stored, table.origLength) : stored;
81
+ }));
82
+ const sfnt = new Uint8Array(sfntSize);
83
+ const out = new DataView(sfnt.buffer);
84
+ const entrySelector = Math.floor(Math.log2(numTables));
85
+ const searchRange = 2 ** entrySelector * SFNT_TABLE_RECORD_SIZE;
86
+ out.setUint32(0, flavor);
87
+ out.setUint16(4, numTables);
88
+ out.setUint16(6, searchRange);
89
+ out.setUint16(8, entrySelector);
90
+ out.setUint16(10, numTables * SFNT_TABLE_RECORD_SIZE - searchRange);
91
+ let offset = headerSize;
92
+ tables.forEach((table, index) => {
93
+ const record = 12 + index * SFNT_TABLE_RECORD_SIZE;
94
+ out.setUint32(record, table.tag);
95
+ out.setUint32(record + 4, table.checksum);
96
+ out.setUint32(record + 8, offset);
97
+ out.setUint32(record + 12, table.origLength);
98
+ sfnt.set(tableData[index], offset);
99
+ offset += table.origLength + 3 & ~3;
100
+ });
101
+ return sfnt;
102
+ }
103
+ export function isWoff2Binary(source) {
104
+ return readSignature(toBytes(source)) === SIGNATURES.woff2;
105
+ }
106
+ export async function parseFontBinary(source) {
107
+ const data = toBytes(source);
108
+ switch (readSignature(data)) {
109
+ case SIGNATURES.truetype:
110
+ case SIGNATURES.appleTrueType:
111
+ case SIGNATURES.openType:
112
+ case SIGNATURES.collection:
113
+ case SIGNATURES.woff2:
114
+ return { _tag: "Ok", data };
115
+ case SIGNATURES.woff:
116
+ return decodeWoff(data).then(
117
+ (sfnt) => ({ _tag: "Ok", data: sfnt }),
118
+ (error) => ({ _tag: "Err", reason: `it is a WOFF file that could not be decoded: ${error.message}` })
119
+ );
120
+ default:
121
+ return { _tag: "Err", reason: "it is not a TTF, OTF, WOFF or WOFF2 file" };
122
+ }
123
+ }
@@ -5,9 +5,9 @@ export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from '.
5
5
  export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from './unicode-range.js';
6
6
  export interface LoadFontsOptions {
7
7
  /**
8
- * Font formats the renderer can parse.
8
+ * Font source formats the renderer can take.
9
9
  * Satori: ttf, otf, woff
10
- * Takumi: ttf, otf, woff, woff2
10
+ * Takumi: ttf, otf, woff, woff2. The loader inflates WOFF, which Takumi's WASM build rejects.
11
11
  */
12
12
  supportedFormats: Set<FontFormat>;
13
13
  /** Component pascalName — filters fonts to only what this component needs */
@@ -40,7 +40,7 @@ export declare function loadAllFonts(event: H3Event, options: LoadFontsOptions):
40
40
  * parsed at this boundary (require string name/path, coerce weight) and the
41
41
  * array is bounded before any font is fetched.
42
42
  */
43
- export declare function loadDefinedFonts(event: OgImageRenderEventContext, fontDefs: any[]): Promise<RuntimeFontConfig[]>;
43
+ export declare function loadDefinedFonts(event: OgImageRenderEventContext, fontDefs: any[], supportedFormats: Set<FontFormat>): Promise<RuntimeFontConfig[]>;
44
44
  export interface LoadFontsForRendererOptions extends LoadFontsOptions {
45
45
  /** Custom font definitions from defineOgImage({ fonts: [...] }) — satori only */
46
46
  fontDefs?: any[];
@@ -4,11 +4,21 @@ 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 { isWoff2Binary, parseFontBinary } from "./font-binary.js";
7
8
  import { fontFormat, selectFontSource } from "./font-source.js";
8
9
  import { renameSubsetFonts } from "./font-subsets.js";
9
10
  import { codepointsIntersectRanges, parseUnicodeRange } from "./unicode-range.js";
10
11
  export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from "./font-subsets.js";
11
12
  export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from "./unicode-range.js";
13
+ const _warnedFontKeys = /* @__PURE__ */ new Set();
14
+ function warnOnce(key, message) {
15
+ if (_warnedFontKeys.has(key))
16
+ return;
17
+ if (_warnedFontKeys.size >= 100)
18
+ _warnedFontKeys.clear();
19
+ _warnedFontKeys.add(key);
20
+ logger.warn(message);
21
+ }
12
22
  async function loadFont(event, font, src) {
13
23
  const cacheKey = `${font.family}-${font.weight}-${font.style}-${src}`;
14
24
  const cached = fontCache.get(cacheKey);
@@ -20,8 +30,13 @@ async function loadFont(event, font, src) {
20
30
  });
21
31
  if (!data)
22
32
  return null;
23
- fontCache.set(cacheKey, data);
24
- return data;
33
+ const binary = await parseFontBinary(data);
34
+ if (binary._tag === "Err") {
35
+ warnOnce(`binary-${src}`, `OG images skip font "${font.family}" (${src}) because ${binary.reason}. Serve it as TTF, OTF, WOFF or WOFF2.`);
36
+ return null;
37
+ }
38
+ fontCache.set(cacheKey, binary.data);
39
+ return binary.data;
25
40
  }
26
41
  function findClosestWeight(target, available) {
27
42
  return available.reduce(
@@ -65,7 +80,6 @@ function selectFontsForRequirements(allFonts, requirements) {
65
80
  selected.push(...fallbackFonts);
66
81
  return selected;
67
82
  }
68
- const _warnedFontKeys = /* @__PURE__ */ new Set();
69
83
  export function loadAllFontsDebug(component) {
70
84
  const map = getComponentFontMap();
71
85
  const componentReqs = component ? map[component] : null;
@@ -122,6 +136,10 @@ export async function loadAllFonts(event, options) {
122
136
  }
123
137
  if (!data)
124
138
  return null;
139
+ if (!options.supportedFormats.has("woff2") && isWoff2Binary(data)) {
140
+ warnOnce(`woff2-${src}`, `OG images skip font "${f.family}" (${src}) for this renderer because it is a WOFF2 file. Satori reads TTF, OTF and WOFF.`);
141
+ return null;
142
+ }
125
143
  return {
126
144
  ...f,
127
145
  src,
@@ -183,7 +201,7 @@ function normalizeFontWeight(weight) {
183
201
  return 400;
184
202
  return Math.min(Math.max(Math.round(n), 1), 1e3);
185
203
  }
186
- export async function loadDefinedFonts(event, fontDefs) {
204
+ export async function loadDefinedFonts(event, fontDefs, supportedFormats) {
187
205
  const results = [];
188
206
  const defs = Array.isArray(fontDefs) ? fontDefs : [];
189
207
  if (import.meta.dev && defs.length > MAX_DEFINED_FONTS)
@@ -201,19 +219,30 @@ export async function loadDefinedFonts(event, fontDefs) {
201
219
  const data = await resolve(event.e, fontConfig).catch(() => {
202
220
  return null;
203
221
  });
204
- if (data) {
205
- results.push({
206
- ...fontConfig,
207
- cacheKey: `custom-${name}-${weight}-${style}-${path}`,
208
- data
209
- });
222
+ if (!data)
223
+ continue;
224
+ const binary = await parseFontBinary(data);
225
+ if (binary._tag === "Err") {
226
+ if (import.meta.dev)
227
+ warnOnce(`binary-${path}`, `defineOgImage() font "${name}" is skipped because ${binary.reason}. Use a TTF, OTF, WOFF or WOFF2 file.`);
228
+ continue;
210
229
  }
230
+ if (!supportedFormats.has("woff2") && isWoff2Binary(binary.data)) {
231
+ if (import.meta.dev)
232
+ warnOnce(`woff2-${path}`, `defineOgImage() font "${name}" is skipped because Satori cannot read WOFF2. Use a TTF, OTF or WOFF file.`);
233
+ continue;
234
+ }
235
+ results.push({
236
+ ...fontConfig,
237
+ cacheKey: `custom-${name}-${weight}-${style}-${path}`,
238
+ data: binary.data
239
+ });
211
240
  }
212
241
  return results;
213
242
  }
214
243
  export async function loadFontsForRenderer(event, options) {
215
244
  const baseFonts = await loadAllFonts(event.e, options);
216
- const customFonts = Array.isArray(options.fontDefs) && options.fontDefs.length > 0 ? await loadDefinedFonts(event, options.fontDefs) : [];
245
+ const customFonts = Array.isArray(options.fontDefs) && options.fontDefs.length > 0 ? await loadDefinedFonts(event, options.fontDefs, options.supportedFormats) : [];
217
246
  const allFonts = customFonts.length > 0 ? [...baseFonts, ...customFonts] : baseFonts;
218
247
  return renameSubsetFonts(allFonts);
219
248
  }
@@ -9,7 +9,7 @@ export function generateMeta(url, resolvedOptions, metaOptions = {}) {
9
9
  const includeTwitter = (key === "og" || key === "twitter") && metaOptions.includeTwitter !== false;
10
10
  const meta = [];
11
11
  if (includeTwitter) {
12
- meta.push({ name: "twitter:card", content: "summary_large_image" });
12
+ meta.push({ name: "twitter:card", content: "summary_large_image", tagPriority: "low" });
13
13
  meta.push({ name: "twitter:image", content: url });
14
14
  }
15
15
  if (!isTwitterOnly) {
@@ -30,6 +30,8 @@ export function generateMeta(url, resolvedOptions, metaOptions = {}) {
30
30
  if (includeTwitter)
31
31
  meta.push({ name: "twitter:image:alt", content: resolvedOptions.alt });
32
32
  }
33
+ for (const tag of meta)
34
+ tag.tagPriority ??= "high";
33
35
  return meta;
34
36
  }
35
37
  export function isInternalRoute(path) {
@@ -2553,7 +2553,12 @@ async function resolveAndDownloadFamily(options) {
2553
2553
  const resolution = await options.resolver(family, override);
2554
2554
  if (!resolution?.fonts?.length)
2555
2555
  return results;
2556
+ const candidates = /* @__PURE__ */ new Map();
2556
2557
  for (const font of resolution.fonts) {
2558
+ const style = font.style || "normal";
2559
+ if (!styles.includes(style))
2560
+ continue;
2561
+ const resolvedWeights = resolveWeightsFromFontEntry(font.weight, weights);
2557
2562
  for (const src of font.src) {
2558
2563
  if (!("url" in src))
2559
2564
  continue;
@@ -2562,23 +2567,22 @@ async function resolveAndDownloadFamily(options) {
2562
2567
  const isTtf = format === "truetype" || format === "ttf";
2563
2568
  if (!isTtf && format !== "woff")
2564
2569
  continue;
2565
- const style = font.style || "normal";
2566
- if (!styles.includes(style))
2567
- continue;
2568
- const resolvedWeights = resolveWeightsFromFontEntry(font.weight, weights);
2569
- if (resolvedWeights.length === 0)
2570
- continue;
2571
- const ext = isTtf ? "ttf" : "woff";
2572
2570
  for (const weight of resolvedWeights) {
2573
- const filename = `${family.replace(RE_NON_ALPHANUMERIC, "_")}-${weight}-${style}.${ext}`;
2574
- const destPath = join(staticFontDir, filename);
2575
- if (!await downloadFontFile(url, destPath))
2576
- continue;
2577
- results.push({ family, weight, style, format, filename, url });
2578
- logger.debug(`Resolved static font: ${family} ${weight}`);
2571
+ const key = `${weight}-${style}`;
2572
+ candidates.set(key, [...candidates.get(key) || [], { weight, style, url, isTtf }]);
2579
2573
  }
2580
2574
  }
2581
2575
  }
2576
+ for (const sources of candidates.values()) {
2577
+ for (const { weight, style, url, isTtf } of sources.toSorted((a, b) => Number(b.isTtf) - Number(a.isTtf))) {
2578
+ const filename = `${family.replace(RE_NON_ALPHANUMERIC, "_")}-${weight}-${style}.${isTtf ? "ttf" : "woff"}`;
2579
+ if (!await downloadFontFile(url, join(staticFontDir, filename)))
2580
+ continue;
2581
+ results.push({ family, weight, style, format: isTtf ? "truetype" : "woff", filename, url });
2582
+ logger.debug(`Resolved static font: ${family} ${weight}`);
2583
+ break;
2584
+ }
2585
+ }
2582
2586
  } catch (err) {
2583
2587
  options.logger.debug(`Failed to resolve fallback font for ${family}:`, err);
2584
2588
  }
@@ -2912,6 +2916,7 @@ function getMissingSiteUrlWarning(input) {
2912
2916
  function setupPrerenderHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt()) {
2913
2917
  nuxt.hooks.hook("nitro:init", async (nitro) => {
2914
2918
  nitro.hooks.hook("prerender:config", async (nitroConfig) => {
2919
+ nitroConfig.plugins = [...nitroConfig.plugins || [], resolve.resolve("./runtime/server/plugins/prerender")];
2915
2920
  await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve, detectedRenderers: getDetectedRenderers() });
2916
2921
  nitroConfig.wasm = nitroConfig.wasm || {};
2917
2922
  nitroConfig.wasm.esmImport = false;
@@ -6120,7 +6125,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6120
6125
  });
6121
6126
  });
6122
6127
  }
6123
- nuxt.options.nitro.virtual["#og-image/fonts"] = async () => {
6128
+ async function resolveFontList() {
6124
6129
  builtFontKey = currentFontKey();
6125
6130
  await loadCssMetadata();
6126
6131
  if (!fontProcessingDone && hasSatoriRenderer() && hasNuxtFonts) {
@@ -6137,7 +6142,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6137
6142
  });
6138
6143
  fontProcessingDone = true;
6139
6144
  }
6140
- const fonts = await resolveOgImageFonts({
6145
+ return await resolveOgImageFonts({
6141
6146
  nuxt,
6142
6147
  hasNuxtFonts,
6143
6148
  hasSatoriRenderer: hasSatoriRenderer(),
@@ -6149,8 +6154,15 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6149
6154
  logger,
6150
6155
  ogFontsDir: resolve("./runtime/public/_og-fonts")
6151
6156
  });
6152
- return `export default ${JSON.stringify(fonts)}`;
6153
- };
6157
+ }
6158
+ let builtFontList;
6159
+ function getFontList() {
6160
+ const key = currentFontKey();
6161
+ if (nuxt.options.dev || builtFontList?.key !== key)
6162
+ builtFontList = { key, fonts: resolveFontList() };
6163
+ return builtFontList.fonts;
6164
+ }
6165
+ nuxt.options.nitro.virtual["#og-image/fonts"] = async () => `export default ${JSON.stringify(await getFontList())}`;
6154
6166
  nuxt.options.nitro.virtual["#og-image/fonts-available"] = async () => {
6155
6167
  const fonts = hasNuxtFonts ? await getResolvedNuxtFonts(nuxt, { fontState, requiredWeights: fontRequirementsState.weights }) : [];
6156
6168
  return `export default ${JSON.stringify(fonts)}`;
@@ -6230,7 +6242,8 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6230
6242
  });
6231
6243
  fontProcessingDone = true;
6232
6244
  });
6233
- nuxt.hook("nitro:build:public-assets", (nitro) => {
6245
+ nuxt.hook("nitro:build:public-assets", async (nitro) => {
6246
+ await getFontList();
6234
6247
  copyStaticFontsToOutput({
6235
6248
  buildDir: nuxt.options.buildDir,
6236
6249
  bundledFontsDir: resolve("./runtime/public/_og-fonts"),
@@ -6404,8 +6417,6 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6404
6417
  nuxt.options.nitro.virtual["#og-image-virtual/signing.mjs"] = () => `export const rendersImages = ${getDetectedRenderers().size > 0}`;
6405
6418
  addServerHandler({ middleware: true, handler: resolve("./runtime/server/middleware/signing") });
6406
6419
  }
6407
- if (nuxt.options.build)
6408
- addServerPlugin(resolve("./runtime/server/plugins/prerender"));
6409
6420
  if (nuxt.options.build && config.publish && !config.zeroRuntime)
6410
6421
  addServerPlugin(resolve("./runtime/server/plugins/publish"));
6411
6422
  if (nuxt.options.dev)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-og-image",
3
3
  "type": "module",
4
- "version": "7.0.2",
4
+ "version": "7.0.4",
5
5
  "description": "Enlightened OG Image generation for Nuxt.",
6
6
  "author": {
7
7
  "website": "https://harlanzw.com",
@@ -88,7 +88,7 @@ To use an existing image, call `useSeoMeta({ ogImage: '/cover.png' })`. The v5 `
88
88
 
89
89
  ## Fonts
90
90
 
91
- Custom fonts load only through `@nuxt/fonts`, and each family needs `global: true`. CSS `@font-face` rules and font CDN links do not reach the renderer. Local fonts: Takumi reads woff2 and ttf; Satori reads woff, ttf, and otf.
91
+ Custom fonts load only through `@nuxt/fonts`, and each family needs `global: true`. CSS `@font-face` rules and font CDN links do not reach the renderer. Local fonts: Takumi reads woff2, woff, ttf, and otf; Satori reads woff, ttf, and otf.
92
92
 
93
93
  ```ts
94
94
  export default defineNuxtConfig({