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 +277 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.ts +46 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +214 -0
- package/dist/runtime/components/OgImage.vue +95 -0
- package/dist/runtime/composables/defineOgImage.d.ts +8 -0
- package/dist/runtime/composables/defineOgImage.mjs +35 -0
- package/dist/runtime/nitro/html.d.ts +4 -0
- package/dist/runtime/nitro/html.mjs +55 -0
- package/dist/runtime/nitro/image.d.ts +3 -0
- package/dist/runtime/nitro/image.mjs +15 -0
- package/dist/types.d.ts +10 -0
- package/package.json +70 -0
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)
|
package/dist/module.cjs
ADDED
package/dist/module.d.ts
ADDED
|
@@ -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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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,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,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
|
+
});
|
package/dist/types.d.ts
ADDED
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
|
+
}
|