nuxt-og-image 0.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.
package/README.md ADDED
@@ -0,0 +1,277 @@
1
+ <h1 align='center'>nuxt-og-image</h1>
2
+
3
+ <p align="center">
4
+ <a href='https://github.com/harlan-zw/nuxt-og-image/actions/workflows/test.yml'>
5
+ </a>
6
+ <a href="https://www.npmjs.com/package/nuxt-og-image" target="__blank"><img src="https://img.shields.io/npm/v/nuxt-og-image?style=flat&colorA=002438&colorB=28CF8D" alt="NPM version"></a>
7
+ <a href="https://www.npmjs.com/package/nuxt-og-image" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/nuxt-og-image?flat&colorA=002438&colorB=28CF8D"></a>
8
+ <a href="https://github.com/harlan-zw/nuxt-og-image" target="__blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/harlan-zw/nuxt-og-image?flat&colorA=002438&colorB=28CF8D"></a>
9
+ </p>
10
+
11
+
12
+ <p align="center">
13
+ Generate social share images for your pre-rendered Nuxt v3 app.
14
+ </p>
15
+
16
+ <p align="center">
17
+ <table>
18
+ <tbody>
19
+ <td align="center">
20
+ <img width="800" height="0" /><br>
21
+ <i>Status:</i> Early Access</b> <br>
22
+ <sup> Please report any issues 🐛</sup><br>
23
+ <sub>Made possible by my <a href="https://github.com/sponsors/harlan-zw">Sponsor Program 💖</a><br> Follow me <a href="https://twitter.com/harlan_zw">@harlan_zw</a> 🐦 • Join <a href="https://discord.gg/275MBUBvgP">Discord</a> for help</sub><br>
24
+ <img width="800" height="0" />
25
+ </td>
26
+ </tbody>
27
+ </table>
28
+ </p>
29
+
30
+ ## Features
31
+
32
+ - 🔄 Configure using route rules
33
+ - 📸 Generates site screenshots
34
+ - 🎨 OR build your own template with Vue (powered by Nuxt islands)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install --save-dev nuxt-og-image
40
+
41
+ # Using yarn
42
+ yarn add --dev nuxt-og-image
43
+ ```
44
+
45
+ ## Setup
46
+
47
+ _nuxt.config.ts_
48
+
49
+ ```ts
50
+ export default defineNuxtConfig({
51
+ modules: [
52
+ 'nuxt-og-image',
53
+ ],
54
+ })
55
+ ```
56
+
57
+ To have routes included for og:image creation automatically, they need to be pre-rendered by Nitro.
58
+
59
+ ```ts
60
+ export default defineNuxtConfig({
61
+ nitro: {
62
+ prerender: {
63
+ crawlLinks: true,
64
+ routes: [
65
+ '/',
66
+ // any URLs that can't be discovered by crawler
67
+ '/my-hidden-url'
68
+ ]
69
+ }
70
+ }
71
+ })
72
+ ```
73
+
74
+ ## Default Behaviour
75
+
76
+ By default, all pre-rendered routes will generate an og:image of a screenshot of the page.
77
+
78
+ ## Using a template
79
+
80
+ You can create your own template to use for generating og:image. This is done with
81
+ Nuxt islands.
82
+
83
+ ### Requirements
84
+
85
+ To use this feature you will need to opt in to the edge channel, see [the instructions](https://nuxt.com/docs/guide/going-further/edge-channel#edge-release-channel).
86
+
87
+ The `componentIslands` experimental feature is required for this module to work and will is enabled for you.
88
+
89
+ ### Setup
90
+
91
+ #### Create the Island component
92
+
93
+ Firstly, you're going to create the Vue component to be used to render the og:image.
94
+
95
+ Create the file at `components/islands/OgImageDefault.vue`.
96
+
97
+ ```vue
98
+ <script setup lang="ts">
99
+ const props = defineProps({
100
+ // payload props
101
+ path: String,
102
+ title: String,
103
+ description: String,
104
+ // add your own props here
105
+ myCustomProp: String
106
+ })
107
+ </script>
108
+
109
+ <template>
110
+ <div class="wrap">
111
+ <div>
112
+ <h1>
113
+ {{ title }}
114
+ </h1>
115
+ <p>{{ description }}</p>
116
+ </div>
117
+ </div>
118
+ </template>
119
+
120
+ <style scoped>
121
+ .wrap {
122
+ width: 100%;
123
+ height: 100%;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ color: white;
128
+ font-weight: bold;
129
+ font-family: sans-serif;
130
+ background: linear-gradient(to bottom, #30e8bf, #ff8235);
131
+ }
132
+
133
+ h1 {
134
+ font-size: 4rem;
135
+ margin: 0;
136
+ }
137
+ </style>
138
+ ```
139
+
140
+ #### Configure the payload
141
+
142
+ Within a page
143
+
144
+ ### Set host
145
+
146
+ You'll need to provide the host of your site in order to generate the sitemap.xml.
147
+
148
+ ```ts
149
+ export default defineNuxtConfig({
150
+ // Recommended
151
+ runtimeConfig: {
152
+ siteUrl: 'https://example.com',
153
+ },
154
+ // OR
155
+ sitemap: {
156
+ hostname: 'https://example.com',
157
+ },
158
+ })
159
+ ```
160
+
161
+
162
+ ## Route Rules
163
+
164
+ To change the behavior of the sitemap, you can use route rules. Route rules are provided as [Nitro route rules](https://v3.nuxtjs.org/docs/directory-structure/nitro/#route-rules).
165
+
166
+ _nuxt.config.ts_
167
+
168
+ ```ts
169
+ export default defineNuxtConfig({
170
+ routeRules: {
171
+ // Don't add any /secret/** URLs to the sitemap
172
+ '/secret/**': { index: false },
173
+ // modify the sitemap entry for specific URLs
174
+ '/about': { sitemap: { changefreq: 'daily', priority: 0.3 } }
175
+ }
176
+ })
177
+ ```
178
+
179
+ The following options are available for each route rule:
180
+
181
+ - `index`: Whether to include the route in the sitemap.xml. Defaults to `true`.
182
+ - `sitemap.changefreq`: The change frequency of the route.
183
+ - `sitemap.priority`: The priority of the route.
184
+
185
+ ## Module Config
186
+
187
+ If you need further control over the sitemap URLs, you can provide config on the `sitemap` key.
188
+
189
+ ### `host`
190
+
191
+ - Type: `string`
192
+ - Default: `undefined`
193
+ - Required: `true`
194
+
195
+ The host of your site. This is required to generate the sitemap.xml.
196
+
197
+ ### `trailingSlash`
198
+
199
+ - Type: `boolean`
200
+ - Default: `false`
201
+
202
+ Whether to add a trailing slash to the URLs in the sitemap.xml.
203
+
204
+ ### `enabled`
205
+
206
+ - Type: `boolean`
207
+ - Default: `true`
208
+
209
+ Whether to generate the sitemap.xml.
210
+
211
+ ### `include`
212
+
213
+ - Type: `string[]`
214
+ - Default: `undefined`
215
+
216
+ Filter routes that match the given rules.
217
+
218
+ ```ts
219
+ export default defineNuxtConfig({
220
+ sitemap: {
221
+ include: [
222
+ '/my-hidden-url'
223
+ ]
224
+ }
225
+ })
226
+ ```
227
+
228
+ ### `exclude`
229
+
230
+ - Type: `string[]`
231
+ - Default: `undefined`
232
+
233
+ Filter routes that match the given rules.
234
+
235
+ ```ts
236
+ export default defineNuxtConfig({
237
+ sitemap: {
238
+ exclude: [
239
+ '/my-secret-section/**'
240
+ ]
241
+ }
242
+ })
243
+ ```
244
+
245
+ Additional config extends [sitemap.js](https://github.com/ekalinin/sitemap.js).
246
+
247
+ ## Examples
248
+
249
+ ### Add custom routes without pre-rendering
250
+
251
+ ```ts
252
+ export default defineNuxtConfig({
253
+ hooks: {
254
+ 'sitemap:generate': (ctx) => {
255
+ // add custom URLs
256
+ ctx.urls.push({
257
+ url: '/my-custom-url',
258
+ changefreq: 'daily',
259
+ priority: 0.3
260
+ })
261
+ }
262
+ }
263
+ })
264
+ ```
265
+
266
+ ## Sponsors
267
+
268
+ <p align="center">
269
+ <a href="https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg">
270
+ <img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>
271
+ </a>
272
+ </p>
273
+
274
+
275
+ ## License
276
+
277
+ MIT License © 2022-PRESENT [Harlan Wilton](https://github.com/harlan-zw)
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,46 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ScreenshotOptions {
4
+ colorScheme?: 'dark' | 'light';
5
+ selector?: string;
6
+ mask?: string;
7
+ /**
8
+ * The width of the screenshot.
9
+ *
10
+ * @default 1200
11
+ */
12
+ width: number;
13
+ /**
14
+ * The height of the screenshot.
15
+ *
16
+ * @default 630
17
+ */
18
+ height: number;
19
+ }
20
+ declare module 'nitropack' {
21
+ interface NitroRouteRules {
22
+ ogImage?: 'screenshot' | string | false;
23
+ ogImagePayload?: Record<string, any>;
24
+ }
25
+ }
26
+
27
+ interface ModuleOptions extends ScreenshotOptions {
28
+ defaultIslandComponent: string;
29
+ /**
30
+ * The directory within `public` where the og images will be stored.
31
+ *
32
+ * @default "_og-images"
33
+ */
34
+ outputDir: string;
35
+ /**
36
+ * The hostname of your website.
37
+ */
38
+ host: string;
39
+ /**
40
+ * Should images be allowed to generated at runtime.
41
+ */
42
+ runtimeImages: boolean;
43
+ }
44
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
45
+
46
+ export { ModuleOptions, _default as default };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-og-image",
3
+ "compatibility": {
4
+ "nuxt": "^3.0.0",
5
+ "bridge": false
6
+ },
7
+ "configKey": "ogImage",
8
+ "version": "0.0.1"
9
+ }
@@ -0,0 +1,214 @@
1
+ import { mkdir, writeFile, readFile, rm } from 'node:fs/promises';
2
+ import { defineNuxtModule, createResolver, addTemplate, addServerHandler, addImports, addComponent } from '@nuxt/kit';
3
+ import { execa } from 'execa';
4
+ import { hash } from 'ohash';
5
+ import chalk from 'chalk';
6
+ import defu from 'defu';
7
+ import { toRouteMatcher, createRouter } from 'radix3';
8
+ import { withBase } from 'ufo';
9
+ import fg from 'fast-glob';
10
+
11
+ async function createLambdaBrowser() {
12
+ try {
13
+ const playwright = await import('playwright-core');
14
+ const awsChrome = await import('chrome-aws-lambda');
15
+ return await playwright.chromium.launch({
16
+ args: awsChrome.args,
17
+ executablePath: await awsChrome.executablePath,
18
+ headless: awsChrome.headless
19
+ });
20
+ } catch (e) {
21
+ }
22
+ return false;
23
+ }
24
+ async function createBrowser() {
25
+ const lambdaBrowser = await createLambdaBrowser();
26
+ if (lambdaBrowser)
27
+ return lambdaBrowser;
28
+ const playwright = await import('playwright');
29
+ return await playwright.chromium.launch({
30
+ chromiumSandbox: true
31
+ });
32
+ }
33
+ async function screenshot(browser, url, options) {
34
+ const page = await browser.newPage({
35
+ colorScheme: options.colorScheme
36
+ });
37
+ await page.setViewportSize({
38
+ width: options.width,
39
+ height: options.height
40
+ });
41
+ await page.goto(url, {
42
+ timeout: 1e4,
43
+ waitUntil: "networkidle"
44
+ });
45
+ if (options.mask) {
46
+ await page.evaluate((mask) => {
47
+ for (const el of document.querySelectorAll(mask))
48
+ el.style.display = "none";
49
+ }, options.mask);
50
+ }
51
+ if (options.selector)
52
+ await page.locator(options.selector).screenshot();
53
+ return await page.screenshot();
54
+ }
55
+
56
+ const HtmlRendererRoute = "__og_image";
57
+ const PayloadScriptId = "nuxt-og-image-payload";
58
+ const MetaOgImageContentPlaceholder = "__NUXT_OG_IMAGE_PLACEHOLDER__";
59
+ const LinkPrerenderId = "nuxt-og-image-screenshot-path";
60
+
61
+ const module = defineNuxtModule({
62
+ meta: {
63
+ name: "nuxt-og-image",
64
+ compatibility: {
65
+ nuxt: "^3.0.0",
66
+ bridge: false
67
+ },
68
+ configKey: "ogImage"
69
+ },
70
+ defaults(nuxt) {
71
+ return {
72
+ host: nuxt.options.runtimeConfig.public?.siteUrl,
73
+ width: 1200,
74
+ height: 630,
75
+ defaultIslandComponent: "OgImage",
76
+ outputDir: "_og-images",
77
+ runtimeImages: nuxt.options.dev
78
+ };
79
+ },
80
+ async setup(config, nuxt) {
81
+ const { resolve } = createResolver(import.meta.url);
82
+ nuxt.options.experimental.componentIslands = true;
83
+ addTemplate({
84
+ filename: "nuxt-og-image.d.ts",
85
+ getContents: () => {
86
+ return `// Generated by nuxt-og-image
87
+ declare module 'nitropack' {
88
+ interface NitroRouteRules {
89
+ ogImage?: 'screenshot' | string | false
90
+ }
91
+ }
92
+ `;
93
+ }
94
+ });
95
+ nuxt.hooks.hook("prepare:types", ({ references }) => {
96
+ references.push({ path: resolve(nuxt.options.buildDir, "nuxt-og-image.d.ts") });
97
+ });
98
+ addServerHandler({
99
+ handler: resolve("./runtime/nitro/html")
100
+ });
101
+ if (config.runtimeImages) {
102
+ addServerHandler({
103
+ handler: resolve("./runtime/nitro/image")
104
+ });
105
+ }
106
+ addImports({
107
+ name: "defineOgImage",
108
+ from: resolve("./runtime/composables/defineOgImage")
109
+ });
110
+ addComponent({
111
+ name: "OgImage",
112
+ filePath: resolve("./runtime/components/OgImage.vue"),
113
+ island: true
114
+ });
115
+ nuxt.hooks.hook("nitro:init", async (nitro) => {
116
+ const entries = [];
117
+ const _routeRulesMatcher = toRouteMatcher(
118
+ createRouter({ routes: nitro.options.routeRules })
119
+ );
120
+ const outputPath = `${nitro.options.output.dir}/public/${config.outputDir}`;
121
+ nitro.hooks.hook("prerender:generate", async (ctx) => {
122
+ if (ctx.route.includes(".") || ctx.route.endsWith(HtmlRendererRoute))
123
+ return;
124
+ let html = ctx.contents;
125
+ if (!html)
126
+ return;
127
+ const routeRules = defu({}, ..._routeRulesMatcher.matchAll(ctx.route).reverse());
128
+ if (routeRules.ogImage === false)
129
+ return;
130
+ const screenshotPath = ctx._contents.match(new RegExp(`<link id="${LinkPrerenderId}" rel="prerender" href="(.*?)">`))?.[1];
131
+ const fileName = `${hash({ route: ctx.route, time: Date.now() })}.png`;
132
+ const absoluteUrl = withBase(`${config.outputDir}/${fileName}`, config.host);
133
+ const entry = {
134
+ fileName,
135
+ absoluteUrl,
136
+ outputPath: `${nitro.options.output.dir}/public/${config.outputDir}/${fileName}`,
137
+ route: ctx.route,
138
+ routeRules: routeRules.ogImage || "",
139
+ screenshotPath: screenshotPath || ctx.route
140
+ };
141
+ entries.push(entry);
142
+ html = html.replace(MetaOgImageContentPlaceholder, entry.absoluteUrl);
143
+ ctx.contents = html;
144
+ });
145
+ if (nuxt.options.dev)
146
+ return;
147
+ const outputOgImages = async () => {
148
+ if (entries.length === 0)
149
+ return;
150
+ try {
151
+ await mkdir(outputPath, { recursive: true });
152
+ } catch (e) {
153
+ }
154
+ const previewProcess = execa("npx", ["serve", `${nitro.options.output.dir}/public`]);
155
+ try {
156
+ previewProcess.stderr?.pipe(process.stderr);
157
+ const host = (await new Promise((resolve2) => {
158
+ previewProcess.stdout?.on("data", (data) => {
159
+ if (data.includes("Accepting connections at")) {
160
+ resolve2(data.toString().split("Accepting connections at ")[1]);
161
+ }
162
+ });
163
+ })).trim();
164
+ const browser = await createBrowser();
165
+ nitro.logger.info(`Generating ${entries.length} og:image screenshots`);
166
+ try {
167
+ const imageGenPromises = entries.map(async (entry, index) => {
168
+ return new Promise((resolve2) => {
169
+ const start = Date.now();
170
+ screenshot(browser, `${host}${entry.screenshotPath}`, config).then((imgBuffer) => {
171
+ writeFile(entry.outputPath, imgBuffer).then(() => {
172
+ const generateTimeMS = Date.now() - start;
173
+ nitro.logger.log(chalk.gray(
174
+ ` ${index === entries.length - 1 ? "\u2514\u2500" : "\u251C\u2500"} /${config.outputDir}/${entry.fileName} (${generateTimeMS}ms)`
175
+ ));
176
+ resolve2();
177
+ });
178
+ });
179
+ });
180
+ });
181
+ await Promise.all(imageGenPromises);
182
+ } catch (e) {
183
+ console.error(e);
184
+ } finally {
185
+ await browser.close();
186
+ }
187
+ } catch (e) {
188
+ console.error(e);
189
+ } finally {
190
+ previewProcess.kill();
191
+ }
192
+ const htmlFiles = await fg(["**/*.html"], { cwd: nitro.options.output.dir });
193
+ for (const htmlFile of htmlFiles) {
194
+ const html = await readFile(`${nitro.options.output.dir}/${htmlFile}`, "utf-8");
195
+ const newHtml = html.replace(new RegExp(`<link id="${LinkPrerenderId}" rel="prerender" href="(.*?)">`), "").replace(new RegExp(`<script id="${PayloadScriptId}" type="application/json">(.*?)<\/script>`), "").replace("\n\n", "\n");
196
+ if (html !== newHtml) {
197
+ await writeFile(`${nitro.options.output.dir}/${htmlFile}`, newHtml, { encoding: "utf-8" });
198
+ }
199
+ }
200
+ const ogImageFolders = await fg([`**/${HtmlRendererRoute}`], { cwd: nitro.options.output.dir, onlyDirectories: true });
201
+ for (const ogImageFolder of ogImageFolders)
202
+ await rm(`${nitro.options.output.dir}/${ogImageFolder}`, { recursive: true, force: true });
203
+ };
204
+ nitro.hooks.hook("rollup:before", async () => {
205
+ await outputOgImages();
206
+ });
207
+ nitro.hooks.hook("close", async () => {
208
+ await outputOgImages();
209
+ });
210
+ });
211
+ }
212
+ });
213
+
214
+ export { module as default };
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps({
3
+ path: String,
4
+ title: String,
5
+ description: String,
6
+ })
7
+ </script>
8
+
9
+ <template>
10
+ <div class="wrap">
11
+ <div class="bg1" />
12
+ <div class="bg2" />
13
+ <div>
14
+ <p>This is the default og:image template from <a href="https://github.com/harlan-zw/nuxt-og-image" target="_blank">nuxt-og-image</a>.</p>
15
+ <p>Create your own at <code>components/islands/OgImage.vue</code>.</p>
16
+ </div>
17
+ <div>
18
+ <strong>Payload</strong>
19
+ <code>
20
+ <pre>{{ props }}</pre>
21
+ </code>
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <style scoped>
27
+ .wrap {
28
+ width: 100%;
29
+ height: 100%;
30
+ display: flex;
31
+ align-items: center;
32
+ flex-direction: column;
33
+ color: white;
34
+ font-weight: bold;
35
+ font-family: sans-serif;
36
+ background-color: #0c0c0c;
37
+ position: relative;
38
+ }
39
+
40
+ .bg1 {
41
+ top: 0;
42
+ left: 0;
43
+ display: block;
44
+ position: absolute;
45
+ width: 100%;
46
+ height: 100%;
47
+ padding: 0 !important;
48
+ margin: 0 !important;
49
+ background-color: #0c0c0c;
50
+ }
51
+
52
+ .bg2 {
53
+ top: 0;
54
+ left: 0;
55
+ z-index: 1;
56
+ display: block;
57
+ position: absolute;
58
+ width: 100%;
59
+ height: 100%;
60
+ padding: 0 !important;
61
+ margin: 0 !important;
62
+ background: radial-gradient(at 100% 100%, #0f766e, rgba(12, 12, 12, 0.1) 60%);
63
+ }
64
+
65
+ a {
66
+ color: inherit;
67
+ padding-bottom: 3px;
68
+ text-decoration: none;
69
+ border-bottom: 3px solid #ff8235;
70
+ }
71
+
72
+ .wrap > div {
73
+ z-index: 2;
74
+ padding: 2rem;
75
+ }
76
+
77
+ code pre {
78
+ background: #333;
79
+ color: white;
80
+ padding: 1rem;
81
+ border-radius: 0.5rem;
82
+ font-weight: lighter;
83
+ font-size: 1.1rem;
84
+ }
85
+
86
+ p {
87
+ font-size: 1.5em;
88
+ font-weight: normal;
89
+ }
90
+
91
+ h1 {
92
+ font-size: 4rem;
93
+ margin: 0;
94
+ }
95
+ </style>
@@ -0,0 +1,8 @@
1
+ export interface OgImagePayload {
2
+ runtime: boolean;
3
+ title: string;
4
+ description: string;
5
+ component: string;
6
+ [key: string]: any;
7
+ }
8
+ export declare function defineOgImage(options: OgImagePayload): void;
@@ -0,0 +1,35 @@
1
+ import { useServerHead } from "@vueuse/head";
2
+ import {
3
+ HtmlRendererRoute,
4
+ LinkPrerenderId,
5
+ MetaOgImageContentPlaceholder,
6
+ PayloadScriptId,
7
+ RuntimeImageSuffix
8
+ } from "../../const";
9
+ export function defineOgImage(options) {
10
+ if (process.server) {
11
+ const router = useRouter();
12
+ useServerHead({
13
+ meta: [
14
+ {
15
+ property: "og:image",
16
+ content: () => options.runtime ? `${router.currentRoute.value.path}/${RuntimeImageSuffix}` : MetaOgImageContentPlaceholder
17
+ }
18
+ ],
19
+ link: [
20
+ {
21
+ id: LinkPrerenderId,
22
+ rel: "prerender",
23
+ href: `${router.currentRoute.value.path}/${HtmlRendererRoute}`
24
+ }
25
+ ],
26
+ script: [
27
+ {
28
+ id: PayloadScriptId,
29
+ type: "application/json",
30
+ children: () => JSON.stringify(options)
31
+ }
32
+ ]
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+ export declare const extractOgPayload: (html: string) => any;
2
+ export declare const inferOgPayload: (html: string) => Record<string, any>;
3
+ declare const _default: import("h3").EventHandler<string | undefined>;
4
+ export default _default;
@@ -0,0 +1,55 @@
1
+ import { withQuery } from "ufo";
2
+ import { renderSSRHead } from "@unhead/ssr";
3
+ import { createHeadCore } from "@unhead/vue";
4
+ import { defineEventHandler, getQuery } from "h3";
5
+ import { HtmlRendererRoute, PayloadScriptId } from "../../const";
6
+ export const extractOgPayload = (html) => {
7
+ const payload = html.match(new RegExp(`<script id="${PayloadScriptId}" type="application/json">(.+?)<\/script>`))?.[1];
8
+ if (payload) {
9
+ return JSON.parse(payload);
10
+ }
11
+ return false;
12
+ };
13
+ export const inferOgPayload = (html) => {
14
+ const payload = {};
15
+ const title = html.match(/<meta property="og:title" content="(.*?)">/)?.[1];
16
+ if (title)
17
+ payload.title = title;
18
+ const description = html.match(/<meta property="og:description" content="(.*?)">/)?.[1];
19
+ if (description)
20
+ payload.description = description;
21
+ return payload;
22
+ };
23
+ export default defineEventHandler(async (req) => {
24
+ if (!req.path?.endsWith(HtmlRendererRoute))
25
+ return;
26
+ const path = req.path.replace(`/${HtmlRendererRoute}`, "");
27
+ const html = await $fetch(path);
28
+ const payload = {
29
+ path,
30
+ title: "Hello World",
31
+ description: "Example description",
32
+ image: "https://example.com/image.png",
33
+ ...extractOgPayload(html),
34
+ ...inferOgPayload(html),
35
+ ...getQuery(req)
36
+ };
37
+ const result = await $fetch(withQuery(`/__nuxt_island/${payload.template || "OgImage"}`, {
38
+ props: JSON.stringify(payload)
39
+ }));
40
+ const head = createHeadCore();
41
+ head.push(result.head);
42
+ head.push({
43
+ style: [
44
+ {
45
+ innerHTML: "body { margin: 0; padding: 0; } .og-image-container { width: 1200px; height: 630px; display: flex; margin: 0 auto; }"
46
+ }
47
+ ]
48
+ });
49
+ const headChunk = await renderSSRHead(head);
50
+ return `<!DOCTYPE html>
51
+ <html ${headChunk.htmlAttrs}>
52
+ <head>${headChunk.headTags}</head>
53
+ <body ${headChunk.bodyAttrs}>${headChunk.bodyTagsOpen}<div class="og-image-container">${result.html}</div>${headChunk.bodyTags}</body>
54
+ </html>`;
55
+ });
@@ -0,0 +1,3 @@
1
+ /// <reference types="node" />
2
+ declare const _default: import("h3").EventHandler<Buffer | undefined>;
3
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { defineEventHandler, getRequestHeader, setHeader } from "h3";
2
+ import { HtmlRendererRoute, RuntimeImageSuffix } from "../../const";
3
+ import { createBrowser, screenshot } from "../../browserService";
4
+ export default defineEventHandler(async (e) => {
5
+ if (!e.path?.endsWith(RuntimeImageSuffix))
6
+ return;
7
+ const path = e.path.replace(RuntimeImageSuffix, HtmlRendererRoute);
8
+ const host = getRequestHeader(e, "host") || "localhost:3000";
9
+ const browser = await createBrowser();
10
+ setHeader(e, "Content-Type", "image/png");
11
+ return await screenshot(browser, `http${host.startsWith("localhost") ? "" : "s"}://${host}/${path}`, {
12
+ width: 1200,
13
+ height: 630
14
+ });
15
+ });
@@ -0,0 +1,10 @@
1
+
2
+ import { ModuleOptions } from './module'
3
+
4
+ declare module '@nuxt/schema' {
5
+ interface NuxtConfig { ['ogImage']?: Partial<ModuleOptions> }
6
+ interface NuxtOptions { ['ogImage']?: ModuleOptions }
7
+ }
8
+
9
+
10
+ export { ModuleOptions, default } from './module'
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "nuxt-og-image",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "packageManager": "pnpm@7.8.0",
6
+ "license": "MIT",
7
+ "funding": "https://github.com/sponsors/harlan-zw",
8
+ "homepage": "https://github.com/harlan-zw/nuxt-og-image#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/harlan-zw/nuxt-og-image.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/harlan-zw/nuxt-og-image/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/types.d.ts",
19
+ "require": "./dist/module.cjs",
20
+ "import": "./dist/module.mjs"
21
+ }
22
+ },
23
+ "main": "./dist/module.cjs",
24
+ "types": "./dist/types.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "dependencies": {
29
+ "@nuxt/kit": "3.0.0",
30
+ "chalk": "^5.2.0",
31
+ "defu": "^6.1.1",
32
+ "execa": "^6.1.0",
33
+ "fast-glob": "^3.2.12",
34
+ "ohash": "^1.0.0",
35
+ "playwright": "^1.28.1",
36
+ "radix3": "^1.0.0",
37
+ "ufo": "^1.0.1"
38
+ },
39
+ "unbuild": {
40
+ "externals": [
41
+ "playwright",
42
+ "playwright-core",
43
+ "chrome-aws-lambda"
44
+ ]
45
+ },
46
+ "devDependencies": {
47
+ "@antfu/eslint-config": "^0.33.1",
48
+ "@nuxt/kit": "3.0.0",
49
+ "@nuxt/module-builder": "^0.2.1",
50
+ "@nuxt/test-utils": "3.0.0",
51
+ "@nuxtjs/eslint-config-typescript": "^12.0.0",
52
+ "bumpp": "^8.2.1",
53
+ "chrome-aws-lambda": "^10.1.0",
54
+ "eslint": "8.29.0",
55
+ "nuxt": "npm:nuxt3@latest",
56
+ "pathe": "^1.0.0",
57
+ "playwright-chromium": "^1.28.1",
58
+ "playwright-core": "^1.28.1",
59
+ "vitest": "^0.25.5"
60
+ },
61
+ "scripts": {
62
+ "lint": "eslint \"**/*.{ts,vue,json,yml}\"",
63
+ "build": "nuxi prepare .playground && nuxt-module-build",
64
+ "dev": "nuxi dev .playground",
65
+ "dev:build": "nuxi build .playground",
66
+ "dev:prepare": "nuxt-module-build --stub && nuxi prepare .playground",
67
+ "release": "bumpp package.json --commit --push --tag",
68
+ "test": "pnpm lint"
69
+ }
70
+ }