@ampless/plugin-og-image 0.2.0-alpha.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 +68 -0
- package/dist/chunk-BDDYGEJD.js +60 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +196 -0
- package/dist/load-image.d.ts +16 -0
- package/dist/load-image.js +6 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ampless contributors
|
|
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,68 @@
|
|
|
1
|
+
# @ampless/plugin-og-image
|
|
2
|
+
|
|
3
|
+
Dynamic Open Graph image generation for [ampless](https://github.com/heavymoons/ampless). SNS crawlers hit `https://<your-site>/og/<slug>`, the Next.js route renders a JSX card containing the post title + excerpt + site name + an optional image (theme banner or the first image in the post body), and Next.js `ImageResponse` returns a PNG. WebP / AVIF source images are decoded to PNG on the fly via [@jsquash](https://github.com/jamsinclair/jSquash) so Satori can paint them.
|
|
4
|
+
|
|
5
|
+
> **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ampless/plugin-og-image@alpha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
In `cms.config.ts`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineConfig } from 'ampless'
|
|
19
|
+
import ogImagePlugin, { loadFontFromUrl } from '@ampless/plugin-og-image'
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
// ...
|
|
23
|
+
plugins: [
|
|
24
|
+
ogImagePlugin({
|
|
25
|
+
// Required: at least one font. Satori cannot render without one.
|
|
26
|
+
fonts: [
|
|
27
|
+
{
|
|
28
|
+
name: 'Inter',
|
|
29
|
+
// Lazy loader — runs once per route invocation, then cached
|
|
30
|
+
// in-process. Ship the .ttf / .otf from your CDN or public/.
|
|
31
|
+
data: loadFontFromUrl('https://example.com/fonts/Inter-Regular.ttf'),
|
|
32
|
+
weight: 400,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
// Image strategy: 'content' (first image in post body) | 'theme'
|
|
36
|
+
// (use `themeImageUrl`) | 'none' | (post) => url | null
|
|
37
|
+
image: 'content',
|
|
38
|
+
// Used when image === 'theme'
|
|
39
|
+
// themeImageUrl: 'https://example.com/og-banner.png',
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add the dispatcher route in your Next.js app (the `_shared` template ships one at `app/site/[siteId]/og/[slug]/route.ts`).
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
| Option | Default | Notes |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `fonts` | required | At least one font must be provided |
|
|
52
|
+
| `size` | `{ width: 1200, height: 630 }` | OG card dimensions |
|
|
53
|
+
| `image` | `'content'` | `'theme'` / `'content'` / `'none'` / `(post) => url \| null` |
|
|
54
|
+
| `themeImageUrl` | none | Used when `image === 'theme'` |
|
|
55
|
+
| `render` | built-in card | Override to fully customize the JSX |
|
|
56
|
+
|
|
57
|
+
## What it produces
|
|
58
|
+
|
|
59
|
+
- A `metadata()` hook that injects `openGraph.images: [{ url: '<site>/og/<slug>', width, height }]` into per-post metadata, so SNS crawlers know where to fetch the card.
|
|
60
|
+
- An `ogImage.render(ctx)` that the dispatcher route turns into a PNG via `next/og`.
|
|
61
|
+
|
|
62
|
+
## Trust level
|
|
63
|
+
|
|
64
|
+
`untrusted` — runs only in the Next.js request path (no Lambda hooks). No AWS credentials are touched; fonts and post images are fetched over plain HTTPS.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/load-image.ts
|
|
2
|
+
function toBase64(bytes) {
|
|
3
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
4
|
+
}
|
|
5
|
+
function toArrayBuffer(bytes) {
|
|
6
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
7
|
+
}
|
|
8
|
+
async function decodeWebp(bytes) {
|
|
9
|
+
try {
|
|
10
|
+
const webp = await import("@jsquash/webp");
|
|
11
|
+
const png = await import("@jsquash/png");
|
|
12
|
+
const imageData = await webp.decode(toArrayBuffer(bytes));
|
|
13
|
+
const pngBytes = await png.encode(imageData);
|
|
14
|
+
return `data:image/png;base64,${toBase64(new Uint8Array(pngBytes))}`;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error("[plugin-og-image] webp decode failed:", err);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function decodeAvif(bytes) {
|
|
21
|
+
try {
|
|
22
|
+
const avif = await import("@jsquash/avif");
|
|
23
|
+
const png = await import("@jsquash/png");
|
|
24
|
+
const imageData = await avif.decode(toArrayBuffer(bytes));
|
|
25
|
+
const pngBytes = await png.encode(imageData);
|
|
26
|
+
return `data:image/png;base64,${toBase64(new Uint8Array(pngBytes))}`;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error("[plugin-og-image] avif decode failed:", err);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function loadImageForOg(url) {
|
|
33
|
+
let res;
|
|
34
|
+
try {
|
|
35
|
+
res = await fetch(url);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error("[plugin-og-image] fetch failed:", err);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
console.error("[plugin-og-image] fetch non-ok:", res.status, url);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const contentType = (res.headers.get("content-type") ?? "").toLowerCase().split(";")[0].trim();
|
|
45
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
46
|
+
if (contentType === "image/png" || contentType === "image/jpeg" || contentType === "image/jpg") {
|
|
47
|
+
return `data:${contentType};base64,${toBase64(bytes)}`;
|
|
48
|
+
}
|
|
49
|
+
if (contentType === "image/webp") {
|
|
50
|
+
return decodeWebp(bytes);
|
|
51
|
+
}
|
|
52
|
+
if (contentType === "image/avif") {
|
|
53
|
+
return decodeAvif(bytes);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
loadImageForOg
|
|
60
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { OgImageFont, Post, Config, OgImageRenderContext, AmplessPlugin } from 'ampless';
|
|
2
|
+
import { ReactElement } from 'react';
|
|
3
|
+
export { loadImageForOg } from './load-image.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return a font loader that fetches `url` lazily and caches the result.
|
|
7
|
+
* Use in `cms.config.ts`:
|
|
8
|
+
*
|
|
9
|
+
* ogImagePlugin({
|
|
10
|
+
* fonts: [
|
|
11
|
+
* { name: 'Inter', data: loadFontFromUrl('https://.../Inter.ttf') },
|
|
12
|
+
* ],
|
|
13
|
+
* })
|
|
14
|
+
*/
|
|
15
|
+
declare function loadFontFromUrl(url: string): () => Promise<ArrayBuffer>;
|
|
16
|
+
|
|
17
|
+
interface DefaultCardProps {
|
|
18
|
+
title: string;
|
|
19
|
+
excerpt?: string;
|
|
20
|
+
siteName: string;
|
|
21
|
+
/** Optional pre-decoded data URL — produced by loadImageForOg(). */
|
|
22
|
+
imageDataUrl?: string | null;
|
|
23
|
+
/** Font name registered with ogImagePlugin's `fonts` array. */
|
|
24
|
+
fontFamily: string;
|
|
25
|
+
}
|
|
26
|
+
declare function DefaultCard(props: DefaultCardProps): ReactElement;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Image strategy:
|
|
30
|
+
* - 'theme' : use the URL passed in `themeImageUrl`
|
|
31
|
+
* - 'content' : use the first image found in the post body
|
|
32
|
+
* - 'none' : never include an image
|
|
33
|
+
* - function : pick the URL yourself (return null to omit)
|
|
34
|
+
*/
|
|
35
|
+
type OgImageStrategy = 'theme' | 'content' | 'none' | ((post: Post, site: Config['site']) => string | null | undefined);
|
|
36
|
+
interface OgImagePluginOptions {
|
|
37
|
+
fonts: OgImageFont[];
|
|
38
|
+
size?: {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
image?: OgImageStrategy;
|
|
43
|
+
themeImageUrl?: string;
|
|
44
|
+
/** Override the entire card render. Receives the same context the route hands the plugin. */
|
|
45
|
+
render?: (ctx: OgImageRenderContext) => Promise<ReactElement> | ReactElement;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* OG image plugin. Contributes:
|
|
49
|
+
* - `metadata(post)` → tells SNS crawlers the per-post OG image URL.
|
|
50
|
+
* - `ogImage.render(ctx)` → invoked by the dispatcher route at request time.
|
|
51
|
+
*
|
|
52
|
+
* The dispatcher route picks the first plugin in `cms.config.plugins`
|
|
53
|
+
* that has an `ogImage` config, so only register one OG image plugin per site.
|
|
54
|
+
*/
|
|
55
|
+
declare function ogImagePlugin(options: OgImagePluginOptions): AmplessPlugin;
|
|
56
|
+
|
|
57
|
+
export { DefaultCard, type OgImagePluginOptions, type OgImageStrategy, ogImagePlugin as default, loadFontFromUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadImageForOg
|
|
3
|
+
} from "./chunk-BDDYGEJD.js";
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import {
|
|
7
|
+
definePlugin,
|
|
8
|
+
extractFirstImageUrl
|
|
9
|
+
} from "ampless";
|
|
10
|
+
|
|
11
|
+
// src/default-card.tsx
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
function DefaultCard(props) {
|
|
14
|
+
const { title, excerpt, siteName, imageDataUrl, fontFamily } = props;
|
|
15
|
+
return /* @__PURE__ */ jsxs(
|
|
16
|
+
"div",
|
|
17
|
+
{
|
|
18
|
+
style: {
|
|
19
|
+
display: "flex",
|
|
20
|
+
width: "100%",
|
|
21
|
+
height: "100%",
|
|
22
|
+
backgroundColor: "#ffffff",
|
|
23
|
+
padding: 64,
|
|
24
|
+
fontFamily
|
|
25
|
+
},
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsxs(
|
|
28
|
+
"div",
|
|
29
|
+
{
|
|
30
|
+
style: {
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
justifyContent: "space-between",
|
|
34
|
+
flex: 1,
|
|
35
|
+
// Leave room for the image on the right when present, otherwise
|
|
36
|
+
// let the text occupy the whole card.
|
|
37
|
+
paddingRight: imageDataUrl ? 48 : 0
|
|
38
|
+
},
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
|
|
41
|
+
/* @__PURE__ */ jsx(
|
|
42
|
+
"div",
|
|
43
|
+
{
|
|
44
|
+
style: {
|
|
45
|
+
fontSize: 64,
|
|
46
|
+
fontWeight: 700,
|
|
47
|
+
color: "#111111",
|
|
48
|
+
lineHeight: 1.2,
|
|
49
|
+
// Satori has no text-overflow:ellipsis — but it does clip to
|
|
50
|
+
// the box. A high lineClamp is the recommended hint.
|
|
51
|
+
display: "flex"
|
|
52
|
+
},
|
|
53
|
+
children: title
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
excerpt ? /* @__PURE__ */ jsx(
|
|
57
|
+
"div",
|
|
58
|
+
{
|
|
59
|
+
style: {
|
|
60
|
+
fontSize: 28,
|
|
61
|
+
color: "#555555",
|
|
62
|
+
lineHeight: 1.4,
|
|
63
|
+
marginTop: 24,
|
|
64
|
+
display: "flex"
|
|
65
|
+
},
|
|
66
|
+
children: excerpt
|
|
67
|
+
}
|
|
68
|
+
) : null
|
|
69
|
+
] }),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
style: {
|
|
74
|
+
fontSize: 24,
|
|
75
|
+
color: "#888888",
|
|
76
|
+
display: "flex"
|
|
77
|
+
},
|
|
78
|
+
children: siteName
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
imageDataUrl ? /* @__PURE__ */ jsx(
|
|
85
|
+
"div",
|
|
86
|
+
{
|
|
87
|
+
style: {
|
|
88
|
+
display: "flex",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
justifyContent: "center",
|
|
91
|
+
width: 400,
|
|
92
|
+
height: 400
|
|
93
|
+
},
|
|
94
|
+
children: /* @__PURE__ */ jsx(
|
|
95
|
+
"img",
|
|
96
|
+
{
|
|
97
|
+
src: imageDataUrl,
|
|
98
|
+
width: 400,
|
|
99
|
+
height: 400,
|
|
100
|
+
style: {
|
|
101
|
+
width: 400,
|
|
102
|
+
height: 400,
|
|
103
|
+
borderRadius: 16,
|
|
104
|
+
objectFit: "cover"
|
|
105
|
+
},
|
|
106
|
+
alt: ""
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
) : null
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/load-font.ts
|
|
117
|
+
var cache = /* @__PURE__ */ new Map();
|
|
118
|
+
function loadFontFromUrl(url) {
|
|
119
|
+
return () => {
|
|
120
|
+
const hit = cache.get(url);
|
|
121
|
+
if (hit) return hit;
|
|
122
|
+
const promise = fetch(url).then(async (res) => {
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
cache.delete(url);
|
|
125
|
+
throw new Error(`[plugin-og-image] failed to load font ${url}: HTTP ${res.status}`);
|
|
126
|
+
}
|
|
127
|
+
return res.arrayBuffer();
|
|
128
|
+
});
|
|
129
|
+
cache.set(url, promise);
|
|
130
|
+
return promise;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/index.ts
|
|
135
|
+
function pickImageUrl(strategy, themeImageUrl, post, site) {
|
|
136
|
+
if (typeof strategy === "function") {
|
|
137
|
+
return strategy(post, site) ?? null;
|
|
138
|
+
}
|
|
139
|
+
switch (strategy) {
|
|
140
|
+
case "theme":
|
|
141
|
+
return themeImageUrl ?? null;
|
|
142
|
+
case "content":
|
|
143
|
+
return extractFirstImageUrl(post);
|
|
144
|
+
case "none":
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function ogImagePlugin(options) {
|
|
150
|
+
if (!options.fonts || options.fonts.length === 0) {
|
|
151
|
+
throw new Error("[plugin-og-image] at least one font must be provided in `fonts`");
|
|
152
|
+
}
|
|
153
|
+
const size = options.size ?? { width: 1200, height: 630 };
|
|
154
|
+
const strategy = options.image ?? "content";
|
|
155
|
+
const fontFamily = options.fonts[0].name;
|
|
156
|
+
const defaultRender = async (ctx) => {
|
|
157
|
+
const url = pickImageUrl(strategy, options.themeImageUrl, ctx.post, ctx.site);
|
|
158
|
+
const imageDataUrl = url ? await ctx.image(url) : null;
|
|
159
|
+
return DefaultCard({
|
|
160
|
+
title: ctx.post.title,
|
|
161
|
+
excerpt: ctx.post.excerpt,
|
|
162
|
+
siteName: ctx.site.name,
|
|
163
|
+
imageDataUrl,
|
|
164
|
+
fontFamily
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
return definePlugin({
|
|
168
|
+
name: "og-image",
|
|
169
|
+
apiVersion: 1,
|
|
170
|
+
trust_level: "untrusted",
|
|
171
|
+
metadata(post, site) {
|
|
172
|
+
const baseUrl = site.url.replace(/\/$/, "");
|
|
173
|
+
const url = `${baseUrl}/og/${post.slug}`;
|
|
174
|
+
return {
|
|
175
|
+
openGraph: {
|
|
176
|
+
images: [{ url, width: size.width, height: size.height }]
|
|
177
|
+
},
|
|
178
|
+
twitter: {
|
|
179
|
+
card: "summary_large_image",
|
|
180
|
+
images: [url]
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
ogImage: {
|
|
185
|
+
fonts: options.fonts,
|
|
186
|
+
size,
|
|
187
|
+
render: options.render ?? defaultRender
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
DefaultCard,
|
|
193
|
+
ogImagePlugin as default,
|
|
194
|
+
loadFontFromUrl,
|
|
195
|
+
loadImageForOg
|
|
196
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch `url` and return a data URL usable inside Satori JSX.
|
|
3
|
+
*
|
|
4
|
+
* Behavior:
|
|
5
|
+
* - PNG / JPEG: pass through as a data URL (no re-encode).
|
|
6
|
+
* - WebP: decode to ImageData, re-encode to PNG, return data URL.
|
|
7
|
+
* - AVIF: same as WebP.
|
|
8
|
+
* - GIF / SVG / other: returns null (Satori can't render animated GIFs,
|
|
9
|
+
* and SVG isn't supported via the image() helper — embed SVG with the
|
|
10
|
+
* Satori `<img src="data:image/svg+xml;...">` form if you need it).
|
|
11
|
+
* - Any fetch / decode error: returns null so the OG card still renders
|
|
12
|
+
* without an image instead of 500ing the route.
|
|
13
|
+
*/
|
|
14
|
+
declare function loadImageForOg(url: string): Promise<string | null>;
|
|
15
|
+
|
|
16
|
+
export { loadImageForOg };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ampless/plugin-og-image",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "Dynamic Open Graph image generation plugin for ampless",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./load-image": {
|
|
13
|
+
"import": "./dist/load-image.js",
|
|
14
|
+
"types": "./dist/load-image.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/heavymoons/ampless.git",
|
|
27
|
+
"directory": "packages/plugin-og-image"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image#readme",
|
|
30
|
+
"bugs": "https://github.com/heavymoons/ampless/issues",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@jsquash/avif": "^1.2.0",
|
|
33
|
+
"@jsquash/png": "^3.0.0",
|
|
34
|
+
"@jsquash/webp": "^1.4.0",
|
|
35
|
+
"ampless": "0.2.0-alpha.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "^18 || ^19"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"ampless",
|
|
45
|
+
"plugin",
|
|
46
|
+
"og-image",
|
|
47
|
+
"opengraph"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"lint": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run --passWithNoTests"
|
|
54
|
+
}
|
|
55
|
+
}
|