nuxt-og-image 6.7.6 → 6.7.7

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 (32) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/chunks/decode.mjs +1087 -0
  3. package/dist/chunks/tw4.mjs +1 -1
  4. package/dist/chunks/uno.mjs +2 -2
  5. package/dist/module.d.ts +1 -1
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +1 -1
  8. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +5 -6
  9. package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
  10. package/dist/runtime/server/og-image/bindings/font-assets/node.js +2 -3
  11. package/dist/runtime/server/og-image/browser/screenshot.js +2 -1
  12. package/dist/runtime/server/og-image/font-source.d.ts +3 -12
  13. package/dist/runtime/server/og-image/font-source.js +1 -4
  14. package/dist/runtime/server/og-image/fonts.d.ts +0 -7
  15. package/dist/runtime/server/og-image/fonts.js +1 -1
  16. package/dist/runtime/server/og-image/takumi/renderer.js +2 -2
  17. package/dist/runtime/server/routes/resolve.js +2 -1
  18. package/dist/runtime/server/util/fetchLocalAsset.d.ts +1 -1
  19. package/dist/runtime/server/util/fetchLocalAsset.js +2 -1
  20. package/dist/runtime/types.d.ts +1 -1
  21. package/dist/shared/{nuxt-og-image.BIbfn5xQ.mjs → nuxt-og-image.BMHzg1Vz.mjs} +227 -76
  22. package/package.json +21 -22
  23. package/dist/chunks/tw4.cjs +0 -493
  24. package/dist/chunks/uno.cjs +0 -249
  25. package/dist/cli.cjs +0 -1345
  26. package/dist/cli.d.cts +0 -1
  27. package/dist/content.cjs +0 -21
  28. package/dist/content.d.cts +0 -16
  29. package/dist/module.cjs +0 -39
  30. package/dist/module.d.cts +0 -219
  31. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
  32. package/dist/shared/nuxt-og-image.CdpC3njh.cjs +0 -5898
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, rmSync } from 'node:fs';
4
4
  import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
5
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { resolvePath, useNuxt, addTemplate, updateTemplates, directoryToURL, tryResolveModule, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addBuildPlugin, addServerHandler, addPlugin, addComponentsDir, getNuxtVersion, addServerPlugin } from '@nuxt/kit';
7
7
  import { defu } from 'defu';
8
8
  import { fnv1a64Base36 } from 'fnv1a-64';
@@ -868,18 +868,18 @@ ${content}`, "utf-8");
868
868
  if (isCssChange || normalizedPath.includes("uno.config")) {
869
869
  clearTimeout(cssRefreshTimer);
870
870
  cssRefreshTimer = setTimeout(() => {
871
- safeBroadcast(() => rpc.broadcast.refresh());
871
+ safeBroadcast(() => rpc.broadcast.refresh.asEvent());
872
872
  }, 200);
873
873
  return;
874
874
  }
875
875
  if ((e === "change" || e.includes("link")) && (normalizedPath.startsWith("pages") || normalizedPath.startsWith("content"))) {
876
- safeBroadcast(() => rpc.broadcast.refreshRouteData(normalizedPath));
876
+ safeBroadcast(() => rpc.broadcast.refreshRouteData.asEvent(normalizedPath));
877
877
  }
878
878
  if (options.componentDirs.some((dir) => normalizedPath.includes(dir))) {
879
879
  if (e === "change")
880
- safeBroadcast(() => rpc.broadcast.refresh());
880
+ safeBroadcast(() => rpc.broadcast.refresh.asEvent());
881
881
  else
882
- safeBroadcast(() => rpc.broadcast.refreshGlobalData());
882
+ safeBroadcast(() => rpc.broadcast.refreshGlobalData.asEvent());
883
883
  }
884
884
  });
885
885
  });
@@ -1700,6 +1700,9 @@ const RE_FONT_EXT = /\.(?:woff2?|ttf|otf)$/i;
1700
1700
  const RE_NON_WORD = /\W+/g;
1701
1701
  const FONTS_URL_PREFIX = "/_fonts";
1702
1702
  const STATIC_FONTS_PREFIX = "/_og-static-fonts";
1703
+ function getStaticFontCacheDir(buildDir) {
1704
+ return join(buildDir, "cache", "og-image", "static-fonts");
1705
+ }
1703
1706
  function fontKey(f) {
1704
1707
  return `${f.family}-${f.weight}-${f.style}-${f.unicodeRange || "default"}`;
1705
1708
  }
@@ -1883,7 +1886,9 @@ function parseConfiguredLocalFonts(nuxt) {
1883
1886
  }
1884
1887
  async function parseFontsFromTemplate(nuxt, options) {
1885
1888
  const weightsKey = options.requiredWeights?.toSorted((a, b) => a - b).join(",") || "";
1886
- const cacheKey = `${options.convertedWoff2Files.size}:${[...options.convertedWoff2Files.keys()].toSorted().join(",")}:${weightsKey}`;
1889
+ const sourceKey = [...options.fontState.sourceMap.entries()].toSorted(([a], [b]) => a.localeCompare(b)).flat().join(",");
1890
+ const fallbackKey = [...options.fontState.fallbackMap.entries()].toSorted(([a], [b]) => a.localeCompare(b)).flat().join(",");
1891
+ const cacheKey = `${sourceKey}:${fallbackKey}:${weightsKey}`;
1887
1892
  const cache = nuxt._ogImageParsedFontsCache ||= /* @__PURE__ */ new Map();
1888
1893
  const cached = cache.get(cacheKey);
1889
1894
  if (cached)
@@ -1904,11 +1909,11 @@ async function parseFontsFromTemplate(nuxt, options) {
1904
1909
  }
1905
1910
  }
1906
1911
  const fontlessFamilies = new Set(
1907
- [...fontMap.values()].filter((f) => options.convertedWoff2Files.has(`${f.family}-${f.weight}-${f.style}`)).map((f) => f.family)
1912
+ [...fontMap.values()].filter((f) => options.fontState.fallbackMap.has(`${f.family}-${f.weight}-${f.style}`)).map((f) => f.family)
1908
1913
  );
1909
1914
  const expandedFonts = [...fontMap.values()];
1910
1915
  const existingKeys = new Set(expandedFonts.map((f) => `${f.family}-${f.weight}-${f.style}`));
1911
- for (const [key] of options.convertedWoff2Files) {
1916
+ for (const [key] of options.fontState.fallbackMap) {
1912
1917
  if (existingKeys.has(key))
1913
1918
  continue;
1914
1919
  const match = key.match(RE_FONT_KEY);
@@ -1942,18 +1947,18 @@ async function parseFontsFromTemplate(nuxt, options) {
1942
1947
  }
1943
1948
  }
1944
1949
  }
1945
- const defaultUnicodeRange = "U+0-FF, U+131, U+152-153, U+2BB-2BC, U+2C6, U+2DA, U+2DC, U+304, U+308, U+329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD";
1946
1950
  const result = expandedFonts.map((font) => {
1947
1951
  const fKey = `${font.family}-${font.weight}-${font.style}`;
1948
- const fontlessPath = options.convertedWoff2Files.get(fKey);
1949
- const satoriSrc = fontlessPath || (font.isWoff2 ? void 0 : fontlessFamilies.has(font.family) ? void 0 : font.src);
1952
+ const convertedSource = options.fontState.sourceMap.get(font.src);
1953
+ const fontlessPath = options.fontState.fallbackMap.get(fKey);
1954
+ const satoriSrc = convertedSource || fontlessPath || (font.isWoff2 ? void 0 : fontlessFamilies.has(font.family) ? void 0 : font.src);
1950
1955
  return {
1951
1956
  family: font.family,
1952
1957
  src: font.src,
1953
1958
  weight: font.weight,
1954
1959
  style: font.style,
1955
1960
  satoriSrc,
1956
- unicodeRange: font.unicodeRange || defaultUnicodeRange,
1961
+ unicodeRange: font.unicodeRange,
1957
1962
  subset: font.subset,
1958
1963
  weightRange: font.weightRange
1959
1964
  };
@@ -2005,10 +2010,10 @@ function copyStaticFontsToOutput(options) {
2005
2010
  count++;
2006
2011
  }
2007
2012
  }
2008
- const ttfSourceDir = join(buildDir, "cache", "og-image", "fonts-ttf");
2009
- if (existsSync(ttfSourceDir)) {
2010
- for (const file of fs.readdirSync(ttfSourceDir).filter((f) => f.endsWith(".ttf") || f.endsWith(".woff"))) {
2011
- fs.copyFileSync(join(ttfSourceDir, file), join(outputDir, file));
2013
+ const staticFontDir = getStaticFontCacheDir(buildDir);
2014
+ if (existsSync(staticFontDir)) {
2015
+ for (const file of fs.readdirSync(staticFontDir).filter((f) => f.endsWith(".ttf") || f.endsWith(".woff"))) {
2016
+ fs.copyFileSync(join(staticFontDir, file), join(outputDir, file));
2012
2017
  count++;
2013
2018
  }
2014
2019
  }
@@ -2028,6 +2033,20 @@ function isConfiguredLocalFontFamily(nuxt, family) {
2028
2033
  const config = getNuxtFontsFamilyConfig(nuxt, family);
2029
2034
  return !!config && config.global === true && (config.provider === "local" || typeof config.src === "string");
2030
2035
  }
2036
+ function getFamilyRequirements(fontRequirements, family) {
2037
+ const familyName = family.toLowerCase();
2038
+ const matchingComponents = Object.values(fontRequirements.componentMap).filter((component) => component.families.some((candidate) => candidate.toLowerCase() === familyName));
2039
+ if (matchingComponents.length === 0 || matchingComponents.some((component) => component.hasDynamicBindings)) {
2040
+ return {
2041
+ weights: fontRequirements.weights,
2042
+ styles: fontRequirements.styles
2043
+ };
2044
+ }
2045
+ return {
2046
+ weights: [...new Set(matchingComponents.flatMap((component) => component.weights))],
2047
+ styles: [...new Set(matchingComponents.flatMap((component) => component.styles))]
2048
+ };
2049
+ }
2031
2050
  async function initFontless(options) {
2032
2051
  if (getFontlessContext(options.nuxt))
2033
2052
  return;
@@ -2085,7 +2104,7 @@ async function initFontless(options) {
2085
2104
  weights: [400, 700],
2086
2105
  styles: ["normal", "italic"],
2087
2106
  subsets: options.fontSubsets || ["latin"],
2088
- // Satori/Takumi can't reliably use WOFF2 subsets — request static formats
2107
+ // Static fallbacks must use formats supported by both renderers.
2089
2108
  formats: ["woff", "ttf"]
2090
2109
  }
2091
2110
  },
@@ -2127,8 +2146,8 @@ function resolveWeightsFromFontEntry(fontWeight, requestedWeights) {
2127
2146
  async function downloadStaticFonts(options) {
2128
2147
  if (options.families.length === 0)
2129
2148
  return [];
2130
- const ttfDir = join(options.nuxt.options.buildDir, "cache", "og-image", "fonts-ttf");
2131
- fs.mkdirSync(ttfDir, { recursive: true });
2149
+ const staticFontDir = getStaticFontCacheDir(options.nuxt.options.buildDir);
2150
+ fs.mkdirSync(staticFontDir, { recursive: true });
2132
2151
  await initFontless({ nuxt: options.nuxt, logger: options.logger, fontSubsets: options.fontSubsets });
2133
2152
  const fontlessCtx = getFontlessContext(options.nuxt);
2134
2153
  if (!fontlessCtx) {
@@ -2143,7 +2162,7 @@ async function downloadStaticFonts(options) {
2143
2162
  family,
2144
2163
  weights,
2145
2164
  styles,
2146
- ttfDir,
2165
+ staticFontDir,
2147
2166
  resolver: fontlessCtx.resolver,
2148
2167
  logger: options.logger
2149
2168
  });
@@ -2155,7 +2174,7 @@ async function downloadStaticFonts(options) {
2155
2174
  if (hasVariableBinary && weights.length > 1) {
2156
2175
  options.logger.debug(`${family}: provider returned variable font binary, retrying with alternative providers`);
2157
2176
  for (const r of familyResults) {
2158
- const filePath = join(ttfDir, r.filename);
2177
+ const filePath = join(staticFontDir, r.filename);
2159
2178
  if (fs.existsSync(filePath))
2160
2179
  fs.unlinkSync(filePath);
2161
2180
  }
@@ -2165,7 +2184,7 @@ async function downloadStaticFonts(options) {
2165
2184
  family,
2166
2185
  weights,
2167
2186
  styles,
2168
- ttfDir,
2187
+ staticFontDir,
2169
2188
  logger: options.logger,
2170
2189
  resolver: fontlessCtx.resolver,
2171
2190
  provider
@@ -2176,7 +2195,7 @@ async function downloadStaticFonts(options) {
2176
2195
  for (const r of altResults) altUrls.set(r.url, [...altUrls.get(r.url) || [], r.weight]);
2177
2196
  if ([...altUrls.values()].some((ws) => ws.length > 1)) {
2178
2197
  for (const r of altResults) {
2179
- const filePath = join(ttfDir, r.filename);
2198
+ const filePath = join(staticFontDir, r.filename);
2180
2199
  if (fs.existsSync(filePath))
2181
2200
  fs.unlinkSync(filePath);
2182
2201
  }
@@ -2255,7 +2274,7 @@ async function listLocalPublicFontFiles(nuxt) {
2255
2274
  return out;
2256
2275
  }
2257
2276
  async function resolveAndDownloadFamily(options) {
2258
- const { family, weights, styles, ttfDir, logger } = options;
2277
+ const { family, weights, styles, staticFontDir, logger } = options;
2259
2278
  const results = [];
2260
2279
  try {
2261
2280
  const override = options.provider ? { name: family, weights, styles, provider: options.provider } : { name: family, weights, styles };
@@ -2281,7 +2300,7 @@ async function resolveAndDownloadFamily(options) {
2281
2300
  const ext = isTtf ? "ttf" : "woff";
2282
2301
  for (const weight of resolvedWeights) {
2283
2302
  const filename = `${family.replace(RE_NON_ALPHANUMERIC, "_")}-${weight}-${style}.${ext}`;
2284
- const destPath = join(ttfDir, filename);
2303
+ const destPath = join(staticFontDir, filename);
2285
2304
  if (!await downloadFontFile(url, destPath))
2286
2305
  continue;
2287
2306
  results.push({ family, weight, style, format, filename, url });
@@ -2294,51 +2313,175 @@ async function resolveAndDownloadFamily(options) {
2294
2313
  }
2295
2314
  return results;
2296
2315
  }
2297
- async function convertWoff2ToTtf(options) {
2298
- const { nuxt, logger, fontRequirements, convertedWoff2Files, fontSubsets, warnOnMissingStaticFonts = true } = options;
2299
- const parsedFonts = await parseFontsFromTemplate(nuxt, { convertedWoff2Files });
2316
+ function hasOpenTypeTable(data, expectedTag) {
2317
+ if (data.byteLength < 12)
2318
+ return false;
2319
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
2320
+ const tableCount = view.getUint16(4);
2321
+ for (let index = 0; index < tableCount; index++) {
2322
+ const offset = 12 + index * 16;
2323
+ if (offset + 4 > data.byteLength)
2324
+ return false;
2325
+ const tag = String.fromCharCode(
2326
+ view.getUint8(offset),
2327
+ view.getUint8(offset + 1),
2328
+ view.getUint8(offset + 2),
2329
+ view.getUint8(offset + 3)
2330
+ );
2331
+ if (tag === expectedTag)
2332
+ return true;
2333
+ }
2334
+ return false;
2335
+ }
2336
+ function getNuxtFontOriginalSource(fontSrc, context) {
2337
+ if (!context)
2338
+ return;
2339
+ const prefix = `${context.assetsBaseURL.replace(/\/$/, "")}/`;
2340
+ if (!fontSrc.startsWith(prefix))
2341
+ return;
2342
+ return context.renderedFontURLs.get(fontSrc.slice(prefix.length));
2343
+ }
2344
+ async function readNuxtFontAsset(nuxt, source) {
2345
+ if (source.startsWith("/")) {
2346
+ const configuredPublicDir = nuxt.options.dir.public || "public";
2347
+ const publicDir = isAbsolute(configuredPublicDir) ? configuredPublicDir : join(nuxt.options.srcDir, configuredPublicDir);
2348
+ const path = join(publicDir, source.slice(1).split(/[?#]/, 1)[0]);
2349
+ return fs.promises.readFile(path).then((data) => ({ _tag: "Ok", data })).catch((error) => ({
2350
+ _tag: "Err",
2351
+ reason: error.code === "ENOENT" ? `local source not found: ${path}` : error.message
2352
+ }));
2353
+ }
2354
+ if (source.startsWith("file:")) {
2355
+ const path = fileURLToPath(source);
2356
+ return fs.promises.readFile(path).then((data) => ({ _tag: "Ok", data })).catch((error) => ({ _tag: "Err", reason: error.message }));
2357
+ }
2358
+ return fetch(source).then(async (response) => response.ok ? { _tag: "Ok", data: new Uint8Array(await response.arrayBuffer()) } : { _tag: "Err", reason: `source returned HTTP ${response.status}` }).catch((error) => ({ _tag: "Err", reason: error.message }));
2359
+ }
2360
+ async function convertNuxtWoff2Sources(options) {
2361
+ const staticFontDir = getStaticFontCacheDir(options.nuxt.options.buildDir);
2362
+ fs.mkdirSync(staticFontDir, { recursive: true });
2363
+ const fontsBySource = /* @__PURE__ */ new Map();
2364
+ for (const font of options.fonts) {
2365
+ if (!font.weightRange && !fontsBySource.has(font.src))
2366
+ fontsBySource.set(font.src, font);
2367
+ }
2368
+ const mappedSources = [];
2369
+ for (const [fontSrc, font] of fontsBySource) {
2370
+ const originalSource = getNuxtFontOriginalSource(fontSrc, options.context) || (isConfiguredLocalFontFamily(options.nuxt, font.family) ? fontSrc : void 0);
2371
+ if (originalSource)
2372
+ mappedSources.push({ font, fontSrc, originalSource });
2373
+ else
2374
+ options.logger.debug(`Could not map Nuxt Fonts asset to its original source: ${fontSrc}`);
2375
+ }
2376
+ if (mappedSources.length === 0)
2377
+ return;
2378
+ const { woff2Decode } = await import('../chunks/decode.mjs');
2379
+ for (const { fontSrc, font, originalSource } of mappedSources) {
2380
+ const asset = await readNuxtFontAsset(options.nuxt, originalSource);
2381
+ if (asset._tag === "Err") {
2382
+ options.logger.debug(`Could not read Nuxt Fonts asset ${fontSrc}: ${asset.reason}`);
2383
+ continue;
2384
+ }
2385
+ const decoded = await Promise.resolve().then(() => woff2Decode(asset.data)).catch((error) => {
2386
+ options.logger.warn(`Failed to decode Nuxt Fonts asset ${fontSrc}: ${error.message}`);
2387
+ return null;
2388
+ });
2389
+ if (!decoded)
2390
+ continue;
2391
+ if (hasOpenTypeTable(decoded, "fvar")) {
2392
+ options.logger.debug(`${font.family}: ${fontSrc} is variable; a static Satori fallback is required`);
2393
+ continue;
2394
+ }
2395
+ const sourceFilename = fontSrc.split("/").pop();
2396
+ const filename = sourceFilename.replace(/\.woff2$/i, ".ttf");
2397
+ await fs.promises.writeFile(join(staticFontDir, filename), decoded);
2398
+ options.fontState.sourceMap.set(fontSrc, `${STATIC_FONTS_PREFIX}/${filename}`);
2399
+ }
2400
+ if (options.fontState.sourceMap.size > 0)
2401
+ options.logger.debug(`Converted ${options.fontState.sourceMap.size} Nuxt Fonts WOFF2 assets to TTF`);
2402
+ }
2403
+ async function prepareWoff2Fonts(options) {
2404
+ const { nuxt, logger, fontRequirements, fontState, nuxtFontsContext, fontSubsets, warnOnMissingStaticFonts = true } = options;
2405
+ const parsedFonts = await parseFontsFromTemplate(nuxt, { fontState });
2406
+ const requirementsByFamily = /* @__PURE__ */ new Map();
2407
+ for (const font of parsedFonts) {
2408
+ if (!requirementsByFamily.has(font.family))
2409
+ requirementsByFamily.set(font.family, getFamilyRequirements(fontRequirements, font.family));
2410
+ }
2300
2411
  const hasNonWoff2 = new Set(
2301
2412
  parsedFonts.filter((f) => !f.src.endsWith(".woff2")).map((f) => fontKey(f))
2302
2413
  );
2303
- const woff2Fonts = parsedFonts.filter(
2304
- (f) => f.src.endsWith(".woff2") && !hasNonWoff2.has(fontKey(f)) && !isConfiguredLocalFontFamily(nuxt, f.family) && fontRequirements.weights.includes(f.weight) && fontRequirements.styles.includes(f.style)
2305
- );
2414
+ const woff2Fonts = parsedFonts.filter((font) => {
2415
+ const requirements = requirementsByFamily.get(font.family);
2416
+ return font.src.endsWith(".woff2") && !hasNonWoff2.has(fontKey(font)) && requirements.weights.includes(font.weight) && requirements.styles.includes(font.style);
2417
+ });
2306
2418
  if (woff2Fonts.length === 0) {
2307
2419
  logger.debug("No WOFF2 fonts to process");
2308
2420
  return;
2309
2421
  }
2310
- const familyMap = /* @__PURE__ */ new Map();
2422
+ await convertNuxtWoff2Sources({
2423
+ fonts: woff2Fonts,
2424
+ nuxt,
2425
+ context: nuxtFontsContext,
2426
+ fontState,
2427
+ logger
2428
+ });
2429
+ const configuredLocalFamilies = new Set(
2430
+ woff2Fonts.filter((font) => isConfiguredLocalFontFamily(nuxt, font.family)).map((font) => font.family)
2431
+ );
2432
+ const unresolvedLocalFamilies = new Set(
2433
+ woff2Fonts.filter((font) => configuredLocalFamilies.has(font.family) && !fontState.sourceMap.has(font.src)).map((font) => font.family)
2434
+ );
2435
+ if (warnOnMissingStaticFonts) {
2436
+ for (const family of unresolvedLocalFamilies)
2437
+ logger.warn(`Configured Nuxt Fonts assets for "${family}" could not be converted for Satori. Satori does not support WOFF2 or variable fonts; use static WOFF/TTF sources or the Takumi renderer.`);
2438
+ }
2439
+ const unavailableStaticFonts = new Set(
2440
+ woff2Fonts.filter((font) => !fontState.sourceMap.has(font.src)).map((font) => `${font.family}-${font.weight}-${font.style}`)
2441
+ );
2442
+ const availableStaticFonts = new Set(
2443
+ parsedFonts.filter((font) => !unavailableStaticFonts.has(`${font.family}-${font.weight}-${font.style}`)).filter((font) => !font.src.endsWith(".woff2") || fontState.sourceMap.has(font.src)).map((font) => `${font.family}-${font.weight}-${font.style}`)
2444
+ );
2445
+ const fallbackRequests = /* @__PURE__ */ new Map();
2311
2446
  for (const font of woff2Fonts) {
2312
- const existing = familyMap.get(font.family) || { weights: new Set(fontRequirements.weights), styles: /* @__PURE__ */ new Set() };
2313
- existing.styles.add(font.style);
2314
- familyMap.set(font.family, existing);
2447
+ if (configuredLocalFamilies.has(font.family))
2448
+ continue;
2449
+ const style = font.style;
2450
+ const requirements = requirementsByFamily.get(font.family);
2451
+ const key = `${font.family}\0${style}`;
2452
+ const request = fallbackRequests.get(key) || { family: font.family, weights: /* @__PURE__ */ new Set(), style };
2453
+ for (const weight of requirements.weights) {
2454
+ if (!availableStaticFonts.has(`${font.family}-${weight}-${style}`))
2455
+ request.weights.add(weight);
2456
+ }
2457
+ fallbackRequests.set(key, request);
2315
2458
  }
2316
- const families = Array.from(familyMap.entries(), ([family, { weights, styles }]) => ({
2459
+ const families = Array.from(fallbackRequests.values(), ({ family, weights, style }) => ({
2317
2460
  family,
2318
2461
  weights: [...weights],
2319
- styles: [...styles]
2320
- }));
2321
- logger.debug(`Resolving static fonts for: ${families.map((f) => f.family).join(", ")}`);
2322
- try {
2323
- const downloaded = await downloadStaticFonts({
2324
- families,
2325
- nuxt,
2326
- logger,
2327
- fontSubsets,
2328
- warnOnMissingStaticFonts
2329
- });
2330
- for (const font of downloaded) {
2331
- const key = `${font.family}-${font.weight}-${font.style}`;
2332
- convertedWoff2Files.set(key, `${STATIC_FONTS_PREFIX}/${font.filename}`);
2333
- }
2334
- if (convertedWoff2Files.size > 0) {
2335
- logger.debug(`Resolved ${convertedWoff2Files.size} static font files via fontless`);
2336
- } else if (warnOnMissingStaticFonts) {
2337
- logger.warn(`No static fonts available for Satori. Falling back to bundled Inter font. Consider using 'takumi' renderer for variable font support.`);
2338
- }
2339
- } catch (err) {
2340
- logger.debug("fontless resolution failed:", err);
2462
+ styles: [style]
2463
+ })).filter((family) => family.weights.length > 0);
2464
+ if (families.length === 0)
2465
+ return;
2466
+ logger.debug(`Resolving static font fallbacks for: ${families.map((f) => f.family).join(", ")}`);
2467
+ const downloaded = await downloadStaticFonts({
2468
+ families,
2469
+ nuxt,
2470
+ logger,
2471
+ fontSubsets,
2472
+ warnOnMissingStaticFonts
2473
+ }).catch((error) => {
2474
+ logger.debug("fontless resolution failed:", error);
2475
+ return [];
2476
+ });
2477
+ for (const font of downloaded) {
2478
+ const key = `${font.family}-${font.weight}-${font.style}`;
2479
+ fontState.fallbackMap.set(key, `${STATIC_FONTS_PREFIX}/${font.filename}`);
2341
2480
  }
2481
+ if (fontState.fallbackMap.size > 0)
2482
+ logger.debug(`Resolved ${fontState.fallbackMap.size} static font fallbacks via fontless`);
2483
+ else if (warnOnMissingStaticFonts && fontState.sourceMap.size === 0)
2484
+ logger.warn(`No static fonts available for Satori. Falling back to bundled Inter font. Consider using the Takumi renderer for variable font support.`);
2342
2485
  }
2343
2486
  async function resolveMissingFontFamilies(options) {
2344
2487
  const { missingFamilies, weights, styles, nuxt, logger, fontSubsets } = options;
@@ -2356,9 +2499,9 @@ async function resolveMissingFontFamilies(options) {
2356
2499
  return results;
2357
2500
  }
2358
2501
  async function resolveOgImageFonts(options) {
2359
- const { nuxt, hasNuxtFonts, hasSatoriRenderer, hasTakumiRenderer, convertedWoff2Files, fontSubsets, fontRequirements, tw4FontVars, logger, ogFontsDir } = options;
2502
+ const { nuxt, hasNuxtFonts, hasSatoriRenderer, hasTakumiRenderer, fontState, fontSubsets, fontRequirements, tw4FontVars, logger, ogFontsDir } = options;
2360
2503
  const staticInterFonts = getStaticInterFonts(ogFontsDir);
2361
- const allFonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { convertedWoff2Files, requiredWeights: fontRequirements.weights }) : [];
2504
+ const allFonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirements.weights }) : [];
2362
2505
  if (hasNuxtFonts) {
2363
2506
  const configuredLocalFonts = parseConfiguredLocalFonts(nuxt);
2364
2507
  if (configuredLocalFonts.length > 0) {
@@ -2413,7 +2556,7 @@ async function resolveOgImageFonts(options) {
2413
2556
  }
2414
2557
  }
2415
2558
  const nuxtFontFamilies = new Set(
2416
- hasNuxtFonts ? (await parseFontsFromTemplate(nuxt, { convertedWoff2Files, requiredWeights: fontRequirements.weights })).map((f) => f.family) : []
2559
+ hasNuxtFonts ? (await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirements.weights })).map((f) => f.family) : []
2417
2560
  );
2418
2561
  const fonts = !fontRequirements.hasDynamicBindings ? allFonts.filter(
2419
2562
  (f) => nuxtFontFamilies.has(f.family) ? fontRequirements.styles.includes(f.style) : matchesFontRequirements(f, fontRequirements)
@@ -4581,6 +4724,7 @@ declare module '#og-image-virtual/component-names.mjs' {
4581
4724
  declare module '#og-image-virtual/build-dir.mjs' {
4582
4725
  export const buildDir: string
4583
4726
  export const rootDir: string
4727
+ export const staticFontCacheDir: string
4584
4728
  }
4585
4729
 
4586
4730
  declare module '#og-image/compatibility' {
@@ -4666,8 +4810,8 @@ declare module '#og-image-cache' {
4666
4810
  NitroApp: `_ogImageCacheBackendWarned?: boolean
4667
4811
  _ogImageIconsData?: OgImageIconsData`,
4668
4812
  NitroRuntimeConfig: `'nuxt-og-image': OgImageRuntimeConfig
4669
- ogImage?: {
4670
- secret?: string
4813
+ ogImage: {
4814
+ secret: string
4671
4815
  }`
4672
4816
  },
4673
4817
  routeRules: "ogImage?: false | OgImageOptions & Record<string, any>",
@@ -5074,7 +5218,10 @@ Or pin to a specific version in package.json:
5074
5218
  const resolvedOgComponentPaths = [];
5075
5219
  let allNuxtComponents = [];
5076
5220
  const ogImageTransformedFiles = /* @__PURE__ */ new Set();
5077
- const convertedWoff2Files = /* @__PURE__ */ new Map();
5221
+ const fontState = {
5222
+ sourceMap: /* @__PURE__ */ new Map(),
5223
+ fallbackMap: /* @__PURE__ */ new Map()
5224
+ };
5078
5225
  const ogImageComponentCtx = { components: [], detectedRenderers: /* @__PURE__ */ new Set() };
5079
5226
  nuxt.hook("modules:done", () => {
5080
5227
  addVitePlugin(ComponentImportRewritePlugin.vite({
@@ -5101,7 +5248,7 @@ Or pin to a specific version in package.json:
5101
5248
  const p = loadCssMetadata().then(async () => {
5102
5249
  let knownFamilies;
5103
5250
  if (hasNuxtFonts) {
5104
- const resolvedFonts = await parseFontsFromTemplate(nuxt, { convertedWoff2Files });
5251
+ const resolvedFonts = await parseFontsFromTemplate(nuxt, { fontState });
5105
5252
  knownFamilies = buildFontFamilyCanonicalMap(resolvedFonts.map((f) => f.family));
5106
5253
  }
5107
5254
  const families = resolveFontFamilies([...reqs.familyClasses], [...reqs.familyNames], cssMetadata.fontVars, knownFamilies);
@@ -5567,9 +5714,9 @@ export function getIslandHash({ name, props, context, source }) {
5567
5714
  }`;
5568
5715
  };
5569
5716
  nuxt.options.nitro.publicAssets ||= [];
5570
- const staticFontsTtfDir = join(nuxt.options.buildDir, "cache", "og-image", "fonts-ttf");
5717
+ const staticFontCacheDir = getStaticFontCacheDir(nuxt.options.buildDir);
5571
5718
  nuxt.options.nitro.publicAssets.push({
5572
- dir: staticFontsTtfDir,
5719
+ dir: staticFontCacheDir,
5573
5720
  baseURL: "/_og-static-fonts",
5574
5721
  maxAge: 60 * 60 * 24 * 365
5575
5722
  });
@@ -5607,24 +5754,26 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
5607
5754
  if (hasNuxtFonts && fontContext) {
5608
5755
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
5609
5756
  }
5610
- if (!fontProcessingDone && convertedWoff2Files.size === 0 && (hasSatoriRenderer() || hasTakumiRenderer()) && hasNuxtFonts) {
5757
+ if (!fontProcessingDone && hasSatoriRenderer() && hasNuxtFonts) {
5611
5758
  if (pendingFontRequirements.length > 0)
5612
5759
  await Promise.all(pendingFontRequirements);
5613
- await convertWoff2ToTtf({
5760
+ await prepareWoff2Fonts({
5614
5761
  nuxt,
5615
5762
  logger,
5616
5763
  fontRequirements: fontRequirementsState,
5617
- convertedWoff2Files,
5764
+ fontState,
5765
+ nuxtFontsContext: fontContext,
5618
5766
  fontSubsets: config.fontSubsets,
5619
5767
  warnOnMissingStaticFonts: hasSatoriRenderer()
5620
5768
  });
5769
+ fontProcessingDone = true;
5621
5770
  }
5622
5771
  const fonts = await resolveOgImageFonts({
5623
5772
  nuxt,
5624
5773
  hasNuxtFonts,
5625
5774
  hasSatoriRenderer: hasSatoriRenderer(),
5626
5775
  hasTakumiRenderer: hasTakumiRenderer(),
5627
- convertedWoff2Files,
5776
+ fontState,
5628
5777
  fontSubsets: config.fontSubsets,
5629
5778
  fontRequirements: fontRequirementsState,
5630
5779
  tw4FontVars: cssMetadata.fontVars,
@@ -5634,7 +5783,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
5634
5783
  return `export default ${JSON.stringify(fonts)}`;
5635
5784
  };
5636
5785
  nuxt.options.nitro.virtual["#og-image/fonts-available"] = async () => {
5637
- const fonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { convertedWoff2Files, requiredWeights: fontRequirementsState.weights }) : [];
5786
+ const fonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirementsState.weights }) : [];
5638
5787
  return `export default ${JSON.stringify(fonts)}`;
5639
5788
  };
5640
5789
  nuxt.options.nitro.virtual["#og-image/font-requirements"] = async () => {
@@ -5669,7 +5818,8 @@ export const tw4Colors = ${JSON.stringify(cssMetadata.colors)}`;
5669
5818
  };
5670
5819
  nuxt.options.nitro.virtual["#og-image-virtual/build-dir.mjs"] = () => {
5671
5820
  return `export const buildDir = ${JSON.stringify(nuxt.options.buildDir)}
5672
- export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5821
+ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}
5822
+ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.options.buildDir))}`;
5673
5823
  };
5674
5824
  if (hasNuxtFonts) {
5675
5825
  nuxt.hook("fonts:public-asset-context", (ctx) => {
@@ -5677,16 +5827,17 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5677
5827
  });
5678
5828
  nuxt.hook("vite:compiled", async () => {
5679
5829
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
5680
- if (fontProcessingDone || !hasSatoriRenderer() && !hasTakumiRenderer())
5830
+ if (fontProcessingDone || !hasSatoriRenderer())
5681
5831
  return;
5682
5832
  if (pendingFontRequirements.length === 0)
5683
5833
  return;
5684
5834
  await Promise.all(pendingFontRequirements);
5685
- await convertWoff2ToTtf({
5835
+ await prepareWoff2Fonts({
5686
5836
  nuxt,
5687
5837
  logger,
5688
5838
  fontRequirements: fontRequirementsState,
5689
- convertedWoff2Files,
5839
+ fontState,
5840
+ nuxtFontsContext: fontContext,
5690
5841
  fontSubsets: config.fontSubsets,
5691
5842
  warnOnMissingStaticFonts: hasSatoriRenderer()
5692
5843
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-og-image",
3
3
  "type": "module",
4
- "version": "6.7.6",
4
+ "version": "6.7.7",
5
5
  "description": "Enlightened OG Image generation for Nuxt.",
6
6
  "author": {
7
7
  "website": "https://harlanzw.com",
@@ -111,8 +111,8 @@
111
111
  },
112
112
  "dependencies": {
113
113
  "@clack/prompts": "^1.7.0",
114
- "@nuxt/kit": "^4.5.1",
115
- "@vue/compiler-sfc": "^3.5.40",
114
+ "@nuxt/kit": "^4.5.2",
115
+ "@vue/compiler-sfc": "^3.5.41",
116
116
  "chrome-launcher": "^1.2.1",
117
117
  "consola": "^3.4.2",
118
118
  "defu": "^6.1.7",
@@ -123,13 +123,13 @@
123
123
  "magic-string": "^1.1.0",
124
124
  "magicast": "^0.5.4",
125
125
  "mocked-exports": "^0.1.1",
126
- "nuxt-site-config": "^4.1.4",
127
- "nuxtseo-shared": "^5.3.8",
126
+ "nuxt-site-config": "^4.2.0",
127
+ "nuxtseo-shared": "^5.3.12",
128
128
  "nypm": "^0.6.9",
129
129
  "object-identity": "^0.2.3",
130
130
  "ofetch": "^1.5.1",
131
131
  "ohash": "^2.0.11",
132
- "oxc-parser": "^0.142.0",
132
+ "oxc-parser": "^0.143.0",
133
133
  "oxc-walker": "^1.1.1",
134
134
  "pathe": "^2.0.3",
135
135
  "pkg-types": "^2.3.1",
@@ -142,10 +142,10 @@
142
142
  "unplugin": "^3.3.0"
143
143
  },
144
144
  "devDependencies": {
145
- "@antfu/eslint-config": "^9.2.0",
145
+ "@antfu/eslint-config": "^9.3.0",
146
146
  "@fontsource/noto-sans-sc": "^5.3.0",
147
147
  "@iconify-json/carbon": "^1.2.25",
148
- "@iconify-json/logos": "^1.2.11",
148
+ "@iconify-json/logos": "^1.2.12",
149
149
  "@iconify-json/noto": "^1.2.7",
150
150
  "@iconify-json/ri": "^1.2.10",
151
151
  "@iconify-json/tabler": "^1.2.38",
@@ -167,11 +167,11 @@
167
167
  "@resvg/resvg-js": "^2.6.2",
168
168
  "@resvg/resvg-wasm": "^2.6.2",
169
169
  "@tailwindcss/vite": "^4.3.3",
170
- "@takumi-rs/core": "^2.5.4",
170
+ "@takumi-rs/core": "^2.5.11",
171
171
  "@takumi-rs/core-v1": "npm:@takumi-rs/core@1.8.7",
172
- "@takumi-rs/helpers": "^2.5.4",
172
+ "@takumi-rs/helpers": "^2.5.11",
173
173
  "@takumi-rs/helpers-v1": "npm:@takumi-rs/helpers@1.8.7",
174
- "@takumi-rs/wasm": "^2.5.4",
174
+ "@takumi-rs/wasm": "^2.5.11",
175
175
  "@takumi-rs/wasm-v1": "npm:@takumi-rs/wasm@1.8.7",
176
176
  "@unocss/config": "^66.7.5",
177
177
  "@unocss/core": "^66.7.5",
@@ -180,22 +180,21 @@
180
180
  "@unocss/preset-wind": "^66.7.5",
181
181
  "@unocss/preset-wind4": "^66.7.5",
182
182
  "@vitejs/plugin-vue": "^6.0.8",
183
- "@vue/compiler-core": "^3.5.40",
183
+ "@vue/compiler-core": "^3.5.41",
184
184
  "@vueuse/nuxt": "^14.4.0",
185
185
  "birpc": "^4.0.0",
186
- "bumpp": "^12.1.1",
187
- "eslint": "^10.8.0",
186
+ "bumpp": "^12.2.0",
187
+ "eslint": "^10.8.1",
188
188
  "eslint-plugin-harlanzw": "^0.17.1",
189
189
  "fontless": "^0.2.1",
190
- "get-image-colors": "^4.0.1",
191
- "globby": "^16.2.2",
192
- "happy-dom": "^20.11.1",
190
+ "globby": "^16.2.3",
191
+ "happy-dom": "^20.11.2",
193
192
  "hookable": "^6.1.1",
194
193
  "jest-image-snapshot": "^6.5.2",
195
194
  "lightningcss": "^1.33.0",
196
195
  "nitropack": "^2.13.4",
197
- "nuxt": "^4.5.1",
198
- "nuxtseo-layer-devtools": "^5.3.8",
196
+ "nuxt": "^4.5.2",
197
+ "nuxtseo-layer-devtools": "^5.3.12",
199
198
  "playwright": "^1.62.1",
200
199
  "playwright-core": "^1.62.1",
201
200
  "sass": "^1.102.0",
@@ -208,9 +207,9 @@
208
207
  "unifont": "^0.7.4",
209
208
  "unocss": "^66.7.5",
210
209
  "vitest": "^4.1.10",
211
- "vue": "^3.5.40",
210
+ "vue": "^3.5.41",
212
211
  "vue-tsc": "^3.3.9",
213
- "wrangler": "^4.118.0",
212
+ "wrangler": "^4.120.0",
214
213
  "yoga-wasm-web": "^0.3.3",
215
214
  "zod": "^4.4.3"
216
215
  },
@@ -230,7 +229,7 @@
230
229
  "typecheck": "pnpm stub && nuxt typecheck",
231
230
  "test": "pnpm run prepare:fixtures && vitest",
232
231
  "test:run": "pnpm test -- --run && pnpm test:nuxt5",
233
- "test:nuxt5": "pnpm pack --out test/fixtures/nuxt5/nuxt-og-image.tgz && pnpm --dir test/fixtures/nuxt5 install --frozen-lockfile && pnpm --dir test/fixtures/nuxt5 test",
232
+ "test:nuxt5": "pnpm pack --out test/fixtures/nuxt5/nuxt-og-image.tgz && pnpm install --dir test/fixtures/nuxt5 --no-frozen-lockfile --force --update-checksums && pnpm --dir test/fixtures/nuxt5 test",
234
233
  "test:chrome": "HAS_CHROME=true vitest",
235
234
  "test:packed-install": "node scripts/smoke-packed-install.mjs",
236
235
  "test:takumi:e2e": "node scripts/test-takumi-e2e.mjs",