@soonit/rspress-plugin-og 0.0.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +382 -0
- package/dist/types-d5VtTQ1o.d.mts +33 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 - Estéban Soubiran <esteban@soubiran.dev> (https://soubiran.dev)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# rspress-plugin-og
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
[](https://pkg.pr.new/~/Barbapapazes/vitepress-plugin-og)
|
|
7
|
+
|
|
8
|
+
Automatically generate Open Graph images for your Rspress pages.
|
|
9
|
+
|
|
10
|
+
- 🖼️ Generates OG images from an SVG template
|
|
11
|
+
- 🚀 Integrates with Rspress
|
|
12
|
+
- 🧩 Bring your own SVG template
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add -D rspress-plugin-og
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Add the plugin to your Rspress configuration file (`rspress.config.ts`):
|
|
23
|
+
|
|
24
|
+
For Rspress v2:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineConfig } from '@rspress/core'
|
|
28
|
+
import pluginOg from 'rspress-plugin-og'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
pluginOg({
|
|
33
|
+
domain: 'https://example.com',
|
|
34
|
+
// ...other options
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For Rspress v1:
|
|
41
|
+
|
|
42
|
+
This is a plugin that is compatible with both V2 and V1, but when used in V1, there may be type errors.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import pluginOg from 'rspress-plugin-og'
|
|
46
|
+
import { defineConfig } from 'rspress/config'
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
plugins: [
|
|
50
|
+
pluginOg({
|
|
51
|
+
domain: 'https://example.com',
|
|
52
|
+
// ...other options
|
|
53
|
+
}) as any,
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Option | Type | Default | Description |
|
|
61
|
+
| :--- | :--- | :--- | :--- |
|
|
62
|
+
| `domain` | `string` | **Required** | The domain to use for the generated OG image URLs. |
|
|
63
|
+
| `outDir` | `string` | `'og'` | Output directory for the generated OG images (inside `public`). |
|
|
64
|
+
| `ogTemplate` | `string` | `'og-template.svg'` | The path to the OG image template file. |
|
|
65
|
+
| `maxTitleSizePerLine` | `number` | `30` | Maximum number of characters per line in the title. |
|
|
66
|
+
|
|
67
|
+
## Template
|
|
68
|
+
|
|
69
|
+
Create an SVG template at `og-template.svg` (or your configured path). Use `{{line1}}`, `{{line2}}`, and `{{line3}}` placeholders for the title lines.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
|
|
73
|
+
```xml
|
|
74
|
+
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
|
75
|
+
<!-- Background -->
|
|
76
|
+
<rect width="1200" height="630" fill="#1e1e1e" />
|
|
77
|
+
|
|
78
|
+
<!-- Title -->
|
|
79
|
+
<text x="60" y="200" font-family="Arial" font-size="80" fill="#ffffff">
|
|
80
|
+
<tspan x="60" dy="0">{{line1}}</tspan>
|
|
81
|
+
<tspan x="60" dy="100">{{line2}}</tspan>
|
|
82
|
+
<tspan x="60" dy="100">{{line3}}</tspan>
|
|
83
|
+
</text>
|
|
84
|
+
</svg>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Sponsors
|
|
88
|
+
|
|
89
|
+
<p align="center">
|
|
90
|
+
<a href="https://github.com/sponsors/barbapapazes">
|
|
91
|
+
<img src='https://cdn.jsdelivr.net/gh/barbapapazes/static/sponsors.svg'/>
|
|
92
|
+
</a>
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
[MIT](../../LICENSE) License © 2025-PRESENT [Estéban Soubiran](https://github.com/barbapapazes)
|
|
98
|
+
|
|
99
|
+
<!-- Badges -->
|
|
100
|
+
[npm-version-src]: https://img.shields.io/npm/v/rspress-plugin-og/latest.svg?style=flat&colorA=000&colorB=171717
|
|
101
|
+
[npm-version-href]: https://npmjs.com/package/rspress-plugin-og
|
|
102
|
+
|
|
103
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/rspress-plugin-og.svg?style=flat&colorA=000&colorB=171717
|
|
104
|
+
[npm-downloads-href]: https://npmjs.com/package/rspress-plugin-og
|
|
105
|
+
|
|
106
|
+
[license-src]: https://img.shields.io/npm/l/rspress-plugin-og.svg?style=flat&colorA=000&colorB=171717
|
|
107
|
+
[license-href]: https://npmjs.com/package/rspress-plugin-og
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as Options } from "./types-d5VtTQ1o.mjs";
|
|
2
|
+
import * as _rspress_core0 from "@rspress/core";
|
|
3
|
+
import { PageIndexInfo } from "@rspress/core";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
declare function export_default(userOptions: Options): {
|
|
7
|
+
name: string;
|
|
8
|
+
config(config: _rspress_core0.UserConfig): _rspress_core0.UserConfig;
|
|
9
|
+
extendPageData: (pageData: PageIndexInfo) => void;
|
|
10
|
+
afterBuild(config: _rspress_core0.UserConfig): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { export_default as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { dirname, join, relative } from "node:path";
|
|
2
|
+
import node_process, { cwd } from "node:process";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import sharp from "sharp";
|
|
6
|
+
import node_os from "node:os";
|
|
7
|
+
import node_tty from "node:tty";
|
|
8
|
+
import { joinURL } from "ufo";
|
|
9
|
+
|
|
10
|
+
//#region ../core/src/head.ts
|
|
11
|
+
function createTwitterImageHead(imageUrl) {
|
|
12
|
+
return ["meta", {
|
|
13
|
+
name: "twitter:image",
|
|
14
|
+
content: imageUrl
|
|
15
|
+
}];
|
|
16
|
+
}
|
|
17
|
+
function createTwitterCardHead() {
|
|
18
|
+
return ["meta", {
|
|
19
|
+
name: "twitter:card",
|
|
20
|
+
content: "summary_large_image"
|
|
21
|
+
}];
|
|
22
|
+
}
|
|
23
|
+
function createOgImageHead(imageUrl) {
|
|
24
|
+
return ["meta", {
|
|
25
|
+
property: "og:image",
|
|
26
|
+
content: imageUrl
|
|
27
|
+
}];
|
|
28
|
+
}
|
|
29
|
+
function createOgImageWidthHead() {
|
|
30
|
+
return ["meta", {
|
|
31
|
+
property: "og:image:width",
|
|
32
|
+
content: "1200"
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
function createOgImageHeightHead() {
|
|
36
|
+
return ["meta", {
|
|
37
|
+
property: "og:image:height",
|
|
38
|
+
content: "630"
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
function createOgImageTypeHead() {
|
|
42
|
+
return ["meta", {
|
|
43
|
+
property: "og:image:type",
|
|
44
|
+
content: "image/png"
|
|
45
|
+
}];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region ../core/src/og.ts
|
|
50
|
+
const templates = /* @__PURE__ */ new Map();
|
|
51
|
+
function escapeHtml(unsafe) {
|
|
52
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
53
|
+
}
|
|
54
|
+
async function generateOgImage({ title }, output, options) {
|
|
55
|
+
if (existsSync(output)) return;
|
|
56
|
+
if (!templates.has(options.ogTemplate)) templates.set(options.ogTemplate, readFileSync(options.ogTemplate, "utf-8"));
|
|
57
|
+
const ogTemplate = templates.get(options.ogTemplate);
|
|
58
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
59
|
+
const lines = title.trim().split(new RegExp(`(.{0,${options.maxTitleSizePerLine}})(?:\\s|$)`, "g")).filter(Boolean);
|
|
60
|
+
const data = {
|
|
61
|
+
line1: lines[0] ? escapeHtml(lines[0]) : "",
|
|
62
|
+
line2: lines[1] ? escapeHtml(lines[1]) : "",
|
|
63
|
+
line3: lines[2] ? escapeHtml(lines[2]) : ""
|
|
64
|
+
};
|
|
65
|
+
const svg = ogTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || "");
|
|
66
|
+
await sharp(Buffer.from(svg)).resize(1200, 630).png().toFile(output);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region ../core/src/utils.ts
|
|
71
|
+
function slugifyPath(path) {
|
|
72
|
+
return `${path.replace(/\//g, "-").replace(/\.mdx?$/, "")}.png`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region ../../node_modules/.pnpm/rslog@1.3.2/node_modules/rslog/dist/index.js
|
|
77
|
+
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : node_process.argv) {
|
|
78
|
+
const prefix = flag.startsWith("-") ? "" : 1 === flag.length ? "-" : "--";
|
|
79
|
+
const position = argv.indexOf(prefix + flag);
|
|
80
|
+
const terminatorPosition = argv.indexOf("--");
|
|
81
|
+
return -1 !== position && (-1 === terminatorPosition || position < terminatorPosition);
|
|
82
|
+
}
|
|
83
|
+
const { env } = node_process;
|
|
84
|
+
let flagForceColor;
|
|
85
|
+
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) flagForceColor = 0;
|
|
86
|
+
else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) flagForceColor = 1;
|
|
87
|
+
function envForceColor() {
|
|
88
|
+
if (!("FORCE_COLOR" in env)) return;
|
|
89
|
+
if ("true" === env.FORCE_COLOR) return 1;
|
|
90
|
+
if ("false" === env.FORCE_COLOR) return 0;
|
|
91
|
+
if (0 === env.FORCE_COLOR.length) return 1;
|
|
92
|
+
const level = Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
|
93
|
+
if (![
|
|
94
|
+
0,
|
|
95
|
+
1,
|
|
96
|
+
2,
|
|
97
|
+
3
|
|
98
|
+
].includes(level)) return;
|
|
99
|
+
return level;
|
|
100
|
+
}
|
|
101
|
+
function translateLevel(level) {
|
|
102
|
+
if (0 === level) return false;
|
|
103
|
+
return {
|
|
104
|
+
level,
|
|
105
|
+
hasBasic: true,
|
|
106
|
+
has256: level >= 2,
|
|
107
|
+
has16m: level >= 3
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
111
|
+
const noFlagForceColor = envForceColor();
|
|
112
|
+
if (void 0 !== noFlagForceColor) flagForceColor = noFlagForceColor;
|
|
113
|
+
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
|
114
|
+
if (0 === forceColor) return 0;
|
|
115
|
+
if (sniffFlags) {
|
|
116
|
+
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
|
|
117
|
+
if (hasFlag("color=256")) return 2;
|
|
118
|
+
}
|
|
119
|
+
if ("TF_BUILD" in env && "AGENT_NAME" in env) return 1;
|
|
120
|
+
if (haveStream && !streamIsTTY && void 0 === forceColor) return 0;
|
|
121
|
+
const min = forceColor || 0;
|
|
122
|
+
if ("dumb" === env.TERM) return min;
|
|
123
|
+
if ("win32" === node_process.platform) {
|
|
124
|
+
const osRelease = node_os.release().split(".");
|
|
125
|
+
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if ("CI" in env) {
|
|
129
|
+
if ([
|
|
130
|
+
"GITHUB_ACTIONS",
|
|
131
|
+
"GITEA_ACTIONS",
|
|
132
|
+
"CIRCLECI"
|
|
133
|
+
].some((key) => key in env)) return 3;
|
|
134
|
+
if ([
|
|
135
|
+
"TRAVIS",
|
|
136
|
+
"APPVEYOR",
|
|
137
|
+
"GITLAB_CI",
|
|
138
|
+
"BUILDKITE",
|
|
139
|
+
"DRONE"
|
|
140
|
+
].some((sign) => sign in env) || "codeship" === env.CI_NAME) return 1;
|
|
141
|
+
return min;
|
|
142
|
+
}
|
|
143
|
+
if ("TEAMCITY_VERSION" in env) return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
144
|
+
if ("truecolor" === env.COLORTERM) return 3;
|
|
145
|
+
if ("xterm-kitty" === env.TERM) return 3;
|
|
146
|
+
if ("xterm-ghostty" === env.TERM) return 3;
|
|
147
|
+
if ("wezterm" === env.TERM) return 3;
|
|
148
|
+
if ("TERM_PROGRAM" in env) {
|
|
149
|
+
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
150
|
+
switch (env.TERM_PROGRAM) {
|
|
151
|
+
case "iTerm.app": return version >= 3 ? 3 : 2;
|
|
152
|
+
case "Apple_Terminal": return 2;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (/-256(color)?$/i.test(env.TERM)) return 2;
|
|
156
|
+
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) return 1;
|
|
157
|
+
if ("COLORTERM" in env) return 1;
|
|
158
|
+
return min;
|
|
159
|
+
}
|
|
160
|
+
function createSupportsColor(stream, options = {}) {
|
|
161
|
+
return translateLevel(_supportsColor(stream, {
|
|
162
|
+
streamIsTTY: stream && stream.isTTY,
|
|
163
|
+
...options
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
const supports_color = {
|
|
167
|
+
stdout: createSupportsColor({ isTTY: node_tty.isatty(1) }),
|
|
168
|
+
stderr: createSupportsColor({ isTTY: node_tty.isatty(2) })
|
|
169
|
+
};
|
|
170
|
+
const colorLevel = supports_color.stdout ? supports_color.stdout.level : 0;
|
|
171
|
+
let errorStackRegExp = /at [^\r\n]{0,200}:\d+:\d+[\s\)]*$/;
|
|
172
|
+
let anonymousErrorStackRegExp = /at [^\r\n]{0,200}\(<anonymous>\)$/;
|
|
173
|
+
let indexErrorStackRegExp = /at [^\r\n]{0,200}\(index\s\d+\)$/;
|
|
174
|
+
let isErrorStackMessage = (message) => errorStackRegExp.test(message) || anonymousErrorStackRegExp.test(message) || indexErrorStackRegExp.test(message);
|
|
175
|
+
let formatter = (open, close, replace = open) => colorLevel >= 2 ? (input) => {
|
|
176
|
+
let string = "" + input;
|
|
177
|
+
let index = string.indexOf(close, open.length);
|
|
178
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
179
|
+
} : String;
|
|
180
|
+
let replaceClose = (string, close, replace, index) => {
|
|
181
|
+
let start = string.substring(0, index) + replace;
|
|
182
|
+
let end = string.substring(index + close.length);
|
|
183
|
+
let nextIndex = end.indexOf(close);
|
|
184
|
+
return ~nextIndex ? start + replaceClose(end, close, replace, nextIndex) : start + end;
|
|
185
|
+
};
|
|
186
|
+
const bold = formatter("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m");
|
|
187
|
+
const red = formatter("\x1B[31m", "\x1B[39m");
|
|
188
|
+
const green = formatter("\x1B[32m", "\x1B[39m");
|
|
189
|
+
const yellow = formatter("\x1B[33m", "\x1B[39m");
|
|
190
|
+
const magenta = formatter("\x1B[35m", "\x1B[39m");
|
|
191
|
+
const cyan = formatter("\x1B[36m", "\x1B[39m");
|
|
192
|
+
const gray = formatter("\x1B[90m", "\x1B[39m");
|
|
193
|
+
let startColor = [
|
|
194
|
+
189,
|
|
195
|
+
255,
|
|
196
|
+
243
|
|
197
|
+
];
|
|
198
|
+
let endColor = [
|
|
199
|
+
74,
|
|
200
|
+
194,
|
|
201
|
+
154
|
|
202
|
+
];
|
|
203
|
+
let isWord = (char) => !/[\s\n]/.test(char);
|
|
204
|
+
let gradient = (message) => {
|
|
205
|
+
if (colorLevel < 3) return 2 === colorLevel ? bold(cyan(message)) : message;
|
|
206
|
+
let chars = [...message];
|
|
207
|
+
let steps = chars.filter(isWord).length;
|
|
208
|
+
let r = startColor[0];
|
|
209
|
+
let g = startColor[1];
|
|
210
|
+
let b = startColor[2];
|
|
211
|
+
let rStep = (endColor[0] - r) / steps;
|
|
212
|
+
let gStep = (endColor[1] - g) / steps;
|
|
213
|
+
let bStep = (endColor[2] - b) / steps;
|
|
214
|
+
let output = "";
|
|
215
|
+
for (let char of chars) {
|
|
216
|
+
if (isWord(char)) {
|
|
217
|
+
r += rStep;
|
|
218
|
+
g += gStep;
|
|
219
|
+
b += bStep;
|
|
220
|
+
}
|
|
221
|
+
output += `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(b)}m${char}\x1b[39m`;
|
|
222
|
+
}
|
|
223
|
+
return bold(output);
|
|
224
|
+
};
|
|
225
|
+
let LOG_LEVEL = {
|
|
226
|
+
silent: -1,
|
|
227
|
+
error: 0,
|
|
228
|
+
warn: 1,
|
|
229
|
+
info: 2,
|
|
230
|
+
log: 2,
|
|
231
|
+
verbose: 3
|
|
232
|
+
};
|
|
233
|
+
let LOG_TYPES = {
|
|
234
|
+
error: {
|
|
235
|
+
label: "error",
|
|
236
|
+
level: "error",
|
|
237
|
+
color: red
|
|
238
|
+
},
|
|
239
|
+
warn: {
|
|
240
|
+
label: "warn",
|
|
241
|
+
level: "warn",
|
|
242
|
+
color: yellow
|
|
243
|
+
},
|
|
244
|
+
info: {
|
|
245
|
+
label: "info",
|
|
246
|
+
level: "info",
|
|
247
|
+
color: cyan
|
|
248
|
+
},
|
|
249
|
+
start: {
|
|
250
|
+
label: "start",
|
|
251
|
+
level: "info",
|
|
252
|
+
color: cyan
|
|
253
|
+
},
|
|
254
|
+
ready: {
|
|
255
|
+
label: "ready",
|
|
256
|
+
level: "info",
|
|
257
|
+
color: green
|
|
258
|
+
},
|
|
259
|
+
success: {
|
|
260
|
+
label: "success",
|
|
261
|
+
level: "info",
|
|
262
|
+
color: green
|
|
263
|
+
},
|
|
264
|
+
log: { level: "info" },
|
|
265
|
+
debug: {
|
|
266
|
+
label: "debug",
|
|
267
|
+
level: "verbose",
|
|
268
|
+
color: magenta
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const normalizeErrorMessage = (err) => {
|
|
272
|
+
if (err.stack) {
|
|
273
|
+
let [name, ...rest] = err.stack.split("\n");
|
|
274
|
+
if (name.startsWith("Error: ")) name = name.slice(7);
|
|
275
|
+
return `${name}\n${gray(rest.join("\n"))}`;
|
|
276
|
+
}
|
|
277
|
+
return err.message;
|
|
278
|
+
};
|
|
279
|
+
let createLogger = (options = {}) => {
|
|
280
|
+
let maxLevel = options.level || "info";
|
|
281
|
+
let log = (type, message, ...args) => {
|
|
282
|
+
let logType = LOG_TYPES[type];
|
|
283
|
+
const { level } = logType;
|
|
284
|
+
if (LOG_LEVEL[level] > LOG_LEVEL[maxLevel]) return;
|
|
285
|
+
if (null == message) return console.log();
|
|
286
|
+
let label = "";
|
|
287
|
+
let text = "";
|
|
288
|
+
if ("label" in logType) {
|
|
289
|
+
label = (logType.label || "").padEnd(7);
|
|
290
|
+
label = bold(logType.color ? logType.color(label) : label);
|
|
291
|
+
}
|
|
292
|
+
if (message instanceof Error) {
|
|
293
|
+
text += normalizeErrorMessage(message);
|
|
294
|
+
const { cause } = message;
|
|
295
|
+
if (cause) {
|
|
296
|
+
text += yellow("\n [cause]: ");
|
|
297
|
+
text += cause instanceof Error ? normalizeErrorMessage(cause) : String(cause);
|
|
298
|
+
}
|
|
299
|
+
} else if ("error" === level && "string" == typeof message) text = message.split("\n").map((line) => isErrorStackMessage(line) ? gray(line) : line).join("\n");
|
|
300
|
+
else text = `${message}`;
|
|
301
|
+
const method = "error" === level || "warn" === level ? level : "log";
|
|
302
|
+
console[method](label.length ? `${label} ${text}` : text, ...args);
|
|
303
|
+
};
|
|
304
|
+
let logger = { greet: (message) => log("log", gradient(message)) };
|
|
305
|
+
Object.keys(LOG_TYPES).forEach((key) => {
|
|
306
|
+
logger[key] = (...args) => log(key, ...args);
|
|
307
|
+
});
|
|
308
|
+
Object.defineProperty(logger, "level", {
|
|
309
|
+
get: () => maxLevel,
|
|
310
|
+
set(val) {
|
|
311
|
+
maxLevel = val;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
logger.override = (customLogger) => {
|
|
315
|
+
Object.assign(logger, customLogger);
|
|
316
|
+
};
|
|
317
|
+
return logger;
|
|
318
|
+
};
|
|
319
|
+
let src_logger = createLogger();
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/options.ts
|
|
323
|
+
function resolveOptions(userOptions) {
|
|
324
|
+
return {
|
|
325
|
+
domain: "",
|
|
326
|
+
outDir: "og",
|
|
327
|
+
ogTemplate: "og-template.svg",
|
|
328
|
+
maxTitleSizePerLine: 30,
|
|
329
|
+
...userOptions
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/index.ts
|
|
335
|
+
const NAME = "rspress-plugin-og";
|
|
336
|
+
const LOG_PREFIX = `[${NAME}]`;
|
|
337
|
+
function src_default(userOptions) {
|
|
338
|
+
const options = resolveOptions(userOptions);
|
|
339
|
+
const images = /* @__PURE__ */ new Map();
|
|
340
|
+
const headCreators = [
|
|
341
|
+
(url) => createTwitterImageHead(url),
|
|
342
|
+
() => createTwitterCardHead(),
|
|
343
|
+
(url) => createOgImageHead(url),
|
|
344
|
+
() => createOgImageWidthHead(),
|
|
345
|
+
() => createOgImageHeightHead(),
|
|
346
|
+
() => createOgImageTypeHead()
|
|
347
|
+
];
|
|
348
|
+
return {
|
|
349
|
+
name: NAME,
|
|
350
|
+
config(config) {
|
|
351
|
+
config.head = [...config.head || [], ...headCreators.map((creator) => (route) => {
|
|
352
|
+
const imageInfo = images.get(route.routePath);
|
|
353
|
+
if (!imageInfo) return;
|
|
354
|
+
return creator(imageInfo.imageUrl);
|
|
355
|
+
})];
|
|
356
|
+
return config;
|
|
357
|
+
},
|
|
358
|
+
extendPageData: (pageData) => {
|
|
359
|
+
if (!(pageData.frontmatter.title || pageData.title)) {
|
|
360
|
+
src_logger.warn(`${LOG_PREFIX} Cannot generate OG image for page without title: ${pageData._relativePath}`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const imageName = slugifyPath(pageData._relativePath);
|
|
364
|
+
images.set(pageData.routePath, {
|
|
365
|
+
title: pageData.frontmatter.title || pageData.title,
|
|
366
|
+
imageName,
|
|
367
|
+
imageUrl: joinURL(options.domain, options.outDir, imageName)
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
async afterBuild(config) {
|
|
371
|
+
const outputFolder = join(cwd(), config.outDir ?? "doc_build", options.outDir);
|
|
372
|
+
src_logger.info(`${LOG_PREFIX} Generating OG images to ${relative(cwd(), outputFolder)} ...`);
|
|
373
|
+
await Promise.all(Array.from(images.entries()).map(([_, { title, imageName }]) => {
|
|
374
|
+
return generateOgImage({ title }, join(outputFolder, imageName), options);
|
|
375
|
+
}));
|
|
376
|
+
src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated.`);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
//#endregion
|
|
382
|
+
export { src_default as default };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region ../core/src/types.d.ts
|
|
2
|
+
interface Options {
|
|
3
|
+
/**
|
|
4
|
+
* The domain to use for the generated OG image URLs.
|
|
5
|
+
*/
|
|
6
|
+
domain: string;
|
|
7
|
+
/**
|
|
8
|
+
* Output directory for the generated OG images.
|
|
9
|
+
*
|
|
10
|
+
* @default 'og'
|
|
11
|
+
*/
|
|
12
|
+
outDir?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum number of characters per line in the title.
|
|
15
|
+
*
|
|
16
|
+
* @default 30
|
|
17
|
+
*/
|
|
18
|
+
maxTitleSizePerLine?: number;
|
|
19
|
+
/**
|
|
20
|
+
* The path to the OG image template file.
|
|
21
|
+
*
|
|
22
|
+
* @default '.vitepress/og-template.svg' for VitePress
|
|
23
|
+
* @default 'og-template.svg' for Rspress
|
|
24
|
+
*/
|
|
25
|
+
ogTemplate?: string;
|
|
26
|
+
}
|
|
27
|
+
interface ResolvedOptions extends Required<Options> {}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/types.d.ts
|
|
30
|
+
interface Options$1 extends Options {}
|
|
31
|
+
interface ResolvedOptions$1 extends ResolvedOptions {}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { ResolvedOptions$1 as n, Options$1 as t };
|
package/dist/types.d.mts
ADDED
package/dist/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soonit/rspress-plugin-og",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "Automatically generate Open Graph images for your Rspress pages.",
|
|
6
|
+
"author": "Estéban Soubiran <esteban@soubiran.dev>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"funding": "https://github.com/sponsors/Barbapapazes",
|
|
9
|
+
"homepage": "https://github.com/Barbapapazes/vitepress-plugin-og",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/Barbapapazes/vitepress-plugin-og"
|
|
13
|
+
},
|
|
14
|
+
"bugs": "https://github.com/Barbapapazes/vitepress-plugin-og/issues",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"import": "./dist/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./dist/types.d.mts",
|
|
26
|
+
"import": "./dist/types.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./*": "./*"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/index.mjs",
|
|
31
|
+
"types": "dist/index.d.mts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@rspress/core": "^2.0.0-rc.1 || ^2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"sharp": "^0.34.5",
|
|
43
|
+
"ufo": "^1.6.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"rslog": "^1.3.2",
|
|
47
|
+
"tsdown": "^0.16.6",
|
|
48
|
+
"vitest": "^4.0.12"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"test": "vitest"
|
|
53
|
+
}
|
|
54
|
+
}
|