astro-og-images 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Di Donato
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,134 @@
1
+ # astro-og-images
2
+
3
+ Generate static Open Graph images during Astro builds from code-owned templates.
4
+
5
+ It renders a Satori template to SVG, rasterizes it with Resvg, and writes the final PNG into your Astro `dist` folder. In dev, queued images are served by Astro middleware.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add astro-og-images
11
+ ```
12
+
13
+ `sharp` is optional and installed by default through `optionalDependencies`. It is used only to compress PNG output. If your target environment cannot install native packages, set `compression: false`.
14
+
15
+ ## Astro Config
16
+
17
+ ```js
18
+ // astro.config.mjs
19
+ import { defineConfig } from "astro/config";
20
+ import { astroOgImages } from "astro-og-images";
21
+
22
+ export default defineConfig({
23
+ integrations: [
24
+ astroOgImages({
25
+ outputDir: "og",
26
+ compression: true,
27
+ }),
28
+ ],
29
+ });
30
+ ```
31
+
32
+ ## Generate An Image
33
+
34
+ ```tsx
35
+ // src/og/card.tsx
36
+ export function Card({ title }: { title: string }) {
37
+ return (
38
+ <div
39
+ style={{
40
+ width: "100%",
41
+ height: "100%",
42
+ display: "flex",
43
+ alignItems: "center",
44
+ padding: 80,
45
+ background: "#101014",
46
+ color: "white",
47
+ fontFamily: "Inter",
48
+ fontSize: 72,
49
+ fontWeight: 700,
50
+ }}
51
+ >
52
+ {title}
53
+ </div>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ```astro
59
+ ---
60
+ import { getOgImage } from "astro-og-images";
61
+ import { Card } from "../og/card";
62
+
63
+ const image = await getOgImage({
64
+ template: Card,
65
+ props: { title: "Hello OG" },
66
+ path: "hello",
67
+ fonts: [
68
+ {
69
+ name: "Inter",
70
+ path: "./public/fonts/Inter-Bold.ttf",
71
+ weight: 700,
72
+ },
73
+ ],
74
+ });
75
+ ---
76
+
77
+ <meta property="og:image" content={new URL(image.src, Astro.site)} />
78
+ ```
79
+
80
+ The default output is `/og/hello.png` at `1200x630`.
81
+
82
+ ## Compression
83
+
84
+ Compression is enabled by default and uses `sharp` to keep social images small.
85
+
86
+ ```js
87
+ astroOgImages({
88
+ compression: {
89
+ png: {
90
+ compressionLevel: 9,
91
+ palette: false,
92
+ },
93
+ },
94
+ });
95
+ ```
96
+
97
+ Disable it when native dependencies are not available:
98
+
99
+ ```js
100
+ astroOgImages({
101
+ compression: false,
102
+ });
103
+ ```
104
+
105
+ ## Image Component
106
+
107
+ For inline previews, import the Astro component:
108
+
109
+ ```astro
110
+ ---
111
+ import OgImage from "astro-og-images/component";
112
+ import { Card } from "../og/card";
113
+ ---
114
+
115
+ <OgImage
116
+ template={Card}
117
+ props={{ title: "Hello OG" }}
118
+ path="hello"
119
+ fonts={[{ name: "Inter", path: "./public/fonts/Inter-Bold.ttf", weight: 700 }]}
120
+ alt=""
121
+ />
122
+ ```
123
+
124
+ ## Caveats
125
+
126
+ - Only PNG output is supported.
127
+ - At least one local font is required.
128
+ - Templates must use styles supported by Satori.
129
+ - Each output path must map to one unique template and props set during build.
130
+ - Compression requires `sharp`; install it or set `compression: false`.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,4 @@
1
+ import type { NormalizedOgImageCompressionOptions, OgImageCompression } from "./types.js";
2
+ export declare function normalizeCompression(compression: OgImageCompression | NormalizedOgImageCompressionOptions | undefined): false | NormalizedOgImageCompressionOptions;
3
+ export declare function compressPng(buffer: Buffer, compression: false | NormalizedOgImageCompressionOptions): Promise<Buffer>;
4
+ //# sourceMappingURL=compression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../src/compression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mCAAmC,EACnC,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,kBAAkB,GAAG,mCAAmC,GAAG,SAAS,GAChF,KAAK,GAAG,mCAAmC,CAoB7C;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,KAAK,GAAG,mCAAmC,GACvD,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
@@ -0,0 +1,41 @@
1
+ export function normalizeCompression(compression) {
2
+ if (compression === false)
3
+ return false;
4
+ if (compression === true || compression === undefined) {
5
+ return {
6
+ png: {
7
+ compressionLevel: 9,
8
+ palette: false,
9
+ },
10
+ };
11
+ }
12
+ return {
13
+ png: {
14
+ compressionLevel: compression.png?.compressionLevel ?? 9,
15
+ palette: compression.png?.palette ?? false,
16
+ quality: compression.png?.quality,
17
+ effort: compression.png?.effort,
18
+ },
19
+ };
20
+ }
21
+ export async function compressPng(buffer, compression) {
22
+ if (!compression)
23
+ return buffer;
24
+ let sharp;
25
+ try {
26
+ const sharpModule = (await import("sharp"));
27
+ sharp = sharpModule.default ?? sharpModule;
28
+ }
29
+ catch {
30
+ throw new Error("astro-og-images compression requires sharp. Install sharp or set compression: false.");
31
+ }
32
+ return sharp(buffer)
33
+ .png({
34
+ compressionLevel: compression.png.compressionLevel,
35
+ palette: compression.png.palette,
36
+ quality: compression.png.quality,
37
+ effort: compression.png.effort,
38
+ })
39
+ .toBuffer();
40
+ }
41
+ //# sourceMappingURL=compression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression.js","sourceRoot":"","sources":["../src/compression.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,oBAAoB,CAClC,WAAiF;IAEjF,IAAI,WAAW,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAExC,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QACtD,OAAO;YACL,GAAG,EAAE;gBACH,gBAAgB,EAAE,CAAC;gBACnB,OAAO,EAAE,KAAK;aACf;SACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,GAAG,EAAE;YACH,gBAAgB,EAAE,WAAW,CAAC,GAAG,EAAE,gBAAgB,IAAI,CAAC;YACxD,OAAO,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,IAAI,KAAK;YAC1C,OAAO,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO;YACjC,MAAM,EAAE,WAAW,CAAC,GAAG,EAAE,MAAM;SAChC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,WAAwD;IAExD,IAAI,CAAC,WAAW;QAAE,OAAO,MAAM,CAAC;IAGhC,IAAI,KAAmB,CAAC;IACxB,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC,CAEzC,CAAC;QACF,KAAK,GAAG,WAAW,CAAC,OAAO,IAAI,WAAW,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,sFAAsF,CACvF,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,CAAC;SACjB,GAAG,CAAC;QACH,gBAAgB,EAAE,WAAW,CAAC,GAAG,CAAC,gBAAgB;QAClD,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO;QAChC,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO;QAChC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM;KAC/B,CAAC;SACD,QAAQ,EAAE,CAAC;AAChB,CAAC"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function sha256(input: string | Buffer): string;
2
+ export declare function stableStringify(value: unknown): string;
3
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD"}
package/dist/hash.js ADDED
@@ -0,0 +1,28 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256(input) {
3
+ return createHash("sha256").update(input).digest("hex");
4
+ }
5
+ export function stableStringify(value) {
6
+ return JSON.stringify(normalizeValue(value));
7
+ }
8
+ function normalizeValue(value) {
9
+ if (value instanceof Date) {
10
+ return value.toISOString();
11
+ }
12
+ if (typeof value === "function") {
13
+ return value.toString();
14
+ }
15
+ if (typeof value === "bigint") {
16
+ return value.toString();
17
+ }
18
+ if (Array.isArray(value)) {
19
+ return value.map(normalizeValue);
20
+ }
21
+ if (value && typeof value === "object") {
22
+ return Object.fromEntries(Object.entries(value)
23
+ .sort(([a], [b]) => a.localeCompare(b))
24
+ .map(([key, nestedValue]) => [key, normalizeValue(nestedValue)]));
25
+ }
26
+ return value;
27
+ }
28
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,MAAM,CAAC,KAAsB;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;aAClB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CACnE,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { astroOgImages } from "./integration.js";
2
+ export { getOgImage } from "./registry.js";
3
+ export type { GetOgImageOptions, OgImageCompression, OgImageCompressionOptions, OgImageFont, OgImageFontStyle, OgImageFontWeight, OgImageFormat, OgImageIntegrationOptions, OgImagePngCompressionOptions, OgImageResult, OgImageSize, OgImageTemplate, } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,yBAAyB,EACzB,4BAA4B,EAC5B,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { astroOgImages } from "./integration.js";
2
+ export { getOgImage } from "./registry.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { OgImageIntegrationOptions } from "./types.js";
2
+ type AstroLogger = {
3
+ info(message: string): void;
4
+ error(message: string): void;
5
+ };
6
+ type AstroIntegration = {
7
+ name: string;
8
+ hooks: {
9
+ "astro:config:setup": (args: {
10
+ config: {
11
+ root: URL;
12
+ };
13
+ command: "dev" | "build" | "preview" | "sync";
14
+ }) => void;
15
+ "astro:server:setup": (args: {
16
+ server: any;
17
+ logger: AstroLogger;
18
+ }) => void;
19
+ "astro:build:done": (args: {
20
+ dir: URL;
21
+ logger: AstroLogger;
22
+ }) => Promise<void>;
23
+ };
24
+ };
25
+ export declare function astroOgImages(options?: OgImageIntegrationOptions): AstroIntegration;
26
+ export {};
27
+ //# sourceMappingURL=integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QACL,oBAAoB,EAAE,CAAC,IAAI,EAAE;YAC3B,MAAM,EAAE;gBAAE,IAAI,EAAE,GAAG,CAAA;aAAE,CAAC;YACtB,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;SAC/C,KAAK,IAAI,CAAC;QACX,oBAAoB,EAAE,CAAC,IAAI,EAAE;YAAE,MAAM,EAAE,GAAG,CAAC;YAAC,MAAM,EAAE,WAAW,CAAA;SAAE,KAAK,IAAI,CAAC;QAC3E,kBAAkB,EAAE,CAAC,IAAI,EAAE;YAAE,GAAG,EAAE,GAAG,CAAC;YAAC,MAAM,EAAE,WAAW,CAAA;SAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAChF,CAAC;CACH,CAAC;AAEF,wBAAgB,aAAa,CAC3B,OAAO,GAAE,yBAA8B,GACtC,gBAAgB,CAgDlB"}
@@ -0,0 +1,52 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { configureOgImages, flushQueuedOgImagesToDir, getQueuedOgImageCount, isQueuedOgImagePath, renderQueuedOgImage, } from "./registry.js";
3
+ export function astroOgImages(options = {}) {
4
+ return {
5
+ name: "astro-og-images",
6
+ hooks: {
7
+ "astro:config:setup": ({ config, command }) => {
8
+ configureOgImages({
9
+ rootDir: fileURLToPath(config.root),
10
+ outputDir: options.outputDir ?? "og",
11
+ cacheDir: options.cacheDir ?? ".astro/og-images",
12
+ cache: options.cache ?? true,
13
+ compression: options.compression,
14
+ command,
15
+ });
16
+ },
17
+ "astro:server:setup": ({ server, logger }) => {
18
+ server.middlewares.use(async (request, response, next) => {
19
+ if (!request.url)
20
+ return next();
21
+ const pathname = new URL(request.url, "http://localhost").pathname;
22
+ if (!isQueuedOgImagePath(pathname))
23
+ return next();
24
+ try {
25
+ const buffer = await renderQueuedOgImage(pathname);
26
+ if (!buffer)
27
+ return next();
28
+ response.statusCode = 200;
29
+ response.setHeader("Content-Type", "image/png");
30
+ response.setHeader("Cache-Control", "no-cache");
31
+ response.end(buffer);
32
+ }
33
+ catch (error) {
34
+ logger.error(error instanceof Error ? error.message : String(error));
35
+ response.statusCode = 500;
36
+ response.end("Failed to render OG image.");
37
+ }
38
+ });
39
+ },
40
+ "astro:build:done": async ({ dir, logger }) => {
41
+ const queued = getQueuedOgImageCount();
42
+ if (queued === 0) {
43
+ logger.info("No OG images queued.");
44
+ return;
45
+ }
46
+ const written = await flushQueuedOgImagesToDir(dir);
47
+ logger.info(`Generated ${written} OG image${written === 1 ? "" : "s"}.`);
48
+ },
49
+ },
50
+ };
51
+ }
52
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAoBvB,MAAM,UAAU,aAAa,CAC3B,UAAqC,EAAE;IAEvC,OAAO;QACL,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE;YACL,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;gBAC5C,iBAAiB,CAAC;oBAChB,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC;oBACnC,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;oBACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,kBAAkB;oBAChD,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI;oBAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,OAAO;iBACR,CAAC,CAAC;YACL,CAAC;YACD,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;gBAC3C,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,OAAY,EAAE,QAAa,EAAE,IAAgB,EAAE,EAAE;oBAC7E,IAAI,CAAC,OAAO,CAAC,GAAG;wBAAE,OAAO,IAAI,EAAE,CAAC;oBAEhC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,QAAQ,CAAC;oBACnE,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;wBAAE,OAAO,IAAI,EAAE,CAAC;oBAElD,IAAI,CAAC;wBACH,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;wBACnD,IAAI,CAAC,MAAM;4BAAE,OAAO,IAAI,EAAE,CAAC;wBAE3B,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;wBAC1B,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;wBAChD,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;wBAChD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;oBACvB,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,MAAM,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;wBACrE,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;wBAC1B,QAAQ,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;oBAC7C,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC;YACD,kBAAkB,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;gBAC5C,MAAM,MAAM,GAAG,qBAAqB,EAAE,CAAC;gBACvC,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;oBACjB,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;oBACpC,OAAO;gBACT,CAAC;gBAED,MAAM,OAAO,GAAG,MAAM,wBAAwB,CAAC,GAAG,CAAC,CAAC;gBACpD,MAAM,CAAC,IAAI,CAAC,aAAa,OAAO,YAAY,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;YAC3E,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function cleanOutputDir(outputDir: string): string;
2
+ export declare function ensureFormatExtension(path: string, format: string): string;
3
+ export declare function normalizePublicPath(inputPath: string, outputDir: string, format: string): string;
4
+ export declare function resolveFromRoot(pathOrUrl: string | URL, rootDir: string): string;
5
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAQR;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMhF"}
package/dist/paths.js ADDED
@@ -0,0 +1,24 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function cleanOutputDir(outputDir) {
4
+ const cleaned = outputDir.replace(/^\/+|\/+$/g, "");
5
+ return cleaned || "og";
6
+ }
7
+ export function ensureFormatExtension(path, format) {
8
+ return /\.[a-z0-9]+$/i.test(path) ? path : `${path}.${format}`;
9
+ }
10
+ export function normalizePublicPath(inputPath, outputDir, format) {
11
+ const cleanedOutputDir = cleanOutputDir(outputDir);
12
+ const withoutLeadingSlash = inputPath.replace(/^\/+/, "");
13
+ const withOutputDir = withoutLeadingSlash.startsWith(`${cleanedOutputDir}/`)
14
+ ? withoutLeadingSlash
15
+ : `${cleanedOutputDir}/${withoutLeadingSlash}`;
16
+ return `/${ensureFormatExtension(withOutputDir, format)}`;
17
+ }
18
+ export function resolveFromRoot(pathOrUrl, rootDir) {
19
+ if (pathOrUrl instanceof URL) {
20
+ return fileURLToPath(pathOrUrl);
21
+ }
22
+ return isAbsolute(pathOrUrl) ? pathOrUrl : resolve(rootDir, pathOrUrl);
23
+ }
24
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO,OAAO,IAAI,IAAI,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,MAAc;IAChE,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,SAAiB,EACjB,SAAiB,EACjB,MAAc;IAEd,MAAM,gBAAgB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IACnD,MAAM,mBAAmB,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,aAAa,GAAG,mBAAmB,CAAC,UAAU,CAAC,GAAG,gBAAgB,GAAG,CAAC;QAC1E,CAAC,CAAC,mBAAmB;QACrB,CAAC,CAAC,GAAG,gBAAgB,IAAI,mBAAmB,EAAE,CAAC;IAEjD,OAAO,IAAI,qBAAqB,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAuB,EAAE,OAAe;IACtE,IAAI,SAAS,YAAY,GAAG,EAAE,CAAC;QAC7B,OAAO,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { GetOgImageOptions, NormalizedOgImageCompressionOptions, OgImageIntegrationOptions, OgImageResult } from "./types.js";
2
+ interface OgImageRuntime {
3
+ rootDir: string;
4
+ outputDir: string;
5
+ cacheDir: string;
6
+ cache: boolean;
7
+ command: "dev" | "build" | "preview" | "sync";
8
+ compression: false | NormalizedOgImageCompressionOptions;
9
+ }
10
+ export declare function configureOgImages(options: OgImageIntegrationOptions & Partial<Pick<OgImageRuntime, "rootDir" | "command">>): void;
11
+ export declare function getOgImage<Props>(options: GetOgImageOptions<Props>): Promise<OgImageResult>;
12
+ export declare function getQueuedOgImageCount(): number;
13
+ export declare function isQueuedOgImagePath(pathname: string): boolean;
14
+ export declare function renderQueuedOgImage(pathname: string): Promise<Buffer | null>;
15
+ export declare function flushQueuedOgImagesToDir(outputDirUrl: URL): Promise<number>;
16
+ export {};
17
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,iBAAiB,EACjB,mCAAmC,EAEnC,yBAAyB,EAEzB,aAAa,EAEd,MAAM,YAAY,CAAC;AAEpB,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IAC9C,WAAW,EAAE,KAAK,GAAG,mCAAmC,CAAC;CAC1D;AA4BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,yBAAyB,GAChC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC,GACrD,IAAI,CAcN;AAED,wBAAsB,UAAU,CAAC,KAAK,EACpC,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAChC,OAAO,CAAC,aAAa,CAAC,CA0DxB;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKlF;AAED,wBAAsB,wBAAwB,CAAC,YAAY,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAajF"}
@@ -0,0 +1,160 @@
1
+ import { constants } from "node:fs";
2
+ import { access, mkdir, readFile, stat, writeFile, } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { normalizeCompression } from "./compression.js";
6
+ import { sha256, stableStringify } from "./hash.js";
7
+ import { cleanOutputDir, normalizePublicPath, resolveFromRoot } from "./paths.js";
8
+ import { renderOgImageJob } from "./renderer.js";
9
+ const DEFAULT_SIZE = {
10
+ width: 1200,
11
+ height: 630,
12
+ };
13
+ const STORE_KEY = Symbol.for("astro-og-images.store");
14
+ const store = (globalThis[STORE_KEY] ??= {
15
+ runtime: {
16
+ rootDir: process.cwd(),
17
+ outputDir: "og",
18
+ cacheDir: join(process.cwd(), ".astro", "og-images"),
19
+ cache: true,
20
+ command: "build",
21
+ compression: normalizeCompression(undefined),
22
+ },
23
+ jobs: new Map(),
24
+ });
25
+ export function configureOgImages(options) {
26
+ const rootDir = options.rootDir ?? store.runtime.rootDir;
27
+ const outputDir = cleanOutputDir(options.outputDir ?? store.runtime.outputDir);
28
+ store.runtime = {
29
+ rootDir,
30
+ outputDir,
31
+ cacheDir: options.cacheDir
32
+ ? resolveFromRoot(options.cacheDir, rootDir)
33
+ : join(rootDir, ".astro", "og-images"),
34
+ cache: options.cache ?? store.runtime.cache,
35
+ command: options.command ?? store.runtime.command,
36
+ compression: normalizeCompression(options.compression ?? store.runtime.compression),
37
+ };
38
+ }
39
+ export async function getOgImage(options) {
40
+ const format = options.format ?? "png";
41
+ const size = {
42
+ width: options.size?.width ?? DEFAULT_SIZE.width,
43
+ height: options.size?.height ?? DEFAULT_SIZE.height,
44
+ };
45
+ const publicPath = normalizePublicPath(options.path, store.runtime.outputDir, format);
46
+ const signature = stableStringify({
47
+ template: options.template,
48
+ props: options.props,
49
+ size,
50
+ fonts: options.fonts,
51
+ format,
52
+ debug: options.debug ?? false,
53
+ });
54
+ const existing = store.jobs.get(publicPath);
55
+ const job = {
56
+ publicPath,
57
+ template: options.template,
58
+ props: options.props,
59
+ size,
60
+ fonts: options.fonts,
61
+ format,
62
+ debug: options.debug ?? false,
63
+ signature,
64
+ };
65
+ if (existing && existing.signature !== signature) {
66
+ if (store.runtime.command === "dev") {
67
+ store.jobs.set(publicPath, job);
68
+ return {
69
+ src: publicPath,
70
+ width: size.width,
71
+ height: size.height,
72
+ format,
73
+ };
74
+ }
75
+ throw new Error(`Conflicting OG image jobs registered for ${publicPath}. Use a unique path per image.`);
76
+ }
77
+ if (!existing) {
78
+ store.jobs.set(publicPath, job);
79
+ }
80
+ return {
81
+ src: publicPath,
82
+ width: size.width,
83
+ height: size.height,
84
+ format,
85
+ };
86
+ }
87
+ export function getQueuedOgImageCount() {
88
+ return store.jobs.size;
89
+ }
90
+ export function isQueuedOgImagePath(pathname) {
91
+ return store.jobs.has(pathname);
92
+ }
93
+ export async function renderQueuedOgImage(pathname) {
94
+ const job = store.jobs.get(pathname);
95
+ if (!job)
96
+ return null;
97
+ return renderWithCache(job);
98
+ }
99
+ export async function flushQueuedOgImagesToDir(outputDirUrl) {
100
+ const outputRoot = fileURLToPath(outputDirUrl);
101
+ let written = 0;
102
+ for (const job of store.jobs.values()) {
103
+ const buffer = await renderWithCache(job);
104
+ const destination = join(outputRoot, job.publicPath.replace(/^\/+/, ""));
105
+ await mkdir(dirname(destination), { recursive: true });
106
+ await writeFile(destination, buffer);
107
+ written += 1;
108
+ }
109
+ return written;
110
+ }
111
+ async function renderWithCache(job) {
112
+ const hash = await computeJobHash(job);
113
+ const cacheFile = join(store.runtime.cacheDir, `${hash}.${job.format}`);
114
+ if (store.runtime.cache && (await exists(cacheFile))) {
115
+ return readFile(cacheFile);
116
+ }
117
+ const buffer = await renderOgImageJob(job, store.runtime.rootDir, store.runtime.compression);
118
+ if (store.runtime.cache) {
119
+ await mkdir(store.runtime.cacheDir, { recursive: true });
120
+ await writeFile(cacheFile, buffer);
121
+ }
122
+ return buffer;
123
+ }
124
+ async function computeJobHash(job) {
125
+ const fontHashes = await Promise.all(job.fonts.map(async (font) => {
126
+ const fontPath = resolveFromRoot(font.path, store.runtime.rootDir);
127
+ const [contents, metadata] = await Promise.all([
128
+ readFile(fontPath),
129
+ stat(fontPath),
130
+ ]);
131
+ return {
132
+ name: font.name,
133
+ path: fontPath,
134
+ weight: font.weight,
135
+ style: font.style,
136
+ lang: font.lang,
137
+ size: metadata.size,
138
+ hash: sha256(contents),
139
+ };
140
+ }));
141
+ return sha256(stableStringify({
142
+ template: job.template,
143
+ props: job.props,
144
+ size: job.size,
145
+ fonts: fontHashes,
146
+ format: job.format,
147
+ debug: job.debug,
148
+ compression: store.runtime.compression,
149
+ }));
150
+ }
151
+ async function exists(path) {
152
+ try {
153
+ await access(path, constants.F_OK);
154
+ return true;
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { NormalizedOgImageCompressionOptions, OgImageJob } from "./types.js";
2
+ export declare function renderOgImageJob(job: OgImageJob, rootDir: string, compression: false | NormalizedOgImageCompressionOptions): Promise<Buffer>;
3
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mCAAmC,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIlF,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,KAAK,GAAG,mCAAmC,GACvD,OAAO,CAAC,MAAM,CAAC,CAyCjB"}
@@ -0,0 +1,42 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import satori from "satori";
4
+ import { compressPng } from "./compression.js";
5
+ import { resolveFromRoot } from "./paths.js";
6
+ const require = createRequire(import.meta.url);
7
+ export async function renderOgImageJob(job, rootDir, compression) {
8
+ if (job.format !== "png") {
9
+ throw new Error(`Unsupported OG image format: ${job.format}`);
10
+ }
11
+ if (job.fonts.length === 0) {
12
+ throw new Error("At least one local font is required to render an OG image.");
13
+ }
14
+ const fontFiles = job.fonts.map((font) => resolveFromRoot(font.path, rootDir));
15
+ const fonts = await Promise.all(job.fonts.map(async (font, index) => ({
16
+ name: font.name,
17
+ data: await readFile(fontFiles[index]),
18
+ weight: font.weight,
19
+ style: font.style,
20
+ lang: font.lang,
21
+ })));
22
+ const element = await job.template(job.props);
23
+ const satoriOptions = {
24
+ width: job.size.width,
25
+ height: job.size.height,
26
+ fonts,
27
+ debug: job.debug,
28
+ };
29
+ const svg = await satori(element, satoriOptions);
30
+ const { Resvg } = require("@resvg/resvg-js");
31
+ const renderer = new Resvg(svg, {
32
+ fitTo: { mode: "original" },
33
+ font: {
34
+ loadSystemFonts: false,
35
+ fontFiles,
36
+ defaultFontFamily: job.fonts[0]?.name,
37
+ },
38
+ logLevel: "warn",
39
+ });
40
+ return compressPng(renderer.render().asPng(), compression);
41
+ }
42
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAe,EACf,OAAe,EACf,WAAwD;IAExD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAW,MAAM,OAAO,CAAC,GAAG,CACrC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACpC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,MAAM,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC,CAAC,CACJ,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,aAAa,GAAkB;QACnC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK;QACrB,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM;QACvB,KAAK;QACL,KAAK,EAAE,GAAG,CAAC,KAAK;KACjB,CAAC;IAEF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAuC,EAAE,aAAa,CAAC,CAAC;IACjF,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAqC,CAAC;IACjF,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;QAC9B,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC3B,IAAI,EAAE;YACJ,eAAe,EAAE,KAAK;YACtB,SAAS;YACT,iBAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI;SACtC;QACD,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAC;IAEH,OAAO,WAAW,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,65 @@
1
+ export type OgImageFormat = "png";
2
+ export type OgImageFontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
3
+ export type OgImageFontStyle = "normal" | "italic";
4
+ export interface OgImageSize {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface OgImageFont {
9
+ name: string;
10
+ path: string | URL;
11
+ weight?: OgImageFontWeight;
12
+ style?: OgImageFontStyle;
13
+ lang?: string;
14
+ }
15
+ export type OgImageTemplate<Props> = (props: Props) => unknown | Promise<unknown>;
16
+ export interface GetOgImageOptions<Props = Record<string, unknown>> {
17
+ template: OgImageTemplate<Props>;
18
+ props: Props;
19
+ path: string;
20
+ size?: Partial<OgImageSize>;
21
+ fonts: OgImageFont[];
22
+ format?: OgImageFormat;
23
+ debug?: boolean;
24
+ }
25
+ export interface OgImageResult {
26
+ src: string;
27
+ width: number;
28
+ height: number;
29
+ format: OgImageFormat;
30
+ }
31
+ export interface OgImagePngCompressionOptions {
32
+ compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
33
+ palette?: boolean;
34
+ quality?: number;
35
+ effort?: number;
36
+ }
37
+ export interface OgImageCompressionOptions {
38
+ png?: OgImagePngCompressionOptions;
39
+ }
40
+ export type OgImageCompression = boolean | OgImageCompressionOptions;
41
+ export interface OgImageIntegrationOptions {
42
+ outputDir?: string;
43
+ cacheDir?: string;
44
+ cache?: boolean;
45
+ compression?: OgImageCompression;
46
+ }
47
+ export interface OgImageJob<Props = unknown> {
48
+ publicPath: string;
49
+ template: OgImageTemplate<Props>;
50
+ props: Props;
51
+ size: OgImageSize;
52
+ fonts: OgImageFont[];
53
+ format: OgImageFormat;
54
+ debug: boolean;
55
+ signature: string;
56
+ }
57
+ export interface NormalizedOgImageCompressionOptions {
58
+ png: {
59
+ compressionLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
60
+ palette: boolean;
61
+ quality?: number;
62
+ effort?: number;
63
+ };
64
+ }
65
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ export function Card({ title }: { title: string }) {
2
+ return (
3
+ <div
4
+ style={{
5
+ width: "100%",
6
+ height: "100%",
7
+ display: "flex",
8
+ alignItems: "center",
9
+ padding: 80,
10
+ background: "#101014",
11
+ color: "white",
12
+ fontFamily: "Inter",
13
+ fontSize: 72,
14
+ fontWeight: 700,
15
+ }}
16
+ >
17
+ {title}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { getOgImage } from "astro-og-images";
3
+ import { Card } from "./card";
4
+
5
+ const image = await getOgImage({
6
+ template: Card,
7
+ props: { title: "Hello OG" },
8
+ path: "hello",
9
+ fonts: [{ name: "Inter", path: "./public/fonts/Inter-Bold.ttf", weight: 700 }],
10
+ });
11
+ ---
12
+
13
+ <meta property="og:image" content={new URL(image.src, Astro.site)} />
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "astro-og-images",
3
+ "version": "0.1.0",
4
+ "description": "Generate static Open Graph images from code-owned templates during Astro builds.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Francesco Di Donato",
8
+ "homepage": "https://github.com/didof/astro-og-images#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/didof/astro-og-images.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/didof/astro-og-images/issues"
15
+ },
16
+ "keywords": [
17
+ "astro",
18
+ "open-graph",
19
+ "og-image",
20
+ "social-image",
21
+ "satori",
22
+ "resvg"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.17"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src/OgImage.astro",
30
+ "examples",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./component": "./src/OgImage.astro",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "types": "./dist/index.d.ts",
43
+ "sideEffects": false,
44
+ "dependencies": {
45
+ "@resvg/resvg-js": "^2.6.2",
46
+ "satori": "^0.26.0"
47
+ },
48
+ "optionalDependencies": {
49
+ "sharp": "^0.34.2"
50
+ },
51
+ "peerDependencies": {
52
+ "astro": ">=4.0.0 <6.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "25.6.0",
56
+ "@types/react": "^19.2.14",
57
+ "typescript": "^5.9.3"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.json"
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ import { getOgImage, type GetOgImageOptions } from "astro-og-images";
3
+
4
+ interface Props extends GetOgImageOptions<Record<string, unknown>> {
5
+ alt?: string;
6
+ class?: string;
7
+ loading?: "eager" | "lazy";
8
+ decoding?: "async" | "auto" | "sync";
9
+ }
10
+
11
+ const {
12
+ alt = "",
13
+ class: className,
14
+ loading = "lazy",
15
+ decoding = "async",
16
+ ...options
17
+ } = Astro.props;
18
+
19
+ const image = await getOgImage(options);
20
+ ---
21
+
22
+ <img
23
+ src={image.src}
24
+ width={image.width}
25
+ height={image.height}
26
+ alt={alt}
27
+ class={className}
28
+ loading={loading}
29
+ decoding={decoding}
30
+ />