@ubean/seo 0.1.13 → 0.2.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/dist/conventions.d.ts +77 -0
- package/dist/conventions.js +177 -0
- package/dist/index.d.ts +22 -1
- package/dist/index.js +113 -26
- package/dist/json-ld.d.ts +122 -0
- package/dist/json-ld.js +159 -0
- package/dist/og-image.d.ts +196 -0
- package/dist/og-image.js +444 -0
- package/package.json +27 -5
package/dist/og-image.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "pathe";
|
|
3
|
+
//#region src/og-image.ts
|
|
4
|
+
/**
|
|
5
|
+
* P9-06 OG Image 动态生成
|
|
6
|
+
*
|
|
7
|
+
* 对齐 Next.js `ImageResponse`(基于 Satori + resvg)和 SvelteKit `@vercel/og`。
|
|
8
|
+
*
|
|
9
|
+
* 设计要点:
|
|
10
|
+
* - **零硬依赖**:`satori` 和 `@resvg/resvg-js` 作为 optional peer 依赖,
|
|
11
|
+
* 运行时通过动态 `import()` 加载。未安装时抛出明确的引导错误。
|
|
12
|
+
* - **API 对齐 Next.js**:`ImageResponse` 类接收 Satori 兼容的 React-like
|
|
13
|
+
* 元素树(此处用泛型 `unknown` 表示,因为 Satori 自身接受任意 VDOM),
|
|
14
|
+
* 返回 PNG `Response`。
|
|
15
|
+
* - **内置模板**:`renderOgImage(options)` 提供开箱即用的 OG 图模板,
|
|
16
|
+
* 覆盖 80% 用例(博客/文章/产品页),无需手写 JSX。
|
|
17
|
+
* - **字体加载**:提供 `loadDefaultFont()` 从内置 fetch 加载默认字体;
|
|
18
|
+
* `loadFontFromFile()`/`loadFontFromUrl()` 辅助自定义字体加载。
|
|
19
|
+
*
|
|
20
|
+
* 对齐:Next.js `ImageResponse` / `@vercel/og` / Astro `astro-og-image`。
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_WIDTH = 1200;
|
|
23
|
+
const DEFAULT_HEIGHT = 630;
|
|
24
|
+
const DEFAULT_CACHE_CONTROL = "public, max-age=86400";
|
|
25
|
+
/**
|
|
26
|
+
* 默认字体 URL(使用 Google Fonts 的 Inter 字体,常规字重 400)。
|
|
27
|
+
*
|
|
28
|
+
* 在生产环境建议使用 `loadFontFromFile()` 加载本地字体,避免运行时网络依赖。
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_FONT_URL = "https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2";
|
|
31
|
+
/**
|
|
32
|
+
* 默认字体名。
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_FONT_NAME = "Inter";
|
|
35
|
+
/**
|
|
36
|
+
* 从 URL 加载字体(返回 Satori 兼容的 `SatoriFont`)。
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const font = await loadFontFromUrl('https://fonts.gstatic.com/.../inter.woff2', 'Inter');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
async function loadFontFromUrl(url, name, weight = 400, fontStyle = "normal") {
|
|
44
|
+
const res = await fetch(url);
|
|
45
|
+
if (!res.ok) throw new Error(`Failed to load font from ${url}: ${res.status} ${res.statusText}`);
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
data: await res.arrayBuffer(),
|
|
49
|
+
weight,
|
|
50
|
+
style: fontStyle
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 从本地文件加载字体。
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const font = loadFontFromFile('./fonts/inter-regular.woff2', 'Inter');
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
function loadFontFromFile(path, name, weight = 400, fontStyle = "normal") {
|
|
62
|
+
const absPath = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
data: readFileSync(absPath),
|
|
66
|
+
weight,
|
|
67
|
+
style: fontStyle
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 加载默认字体(Inter Regular,从 Google Fonts CDN 拉取)。
|
|
72
|
+
*
|
|
73
|
+
* 注意:此函数依赖网络访问。Cloudflare Workers 等边缘环境需要 fetch API 可用。
|
|
74
|
+
* 离线/生产环境推荐用 `loadFontFromFile()` 替代。
|
|
75
|
+
*/
|
|
76
|
+
function loadDefaultFont() {
|
|
77
|
+
return loadFontFromUrl(DEFAULT_FONT_URL, DEFAULT_FONT_NAME, 400);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 默认 OG 图模板:渐变背景 + 居中标题 + 副标题 + 底部站点名。
|
|
81
|
+
*
|
|
82
|
+
* 返回 Satori 兼容的 VDOM 树。
|
|
83
|
+
*/
|
|
84
|
+
function defaultTemplate(input) {
|
|
85
|
+
const themeColor = input.themeColor || "#0f172a";
|
|
86
|
+
const textColor = input.textColor || "#ffffff";
|
|
87
|
+
const children = [];
|
|
88
|
+
const contentChildren = [{
|
|
89
|
+
type: "div",
|
|
90
|
+
props: {
|
|
91
|
+
style: {
|
|
92
|
+
fontSize: input.title.length > 60 ? "52px" : "72px",
|
|
93
|
+
fontWeight: 700,
|
|
94
|
+
lineHeight: 1.2,
|
|
95
|
+
textAlign: "center",
|
|
96
|
+
color: textColor,
|
|
97
|
+
maxWidth: "1000px"
|
|
98
|
+
},
|
|
99
|
+
children: input.title
|
|
100
|
+
}
|
|
101
|
+
}];
|
|
102
|
+
if (input.description) contentChildren.push({
|
|
103
|
+
type: "div",
|
|
104
|
+
props: {
|
|
105
|
+
style: {
|
|
106
|
+
fontSize: "32px",
|
|
107
|
+
marginTop: "24px",
|
|
108
|
+
color: textColor,
|
|
109
|
+
opacity: .8,
|
|
110
|
+
textAlign: "center",
|
|
111
|
+
maxWidth: "900px",
|
|
112
|
+
lineHeight: 1.4
|
|
113
|
+
},
|
|
114
|
+
children: input.description
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
children.push({
|
|
118
|
+
type: "div",
|
|
119
|
+
props: {
|
|
120
|
+
style: {
|
|
121
|
+
display: "flex",
|
|
122
|
+
flexDirection: "column",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
flexGrow: 1,
|
|
126
|
+
padding: ["0", "80px"]
|
|
127
|
+
},
|
|
128
|
+
children: contentChildren
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (input.siteName) children.push({
|
|
132
|
+
type: "div",
|
|
133
|
+
props: {
|
|
134
|
+
style: {
|
|
135
|
+
display: "flex",
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
padding: [
|
|
138
|
+
"0",
|
|
139
|
+
"80px",
|
|
140
|
+
"60px",
|
|
141
|
+
"80px"
|
|
142
|
+
],
|
|
143
|
+
fontSize: "28px",
|
|
144
|
+
color: textColor,
|
|
145
|
+
opacity: .7
|
|
146
|
+
},
|
|
147
|
+
children: input.siteName
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (input.logo) children.unshift({
|
|
151
|
+
type: "div",
|
|
152
|
+
props: {
|
|
153
|
+
style: {
|
|
154
|
+
display: "flex",
|
|
155
|
+
alignItems: "center",
|
|
156
|
+
padding: [
|
|
157
|
+
"60px",
|
|
158
|
+
"80px",
|
|
159
|
+
"0",
|
|
160
|
+
"80px"
|
|
161
|
+
],
|
|
162
|
+
fontSize: "28px",
|
|
163
|
+
fontWeight: 600,
|
|
164
|
+
color: textColor
|
|
165
|
+
},
|
|
166
|
+
children: input.logo
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
type: "div",
|
|
171
|
+
props: {
|
|
172
|
+
style: {
|
|
173
|
+
display: "flex",
|
|
174
|
+
flexDirection: "column",
|
|
175
|
+
width: "100%",
|
|
176
|
+
height: "100%",
|
|
177
|
+
backgroundColor: themeColor,
|
|
178
|
+
backgroundImage: input.backgroundImage ? `url(${input.backgroundImage})` : `linear-gradient(135deg, ${themeColor} 0%, ${shadeColor(themeColor, -20)} 100%)`,
|
|
179
|
+
backgroundSize: "cover",
|
|
180
|
+
backgroundPosition: "center",
|
|
181
|
+
fontFamily: DEFAULT_FONT_NAME
|
|
182
|
+
},
|
|
183
|
+
children
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 文章模板:左对齐标题 + 日期/作者元信息。
|
|
189
|
+
*/
|
|
190
|
+
function articleTemplate(input) {
|
|
191
|
+
const themeColor = input.themeColor || "#1e293b";
|
|
192
|
+
const textColor = input.textColor || "#ffffff";
|
|
193
|
+
const topSection = input.siteName ? {
|
|
194
|
+
type: "div",
|
|
195
|
+
props: {
|
|
196
|
+
style: {
|
|
197
|
+
fontSize: "28px",
|
|
198
|
+
fontWeight: 600,
|
|
199
|
+
color: textColor,
|
|
200
|
+
opacity: .7
|
|
201
|
+
},
|
|
202
|
+
children: input.siteName
|
|
203
|
+
}
|
|
204
|
+
} : null;
|
|
205
|
+
const middleContent = [{
|
|
206
|
+
type: "div",
|
|
207
|
+
props: {
|
|
208
|
+
style: {
|
|
209
|
+
fontSize: input.title.length > 80 ? "48px" : "64px",
|
|
210
|
+
fontWeight: 700,
|
|
211
|
+
lineHeight: 1.2,
|
|
212
|
+
color: textColor,
|
|
213
|
+
maxWidth: "1000px"
|
|
214
|
+
},
|
|
215
|
+
children: input.title
|
|
216
|
+
}
|
|
217
|
+
}];
|
|
218
|
+
if (input.description) middleContent.push({
|
|
219
|
+
type: "div",
|
|
220
|
+
props: {
|
|
221
|
+
style: {
|
|
222
|
+
fontSize: "30px",
|
|
223
|
+
marginTop: "24px",
|
|
224
|
+
color: textColor,
|
|
225
|
+
opacity: .8,
|
|
226
|
+
maxWidth: "900px",
|
|
227
|
+
lineHeight: 1.4
|
|
228
|
+
},
|
|
229
|
+
children: input.description
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const middleSection = {
|
|
233
|
+
type: "div",
|
|
234
|
+
props: {
|
|
235
|
+
style: {
|
|
236
|
+
display: "flex",
|
|
237
|
+
flexDirection: "column",
|
|
238
|
+
flexGrow: 1,
|
|
239
|
+
justifyContent: "center"
|
|
240
|
+
},
|
|
241
|
+
children: middleContent
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const footerText = [input.author, input.date].filter(Boolean).join(" · ");
|
|
245
|
+
const allChildren = [
|
|
246
|
+
topSection,
|
|
247
|
+
middleSection,
|
|
248
|
+
{
|
|
249
|
+
type: "div",
|
|
250
|
+
props: {
|
|
251
|
+
style: {
|
|
252
|
+
display: "flex",
|
|
253
|
+
alignItems: "center",
|
|
254
|
+
fontSize: "26px",
|
|
255
|
+
color: textColor,
|
|
256
|
+
opacity: .7
|
|
257
|
+
},
|
|
258
|
+
children: footerText
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
].filter((c) => c !== null);
|
|
262
|
+
return {
|
|
263
|
+
type: "div",
|
|
264
|
+
props: {
|
|
265
|
+
style: {
|
|
266
|
+
display: "flex",
|
|
267
|
+
flexDirection: "column",
|
|
268
|
+
justifyContent: "space-between",
|
|
269
|
+
width: "100%",
|
|
270
|
+
height: "100%",
|
|
271
|
+
padding: "80px",
|
|
272
|
+
backgroundColor: themeColor,
|
|
273
|
+
backgroundImage: `linear-gradient(135deg, ${themeColor} 0%, ${shadeColor(themeColor, -25)} 100%)`,
|
|
274
|
+
fontFamily: DEFAULT_FONT_NAME
|
|
275
|
+
},
|
|
276
|
+
children: allChildren
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* 简单的颜色调亮/调暗工具(用于生成渐变背景)。
|
|
282
|
+
*
|
|
283
|
+
* `percent` 为负数调暗,正数调亮。范围 -100..100。
|
|
284
|
+
*/
|
|
285
|
+
function shadeColor(hex, percent) {
|
|
286
|
+
const normalized = hex.replace("#", "");
|
|
287
|
+
if (normalized.length !== 6) return hex;
|
|
288
|
+
const num = parseInt(normalized, 16);
|
|
289
|
+
const amt = Math.round(2.55 * percent);
|
|
290
|
+
const R = Math.max(0, Math.min(255, (num >> 16) + amt));
|
|
291
|
+
const G = Math.max(0, Math.min(255, (num >> 8 & 255) + amt));
|
|
292
|
+
const B = Math.max(0, Math.min(255, (num & 255) + amt));
|
|
293
|
+
return `#${(R << 16 | G << 8 | B).toString(16).padStart(6, "0")}`;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 动态加载 satori。若未安装抛出友好错误。
|
|
297
|
+
*/
|
|
298
|
+
async function loadSatori() {
|
|
299
|
+
try {
|
|
300
|
+
const mod = await import("satori");
|
|
301
|
+
return mod.default || mod;
|
|
302
|
+
} catch {
|
|
303
|
+
throw new Error("[ubean/seo] OG image rendering requires `satori` to be installed.\nInstall it with: pnpm add satori @resvg/resvg-js\n(Both are optional peer dependencies of @ubean/seo to keep the base bundle small.)");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* 动态加载 @resvg/resvg-js。若未安装抛出友好错误。
|
|
308
|
+
*/
|
|
309
|
+
async function loadResvg() {
|
|
310
|
+
try {
|
|
311
|
+
return await import("@resvg/resvg-js");
|
|
312
|
+
} catch {
|
|
313
|
+
throw new Error("[ubean/seo] PNG conversion requires `@resvg/resvg-js` to be installed.\nInstall it with: pnpm add @resvg/resvg-js\n(Optional peer dependency of @ubean/seo.)");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 把 Satori VDOM 节点渲染为 PNG Buffer(或 SVG 字符串,当 `svgOnly: true` 时)。
|
|
318
|
+
*
|
|
319
|
+
* @returns `{ body, contentType }` —— body 为 Buffer/字符串,contentType 为 MIME 类型
|
|
320
|
+
*/
|
|
321
|
+
async function renderToImage(node, options = {}) {
|
|
322
|
+
const width = options.width ?? DEFAULT_WIDTH;
|
|
323
|
+
const height = options.height ?? DEFAULT_HEIGHT;
|
|
324
|
+
const fonts = options.fonts && options.fonts.length > 0 ? options.fonts : [await loadDefaultFont()];
|
|
325
|
+
const svg = await (await loadSatori())(node, {
|
|
326
|
+
width,
|
|
327
|
+
height,
|
|
328
|
+
fonts,
|
|
329
|
+
debug: options.debug ?? false
|
|
330
|
+
});
|
|
331
|
+
if (options.svgOnly) return {
|
|
332
|
+
body: svg,
|
|
333
|
+
contentType: "image/svg+xml"
|
|
334
|
+
};
|
|
335
|
+
const { Resvg } = await loadResvg();
|
|
336
|
+
const { asPng } = new Resvg(svg, options.resvgOptions).render();
|
|
337
|
+
return {
|
|
338
|
+
body: asPng,
|
|
339
|
+
contentType: "image/png"
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 对齐 Next.js `ImageResponse` 类。接收 Satori VDOM 节点,返回 PNG `Response`。
|
|
344
|
+
*
|
|
345
|
+
* 实现策略:body 使用 `ReadableStream`,在 stream 被 consume 时才执行实际的
|
|
346
|
+
* satori + resvg 渲染。这样构造函数可以保持同步,符合 Next.js
|
|
347
|
+
* `return new ImageResponse(...)` 的用法。
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* // src/routes/og.ts
|
|
352
|
+
* import { ImageResponse, defaultTemplate, loadDefaultFont } from '@ubean/seo/og-image';
|
|
353
|
+
*
|
|
354
|
+
* export const GET = defineHandler(async () => {
|
|
355
|
+
* const fonts = [await loadDefaultFont()];
|
|
356
|
+
* const node = defaultTemplate({ title: 'Hello ubean', siteName: 'ubean' });
|
|
357
|
+
* return new ImageResponse(node, { fonts });
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*
|
|
361
|
+
* 也可用于 `src/opengraph-image.ts` 约定文件:
|
|
362
|
+
* ```ts
|
|
363
|
+
* // src/opengraph-image.ts
|
|
364
|
+
* import { ImageResponse, defaultTemplate, loadDefaultFont } from '@ubean/seo/og-image';
|
|
365
|
+
* export default async function GET() {
|
|
366
|
+
* const fonts = [await loadDefaultFont()];
|
|
367
|
+
* return new ImageResponse(defaultTemplate({ title: 'ubean' }), { fonts });
|
|
368
|
+
* }
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
var ImageResponse = class extends Response {
|
|
372
|
+
constructor(node, options = {}) {
|
|
373
|
+
const cacheControl = options.cacheControl ?? DEFAULT_CACHE_CONTROL;
|
|
374
|
+
const stream = new ReadableStream({ async start(controller) {
|
|
375
|
+
try {
|
|
376
|
+
const { body } = await renderToImage(node, options);
|
|
377
|
+
if (typeof body === "string") controller.enqueue(new TextEncoder().encode(body));
|
|
378
|
+
else controller.enqueue(new Uint8Array(body));
|
|
379
|
+
controller.close();
|
|
380
|
+
} catch (err) {
|
|
381
|
+
controller.error(err);
|
|
382
|
+
}
|
|
383
|
+
} });
|
|
384
|
+
super(stream, {
|
|
385
|
+
status: 200,
|
|
386
|
+
headers: {
|
|
387
|
+
"Content-Type": options.svgOnly ? "image/svg+xml" : "image/png",
|
|
388
|
+
"Cache-Control": cacheControl
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* 一步到位渲染 OG 图(模板 + 渲染 + Response)。
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```ts
|
|
398
|
+
* // src/routes/og.ts
|
|
399
|
+
* import { renderOgImage } from '@ubean/seo/og-image';
|
|
400
|
+
* export const GET = defineHandler(async () => {
|
|
401
|
+
* return renderOgImage({ title: 'Hello world', siteName: 'ubean' });
|
|
402
|
+
* });
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
async function renderOgImage(input, options = {}) {
|
|
406
|
+
const { body, contentType } = await renderToImage(defaultTemplate(input), options);
|
|
407
|
+
const cacheControl = options.cacheControl ?? DEFAULT_CACHE_CONTROL;
|
|
408
|
+
return new Response(body, {
|
|
409
|
+
status: 200,
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": contentType,
|
|
412
|
+
"Cache-Control": cacheControl
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 用文章模板渲染 OG 图。
|
|
418
|
+
*/
|
|
419
|
+
async function renderArticleOgImage(input, options = {}) {
|
|
420
|
+
const { body, contentType } = await renderToImage(articleTemplate(input), options);
|
|
421
|
+
const cacheControl = options.cacheControl ?? DEFAULT_CACHE_CONTROL;
|
|
422
|
+
return new Response(body, {
|
|
423
|
+
status: 200,
|
|
424
|
+
headers: {
|
|
425
|
+
"Content-Type": contentType,
|
|
426
|
+
"Cache-Control": cacheControl
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 检测运行时是否安装了 OG 图渲染所需的依赖。
|
|
432
|
+
* 用于在不实际渲染的情况下判断能力可用性(例如 DevTools 信息展示)。
|
|
433
|
+
*/
|
|
434
|
+
async function isOgImageSupported() {
|
|
435
|
+
try {
|
|
436
|
+
await import("satori");
|
|
437
|
+
await import("@resvg/resvg-js");
|
|
438
|
+
return true;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
export { DEFAULT_FONT_NAME, DEFAULT_FONT_URL, ImageResponse, articleTemplate, defaultTemplate, isOgImageSupported, loadDefaultFont, loadFontFromFile, loadFontFromUrl, renderArticleOgImage, renderOgImage, renderToImage, shadeColor };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/seo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SEO utilities for ubean (useSeoMeta, robots, sitemap, manifest)",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SEO utilities for ubean (useSeoMeta, robots, sitemap, manifest, file conventions, JSON-LD, OG image)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
7
7
|
],
|
|
@@ -13,27 +13,49 @@
|
|
|
13
13
|
".": {
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./conventions": {
|
|
18
|
+
"types": "./dist/conventions.d.ts",
|
|
19
|
+
"import": "./dist/conventions.js"
|
|
20
|
+
},
|
|
21
|
+
"./og-image": {
|
|
22
|
+
"types": "./dist/og-image.d.ts",
|
|
23
|
+
"import": "./dist/og-image.js"
|
|
24
|
+
},
|
|
25
|
+
"./json-ld": {
|
|
26
|
+
"types": "./dist/json-ld.d.ts",
|
|
27
|
+
"import": "./dist/json-ld.js"
|
|
16
28
|
}
|
|
17
29
|
},
|
|
18
30
|
"dependencies": {
|
|
19
|
-
"@unhead/vue": "^3.2
|
|
31
|
+
"@unhead/vue": "^3.3.2",
|
|
32
|
+
"pathe": "^2.0.3"
|
|
20
33
|
},
|
|
21
34
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^26.
|
|
35
|
+
"@types/node": "^26.2.0",
|
|
23
36
|
"typescript": "7.0.2",
|
|
24
|
-
"vite-plus": "0.2.
|
|
37
|
+
"vite-plus": "0.2.9"
|
|
25
38
|
},
|
|
26
39
|
"peerDependencies": {
|
|
40
|
+
"@resvg/resvg-js": "^2.6.0",
|
|
41
|
+
"satori": "^0.12.0",
|
|
27
42
|
"vue": "^3.0.0"
|
|
28
43
|
},
|
|
29
44
|
"peerDependenciesMeta": {
|
|
30
45
|
"vue": {
|
|
31
46
|
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"satori": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@resvg/resvg-js": {
|
|
52
|
+
"optional": true
|
|
32
53
|
}
|
|
33
54
|
},
|
|
34
55
|
"scripts": {
|
|
35
56
|
"build": "vp pack",
|
|
36
57
|
"dev": "vp pack --watch",
|
|
58
|
+
"test": "vp test",
|
|
37
59
|
"typecheck": "tsc --noEmit --skipLibCheck"
|
|
38
60
|
}
|
|
39
61
|
}
|