nuxt-og-image 6.9.1 → 6.10.0
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 +9 -4
- package/dist/chunks/tw4.mjs +2 -3
- package/dist/chunks/uno.mjs +3 -4
- package/dist/cli.mjs +98 -15
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -3
- package/dist/runtime/app/composables/zero-runtime.d.ts +9 -0
- package/dist/runtime/app/composables/zero-runtime.js +6 -0
- package/dist/runtime/app/utils.js +3 -0
- package/dist/runtime/server/og-image/context.js +0 -3
- package/dist/runtime/server/og-image/takumi/nodes.d.ts +1 -1
- package/dist/runtime/server/og-image/takumi/nodes.js +20 -45
- package/dist/runtime/server/plugins/auto-eject.d.ts +2 -0
- package/dist/runtime/server/plugins/auto-eject.js +36 -0
- package/dist/runtime/server/util/auto-eject.d.ts +1 -3
- package/dist/runtime/server/util/auto-eject.js +1 -4
- package/dist/runtime/server/util/options.js +5 -15
- package/dist/shared/{nuxt-og-image.CNS1IVAi.mjs → nuxt-og-image.DWcVJOVy.mjs} +219 -119
- package/package.json +19 -18
- package/skills/nuxt-og-image/SKILL.md +134 -0
package/README.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
[![License][license-src]][license-href]
|
|
8
8
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
9
9
|
|
|
10
|
+
<a href="https://skilld.dev/gh/nuxt-modules/og-image">
|
|
11
|
+
<picture>
|
|
12
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://skilld.dev/b/nuxt-modules/og-image?theme=dark">
|
|
13
|
+
<source media="(prefers-color-scheme: light)" srcset="https://skilld.dev/b/nuxt-modules/og-image?theme=light">
|
|
14
|
+
<img alt="Skill repository on skilld.dev" src="https://skilld.dev/b/nuxt-modules/og-image?theme=light">
|
|
15
|
+
</picture>
|
|
16
|
+
</a>
|
|
17
|
+
|
|
10
18
|
Nuxt OG Image allows you to easily generate OG Images using either Vue components or screenshots of your pages.
|
|
11
19
|
|
|
12
20
|
OG Images are known to improve click-through rates on social media.
|
|
@@ -42,10 +50,7 @@ npx nuxi@latest module add og-image
|
|
|
42
50
|
```
|
|
43
51
|
|
|
44
52
|
> [!TIP]
|
|
45
|
-
>
|
|
46
|
-
> ```bash
|
|
47
|
-
> npx skilld add nuxt-og-image
|
|
48
|
-
> ```
|
|
53
|
+
> Using an AI agent? Get the nuxt-og-image Skill on [skilld.dev/gh/nuxt-modules/og-image](https://skilld.dev/gh/nuxt-modules/og-image).
|
|
49
54
|
|
|
50
55
|
💡 Built your OG images? Check how they render across Twitter, Facebook, LinkedIn and Slack with the free [Social Share Debugger](https://nuxtseo.com/tools/social-share-debugger), or monitor them site-wide with [Nuxt SEO Pro](https://nuxtseo.com/pro).
|
|
51
56
|
|
package/dist/chunks/tw4.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { resolveModulePath } from 'exsolve';
|
|
4
4
|
import { dirname, join } from 'pathe';
|
|
5
|
-
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.
|
|
5
|
+
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.DWcVJOVy.mjs';
|
|
6
6
|
import 'node:crypto';
|
|
7
7
|
import 'node:fs';
|
|
8
8
|
import '@nuxt/kit';
|
|
@@ -11,18 +11,17 @@ import 'fnv1a-64';
|
|
|
11
11
|
import 'nuxt-site-config/kit';
|
|
12
12
|
import 'nuxtseo-shared/kit';
|
|
13
13
|
import 'pkg-types';
|
|
14
|
-
import 'std-env';
|
|
15
14
|
import 'magic-string';
|
|
16
15
|
import 'oxc-walker';
|
|
17
16
|
import '../../dist/runtime/logger.js';
|
|
18
17
|
import 'chrome-launcher';
|
|
18
|
+
import 'std-env';
|
|
19
19
|
import 'nypm';
|
|
20
20
|
import 'nuxtseo-shared/devtools';
|
|
21
21
|
import 'unstorage';
|
|
22
22
|
import 'unstorage/drivers/fs-lite';
|
|
23
23
|
import '../../dist/runtime/server/og-image/utils/css.js';
|
|
24
24
|
import 'strip-literal';
|
|
25
|
-
import 'ufo';
|
|
26
25
|
import 'unplugin';
|
|
27
26
|
import '@vue/compiler-sfc';
|
|
28
27
|
import 'ofetch';
|
package/dist/chunks/uno.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import { logger } from '../../dist/runtime/logger.js';
|
|
4
|
-
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.
|
|
5
|
-
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.
|
|
4
|
+
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.DWcVJOVy.mjs';
|
|
5
|
+
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.DWcVJOVy.mjs';
|
|
6
6
|
import 'node:crypto';
|
|
7
7
|
import 'node:fs';
|
|
8
8
|
import 'node:url';
|
|
@@ -12,18 +12,17 @@ import 'nuxt-site-config/kit';
|
|
|
12
12
|
import 'nuxtseo-shared/kit';
|
|
13
13
|
import 'pathe';
|
|
14
14
|
import 'pkg-types';
|
|
15
|
-
import 'std-env';
|
|
16
15
|
import 'magic-string';
|
|
17
16
|
import 'oxc-walker';
|
|
18
17
|
import 'exsolve';
|
|
19
18
|
import 'chrome-launcher';
|
|
19
|
+
import 'std-env';
|
|
20
20
|
import 'nypm';
|
|
21
21
|
import 'nuxtseo-shared/devtools';
|
|
22
22
|
import 'unstorage';
|
|
23
23
|
import 'unstorage/drivers/fs-lite';
|
|
24
24
|
import '../../dist/runtime/server/og-image/utils/css.js';
|
|
25
25
|
import 'strip-literal';
|
|
26
|
-
import 'ufo';
|
|
27
26
|
import 'unplugin';
|
|
28
27
|
import '@vue/compiler-sfc';
|
|
29
28
|
import 'ofetch';
|
package/dist/cli.mjs
CHANGED
|
@@ -13,6 +13,67 @@ import 'magicast';
|
|
|
13
13
|
import 'magicast/helpers';
|
|
14
14
|
import '../dist/runtime/logger.js';
|
|
15
15
|
|
|
16
|
+
const RENDERER_PREFERENCE = ["takumi", "satori", "browser"];
|
|
17
|
+
const RE_TEMPLATE_FILE = /^(.+)\.(takumi|satori|browser)\.vue$/;
|
|
18
|
+
const RENDERER_PACKAGES = {
|
|
19
|
+
takumi: ["@takumi-rs/core", "@takumi-rs/wasm"],
|
|
20
|
+
satori: ["satori"],
|
|
21
|
+
browser: ["playwright-core"]
|
|
22
|
+
};
|
|
23
|
+
function parseTemplateFile(file) {
|
|
24
|
+
const match = RE_TEMPLATE_FILE.exec(file);
|
|
25
|
+
if (!match)
|
|
26
|
+
return null;
|
|
27
|
+
return { baseName: match[1], renderer: match[2] };
|
|
28
|
+
}
|
|
29
|
+
function listEjectableNames(templateFiles) {
|
|
30
|
+
const names = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const file of templateFiles) {
|
|
32
|
+
const parsed = parseTemplateFile(file);
|
|
33
|
+
if (!parsed)
|
|
34
|
+
continue;
|
|
35
|
+
names.add(parsed.baseName);
|
|
36
|
+
names.add(`${parsed.baseName}.${parsed.renderer}`);
|
|
37
|
+
}
|
|
38
|
+
return [...names].sort();
|
|
39
|
+
}
|
|
40
|
+
function resolveEjectTemplate(name, ctx) {
|
|
41
|
+
const requested = name.replace(/\.vue$/, "");
|
|
42
|
+
const variants = ctx.templateFiles.map((file) => ({ file, parsed: parseTemplateFile(file) })).filter((v) => !!v.parsed);
|
|
43
|
+
const exact = variants.find((v) => `${v.parsed.baseName}.${v.parsed.renderer}` === requested);
|
|
44
|
+
if (exact)
|
|
45
|
+
return { _tag: "Ok", file: exact.file, renderer: exact.parsed.renderer, reason: "exact" };
|
|
46
|
+
const candidates = variants.filter((v) => v.parsed.baseName === requested);
|
|
47
|
+
if (candidates.length === 0)
|
|
48
|
+
return { _tag: "NotFound", name, available: listEjectableNames(ctx.templateFiles) };
|
|
49
|
+
if (candidates.length === 1)
|
|
50
|
+
return { _tag: "Ok", file: candidates[0].file, renderer: candidates[0].parsed.renderer, reason: "only-variant" };
|
|
51
|
+
const pick = (renderers, reason) => {
|
|
52
|
+
for (const renderer of RENDERER_PREFERENCE) {
|
|
53
|
+
if (!renderers.includes(renderer))
|
|
54
|
+
continue;
|
|
55
|
+
const match = candidates.find((c) => c.parsed.renderer === renderer);
|
|
56
|
+
if (match)
|
|
57
|
+
return { _tag: "Ok", file: match.file, renderer, reason };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
return pick(ctx.appRenderers, "app-templates") ?? pick(ctx.installedRenderers, "installed") ?? pick(RENDERER_PREFERENCE, "default");
|
|
62
|
+
}
|
|
63
|
+
function detectInstalledRenderers(pkg) {
|
|
64
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
65
|
+
return RENDERER_PREFERENCE.filter((r) => RENDERER_PACKAGES[r].some((p) => p in deps));
|
|
66
|
+
}
|
|
67
|
+
function detectAppRenderers(files) {
|
|
68
|
+
const found = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const parsed = parseTemplateFile(file.split("/").pop() || file);
|
|
71
|
+
if (parsed)
|
|
72
|
+
found.add(parsed.renderer);
|
|
73
|
+
}
|
|
74
|
+
return RENDERER_PREFERENCE.filter((r) => found.has(r));
|
|
75
|
+
}
|
|
76
|
+
|
|
16
77
|
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
17
78
|
const communityDir = resolve(__dirname$1, "runtime/app/components/Templates/Community");
|
|
18
79
|
const RE_RENDERER_SUFFIX = /\.(satori|browser|takumi)\.vue$/;
|
|
@@ -195,28 +256,30 @@ function listTemplates() {
|
|
|
195
256
|
const templates = [...new Set(
|
|
196
257
|
readdirSync(communityDir).filter((f) => f.endsWith(".vue")).map(getBaseName)
|
|
197
258
|
)];
|
|
259
|
+
const variants = listEjectableNames(readdirSync(communityDir)).filter((n) => n.includes("."));
|
|
198
260
|
p.intro("Community Templates");
|
|
199
261
|
p.note(templates.map((t) => `\u2022 ${t}`).join("\n"), "Available");
|
|
200
262
|
p.log.info("Usage: npx nuxt-og-image eject <template-name>");
|
|
263
|
+
p.log.info(`To pick a renderer, add its suffix: ${variants.slice(0, 2).join(", ")}`);
|
|
201
264
|
p.outro("");
|
|
202
265
|
}
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
266
|
+
function ejectTemplate(name, targetDir, cwd = process.cwd()) {
|
|
267
|
+
const appComponents = [.../* @__PURE__ */ new Set([cwd, targetDir])].flatMap(findOgImageComponents);
|
|
268
|
+
const resolution = resolveEjectTemplate(name, {
|
|
269
|
+
templateFiles: readdirSync(communityDir).filter((f) => f.endsWith(".vue")),
|
|
270
|
+
appRenderers: detectAppRenderers(appComponents),
|
|
271
|
+
installedRenderers: detectInstalledRenderers(readPackageJson(cwd))
|
|
272
|
+
});
|
|
273
|
+
if (resolution._tag === "NotFound") {
|
|
211
274
|
p.log.error(`Template "${name}" not found.`);
|
|
212
275
|
listTemplates();
|
|
213
276
|
process.exit(1);
|
|
214
277
|
}
|
|
215
|
-
const templatePath = join(communityDir,
|
|
278
|
+
const templatePath = join(communityDir, resolution.file);
|
|
216
279
|
const outputDir = resolve(targetDir, "components", "OgImage");
|
|
217
280
|
if (!existsSync(outputDir))
|
|
218
281
|
mkdirSync(outputDir, { recursive: true });
|
|
219
|
-
const outputPath = join(outputDir,
|
|
282
|
+
const outputPath = join(outputDir, resolution.file);
|
|
220
283
|
if (existsSync(outputPath)) {
|
|
221
284
|
p.log.error(`File already exists: ${outputPath}`);
|
|
222
285
|
process.exit(1);
|
|
@@ -224,6 +287,17 @@ function ejectTemplate(name, targetDir) {
|
|
|
224
287
|
const content = readFileSync(templatePath, "utf-8");
|
|
225
288
|
writeFileSync(outputPath, content, "utf-8");
|
|
226
289
|
p.log.success(`Ejected "${name}" to ${outputPath}`);
|
|
290
|
+
const why = {
|
|
291
|
+
"exact": null,
|
|
292
|
+
"only-variant": null,
|
|
293
|
+
"app-templates": `your templates use ${resolution.renderer}`,
|
|
294
|
+
"installed": `${resolution.renderer} is in package.json`,
|
|
295
|
+
"default": `${resolution.renderer} is the default renderer`
|
|
296
|
+
}[resolution.reason];
|
|
297
|
+
if (why) {
|
|
298
|
+
const others = listEjectableNames(readdirSync(communityDir)).filter((n) => n.startsWith(`${getBaseName(resolution.file)}.`) && n !== resolution.file.replace(".vue", ""));
|
|
299
|
+
p.log.info(`Picked the ${resolution.renderer} variant because ${why}.${others.length ? ` For another renderer, run: npx nuxt-og-image eject ${others[0]}` : ""}`);
|
|
300
|
+
}
|
|
227
301
|
}
|
|
228
302
|
function findOgImageComponents(dir) {
|
|
229
303
|
const components = [];
|
|
@@ -445,11 +519,19 @@ function collectTemplateReplacements(templateHtml) {
|
|
|
445
519
|
if (el.name !== "OgImage" && el.name !== "OgImageScreenshot")
|
|
446
520
|
return;
|
|
447
521
|
const isScreenshot = el.name === "OgImageScreenshot";
|
|
448
|
-
const composable = isScreenshot ? "defineOgImageScreenshot" : "defineOgImage";
|
|
449
|
-
const propsStr = attrsToProps(el.attributes);
|
|
450
522
|
const start = el.loc[0].start;
|
|
451
523
|
const end = el.loc[1].end;
|
|
452
|
-
|
|
524
|
+
let call;
|
|
525
|
+
if (isScreenshot) {
|
|
526
|
+
const propsStr = attrsToProps(el.attributes);
|
|
527
|
+
call = `defineOgImageScreenshot(${propsStr})`;
|
|
528
|
+
} else {
|
|
529
|
+
const { component, ":component": dynamicComponent, ...props } = el.attributes;
|
|
530
|
+
const componentArg = dynamicComponent || (component ? `'${component}'` : `'NuxtSeo'`);
|
|
531
|
+
const propsStr = attrsToProps(props);
|
|
532
|
+
call = `defineOgImage(${componentArg}${propsStr ? `, ${propsStr}` : ""})`;
|
|
533
|
+
}
|
|
534
|
+
const text = `<!-- Migrated: use ${call} in <script setup> -->`;
|
|
453
535
|
replacements.push({ start, end, text });
|
|
454
536
|
});
|
|
455
537
|
return replacements;
|
|
@@ -755,7 +837,7 @@ async function runMigrate(args2) {
|
|
|
755
837
|
if (shouldEject) {
|
|
756
838
|
const targetDir = existsSync(join(cwd, "app")) ? join(cwd, "app") : cwd;
|
|
757
839
|
for (const name of migrationCheck.usedCommunityTemplates) {
|
|
758
|
-
ejectTemplate(name, targetDir);
|
|
840
|
+
ejectTemplate(name, targetDir, cwd);
|
|
759
841
|
}
|
|
760
842
|
} else {
|
|
761
843
|
p.log.info("Run manually before building for production:");
|
|
@@ -1273,6 +1355,7 @@ function showHelp() {
|
|
|
1273
1355
|
" Options: --renderer <renderer>, --path <dir>",
|
|
1274
1356
|
"list List available community templates",
|
|
1275
1357
|
"eject <name> Eject a community template to your project",
|
|
1358
|
+
" Picks the variant for your renderer; add .takumi or .satori to choose",
|
|
1276
1359
|
"switch Switch components between renderers",
|
|
1277
1360
|
" Options: --from <renderer>, --to <renderer>, --dry-run, --yes",
|
|
1278
1361
|
"enable <renderer> Install dependencies for a renderer (satori, browser, takumi)",
|
|
@@ -1299,7 +1382,7 @@ if (hasHelp || !command) {
|
|
|
1299
1382
|
}
|
|
1300
1383
|
const cwd = process.cwd();
|
|
1301
1384
|
const targetDir = existsSync(join(cwd, "app")) ? join(cwd, "app") : cwd;
|
|
1302
|
-
ejectTemplate(templateName, targetDir);
|
|
1385
|
+
ejectTemplate(templateName, targetDir, cwd);
|
|
1303
1386
|
} else if (command === "list") {
|
|
1304
1387
|
listTemplates();
|
|
1305
1388
|
} else if (command === "migrate") {
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -9,20 +9,19 @@ import 'nuxt-site-config/kit';
|
|
|
9
9
|
import 'nuxtseo-shared/kit';
|
|
10
10
|
import 'pathe';
|
|
11
11
|
import 'pkg-types';
|
|
12
|
-
|
|
13
|
-
export { n as default } from './shared/nuxt-og-image.CNS1IVAi.mjs';
|
|
12
|
+
export { n as default } from './shared/nuxt-og-image.DWcVJOVy.mjs';
|
|
14
13
|
import '../dist/runtime/logger.js';
|
|
15
14
|
import 'magic-string';
|
|
16
15
|
import 'oxc-walker';
|
|
17
16
|
import 'exsolve';
|
|
18
17
|
import 'chrome-launcher';
|
|
18
|
+
import 'std-env';
|
|
19
19
|
import 'nypm';
|
|
20
20
|
import 'nuxtseo-shared/devtools';
|
|
21
21
|
import 'unstorage';
|
|
22
22
|
import 'unstorage/drivers/fs-lite';
|
|
23
23
|
import '../dist/runtime/server/og-image/utils/css.js';
|
|
24
24
|
import 'strip-literal';
|
|
25
|
-
import 'ufo';
|
|
26
25
|
import 'unplugin';
|
|
27
26
|
import '@vue/compiler-sfc';
|
|
28
27
|
import 'ofetch';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineOgImage as _defineOgImage } from './defineOgImage.js';
|
|
2
|
+
import { defineOgImageComponent as _defineOgImageComponent } from './defineOgImageComponent.js';
|
|
3
|
+
import { defineOgImageScreenshot as _defineOgImageScreenshot } from './defineOgImageScreenshot.js';
|
|
4
|
+
export declare const defineOgImage: typeof _defineOgImage;
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use `defineOgImage()` instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare const defineOgImageComponent: typeof _defineOgImageComponent;
|
|
9
|
+
export declare const defineOgImageScreenshot: typeof _defineOgImageScreenshot;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineOgImage as _defineOgImage } from "./defineOgImage.js";
|
|
2
|
+
import { defineOgImageComponent as _defineOgImageComponent } from "./defineOgImageComponent.js";
|
|
3
|
+
import { defineOgImageScreenshot as _defineOgImageScreenshot } from "./defineOgImageScreenshot.js";
|
|
4
|
+
export const defineOgImage = (...args) => import.meta.prerender ? _defineOgImage(...args) : [];
|
|
5
|
+
export const defineOgImageComponent = (...args) => import.meta.prerender ? _defineOgImageComponent(...args) : [];
|
|
6
|
+
export const defineOgImageScreenshot = (...args) => import.meta.prerender ? _defineOgImageScreenshot(...args) : [];
|
|
@@ -184,9 +184,12 @@ export function resolveComponentName(component) {
|
|
|
184
184
|
const originalName = component;
|
|
185
185
|
const normalizedName = originalName.split(".").map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
186
186
|
const inputBase = normalizedName.replace(RE_RENDERER_SUFFIX, "");
|
|
187
|
+
const requestedRenderer = normalizedName.match(RE_RENDERER_SUFFIX)?.[1]?.toLowerCase();
|
|
187
188
|
for (const component2 of componentNames) {
|
|
188
189
|
if (component2.pascalName === normalizedName)
|
|
189
190
|
return component2.pascalName;
|
|
191
|
+
if (requestedRenderer && component2.renderer !== requestedRenderer)
|
|
192
|
+
continue;
|
|
190
193
|
const basePascalName = component2.pascalName.replace(RE_RENDERER_SUFFIX, "");
|
|
191
194
|
if (basePascalName === originalName || basePascalName === inputBase)
|
|
192
195
|
return component2.pascalName;
|
|
@@ -9,7 +9,6 @@ import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
|
9
9
|
import { logger } from "../../logger.js";
|
|
10
10
|
import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProps, verifyOgImageSignature } from "../../shared.js";
|
|
11
11
|
import { hashKey } from "../../shared/hash.js";
|
|
12
|
-
import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
|
|
13
12
|
import { createNitroRouteRuleMatcher } from "../util/kit.js";
|
|
14
13
|
import { normaliseOptions } from "../util/options.js";
|
|
15
14
|
import { getEventQuery } from "../util/query.js";
|
|
@@ -174,8 +173,6 @@ export async function resolveContext(e) {
|
|
|
174
173
|
}
|
|
175
174
|
normalised.options.props = filtered;
|
|
176
175
|
}
|
|
177
|
-
if (normalised.component?.category === "community")
|
|
178
|
-
autoEjectCommunityTemplate(normalised.component, runtimeConfig, { requestPath: e.path });
|
|
179
176
|
const rendererType = normalised.renderer;
|
|
180
177
|
const baseCacheKey = normalised.options.cacheKey || (hashMatch ? `hash:${hashMatch[1]}` : resolvePathCacheKey(e, basePathWithQuery, normalised.options));
|
|
181
178
|
const key = componentHash ? `${baseCacheKey}:${componentHash}` : baseCacheKey;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Node } from '@takumi-rs/helpers';
|
|
2
2
|
import type { OgImageRenderEventContext, VNode } from '../../../types.js';
|
|
3
3
|
export declare function createTakumiNodes(ctx: OgImageRenderEventContext): Promise<Node>;
|
|
4
|
-
export declare function vnodeToTakumiNode(vnode: VNode,
|
|
4
|
+
export declare function vnodeToTakumiNode(vnode: VNode, inheritedColor?: string): Promise<Node>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createVNodes, resolveSvgDimension, SVG_CAMEL_ATTR_VALUES } from "../core/vnodes.js";
|
|
2
|
-
const RE_RELATIVE_UNIT = /^
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const DEFAULT_FONT_SIZE = 16;
|
|
2
|
+
const RE_RELATIVE_UNIT = /^[\d.]+r?em$/;
|
|
3
|
+
const RE_LEADING_SPACE = /^\s/;
|
|
4
|
+
const RE_TRAILING_SPACE = /\s$/;
|
|
6
5
|
const RE_UPPERCASE = /[A-Z]/g;
|
|
7
6
|
const RE_DQUOTE = /"/g;
|
|
8
7
|
const RE_AMP = /&/g;
|
|
@@ -10,7 +9,7 @@ const RE_LT = /</g;
|
|
|
10
9
|
const RE_GT = />/g;
|
|
11
10
|
export async function createTakumiNodes(ctx) {
|
|
12
11
|
const vnodeTree = await createVNodes(ctx);
|
|
13
|
-
return await ctx.timings.measure("takumi-nodes", () => vnodeToTakumiNode(vnodeTree
|
|
12
|
+
return await ctx.timings.measure("takumi-nodes", () => vnodeToTakumiNode(vnodeTree));
|
|
14
13
|
}
|
|
15
14
|
function pickNumericDimension(props, key) {
|
|
16
15
|
const v = props[key];
|
|
@@ -19,35 +18,11 @@ function pickNumericDimension(props, key) {
|
|
|
19
18
|
const n = Number(v);
|
|
20
19
|
return Number.isNaN(n) ? void 0 : n;
|
|
21
20
|
}
|
|
22
|
-
function resolveRelativeUnit(value, inheritedFontSize) {
|
|
23
|
-
if (value == null)
|
|
24
|
-
return void 0;
|
|
25
|
-
const match = String(value).match(RE_RELATIVE_UNIT);
|
|
26
|
-
if (!match)
|
|
27
|
-
return void 0;
|
|
28
|
-
const n = Number.parseFloat(match[1]);
|
|
29
|
-
return match[2] === "rem" ? n * DEFAULT_FONT_SIZE : n * inheritedFontSize;
|
|
30
|
-
}
|
|
31
|
-
function extractFontSize(props, style) {
|
|
32
|
-
if (style?.fontSize != null) {
|
|
33
|
-
const m = String(style.fontSize).match(RE_FONT_SIZE_PX);
|
|
34
|
-
if (m)
|
|
35
|
-
return Number.parseFloat(m[1]);
|
|
36
|
-
}
|
|
37
|
-
const twStr = props.tw || props.class || "";
|
|
38
|
-
const twMatch = twStr.match(RE_TW_TEXT_ARBITRARY);
|
|
39
|
-
if (twMatch) {
|
|
40
|
-
const val = Number.parseFloat(twMatch[1]);
|
|
41
|
-
if (twMatch[2] === "px")
|
|
42
|
-
return val;
|
|
43
|
-
}
|
|
44
|
-
return void 0;
|
|
45
|
-
}
|
|
46
21
|
function extractColor(style) {
|
|
47
22
|
const color = style?.color;
|
|
48
23
|
return typeof color === "string" && color ? color : void 0;
|
|
49
24
|
}
|
|
50
|
-
export async function vnodeToTakumiNode(vnode,
|
|
25
|
+
export async function vnodeToTakumiNode(vnode, inheritedColor) {
|
|
51
26
|
const { style, children, class: cls, tw, src, ...rest } = vnode.props;
|
|
52
27
|
const nodeColor = extractColor(style) ?? inheritedColor;
|
|
53
28
|
const baseMetadata = {
|
|
@@ -55,18 +30,16 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
|
|
|
55
30
|
style
|
|
56
31
|
};
|
|
57
32
|
if (vnode.type === "svg") {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
33
|
+
const relativeW = RE_RELATIVE_UNIT.test(String(rest.width ?? "")) ? String(rest.width) : void 0;
|
|
34
|
+
const relativeH = RE_RELATIVE_UNIT.test(String(rest.height ?? "")) ? String(rest.height) : void 0;
|
|
35
|
+
const imageStyle = relativeW || relativeH ? { ...style, ...relativeW && style?.width == null ? { width: relativeW } : {}, ...relativeH && style?.height == null ? { height: relativeH } : {} } : style;
|
|
61
36
|
return {
|
|
62
37
|
...baseMetadata,
|
|
38
|
+
style: imageStyle,
|
|
63
39
|
type: "image",
|
|
64
40
|
src: vnodeToHtmlString(vnode, nodeColor),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Otherwise → use standard resolution chain (numeric attrs → style → viewBox).
|
|
68
|
-
width: isRelativeW ? hasExplicitFontSize ? resolveRelativeUnit(rest.width, inheritedFontSize) : void 0 : resolveSvgDimension(rest, style, "width"),
|
|
69
|
-
height: isRelativeH ? hasExplicitFontSize ? resolveRelativeUnit(rest.height, inheritedFontSize) : void 0 : resolveSvgDimension(rest, style, "height")
|
|
41
|
+
width: relativeW ? void 0 : resolveSvgDimension(rest, style, "width"),
|
|
42
|
+
height: relativeH ? void 0 : resolveSvgDimension(rest, style, "height")
|
|
70
43
|
};
|
|
71
44
|
}
|
|
72
45
|
if (vnode.type === "img") {
|
|
@@ -78,8 +51,6 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
|
|
|
78
51
|
height: pickNumericDimension(rest, "height")
|
|
79
52
|
};
|
|
80
53
|
}
|
|
81
|
-
const nodeFontSize = extractFontSize(vnode.props, style);
|
|
82
|
-
const childFontSize = nodeFontSize ?? inheritedFontSize;
|
|
83
54
|
const containerStyle = { ...style };
|
|
84
55
|
const w = pickNumericDimension(rest, "width");
|
|
85
56
|
const h = pickNumericDimension(rest, "height");
|
|
@@ -102,11 +73,15 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
|
|
|
102
73
|
}
|
|
103
74
|
if (Array.isArray(children)) {
|
|
104
75
|
const takumiChildren = [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
76
|
+
const isInlineImage = (c) => !!c && typeof c === "object" && ["svg", "img"].includes(c.type);
|
|
77
|
+
for (const [i, child] of children.entries()) {
|
|
78
|
+
if (child && typeof child === "object") {
|
|
79
|
+
takumiChildren.push(await vnodeToTakumiNode(child, nodeColor));
|
|
80
|
+
} else if (typeof child === "string" && child.trim()) {
|
|
81
|
+
const start = isInlineImage(children[i - 1]) && RE_LEADING_SPACE.test(child) ? " " : "";
|
|
82
|
+
const end = isInlineImage(children[i + 1]) && RE_TRAILING_SPACE.test(child) ? " " : "";
|
|
83
|
+
takumiChildren.push({ type: "text", text: `${start}${child.trim()}${end}` });
|
|
84
|
+
}
|
|
110
85
|
}
|
|
111
86
|
return {
|
|
112
87
|
...containerMetadata,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parse } from "devalue";
|
|
2
|
+
import { defineNitroPlugin } from "#nuxtseo/nitro";
|
|
3
|
+
import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
|
|
4
|
+
import { normaliseOptions } from "../util/options.js";
|
|
5
|
+
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
6
|
+
const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
|
|
7
|
+
export default defineNitroPlugin((nitro) => {
|
|
8
|
+
if (!import.meta.dev)
|
|
9
|
+
return;
|
|
10
|
+
nitro.hooks.hook("render:html", (html, ctx) => {
|
|
11
|
+
const payload = [...html.head, ...html.bodyAppend].join("\n").match(PAYLOAD_REGEX)?.[1];
|
|
12
|
+
if (!payload)
|
|
13
|
+
return;
|
|
14
|
+
for (const options of parse(payload)) {
|
|
15
|
+
if (!options?.component)
|
|
16
|
+
continue;
|
|
17
|
+
const normalised = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return normaliseOptions({ ...options });
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
if (normalised?.component?.category === "community")
|
|
25
|
+
(ctx.event.context._ogImageAutoEject ||= []).push(normalised.component);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
nitro.hooks.hook("afterResponse", (event) => {
|
|
29
|
+
const components = event.context._ogImageAutoEject;
|
|
30
|
+
if (!components?.length)
|
|
31
|
+
return;
|
|
32
|
+
const runtimeConfig = useOgImageRuntimeConfig(event);
|
|
33
|
+
for (const component of components)
|
|
34
|
+
autoEjectCommunityTemplate(component, runtimeConfig);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import type { OgImageComponent, OgImageRuntimeConfig } from '../../types.js';
|
|
2
|
-
export declare function autoEjectCommunityTemplate(component: OgImageComponent, runtimeConfig: OgImageRuntimeConfig
|
|
3
|
-
requestPath?: string;
|
|
4
|
-
}): void;
|
|
2
|
+
export declare function autoEjectCommunityTemplate(component: OgImageComponent, runtimeConfig: OgImageRuntimeConfig): void;
|
|
@@ -4,12 +4,9 @@ import { logger } from "./logger.js";
|
|
|
4
4
|
const RE_OG_IMAGE_PREFIX = /^OgImage/;
|
|
5
5
|
const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/;
|
|
6
6
|
const ejectedTemplates = /* @__PURE__ */ new Set();
|
|
7
|
-
export function autoEjectCommunityTemplate(component, runtimeConfig
|
|
7
|
+
export function autoEjectCommunityTemplate(component, runtimeConfig) {
|
|
8
8
|
if (!import.meta.dev)
|
|
9
9
|
return;
|
|
10
|
-
if (options?.requestPath?.includes("/_og/")) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
10
|
const { srcDir, communityTemplatesDir } = runtimeConfig;
|
|
14
11
|
if (!srcDir || !communityTemplatesDir)
|
|
15
12
|
return;
|
|
@@ -104,21 +104,11 @@ export function normaliseOptions(_options) {
|
|
|
104
104
|
renderer = resolved.renderer;
|
|
105
105
|
}
|
|
106
106
|
options.component = resolved.pascalName;
|
|
107
|
-
if (resolved.category === "community") {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
renderer = resolved.renderer;
|
|
113
|
-
options.component = resolved.pascalName;
|
|
114
|
-
} else {
|
|
115
|
-
throw createError({
|
|
116
|
-
statusCode: 500,
|
|
117
|
-
message: `Community template "${resolved.pascalName}" must be ejected before production use. Run: npx nuxt-og-image eject ${resolved.pascalName}
|
|
118
|
-
No app components found \u2014 create one in components/OgImage/`
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
107
|
+
if (resolved.category === "community" && !import.meta.dev) {
|
|
108
|
+
throw createError({
|
|
109
|
+
statusCode: 500,
|
|
110
|
+
message: `[Nuxt OG Image] Community template "${resolved.pascalName}" is not ejected, and production builds do not include community templates. Run: npx nuxt-og-image eject ${stripRenderer(resolved.pascalName)}.${resolved.renderer}`
|
|
111
|
+
});
|
|
122
112
|
}
|
|
123
113
|
return {
|
|
124
114
|
options,
|