nuxt-og-image 2.0.0-beta.3 → 2.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -44
- package/dist/client/200.html +2 -2
- package/dist/client/404.html +2 -2
- package/dist/client/_nuxt/IconCSS.1dce7adc.js +1 -0
- package/dist/client/_nuxt/IconCSS.f16db5c2.css +1 -0
- package/dist/client/_nuxt/ImageLoader.163d23b3.js +1 -0
- package/dist/client/_nuxt/ImageLoader.7571516f.css +1 -0
- package/dist/client/_nuxt/entry.23646169.js +5 -0
- package/dist/client/_nuxt/entry.3d2654cc.css +1 -0
- package/dist/client/_nuxt/{error-404.1ff52902.js → error-404.c0f2dfff.js} +1 -1
- package/dist/client/_nuxt/{error-500.f7d30da5.js → error-500.6a9dcc3d.js} +1 -1
- package/dist/client/_nuxt/{error-component.cf7543e5.js → error-component.0db07bb1.js} +2 -2
- package/dist/client/_nuxt/index.403133d8.css +1 -0
- package/dist/client/_nuxt/index.531c9ed8.js +1 -0
- package/dist/client/_nuxt/{options.56a3e5f9.js → options.fd71c26a.js} +1 -1
- package/dist/client/_nuxt/{png.37f3e77b.js → png.5b6830ea.js} +1 -1
- package/dist/client/_nuxt/{shiki.3a930bb8.js → shiki.1be71764.js} +1 -1
- package/dist/client/_nuxt/{svg.186c6bd1.js → svg.165cf4fc.js} +1 -1
- package/dist/client/_nuxt/{vnodes.a799f183.js → vnodes.11ff70a7.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/options/index.html +2 -2
- package/dist/client/png/index.html +2 -2
- package/dist/client/svg/index.html +2 -2
- package/dist/client/vnodes/index.html +2 -2
- package/dist/module.d.ts +21 -7
- package/dist/module.json +1 -1
- package/dist/module.mjs +252 -117
- package/dist/runtime/browserUtil.d.ts +1 -0
- package/dist/runtime/browserUtil.mjs +10 -5
- package/dist/runtime/composables/defineOgImage.mjs +14 -12
- package/dist/runtime/nitro/middleware/og.png.mjs +50 -7
- package/dist/runtime/nitro/middleware/playground.mjs +4 -3
- package/dist/runtime/nitro/plugins/prerender.d.ts +3 -0
- package/dist/runtime/nitro/plugins/prerender.mjs +26 -0
- package/dist/runtime/nitro/providers/browser/lambda.d.ts +1 -1
- package/dist/runtime/nitro/providers/browser/lambda.mjs +5 -5
- package/dist/runtime/nitro/providers/browser/{node.mjs → playwright.mjs} +0 -9
- package/dist/runtime/nitro/providers/browser/universal.d.ts +1 -0
- package/dist/runtime/nitro/providers/browser/universal.mjs +33 -0
- package/dist/runtime/nitro/providers/png/resvg.d.ts +4 -0
- package/dist/runtime/nitro/providers/png/resvg.mjs +11 -0
- package/dist/runtime/nitro/providers/png/svg2png.mjs +11 -0
- package/dist/runtime/nitro/providers/satori/{webworker.mjs → yoga-wasm.mjs} +4 -5
- package/dist/runtime/nitro/renderers/browser.mjs +12 -6
- package/dist/runtime/nitro/renderers/satori/index.mjs +16 -13
- package/dist/runtime/nitro/renderers/satori/plugins/encoding.mjs +2 -1
- package/dist/runtime/nitro/renderers/satori/plugins/imageSrc.mjs +27 -3
- package/dist/runtime/nitro/renderers/satori/utils.d.ts +2 -2
- package/dist/runtime/nitro/renderers/satori/utils.mjs +20 -11
- package/dist/runtime/nitro/routes/debug.d.ts +4 -0
- package/dist/runtime/nitro/routes/debug.mjs +9 -0
- package/dist/runtime/nitro/routes/html.mjs +52 -14
- package/dist/runtime/nitro/routes/options.mjs +19 -15
- package/dist/runtime/nitro/routes/svg.mjs +3 -1
- package/dist/runtime/nitro/routes/vnode.mjs +3 -1
- package/dist/runtime/nitro/util-hostname.d.ts +2 -0
- package/dist/runtime/nitro/util-hostname.mjs +15 -0
- package/dist/runtime/nitro/utils-pure.d.ts +3 -2
- package/dist/runtime/nitro/utils-pure.mjs +16 -13
- package/dist/runtime/nitro/utils.d.ts +6 -8
- package/dist/runtime/nitro/utils.mjs +47 -47
- package/dist/runtime/public-assets/__nuxt_og_image__/browser-provider-not-supported.png +0 -0
- package/dist/runtime/public-assets-optional/resvg/resvg.wasm +0 -0
- package/dist/types.d.ts +6 -0
- package/package.json +28 -19
- package/dist/client/_nuxt/IconCSS.a041aca0.js +0 -1
- package/dist/client/_nuxt/ImageLoader.9bf39d71.js +0 -1
- package/dist/client/_nuxt/entry.74018bda.js +0 -5
- package/dist/client/_nuxt/entry.7a8c1ab2.css +0 -1
- package/dist/client/_nuxt/index.3f356409.js +0 -1
- package/dist/runtime/nitro/providers/svg2png/universal.mjs +0 -9
- /package/dist/runtime/nitro/providers/browser/{node.d.ts → playwright.d.ts} +0 -0
- /package/dist/runtime/nitro/providers/{svg2png/universal.d.ts → png/svg2png.d.ts} +0 -0
- /package/dist/runtime/nitro/providers/satori/{node.d.ts → default.d.ts} +0 -0
- /package/dist/runtime/nitro/providers/satori/{node.mjs → default.mjs} +0 -0
- /package/dist/runtime/nitro/providers/satori/{webworker.d.ts → yoga-wasm.d.ts} +0 -0
- /package/dist/runtime/{public-assets → public-assets-optional/inter-font}/inter-latin-ext-400-normal.woff +0 -0
- /package/dist/runtime/{public-assets → public-assets-optional/inter-font}/inter-latin-ext-700-normal.woff +0 -0
- /package/dist/runtime/{public-assets → public-assets-optional/svg2png}/svg2png.wasm +0 -0
- /package/dist/runtime/{public-assets → public-assets-optional/yoga}/yoga.wasm +0 -0
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createError, defineEventHandler, sendRedirect, setHeader } from "h3";
|
|
3
|
+
import { joinURL, parseURL, withBase, withoutTrailingSlash } from "ufo";
|
|
4
|
+
import { prefixStorage } from "unstorage";
|
|
3
5
|
import { fetchOptions, useHostname } from "../utils.mjs";
|
|
4
6
|
import { useProvider } from "#nuxt-og-image/provider";
|
|
7
|
+
import { useRuntimeConfig, useStorage } from "#imports";
|
|
5
8
|
export default defineEventHandler(async (e) => {
|
|
9
|
+
const { runtimeBrowser, runtimeCacheStorage } = useRuntimeConfig()["nuxt-og-image"];
|
|
6
10
|
const path = parseURL(e.path).pathname;
|
|
7
11
|
if (!path.endsWith("__og_image__/og.png"))
|
|
8
12
|
return;
|
|
9
13
|
const basePath = withoutTrailingSlash(
|
|
10
14
|
path.replace("__og_image__/og.png", "")
|
|
11
15
|
);
|
|
12
|
-
setHeader(e, "Content-Type", "image/png");
|
|
13
|
-
setHeader(e, "Cache-Control", "no-cache, no-store, must-revalidate");
|
|
14
|
-
setHeader(e, "Pragma", "no-cache");
|
|
15
|
-
setHeader(e, "Expires", "0");
|
|
16
16
|
const options = await fetchOptions(e, basePath);
|
|
17
|
+
if (process.env.NODE_ENV === "production" && !process.env.prerender && !runtimeBrowser && options.provider === "browser")
|
|
18
|
+
return sendRedirect(e, joinURL(useHostname(e), "__nuxt_og_image__/browser-provider-not-supported.png"));
|
|
17
19
|
const provider = await useProvider(options.provider);
|
|
18
20
|
if (!provider) {
|
|
19
21
|
throw createError({
|
|
@@ -21,5 +23,46 @@ export default defineEventHandler(async (e) => {
|
|
|
21
23
|
statusMessage: `Provider ${options.provider} is missing.`
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
const cache = prefixStorage(useStorage(), "og-image-cache:images");
|
|
27
|
+
const key = options.cacheKey || e.node.req.url;
|
|
28
|
+
let png;
|
|
29
|
+
if (runtimeCacheStorage && await cache.hasItem(key)) {
|
|
30
|
+
const { value, expiresAt } = await cache.getItem(key);
|
|
31
|
+
if (expiresAt > Date.now()) {
|
|
32
|
+
setHeader(e, "Cache-Control", "public, max-age=31536000");
|
|
33
|
+
setHeader(e, "Content-Type", "image/png");
|
|
34
|
+
png = Buffer.from(value, "base64");
|
|
35
|
+
} else {
|
|
36
|
+
await cache.removeItem(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!png) {
|
|
40
|
+
try {
|
|
41
|
+
png = await provider.createPng(withBase(basePath, useHostname(e)), options);
|
|
42
|
+
if (png && runtimeCacheStorage && options.static) {
|
|
43
|
+
const base64png = Buffer.from(png).toString("base64");
|
|
44
|
+
await cache.setItem(key, { value: base64png, expiresAt: Date.now() + (options.cacheTtl || 0) });
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw createError({
|
|
48
|
+
statusCode: 500,
|
|
49
|
+
statusMessage: `Failed to create og image: ${err.message}`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (png) {
|
|
54
|
+
if (!process.dev && options.static) {
|
|
55
|
+
setHeader(e, "Cache-Control", "public, max-age=31536000");
|
|
56
|
+
} else {
|
|
57
|
+
setHeader(e, "Cache-Control", "no-cache, no-store, must-revalidate");
|
|
58
|
+
setHeader(e, "Pragma", "no-cache");
|
|
59
|
+
setHeader(e, "Expires", "0");
|
|
60
|
+
}
|
|
61
|
+
setHeader(e, "Content-Type", "image/png");
|
|
62
|
+
return png;
|
|
63
|
+
}
|
|
64
|
+
throw createError({
|
|
65
|
+
statusCode: 500,
|
|
66
|
+
statusMessage: "Failed to create og image, unknown error."
|
|
67
|
+
});
|
|
25
68
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineEventHandler } from "h3";
|
|
2
|
-
import { parseURL, withoutTrailingSlash } from "ufo";
|
|
2
|
+
import { parseURL, withBase, withoutTrailingSlash } from "ufo";
|
|
3
3
|
import { fetchOptions } from "../utils.mjs";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
5
|
export default defineEventHandler(async (e) => {
|
|
5
6
|
const path = withoutTrailingSlash(parseURL(e.path).pathname);
|
|
6
7
|
if (!path.endsWith("/__og_image__"))
|
|
7
8
|
return;
|
|
8
|
-
const basePath = path.replace("/__og_image__", "");
|
|
9
|
+
const basePath = withBase(path.replace("/__og_image__", ""), useRuntimeConfig().app.baseURL);
|
|
9
10
|
const options = await fetchOptions(e, basePath === "" ? "/" : basePath);
|
|
10
11
|
if (!options)
|
|
11
12
|
return `The route ${basePath} has not been set up for og:image generation.`;
|
|
@@ -22,5 +23,5 @@ export default defineEventHandler(async (e) => {
|
|
|
22
23
|
}
|
|
23
24
|
</style>
|
|
24
25
|
<title>OG Image Playground</title>
|
|
25
|
-
<iframe src="/__nuxt_og_image__/client
|
|
26
|
+
<iframe src="/__nuxt_og_image__/client?&path=${basePath}&base=${useRuntimeConfig().app.baseURL}"></iframe>`;
|
|
26
27
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { appendHeader } from "h3";
|
|
2
|
+
import { joinURL } from "ufo";
|
|
3
|
+
import { prefixStorage } from "unstorage";
|
|
4
|
+
import { extractOgImageOptions } from "../utils-pure.mjs";
|
|
5
|
+
import { useStorage } from "#imports";
|
|
6
|
+
const OgImagePrenderNitroPlugin = (nitroApp) => {
|
|
7
|
+
if (!process.env.prerender)
|
|
8
|
+
return;
|
|
9
|
+
const cache = prefixStorage(useStorage(), "og-image-cache:options");
|
|
10
|
+
nitroApp.hooks.hook("render:html", async (ctx, { event }) => {
|
|
11
|
+
const url = event.node.req.url;
|
|
12
|
+
if (url.includes(".") || url.startsWith("/__nuxt_island/"))
|
|
13
|
+
return;
|
|
14
|
+
const options = extractOgImageOptions(ctx.head.join("\n"));
|
|
15
|
+
if (!options)
|
|
16
|
+
return;
|
|
17
|
+
await cache.setItem(url, {
|
|
18
|
+
value: JSON.stringify(options),
|
|
19
|
+
expiresAt: Date.now() + 60 * 60 * 1e3
|
|
20
|
+
// 60 minutes to prerender
|
|
21
|
+
});
|
|
22
|
+
if (options.static && options.provider === "satori")
|
|
23
|
+
appendHeader(event, "x-nitro-prerender", joinURL(url, "/__og_image__/og.png"));
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
export default OgImagePrenderNitroPlugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export default function createBrowser(): Promise<import("
|
|
1
|
+
export default function createBrowser(): Promise<import("playwright-core").Browser>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import awsChromium from "chrome-aws-lambda";
|
|
2
|
+
import { chromium } from "playwright-core";
|
|
3
3
|
export default async function createBrowser() {
|
|
4
|
-
return
|
|
5
|
-
args:
|
|
6
|
-
executablePath: await
|
|
4
|
+
return await chromium.launch({
|
|
5
|
+
args: awsChromium.args,
|
|
6
|
+
executablePath: await awsChromium.executablePath,
|
|
7
7
|
headless: true
|
|
8
8
|
});
|
|
9
9
|
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import playwrightCore from "playwright-core";
|
|
2
2
|
export default async function createBrowser() {
|
|
3
|
-
try {
|
|
4
|
-
const { Launcher } = await import(String("chrome-launcher"));
|
|
5
|
-
const chromePath = Launcher.getFirstInstallation();
|
|
6
|
-
return await playwrightCore.chromium.launch({
|
|
7
|
-
headless: true,
|
|
8
|
-
executablePath: chromePath
|
|
9
|
-
});
|
|
10
|
-
} catch (e) {
|
|
11
|
-
}
|
|
12
3
|
try {
|
|
13
4
|
return await playwrightCore.chromium.launch({
|
|
14
5
|
headless: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function createBrowser(): Promise<any>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import playwrightCore from "playwright-core";
|
|
2
|
+
export default async function createBrowser() {
|
|
3
|
+
if (process.dev || process.env.prerender) {
|
|
4
|
+
try {
|
|
5
|
+
const { Launcher } = await import(String("chrome-launcher"));
|
|
6
|
+
const chromePath = Launcher.getFirstInstallation();
|
|
7
|
+
return await playwrightCore.chromium.launch({
|
|
8
|
+
headless: true,
|
|
9
|
+
executablePath: chromePath
|
|
10
|
+
});
|
|
11
|
+
} catch (e) {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await playwrightCore.chromium.launch({
|
|
16
|
+
headless: true
|
|
17
|
+
});
|
|
18
|
+
} catch (e) {
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const playwright = await import(String("playwright"));
|
|
22
|
+
return await playwright.chromium.launch({
|
|
23
|
+
headless: true
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (process.dev) {
|
|
27
|
+
console.warn("Failed to load chromium instance. Ensure you have chrome installed, otherwise add the dependency: `npm add -D playwright`.");
|
|
28
|
+
} else {
|
|
29
|
+
console.error("Failed to load browser instance. Please open an issue with the exception: https://github.com/harlan-zw/nuxt-og-image/issues.");
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
2
|
+
import { wasmLoader } from "../../utils.mjs";
|
|
3
|
+
const ReSvgLoader = wasmLoader("/* NUXT_OG_IMAGE_RESVG_WASM */", "/resvg.wasm");
|
|
4
|
+
export default async function(svg, options) {
|
|
5
|
+
const ReSvgWasm = await ReSvgLoader.load({ baseUrl: options.baseUrl });
|
|
6
|
+
await initWasm(ReSvgWasm).catch(() => {
|
|
7
|
+
});
|
|
8
|
+
const resvgJS = new Resvg(svg, {});
|
|
9
|
+
const pngData = resvgJS.render();
|
|
10
|
+
return pngData.asPng();
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { initialize, svg2png } from "svg2png-wasm";
|
|
2
|
+
import { wasmLoader } from "../../utils.mjs";
|
|
3
|
+
const Svg2PngLoader = wasmLoader("/* NUXT_OG_IMAGE_SVG2PNG_WASM */", "/svg2png.wasm");
|
|
4
|
+
export default async function(svg, options) {
|
|
5
|
+
const Svg2PngWasm = await Svg2PngLoader.load({ baseUrl: options.baseUrl });
|
|
6
|
+
await initialize(Svg2PngWasm).catch((e) => {
|
|
7
|
+
if (!e.message.trim().endsWith("function can be used only once."))
|
|
8
|
+
throw e;
|
|
9
|
+
});
|
|
10
|
+
return await svg2png(svg, options);
|
|
11
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import satori, { init } from "satori/wasm";
|
|
2
2
|
import initYoga from "yoga-wasm-web";
|
|
3
3
|
import { wasmLoader } from "../../utils.mjs";
|
|
4
|
+
const YogaLoader = wasmLoader("/* NUXT_OG_IMAGE_YOGA_WASM */", "/yoga.wasm");
|
|
4
5
|
export default async function(nodes, options) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
init(yoga);
|
|
9
|
-
}
|
|
6
|
+
const yogaWasm = await YogaLoader.load({ baseUrl: options.baseUrl });
|
|
7
|
+
const yoga = await initYoga(yogaWasm);
|
|
8
|
+
init(yoga);
|
|
10
9
|
return await satori(nodes, options);
|
|
11
10
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { withBase } from "ufo";
|
|
1
2
|
import { screenshot } from "../../browserUtil.mjs";
|
|
2
|
-
import
|
|
3
|
+
import loadBrowserLauncherChunk from "#nuxt-og-image/browser";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
5
|
export default {
|
|
4
6
|
name: "browser",
|
|
5
7
|
createSvg: async function createSvg() {
|
|
@@ -10,19 +12,23 @@ export default {
|
|
|
10
12
|
},
|
|
11
13
|
createPng: async function createPng(basePath, options) {
|
|
12
14
|
const url = new URL(basePath);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
const launchBrowser = await loadBrowserLauncherChunk();
|
|
16
|
+
if (!launchBrowser) {
|
|
17
|
+
throw new Error("Failed to load browser. Is the `browserProvider` enabled?");
|
|
18
|
+
}
|
|
19
|
+
const browser = await launchBrowser();
|
|
20
|
+
let res = null;
|
|
15
21
|
if (browser) {
|
|
16
22
|
try {
|
|
17
|
-
|
|
23
|
+
res = await screenshot(browser, {
|
|
18
24
|
...options,
|
|
19
|
-
host: url.origin,
|
|
25
|
+
host: withBase(useRuntimeConfig().app.baseURL, url.origin),
|
|
20
26
|
path: `/api/og-image-html?path=${url.pathname}`
|
|
21
27
|
});
|
|
22
28
|
} finally {
|
|
23
29
|
browser.close();
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
|
-
return
|
|
32
|
+
return res;
|
|
27
33
|
}
|
|
28
34
|
};
|
|
@@ -7,9 +7,10 @@ import twClasses from "./plugins/twClasses.mjs";
|
|
|
7
7
|
import flex from "./plugins/flex.mjs";
|
|
8
8
|
import emojis from "./plugins/emojis.mjs";
|
|
9
9
|
import encoding from "./plugins/encoding.mjs";
|
|
10
|
-
import
|
|
10
|
+
import loadPngCreator from "#nuxt-og-image/png";
|
|
11
11
|
import loadSatori from "#nuxt-og-image/satori";
|
|
12
|
-
import { useRuntimeConfig } from "#
|
|
12
|
+
import { useRuntimeConfig } from "#imports";
|
|
13
|
+
import { useNitroApp } from "#internal/nitro";
|
|
13
14
|
const satoriFonts = [];
|
|
14
15
|
let fontLoadPromise = null;
|
|
15
16
|
function loadFonts(fonts) {
|
|
@@ -21,20 +22,22 @@ export default {
|
|
|
21
22
|
name: "satori",
|
|
22
23
|
createPng: async function createPng(baseUrl, options) {
|
|
23
24
|
const svg = await this.createSvg(baseUrl, options);
|
|
24
|
-
const
|
|
25
|
-
return
|
|
25
|
+
const pngCreator = await loadPngCreator();
|
|
26
|
+
return pngCreator(svg, { baseUrl, ...options });
|
|
26
27
|
},
|
|
27
28
|
createVNode: async function createVNode(baseUrl, options) {
|
|
28
29
|
const url = parseURL(baseUrl);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
const nitroApp = useNitroApp();
|
|
31
|
+
const html = await (await nitroApp.localFetch(`/api/og-image-html?path=${url.pathname}&options=${encodeURI(JSON.stringify(options))}`)).text();
|
|
32
|
+
let body = html.match(/<body[^>]*>([\s\S]*)<\/body>/)?.[1] || "";
|
|
33
|
+
try {
|
|
34
|
+
body = twemoji.parse(body, {
|
|
35
|
+
folder: "svg",
|
|
36
|
+
ext: ".svg"
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
}
|
|
40
|
+
const satoriTree = convertHtmlToSatori(body);
|
|
38
41
|
await walkSatoriTree(url, satoriTree, [
|
|
39
42
|
// @todo add user land support
|
|
40
43
|
emojis(url),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineSatoriTransformer } from "../utils.mjs";
|
|
2
|
+
import { decodeHtml } from "../../../utils-pure.mjs";
|
|
2
3
|
export default defineSatoriTransformer(() => {
|
|
3
4
|
return {
|
|
4
5
|
filter: (node) => typeof node.props?.children === "string",
|
|
5
6
|
transform: async (node) => {
|
|
6
|
-
node.props.children = node.props.children
|
|
7
|
+
node.props.children = decodeHtml(node.props.children);
|
|
7
8
|
}
|
|
8
9
|
};
|
|
9
10
|
});
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { withBase } from "ufo";
|
|
2
2
|
import { defineSatoriTransformer } from "../utils.mjs";
|
|
3
3
|
import { readPublicAssetBase64 } from "../../../utils.mjs";
|
|
4
|
+
import { useNitroApp } from "#internal/nitro";
|
|
4
5
|
export default defineSatoriTransformer((url) => {
|
|
5
6
|
return {
|
|
6
7
|
filter: (node) => node.type === "img",
|
|
7
8
|
transform: async (node) => {
|
|
8
9
|
const src = node.props?.src;
|
|
9
10
|
if (src && src.startsWith("/")) {
|
|
11
|
+
let updated = false;
|
|
10
12
|
const file = await readPublicAssetBase64(src);
|
|
11
|
-
if (file)
|
|
13
|
+
if (file) {
|
|
12
14
|
node.props.src = file;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
updated = true;
|
|
16
|
+
}
|
|
17
|
+
if (!updated) {
|
|
18
|
+
try {
|
|
19
|
+
const response = await globalThis.$fetch(src);
|
|
20
|
+
node.props.src = response.arrayBuffer();
|
|
21
|
+
updated = true;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!updated) {
|
|
26
|
+
const nitroApp = useNitroApp();
|
|
27
|
+
try {
|
|
28
|
+
const response = await nitroApp.localFetch(src);
|
|
29
|
+
if (response.status === 200) {
|
|
30
|
+
node.props.src = response.arrayBuffer();
|
|
31
|
+
updated = true;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!updated) {
|
|
37
|
+
node.props.src = `${withBase(src, `${url.protocol}//${url.host}`)}?${Date.now()}`;
|
|
38
|
+
}
|
|
15
39
|
}
|
|
16
40
|
}
|
|
17
41
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParsedURL } from 'ufo';
|
|
2
|
-
import type { SatoriTransformer, VNode } from '../../../../types';
|
|
3
|
-
export declare function loadFont(font:
|
|
2
|
+
import type { FontConfig, SatoriTransformer, VNode } from '../../../../types';
|
|
3
|
+
export declare function loadFont(font: FontConfig): Promise<any>;
|
|
4
4
|
export declare function walkSatoriTree(url: ParsedURL, node: VNode, plugins: SatoriTransformer[]): Promise<void>;
|
|
5
5
|
export declare function defineSatoriTransformer(transformer: (url: ParsedURL) => SatoriTransformer): (url: ParsedURL) => SatoriTransformer;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { base64ToArrayBuffer, readPublicAsset } from "../../utils.mjs";
|
|
2
3
|
import { useStorage } from "#internal/nitro";
|
|
3
4
|
const cachedFonts = {};
|
|
4
5
|
export async function loadFont(font) {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
let fontKey = font;
|
|
7
|
+
if (typeof font === "object")
|
|
8
|
+
fontKey = `${font.name}:${font.weight}`;
|
|
9
|
+
if (cachedFonts[fontKey])
|
|
10
|
+
return cachedFonts[fontKey];
|
|
11
|
+
const [name, weight] = fontKey.split(":");
|
|
7
12
|
let data;
|
|
8
|
-
const storageKey = `assets:nuxt-og-imagee:font:${
|
|
9
|
-
if (await useStorage().hasItem(storageKey))
|
|
13
|
+
const storageKey = `assets:nuxt-og-imagee:font:${fontKey}`;
|
|
14
|
+
if (await useStorage().hasItem(storageKey))
|
|
10
15
|
data = base64ToArrayBuffer(await useStorage().getItem(storageKey));
|
|
11
|
-
|
|
16
|
+
if (!data && name === "Inter" && ["400", "700"].includes(weight)) {
|
|
17
|
+
data = await readPublicAsset(`/inter-latin-ext-${weight}-normal.woff`);
|
|
12
18
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
if (typeof font === "object") {
|
|
20
|
+
data = await readPublicAsset(font.path);
|
|
21
|
+
if (!data) {
|
|
22
|
+
data = await globalThis.$fetch(font.path, {
|
|
23
|
+
responseType: "arrayBuffer"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
18
26
|
}
|
|
19
27
|
if (!data) {
|
|
20
28
|
const fontUrl = await globalThis.$fetch("/api/og-image-font", {
|
|
@@ -24,8 +32,9 @@ export async function loadFont(font) {
|
|
|
24
32
|
responseType: "arrayBuffer"
|
|
25
33
|
});
|
|
26
34
|
}
|
|
35
|
+
cachedFonts[fontKey] = { name, weight: Number(weight), data, style: "normal" };
|
|
27
36
|
await useStorage().setItem(storageKey, Buffer.from(data).toString("base64"));
|
|
28
|
-
return cachedFonts[
|
|
37
|
+
return cachedFonts[fontKey];
|
|
29
38
|
}
|
|
30
39
|
export async function walkSatoriTree(url, node, plugins) {
|
|
31
40
|
if (!node.props?.children)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineEventHandler, setHeader } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
export default defineEventHandler(async (e) => {
|
|
4
|
+
setHeader(e, "Content-Type", "application/json");
|
|
5
|
+
const runtimeConfig = useRuntimeConfig()["nuxt-og-image"];
|
|
6
|
+
return {
|
|
7
|
+
runtimeConfig
|
|
8
|
+
};
|
|
9
|
+
});
|
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
import { withBase } from "ufo";
|
|
2
2
|
import { renderSSRHead } from "@unhead/ssr";
|
|
3
3
|
import { createHeadCore } from "@unhead/vue";
|
|
4
|
-
import { defineEventHandler, getQuery, sendRedirect } from "h3";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { createError, defineEventHandler, getQuery, sendRedirect } from "h3";
|
|
5
|
+
import { hash } from "ohash";
|
|
6
|
+
import { fetchOptions, useHostname } from "../utils.mjs";
|
|
7
|
+
import { useRuntimeConfig } from "#imports";
|
|
7
8
|
export default defineEventHandler(async (e) => {
|
|
8
9
|
const { fonts, defaults } = useRuntimeConfig()["nuxt-og-image"];
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const query = getQuery(e);
|
|
11
|
+
const path = withBase(query.path || "/", useRuntimeConfig().app.baseURL);
|
|
12
|
+
const scale = query.scale;
|
|
13
|
+
const mode = query.mode || "light";
|
|
12
14
|
let options;
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
+
if (query.options) {
|
|
16
|
+
try {
|
|
17
|
+
options = JSON.parse(query.options);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
15
21
|
if (!options)
|
|
16
22
|
options = await fetchOptions(e, path);
|
|
17
|
-
if (options.provider === "browser" && !options.component)
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
if (options.provider === "browser" && !options.component) {
|
|
24
|
+
const pathWithoutBase = path.replace(new RegExp(`^${useRuntimeConfig().app.baseURL}`), "");
|
|
25
|
+
return sendRedirect(e, withBase(pathWithoutBase, useHostname(e)));
|
|
26
|
+
}
|
|
27
|
+
if (!options.component) {
|
|
28
|
+
throw createError({
|
|
29
|
+
statusCode: 500,
|
|
30
|
+
statusMessage: `Nuxt OG Image trying to render an invalid component. Received options ${JSON.stringify(options)}`
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const hashId = hash([options.component, options]);
|
|
34
|
+
const island = await $fetch(`/__nuxt_island/${options.component}:${hashId}`, {
|
|
35
|
+
params: {
|
|
36
|
+
props: JSON.stringify(options)
|
|
37
|
+
}
|
|
38
|
+
});
|
|
20
39
|
const head = createHeadCore();
|
|
21
40
|
head.push(island.head);
|
|
41
|
+
let defaultFontFamily = "sans-serif";
|
|
42
|
+
const firstFont = fonts[0];
|
|
43
|
+
if (firstFont) {
|
|
44
|
+
if (typeof firstFont === "string")
|
|
45
|
+
defaultFontFamily = firstFont.split(":")[0];
|
|
46
|
+
else
|
|
47
|
+
defaultFontFamily = firstFont.name;
|
|
48
|
+
}
|
|
22
49
|
head.push({
|
|
23
50
|
style: [
|
|
24
51
|
{
|
|
25
52
|
// default font is the first font family
|
|
26
|
-
innerHTML: `body { font-family: '${
|
|
53
|
+
innerHTML: `body { font-family: '${defaultFontFamily.replace("+", " ")}', sans-serif; }`
|
|
27
54
|
},
|
|
28
55
|
scale ? {
|
|
29
56
|
innerHTML: `body {
|
|
@@ -43,7 +70,18 @@ img.emoji {
|
|
|
43
70
|
vertical-align: -0.1em;
|
|
44
71
|
}
|
|
45
72
|
`
|
|
46
|
-
} : {}
|
|
73
|
+
} : {},
|
|
74
|
+
...fonts.filter((font) => typeof font === "object").map((font) => {
|
|
75
|
+
const { name, weight, path: path2 } = font;
|
|
76
|
+
return `
|
|
77
|
+
@font-face {
|
|
78
|
+
font-family: '${name}';
|
|
79
|
+
font-style: normal;
|
|
80
|
+
font-weight: ${weight};
|
|
81
|
+
src: url('${path2}') format('truetype');
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
})
|
|
47
85
|
],
|
|
48
86
|
meta: [
|
|
49
87
|
{
|
|
@@ -70,7 +108,7 @@ img.emoji {
|
|
|
70
108
|
rel: "stylesheet"
|
|
71
109
|
},
|
|
72
110
|
// have to add each weight as their own stylesheet
|
|
73
|
-
...fonts.map((font) => {
|
|
111
|
+
...fonts.filter((font) => typeof font === "string").map((font) => {
|
|
74
112
|
const [name, weight] = font.split(":");
|
|
75
113
|
return {
|
|
76
114
|
href: `https://fonts.googleapis.com/css2?family=${name}:wght@${weight}&display=swap`,
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { createError, defineEventHandler,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createError, defineEventHandler, getQuery } from "h3";
|
|
2
|
+
import { withoutBase } from "ufo";
|
|
3
|
+
import { extractOgImageOptions } from "../utils.mjs";
|
|
4
|
+
import { getRouteRules, useNitroApp } from "#internal/nitro";
|
|
5
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
6
|
export default defineEventHandler(async (e) => {
|
|
5
7
|
const query = getQuery(e);
|
|
6
|
-
const path = query.path || "/";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const path = withoutBase(query.path || "/", useRuntimeConfig().app.baseURL);
|
|
9
|
+
const nitro = useNitroApp();
|
|
10
|
+
let html;
|
|
11
|
+
try {
|
|
12
|
+
html = await (await nitro.localFetch(path)).text();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
throw createError({
|
|
15
|
+
statusCode: 500,
|
|
16
|
+
statusMessage: `Failed to read the path ${path} for og-image extraction. ${err.message}.`
|
|
17
|
+
});
|
|
18
|
+
}
|
|
15
19
|
const extractedPayload = extractOgImageOptions(html);
|
|
16
20
|
if (!extractedPayload) {
|
|
17
21
|
throw createError({
|
|
@@ -20,8 +24,10 @@ export default defineEventHandler(async (e) => {
|
|
|
20
24
|
});
|
|
21
25
|
}
|
|
22
26
|
e.node.req.url = path;
|
|
27
|
+
const oldRouteRules = e.context._nitro.routeRules;
|
|
23
28
|
e.context._nitro.routeRules = void 0;
|
|
24
29
|
const routeRules = getRouteRules(e)?.ogImage;
|
|
30
|
+
e.context._nitro.routeRules = oldRouteRules;
|
|
25
31
|
e.node.req.url = e.path;
|
|
26
32
|
if (routeRules === false)
|
|
27
33
|
return false;
|
|
@@ -32,8 +38,6 @@ export default defineEventHandler(async (e) => {
|
|
|
32
38
|
// use route rules
|
|
33
39
|
...routeRules || {},
|
|
34
40
|
// use provided data
|
|
35
|
-
...extractedPayload
|
|
36
|
-
// use query data
|
|
37
|
-
...query
|
|
41
|
+
...extractedPayload
|
|
38
42
|
};
|
|
39
43
|
});
|
|
@@ -2,8 +2,10 @@ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
|
2
2
|
import { withBase } from "ufo";
|
|
3
3
|
import { fetchOptions, useHostname } from "../utils.mjs";
|
|
4
4
|
import { useProvider } from "#nuxt-og-image/provider";
|
|
5
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
6
|
export default defineEventHandler(async (e) => {
|
|
6
|
-
const
|
|
7
|
+
const query = getQuery(e);
|
|
8
|
+
const path = withBase(query.path || "/", useRuntimeConfig().app.baseURL);
|
|
7
9
|
const options = await fetchOptions(e, path);
|
|
8
10
|
setHeader(e, "Content-Type", "image/svg+xml");
|
|
9
11
|
const provider = await useProvider(options.provider);
|