nuxt-og-image 6.10.4 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +10 -14
  2. package/dist/chunks/tw4.mjs +6 -6
  3. package/dist/chunks/uno.mjs +7 -7
  4. package/dist/cli.mjs +4 -4
  5. package/dist/devtools/components/og-image/RendererSelectModal.vue +1 -1
  6. package/dist/devtools/lib/og-image/fonts.ts +38 -0
  7. package/dist/devtools/lib/og-image/types.ts +7 -0
  8. package/dist/devtools/pages/og-image/index.vue +19 -14
  9. package/dist/module.d.mts +14 -10
  10. package/dist/module.d.ts +14 -10
  11. package/dist/module.json +2 -2
  12. package/dist/module.mjs +6 -6
  13. package/dist/runtime/app/client-utils.js +7 -4
  14. package/dist/runtime/app/components/Templates/Community/SaaS.satori.vue +1 -1
  15. package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
  16. package/dist/runtime/app/disabled/components.d.ts +1 -0
  17. package/dist/runtime/app/disabled/components.js +0 -0
  18. package/dist/runtime/app/disabled/composables/defineOgImage.d.ts +1 -0
  19. package/dist/runtime/app/disabled/composables/defineOgImage.js +1 -0
  20. package/dist/runtime/app/disabled/composables/defineOgImageComponent.d.ts +1 -0
  21. package/dist/runtime/app/disabled/composables/defineOgImageComponent.js +1 -0
  22. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.d.ts +1 -0
  23. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.js +1 -0
  24. package/dist/runtime/app/disabled/composables/mock.d.ts +1 -0
  25. package/dist/runtime/app/disabled/composables/mock.js +1 -0
  26. package/dist/runtime/app/disabled/index.d.ts +3 -0
  27. package/dist/runtime/app/disabled/index.js +3 -0
  28. package/dist/runtime/app/index.d.ts +4 -0
  29. package/dist/runtime/app/index.js +4 -0
  30. package/dist/runtime/app/utils/plugins.js +1 -1
  31. package/dist/runtime/app/utils.js +2 -2
  32. package/dist/runtime/server/index.d.ts +1 -0
  33. package/dist/runtime/server/index.js +1 -0
  34. package/dist/runtime/server/middleware/signing.d.ts +2 -0
  35. package/dist/runtime/server/middleware/signing.js +19 -0
  36. package/dist/runtime/server/nitro.d.ts +3 -3
  37. package/dist/runtime/server/nitro.js +4 -4
  38. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -49
  39. package/dist/runtime/server/og-image/bindings/font-assets/node.js +3 -2
  40. package/dist/runtime/server/og-image/bindings/satori/node.d.ts +1 -2
  41. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.d.ts +1 -2
  42. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.js +1 -1
  43. package/dist/runtime/server/og-image/bindings/satori/wasm.d.ts +1 -2
  44. package/dist/runtime/server/og-image/bindings/satori/wasm.js +1 -1
  45. package/dist/runtime/server/og-image/browser/screenshot.js +5 -3
  46. package/dist/runtime/server/og-image/cache/publish.d.ts +46 -0
  47. package/dist/runtime/server/og-image/cache/publish.js +91 -0
  48. package/dist/runtime/server/og-image/cache/published.d.ts +17 -0
  49. package/dist/runtime/server/og-image/cache/published.js +49 -0
  50. package/dist/runtime/server/og-image/context.js +1 -1
  51. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -5
  52. package/dist/runtime/server/og-image/core/transforms/emojis/index.d.ts +3 -2
  53. package/dist/runtime/server/og-image/core/vnodes.js +19 -1
  54. package/dist/runtime/server/og-image/font-source.d.ts +1 -0
  55. package/dist/runtime/server/og-image/font-source.js +1 -0
  56. package/dist/runtime/server/og-image/fonts.d.ts +1 -1
  57. package/dist/runtime/server/og-image/fonts.js +1 -1
  58. package/dist/runtime/server/og-image/satori/filters.d.ts +1 -0
  59. package/dist/runtime/server/og-image/satori/filters.js +14 -0
  60. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  61. package/dist/runtime/server/og-image/satori/plugins/classes.js +0 -7
  62. package/dist/runtime/server/og-image/satori/renderer.js +2 -1
  63. package/dist/runtime/server/og-image/satori/vnodes.js +1 -4
  64. package/dist/runtime/server/og-image/takumi/renderer.js +11 -47
  65. package/dist/runtime/server/og-image/utils/css.d.ts +0 -5
  66. package/dist/runtime/server/og-image/utils/css.js +0 -5
  67. package/dist/runtime/server/plugins/auto-eject-nitro3.d.ts +2 -0
  68. package/dist/runtime/server/plugins/auto-eject-nitro3.js +9 -0
  69. package/dist/runtime/server/plugins/auto-eject.js +3 -31
  70. package/dist/runtime/server/plugins/prerender.js +67 -2
  71. package/dist/runtime/server/plugins/publish.d.ts +2 -0
  72. package/dist/runtime/server/plugins/publish.js +44 -0
  73. package/dist/runtime/server/routes/debug.json.d.ts +10 -1
  74. package/dist/runtime/server/routes/debug.json.js +4 -4
  75. package/dist/runtime/server/routes/static-font.d.ts +2 -0
  76. package/dist/runtime/server/routes/static-font.js +22 -0
  77. package/dist/runtime/server/signing-secret.d.ts +8 -0
  78. package/dist/runtime/server/signing-secret.js +75 -0
  79. package/dist/runtime/server/util/auto-eject-lifecycle.d.ts +13 -0
  80. package/dist/runtime/server/util/auto-eject-lifecycle.js +31 -0
  81. package/dist/runtime/server/util/cache.d.ts +1 -0
  82. package/dist/runtime/server/util/cache.js +12 -6
  83. package/dist/runtime/server/util/cloudflare.d.ts +2 -2
  84. package/dist/runtime/server/util/eventHandlers.js +68 -3
  85. package/dist/runtime/server/util/fetchLocalAsset.js +2 -2
  86. package/dist/runtime/server/util/origin.d.ts +3 -0
  87. package/dist/runtime/server/util/origin.js +4 -0
  88. package/dist/runtime/server/util/publishWarnings.d.ts +6 -0
  89. package/dist/runtime/server/util/publishWarnings.js +6 -0
  90. package/dist/runtime/server/utils.d.ts +3 -3
  91. package/dist/runtime/server/utils.js +5 -10
  92. package/dist/runtime/shared/publish.d.ts +13 -0
  93. package/dist/runtime/shared/publish.js +17 -0
  94. package/dist/runtime/shared/urlEncoding.js +9 -4
  95. package/dist/runtime/types.d.ts +11 -16
  96. package/dist/shared/{nuxt-og-image.Bcegsloy.mjs → nuxt-og-image.DR_SxdDv.mjs} +1224 -1109
  97. package/package.json +60 -62
  98. package/skills/nuxt-og-image/SKILL.md +31 -14
  99. package/types/virtual.d.ts +8 -0
  100. package/dist/runtime/server/og-image/satori/plugins/emojis.d.ts +0 -2
  101. package/dist/runtime/server/og-image/satori/plugins/emojis.js +0 -112
  102. package/dist/runtime/server/og-image/satori/plugins/flex.d.ts +0 -2
  103. package/dist/runtime/server/og-image/satori/plugins/flex.js +0 -48
@@ -1,9 +1,9 @@
1
- import type { H3Event } from '#nuxtseo/h3';
1
+ import type { RequestEvent } from 'nuxt/server';
2
2
  import type { OgImageOptionsInternal } from '../types.js';
3
3
  type Options = Partial<OgImageOptionsInternal>;
4
4
  /**
5
5
  * Build the absolute OG image URL for a page. Without a page path, uses the current request path.
6
6
  */
7
- export declare function getOgImageUrl(event: H3Event, options?: Options): string;
8
- export declare function getOgImageUrl(event: H3Event, pagePath: string, options?: Options): string;
7
+ export declare function getOgImageUrl(event: RequestEvent, options?: Options): string;
8
+ export declare function getOgImageUrl(event: RequestEvent, pagePath: string, options?: Options): string;
9
9
  export {};
@@ -1,10 +1,10 @@
1
- import { parseURL, withoutBase } from "ufo";
2
- import { useRuntimeConfig } from "#nuxtseo/nitro";
1
+ import { getRequestURL, useRuntimeConfig } from "nuxt/server";
2
+ import { withoutBase } from "ufo";
3
3
  import { withSiteUrl } from "#site-config/server/composables/utils";
4
4
  import { getOgImagePath } from "./utils.js";
5
5
  export function getOgImageUrl(event, pathOrOptions, maybeOptions) {
6
- const pagePath = typeof pathOrOptions === "string" ? pathOrOptions : withoutBase(parseURL(event.path).pathname, useRuntimeConfig(event).app.baseURL) || "/";
6
+ const pagePath = typeof pathOrOptions === "string" ? pathOrOptions : withoutBase(getRequestURL(event).pathname, useRuntimeConfig().app.baseURL) || "/";
7
7
  const options = typeof pathOrOptions === "string" ? maybeOptions : pathOrOptions;
8
8
  const { path } = getOgImagePath(event, pagePath, options);
9
- return withSiteUrl(event, path, { canonical: !import.meta.dev });
9
+ return withSiteUrl(event, path, { canonical: !import.meta.dev, withBase: true });
10
10
  }
@@ -1,15 +1,14 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { basename, join } from "pathe";
2
+ import { join } from "pathe";
3
3
  import { withBase } from "ufo";
4
4
  import { getRequestURL } from "#nuxtseo/h3";
5
5
  import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
6
- import { buildDir, rootDir, staticFontCacheDir } from "#og-image-virtual/build-dir.mjs";
6
+ import { resolvedFontPaths, rootDir, staticFontCacheDir } from "#og-image-virtual/build-dir.mjs";
7
7
  import { getSiteConfig } from "#site-config/server/composables";
8
8
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
9
9
  import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
10
10
  import { useOgImageRuntimeConfig } from "../../../utils.js";
11
11
  import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
12
- let fontUrlMapping;
13
12
  async function readOptionalFile(path) {
14
13
  return readFile(path).catch((error) => {
15
14
  if (error.code === "ENOENT")
@@ -17,18 +16,6 @@ async function readOptionalFile(path) {
17
16
  throw error;
18
17
  });
19
18
  }
20
- async function fetchOptionalFont(url, timeout) {
21
- return fetch(url, { signal: AbortSignal.timeout(timeout) }).catch(() => {
22
- return null;
23
- });
24
- }
25
- async function loadFontUrlMapping() {
26
- if (fontUrlMapping)
27
- return fontUrlMapping;
28
- const content = await readOptionalFile(join(buildDir, "cache", "og-image", "font-urls.json"));
29
- fontUrlMapping = content ? JSON.parse(content.toString("utf-8")) : {};
30
- return fontUrlMapping;
31
- }
32
19
  export async function resolve(event, font) {
33
20
  const path = font.src || font.localPath;
34
21
  const runtimeConfig = useOgImageRuntimeConfig();
@@ -40,36 +27,17 @@ export async function resolve(event, font) {
40
27
  }
41
28
  if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
42
29
  return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
43
- if ((import.meta.dev || import.meta.prerender) && path.startsWith("/_fonts/")) {
44
- const filename = path.slice("/_fonts/".length);
45
- if (filename && basename(filename) === filename) {
46
- const cached = await readOptionalFile(join(buildDir, "cache", "fonts", filename));
47
- if (cached?.length)
48
- return cached;
49
- }
50
- }
30
+ if (resolvedFontPaths[path])
31
+ return readFile(resolvedFontPaths[path]);
51
32
  if (import.meta.prerender) {
52
33
  if (path.startsWith("/_og-static-fonts/")) {
53
- const filename2 = path.slice("/_og-static-fonts/".length);
54
- const cached = await readOptionalFile(join(staticFontCacheDir, filename2)) || await readOptionalFile(join(rootDir, ".output", "public", "_og-static-fonts", filename2));
34
+ const filename = path.slice("/_og-static-fonts/".length);
35
+ const cached = await readOptionalFile(join(staticFontCacheDir, filename));
55
36
  if (cached?.length)
56
37
  return cached;
57
38
  }
58
- if (path.startsWith("/_fonts/")) {
59
- const filename2 = path.slice("/_fonts/".length);
60
- const cached = await readOptionalFile(join(rootDir, ".output", "public", "_fonts", filename2));
61
- if (cached?.length)
62
- return cached;
63
- const mapping = await loadFontUrlMapping();
64
- if (mapping[filename2]) {
65
- const res = await fetchOptionalFont(mapping[filename2], timeout);
66
- if (res?.ok)
67
- return Buffer.from(await res.arrayBuffer());
68
- }
69
- throw new Error(`Font ${filename2} not found in mapping or cache`);
70
- }
71
- const filename = path.slice(1);
72
- const data = await readOptionalFile(join(rootDir, "public", filename)) || await readOptionalFile(join(rootDir, ".output", "public", filename));
39
+ const publicPath = path.slice(1);
40
+ const data = await readOptionalFile(join(rootDir, "public", publicPath));
73
41
  if (data?.length)
74
42
  return data;
75
43
  }
@@ -79,15 +47,6 @@ export async function resolve(event, font) {
79
47
  if (cached?.length)
80
48
  return cached;
81
49
  }
82
- if (import.meta.dev && path.startsWith("/_fonts/")) {
83
- const filename = path.slice("/_fonts/".length);
84
- const mapping = await loadFontUrlMapping();
85
- if (mapping[filename]) {
86
- const res = await fetchOptionalFont(mapping[filename], timeout);
87
- if (res?.ok)
88
- return Buffer.from(await res.arrayBuffer());
89
- }
90
- }
91
50
  if (import.meta.dev) {
92
51
  const filename = path.slice(1);
93
52
  const data = await readOptionalFile(join(rootDir, "public", filename));
@@ -1,7 +1,8 @@
1
1
  import { withBase } from "ufo";
2
2
  import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
3
- import { getNitroOrigin, getSiteConfig } from "#site-config/server/composables";
3
+ import { getSiteConfig } from "#site-config/server/composables";
4
4
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
5
+ import { getOgImageOrigin } from "../../../util/origin.js";
5
6
  import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
6
7
  import { useOgImageRuntimeConfig } from "../../../utils.js";
7
8
  import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
@@ -10,7 +11,7 @@ export async function resolve(event, font) {
10
11
  const runtimeConfig = useOgImageRuntimeConfig();
11
12
  const timeout = getFetchTimeout(runtimeConfig);
12
13
  const { app } = useRuntimeConfig();
13
- const origin = getNitroOrigin(event);
14
+ const origin = getOgImageOrigin(event);
14
15
  const fullPath = withBase(path, app.baseURL);
15
16
  if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
16
17
  return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
@@ -1,6 +1,5 @@
1
- import _satori from 'satori';
2
1
  declare const _default: {
3
2
  initWasmPromise: Promise<void>;
4
- satori: typeof _satori;
3
+ satori: (element: ReactNode, options: import("satori").SatoriOptions) => Promise<string>;
5
4
  };
6
5
  export default _default;
@@ -1,6 +1,5 @@
1
- import _satori from 'satori/standalone';
2
1
  declare const _default: {
3
2
  initWasmPromise: Promise<void>;
4
- satori: typeof _satori;
3
+ satori: (element: ReactNode, options: import("satori/standalone").SatoriOptions) => Promise<string>;
5
4
  };
6
5
  export default _default;
@@ -1,6 +1,6 @@
1
1
  import _satori, { init } from "satori/standalone";
2
2
  import { readWasmFile } from "../../../util/wasm.js";
3
- const wasmBinary = readWasmFile("satori/yoga.wasm");
3
+ const wasmBinary = readWasmFile("satori/layout.wasm");
4
4
  export default {
5
5
  initWasmPromise: wasmBinary.then(async (wasm) => {
6
6
  await init(wasm);
@@ -1,6 +1,5 @@
1
- import _satori from 'satori/standalone';
2
1
  declare const _default: {
3
2
  initWasmPromise: Promise<void>;
4
- satori: typeof _satori;
3
+ satori: (element: ReactNode, options: import("satori/standalone").SatoriOptions) => Promise<string>;
5
4
  };
6
5
  export default _default;
@@ -1,6 +1,6 @@
1
1
  import _satori, { init } from "satori/standalone";
2
2
  import { importWasm } from "../../../util/wasm.js";
3
3
  export default {
4
- initWasmPromise: importWasm(import("#og-image/yoga-wasm")).then((wasm) => init(wasm)),
4
+ initWasmPromise: importWasm(import("#og-image/layout-wasm")).then((wasm) => init(wasm)),
5
5
  satori: _satori
6
6
  };
@@ -1,10 +1,10 @@
1
1
  import { withQuery } from "ufo";
2
2
  import { toValue } from "vue";
3
3
  import { fetchWithEvent } from "#nuxtseo/nitro";
4
- import { getNitroOrigin } from "#site-config/server/composables";
5
4
  import { buildOgImageUrl } from "../../../shared.js";
6
5
  import { getFetchTimeout } from "../../util/fetchTimeout.js";
7
6
  import { logger } from "../../util/logger.js";
7
+ import { getOgImageOrigin } from "../../util/origin.js";
8
8
  import { resolveSameOriginUrl } from "../../util/ssrf.js";
9
9
  import { useOgImageRuntimeConfig } from "../../utils.js";
10
10
  function isPlaywrightPage(page) {
@@ -71,13 +71,15 @@ export async function createScreenshot({ basePath, e, options, extension, timing
71
71
  } else {
72
72
  page = await browser.newPage({
73
73
  colorScheme: colorPreference || "no-preference",
74
- baseURL: getNitroOrigin(e)
74
+ baseURL: getOgImageOrigin(e)
75
75
  });
76
76
  }
77
77
  try {
78
78
  if (options.html) {
79
79
  logger.warn("The `html` option is deprecated and will be removed in the next major version. Use a Vue component instead.");
80
80
  }
81
+ if (import.meta.prerender && options._prerenderHtml)
82
+ options.html = options._prerenderHtml;
81
83
  if (import.meta.prerender && !options.html) {
82
84
  options.html = await timings.measure("html-fetch", () => fetchWithEvent(e, path, { timeout: getFetchTimeout(runtimeConfig) }).catch((err) => {
83
85
  void err;
@@ -102,7 +104,7 @@ export async function createScreenshot({ basePath, e, options, extension, timing
102
104
  await page.setContent(html, { waitUntil: "networkidle0" });
103
105
  }
104
106
  } else {
105
- const url = resolveSameOriginUrl(withQuery(path, options.props || {}), getNitroOrigin(e));
107
+ const url = resolveSameOriginUrl(withQuery(path, options.props || {}), getOgImageOrigin(e));
106
108
  if (!url)
107
109
  throw new Error("[Nuxt OG Image] The browser renderer can only navigate to a same-origin path.");
108
110
  await gotoWithIdle(page, url, 1e4);
@@ -0,0 +1,46 @@
1
+ import type { Storage } from 'unstorage';
2
+ interface PublishStorage {
3
+ getMount: Storage['getMount'];
4
+ hasItem: (key: string) => Promise<boolean>;
5
+ getItem: (key: string) => Promise<unknown>;
6
+ setItem: (key: string, value: object, options?: Record<string, unknown>) => Promise<void>;
7
+ setItemRaw: (key: string, value: Uint8Array, options?: Record<string, unknown>) => Promise<void>;
8
+ }
9
+ interface PublishInput {
10
+ storage: PublishStorage;
11
+ mount: string;
12
+ baseURL: string;
13
+ key: string;
14
+ extension: 'png' | 'jpg' | 'jpeg' | 'webp';
15
+ maxAgeSeconds: number;
16
+ timeoutMs?: number;
17
+ now: () => number;
18
+ }
19
+ export interface Published {
20
+ _tag: 'Published';
21
+ url: string;
22
+ objectKey: string;
23
+ expiresAt: number;
24
+ }
25
+ interface Unavailable {
26
+ _tag: 'Unavailable';
27
+ reason: unknown;
28
+ }
29
+ /**
30
+ * Normalize signed image paths without loading a renderer during page SSR.
31
+ * The key is a full SHA-256 hex digest, so it has one fixed format.
32
+ */
33
+ export declare function getPublishKey(url: string, defaults: Record<string, unknown>, cacheVersion: string): string;
34
+ /** A page may reference the bucket only while its manifest and object both exist. */
35
+ export declare function getPublishedImage(input: PublishInput): Promise<Published | Unavailable | {
36
+ _tag: 'Miss';
37
+ }>;
38
+ /** Publish immutable bytes. Manifest expiry limits reuse, never object retention. */
39
+ export declare function publishImage(input: PublishInput & {
40
+ render: () => Promise<Uint8Array>;
41
+ /** Original buffer expiry, preserved across uploads and cache hits. */
42
+ expiresAt?: number;
43
+ /** Fresh runtime bytes replace the manifest even if an older object is still reusable. */
44
+ force?: boolean;
45
+ }): Promise<Published | Unavailable>;
46
+ export {};
@@ -0,0 +1,91 @@
1
+ import { parseURL } from "ufo";
2
+ import { parsePublishConfig } from "../../../shared/publish.js";
3
+ import { digestCanonical, extractEncodedSegment } from "../../../shared/urlEncoding.js";
4
+ import { withTimeout } from "../../util/withTimeout.js";
5
+ export function getPublishKey(url, defaults, cacheVersion) {
6
+ const { pathname: path, host, protocol } = parseURL(url);
7
+ const extension = path.split(".").pop();
8
+ const segment = decodeURIComponent(extractEncodedSegment(path, extension)).replace(/,s_[^,]+$/, "");
9
+ return digestCanonical([{ segment, defaults, host, protocol }, cacheVersion]);
10
+ }
11
+ export async function getPublishedImage(input) {
12
+ return withTimeout((async () => {
13
+ const target = resolveTarget(input);
14
+ return target._tag === "Unavailable" ? target : lookupPublishedImage(input, target);
15
+ })(), input.timeoutMs ?? 15e3, "OG image publish lookup").catch((reason) => ({ _tag: "Unavailable", reason }));
16
+ }
17
+ export async function publishImage(input) {
18
+ return withTimeout((async () => {
19
+ const target = resolveTarget(input);
20
+ if (target._tag === "Unavailable")
21
+ return target;
22
+ if (!input.force) {
23
+ const cached = await lookupPublishedImage(input, target);
24
+ if (cached._tag === "Published")
25
+ return cached;
26
+ }
27
+ const { storage } = target;
28
+ const bytes = await input.render();
29
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
30
+ const hash = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
31
+ const objectKey = `${hash}.${input.extension}`;
32
+ if (!await storage.hasItem(objectKey)) {
33
+ await storage.setItemRaw(objectKey, bytes, {
34
+ httpMetadata: {
35
+ contentType: `image/${input.extension === "jpg" ? "jpeg" : input.extension}`,
36
+ cacheControl: "public, max-age=31536000, immutable"
37
+ }
38
+ });
39
+ if (!await storage.hasItem(objectKey))
40
+ return { _tag: "Unavailable", reason: "Publish storage did not retain the image." };
41
+ }
42
+ const expiresAt = input.expiresAt ?? input.now() + (Number.isFinite(input.maxAgeSeconds) ? Math.max(0, input.maxAgeSeconds) : 0) * 1e3;
43
+ await storage.setItem(manifestKey(input), {
44
+ objectKey,
45
+ expiresAt
46
+ }, { ttl: Math.max(60, Number.isFinite(input.maxAgeSeconds) ? Math.ceil(input.maxAgeSeconds) : 60) });
47
+ return published(target, objectKey, expiresAt);
48
+ })(), input.timeoutMs ?? 15e3, "OG image publication").catch((reason) => ({ _tag: "Unavailable", reason }));
49
+ }
50
+ function resolveTarget(input) {
51
+ const parsed = parsePublishConfig({ storage: input.mount, baseURL: input.baseURL });
52
+ if (parsed._tag === "Err")
53
+ return { _tag: "Unavailable", reason: parsed.reason };
54
+ const { storage: mount, baseURL } = parsed.value;
55
+ const mounted = input.storage.getMount(`${mount}:`);
56
+ if (!mount || mounted.base !== `${mount}:` || !mounted.driver.setItemRaw)
57
+ return { _tag: "Unavailable", reason: "Publish storage needs a mounted driver with raw writes." };
58
+ const prefix = `${mount}:`;
59
+ const storage = {
60
+ hasItem: (key) => input.storage.hasItem(prefix + key),
61
+ getItem: (key) => input.storage.getItem(prefix + key),
62
+ setItem: (key, value, options) => input.storage.setItem(prefix + key, value, options),
63
+ setItemRaw: (key, value, options) => input.storage.setItemRaw(prefix + key, value, options)
64
+ };
65
+ return { _tag: "Target", storage, baseURL };
66
+ }
67
+ async function lookupPublishedImage(input, target) {
68
+ if (!(input.maxAgeSeconds > 0))
69
+ return { _tag: "Miss" };
70
+ const raw = await target.storage.getItem(manifestKey(input));
71
+ const manifest = parseManifest(raw, input.extension);
72
+ if (manifest && manifest.expiresAt > input.now() && await target.storage.hasItem(manifest.objectKey))
73
+ return published(target, manifest.objectKey, manifest.expiresAt);
74
+ return { _tag: "Miss" };
75
+ }
76
+ function manifestKey(input) {
77
+ return `manifest:${input.key}.${input.extension}.json`;
78
+ }
79
+ function published(target, objectKey, expiresAt) {
80
+ return { _tag: "Published", objectKey, url: `${target.baseURL}/${objectKey}`, expiresAt };
81
+ }
82
+ function parseManifest(value, extension) {
83
+ if (!value || typeof value !== "object")
84
+ return;
85
+ const record = value;
86
+ if (typeof record.objectKey !== "string" || !new RegExp(`^[a-f0-9]{64}\\.${extension}$`).test(record.objectKey))
87
+ return;
88
+ if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt))
89
+ return;
90
+ return { objectKey: record.objectKey, expiresAt: record.expiresAt };
91
+ }
@@ -0,0 +1,17 @@
1
+ import type { OgImageRenderEventContext } from '../../../types.js';
2
+ import type { Published } from './publish.js';
3
+ export declare function createPublishedImageCache(max?: number): {
4
+ get(key: string, now: number): Published | undefined;
5
+ isPublishing: (key: string, now: number) => boolean;
6
+ remember: (key: string, image: Published, now: number) => void;
7
+ forget: (key: string) => void;
8
+ begin(key: string, now: number, timeoutMs: number): symbol | undefined;
9
+ complete(key: string, token: symbol, result: Published | {
10
+ _tag: "Unavailable";
11
+ }, now: number): void;
12
+ };
13
+ export declare function getPublishedImageCache(app: OgImageRenderEventContext['_nitro']): any;
14
+ export declare function getPublishedImageCacheKey(publish: {
15
+ storage: string;
16
+ baseURL: string;
17
+ }, key: string, extension: string): string;
@@ -0,0 +1,49 @@
1
+ import { LRUCache } from "lru-cache";
2
+ export function createPublishedImageCache(max = 1e3) {
3
+ const entries = new LRUCache({ max });
4
+ function read(key, now) {
5
+ const entry = entries.get(key);
6
+ if (entry && entry.validUntil <= now) {
7
+ entries.delete(key);
8
+ return;
9
+ }
10
+ return entry;
11
+ }
12
+ function remember(key, image, now) {
13
+ if (image.expiresAt > now)
14
+ entries.set(key, { _tag: "Published", image, validUntil: Math.min(image.expiresAt, now + 6e4) });
15
+ }
16
+ return {
17
+ get(key, now) {
18
+ const entry = read(key, now);
19
+ return entry?._tag === "Published" ? entry.image : void 0;
20
+ },
21
+ isPublishing: (key, now) => read(key, now)?._tag === "Publishing",
22
+ remember,
23
+ forget: (key) => {
24
+ entries.delete(key);
25
+ },
26
+ begin(key, now, timeoutMs) {
27
+ if (read(key, now)?._tag === "Publishing")
28
+ return;
29
+ const token = Symbol(key);
30
+ entries.set(key, { _tag: "Publishing", token, validUntil: now + timeoutMs });
31
+ return token;
32
+ },
33
+ complete(key, token, result, now) {
34
+ const entry = entries.peek(key);
35
+ if (entry?._tag !== "Publishing" || entry.token !== token)
36
+ return;
37
+ entries.delete(key);
38
+ if (result._tag === "Published")
39
+ remember(key, result, now);
40
+ }
41
+ };
42
+ }
43
+ export function getPublishedImageCache(app) {
44
+ const state = app;
45
+ return state._ogImagePublishedImages ??= createPublishedImageCache();
46
+ }
47
+ export function getPublishedImageCacheKey(publish, key, extension) {
48
+ return JSON.stringify([publish.storage, publish.baseURL, key, extension]);
49
+ }
@@ -38,7 +38,7 @@ export async function resolveContext(e) {
38
38
  if (!sigMatch) {
39
39
  return createError({
40
40
  statusCode: 403,
41
- statusMessage: "[Nuxt OG Image] Missing URL signature. Configure security.secret to sign URLs."
41
+ statusMessage: "[Nuxt OG Image] Missing URL signature. Set NUXT_APP_SECRET to sign URLs."
42
42
  });
43
43
  }
44
44
  const signature = sigMatch[1];
@@ -1,11 +1,11 @@
1
1
  import { parseURL, withBase, withoutLeadingSlash } from "ufo";
2
2
  import { useStorage } from "#nuxtseo/nitro";
3
- import { getNitroOrigin } from "#site-config/server/composables/getNitroOrigin";
4
3
  import { toBase64Image } from "../../../../shared.js";
5
4
  import { decodeHtml } from "../../../util/encoding.js";
6
5
  import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
7
6
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
8
7
  import { logger } from "../../../util/logger.js";
8
+ import { getOgImageOrigin } from "../../../util/origin.js";
9
9
  import { fetchWithRedirectValidation, isBlockedUrl, parseSameOriginPath } from "../../../util/ssrf.js";
10
10
  import { getImageDimensions } from "../../utils/image-detector.js";
11
11
  import { defineTransformer } from "../plugins.js";
@@ -123,9 +123,6 @@ export default defineTransformer([
123
123
  let src = node.props.src;
124
124
  if (src.startsWith("data:"))
125
125
  return;
126
- if (ctx.renderer.name === "satori" && src.endsWith(".webp")) {
127
- logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the takumi or browser renderer.", src);
128
- }
129
126
  const isRelative = src.startsWith("/");
130
127
  if (!isRelative)
131
128
  src = node.props.src = decodeHtml(src);
@@ -140,7 +137,7 @@ export default defineTransformer([
140
137
  return;
141
138
  }
142
139
  if (isRelative) {
143
- node.props.src = withBase(src, `${getNitroOrigin(ctx.e)}`);
140
+ node.props.src = withBase(src, `${getOgImageOrigin(ctx.e)}`);
144
141
  return;
145
142
  }
146
143
  if (!import.meta.dev)
@@ -1,7 +1,8 @@
1
- import type { NuxtIslandResponse } from 'nuxt/app';
2
1
  import type { OgImageRenderEventContext } from '../../../../../types.js';
3
2
  /**
4
3
  * Apply emoji transformations to island HTML content.
5
4
  * Replaces emoji characters with inline SVG elements.
6
5
  */
7
- export declare function applyEmojis(ctx: OgImageRenderEventContext, island: NuxtIslandResponse): Promise<void>;
6
+ export declare function applyEmojis(ctx: OgImageRenderEventContext, island: {
7
+ html: string;
8
+ }): Promise<void>;
@@ -119,6 +119,21 @@ export function htmlToVNode(html) {
119
119
  throw new Error("Failed to parse HTML - no root div found");
120
120
  return elementToVNode(root);
121
121
  }
122
+ function omitImagesWithoutSource(node) {
123
+ const children = node.props.children;
124
+ if (!Array.isArray(children))
125
+ return;
126
+ node.props.children = children.filter((child) => {
127
+ if (!child || typeof child !== "object")
128
+ return true;
129
+ if (child.type === "img" && !child.props.src)
130
+ return false;
131
+ omitImagesWithoutSource(child);
132
+ return true;
133
+ });
134
+ if (node.props.children.length === 0)
135
+ delete node.props.children;
136
+ }
122
137
  const UNSUPPORTED_SVG_ELEMENTS = /* @__PURE__ */ new Set(["text", "tspan", "textPath", "foreignObject", "switch", "a"]);
123
138
  export function warnUnsupportedSvgElements(vnode, component) {
124
139
  const unsupported = /* @__PURE__ */ new Set();
@@ -157,12 +172,14 @@ export async function createVNodes(ctx, options) {
157
172
  if (!html) {
158
173
  const islandTimeout = ctx.runtimeConfig.security?.renderTimeout ?? 15e3;
159
174
  const island = await ctx.timings.measure("island-fetch", () => fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options, islandTimeout));
160
- await applyEmojis(ctx, island);
161
175
  html = island.html;
162
176
  if (html?.includes("<body>")) {
163
177
  html = html.match(RE_BODY_CONTENT)?.[1] || "";
164
178
  }
165
179
  }
180
+ const content = { html };
181
+ await applyEmojis(ctx, content);
182
+ html = content.html;
166
183
  const baseStyle = `position: relative; display: flex; margin: 0 auto; width: ${ctx.options.width}px; height: ${ctx.options.height}px; overflow: hidden;`;
167
184
  const fullStyle = options?.wrapperStyle ? `${baseStyle} ${options.wrapperStyle}` : baseStyle;
168
185
  const template = `<div style="${fullStyle}">${html}</div>`;
@@ -173,5 +190,6 @@ export async function createVNodes(ctx, options) {
173
190
  }
174
191
  warnUnsupportedSvgElements(vnodeTree, ctx.options.component);
175
192
  await ctx.timings.measure("vnode-walk", () => Promise.all(walkTree(ctx, vnodeTree, [encoding, styleDirectives, imageSrc])));
193
+ omitImagesWithoutSource(vnodeTree);
176
194
  return vnodeTree;
177
195
  }
@@ -1,4 +1,5 @@
1
1
  export type FontFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
2
+ export declare const TAKUMI_FORMATS: Set<FontFormat>;
2
3
  export declare function fontFormat(src: string): FontFormat;
3
4
  /**
4
5
  * Pick the src to actually load for a parsed font entry.
@@ -1,3 +1,4 @@
1
+ export const TAKUMI_FORMATS = /* @__PURE__ */ new Set(["ttf", "otf", "woff", "woff2"]);
1
2
  export function fontFormat(src) {
2
3
  if (src.endsWith(".woff2"))
3
4
  return "woff2";
@@ -7,7 +7,7 @@ export interface LoadFontsOptions {
7
7
  /**
8
8
  * Font formats the renderer can parse.
9
9
  * Satori: ttf, otf, woff
10
- * Takumi: ttf, woff2
10
+ * Takumi: ttf, otf, woff, woff2
11
11
  */
12
12
  supportedFormats: Set<FontFormat>;
13
13
  /** Component pascalName — filters fonts to only what this component needs */
@@ -14,7 +14,7 @@ async function loadFont(event, font, src) {
14
14
  const cached = fontCache.get(cacheKey);
15
15
  if (cached)
16
16
  return cached;
17
- const data = await resolve(event, { ...font, src }).catch((err) => {
17
+ const data = await resolve(event, { ...font, src, absolutePath: src === font.src ? font.absolutePath : void 0 }).catch((err) => {
18
18
  logger.warn(`Failed to load font ${font.family}: ${err.message}`);
19
19
  return null;
20
20
  });
@@ -0,0 +1 @@
1
+ export declare function resolveSvgFilters(svg: string): string;
@@ -0,0 +1,14 @@
1
+ export function resolveSvgFilters(svg) {
2
+ return svg.replace(/<feColorMatrix\s[^>]*type="saturate"\s+values="([^"]+)"[^>]*>/g, (filter, value) => {
3
+ const saturation = Number(value);
4
+ if (!Number.isFinite(saturation) || saturation <= 1)
5
+ return filter;
6
+ const matrix = [
7
+ [0.213 + 0.787 * saturation, 0.715 - 0.715 * saturation, 0.072 - 0.072 * saturation, 0, 0],
8
+ [0.213 - 0.213 * saturation, 0.715 + 0.285 * saturation, 0.072 - 0.072 * saturation, 0, 0],
9
+ [0.213 - 0.213 * saturation, 0.715 - 0.715 * saturation, 0.072 + 0.928 * saturation, 0, 0],
10
+ [0, 0, 0, 1, 0]
11
+ ].flat();
12
+ return filter.replace(`type="saturate" values="${value}"`, `type="matrix" values="${matrix.join(" ")}"`);
13
+ });
14
+ }
@@ -1,4 +1,3 @@
1
- import type _satori from 'satori';
2
1
  export declare function getResvg(): Promise<any>;
3
- export declare function getSatori(): Promise<typeof _satori>;
2
+ export declare function getSatori(): Promise<(element: ReactNode, options: import("satori").SatoriOptions) => Promise<string>>;
4
3
  export declare function getSharp(): Promise<import("sharp").SharpConstructor>;
@@ -7,12 +7,5 @@ export default defineTransformer([
7
7
  node.props.tw = node.props.class;
8
8
  node.props.tw = node.props.tw.replace(RE_ICON_CLASSES, "");
9
9
  }
10
- },
11
- {
12
- filter: (node) => !!node.props?.style?.display,
13
- transform(node) {
14
- if (["inline-block", "inline"].includes(node.props.style.display))
15
- delete node.props.style.display;
16
- }
17
10
  }
18
11
  ]);
@@ -4,6 +4,7 @@ import compatibility from "#og-image/compatibility";
4
4
  import { withTimeout } from "../../util/withTimeout.js";
5
5
  import { useOgImageRuntimeConfig } from "../../utils.js";
6
6
  import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadAllFontsDebug, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
7
+ import { resolveSvgFilters } from "./filters.js";
7
8
  import { getResvg, getSatori, getSharp } from "./instances.js";
8
9
  import { createVNodes } from "./vnodes.js";
9
10
  const _satoriFontCache = /* @__PURE__ */ new WeakMap();
@@ -89,7 +90,7 @@ export async function createSvg(event) {
89
90
  const { result, warnings } = await timings.measure("render-satori", () => withWarningCapture(
90
91
  () => satori(vnodes, satoriOptions)
91
92
  ));
92
- return { svg: result, warnings, fonts };
93
+ return { svg: result && resolveSvgFilters(result), warnings, fonts };
93
94
  }
94
95
  async function createPng(event) {
95
96
  const { resvgOptions } = useOgImageRuntimeConfig();
@@ -2,8 +2,6 @@ import resolvedFonts from "#og-image/fonts";
2
2
  import { walkTree } from "../core/plugins.js";
3
3
  import { createVNodes as coreCreateVNodes, resolveSvgDimension } from "../core/vnodes.js";
4
4
  import classes from "./plugins/classes.js";
5
- import emojis from "./plugins/emojis.js";
6
- import flex from "./plugins/flex.js";
7
5
  import nuxtIcon from "./plugins/nuxt-icon.js";
8
6
  export { htmlToVNode, resolveSvgDimension, SVG_CAMEL_ATTR_VALUES, warnUnsupportedSvgElements } from "../core/vnodes.js";
9
7
  function getDefaultFontFamily() {
@@ -50,8 +48,7 @@ export async function createVNodes(ctx) {
50
48
  const fontFamilyOverride = ctx.options.props?.fontFamily;
51
49
  const font = fontFamilyOverride ? `'${fontFamilyOverride}', sans-serif` : getDefaultFontFamily();
52
50
  const vnodes = await coreCreateVNodes(ctx, { wrapperStyle: `font-family: ${font};` });
53
- walkTree(ctx, vnodes, [classes, flex, nuxtIcon]);
54
- await Promise.all(walkTree(ctx, vnodes, [emojis]));
51
+ walkTree(ctx, vnodes, [classes, nuxtIcon]);
55
52
  resolveSvgPercentDimensions(vnodes);
56
53
  return vnodes;
57
54
  }