astro-opengraph-images 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +5 -156
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +2 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/extract.d.ts +4 -0
  8. package/dist/extract.d.ts.map +1 -0
  9. package/dist/extract.js +23 -0
  10. package/dist/extract.js.map +1 -0
  11. package/dist/hook.d.ts +6 -0
  12. package/dist/hook.d.ts.map +1 -0
  13. package/dist/hook.js +39 -0
  14. package/dist/hook.js.map +1 -0
  15. package/dist/index.d.ts +5 -20
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -46
  18. package/dist/index.js.map +1 -1
  19. package/dist/integration.d.ts +4 -0
  20. package/dist/integration.d.ts.map +1 -0
  21. package/dist/integration.js +21 -0
  22. package/dist/integration.js.map +1 -0
  23. package/dist/presets/blackAndWhite.d.ts +4 -0
  24. package/dist/presets/blackAndWhite.d.ts.map +1 -0
  25. package/dist/presets/blackAndWhite.jsx +26 -0
  26. package/dist/presets/blackAndWhite.jsx.map +1 -0
  27. package/dist/presets/index.d.ts +5 -0
  28. package/dist/presets/index.d.ts.map +1 -0
  29. package/dist/presets/index.js +5 -0
  30. package/dist/presets/index.js.map +1 -0
  31. package/dist/types.d.ts +42 -9
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/util.d.ts +5 -0
  34. package/dist/util.d.ts.map +1 -0
  35. package/dist/util.js +18 -0
  36. package/dist/util.js.map +1 -0
  37. package/package.json +19 -9
  38. package/src/constants.ts +1 -0
  39. package/src/extract.ts +25 -0
  40. package/src/hook.ts +63 -0
  41. package/src/index.ts +5 -83
  42. package/src/integration.ts +25 -0
  43. package/src/presets/blackAndWhite.tsx +35 -0
  44. package/src/presets/index.ts +5 -0
  45. package/src/types.ts +47 -10
  46. package/src/util.ts +18 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.1](https://github.com/shepherdjerred/astro-opengraph-images/compare/v1.2.0...v1.2.1) (2024-06-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * resolve merge conflict ([ba0c596](https://github.com/shepherdjerred/astro-opengraph-images/commit/ba0c596b3f10a95d0cbe67bf9eef51b552fc7afe))
9
+
10
+ ## [1.2.0](https://github.com/shepherdjerred/astro-opengraph-images/compare/v1.1.0...v1.2.0) (2024-06-29)
11
+
12
+
13
+ ### Features
14
+
15
+ * simplify ([d82ab03](https://github.com/shepherdjerred/astro-opengraph-images/commit/d82ab031aa971437a9b02999be52c04a8a9e089c))
16
+ * simplify ([0e1e7c6](https://github.com/shepherdjerred/astro-opengraph-images/commit/0e1e7c6b1d4effa4fb0caa5fcf676168d3b03dd2))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * fix json ([0799f5f](https://github.com/shepherdjerred/astro-opengraph-images/commit/0799f5f055d12a3cabd9cc72dd30c9cef9219b84))
22
+
23
+ ## [1.1.0](https://github.com/shepherdjerred/astro-opengraph-images/compare/v1.0.0...v1.1.0) (2024-06-29)
24
+
25
+
26
+ ### Features
27
+
28
+ * simplify setup ([08a922e](https://github.com/shepherdjerred/astro-opengraph-images/commit/08a922e7d096930e3af40865d34ac18c480384fa))
29
+
3
30
  ## 1.0.0 (2024-06-28)
4
31
 
5
32
 
package/README.md CHANGED
@@ -1,140 +1,17 @@
1
1
  # Astro OpenGraph Images
2
2
 
3
- This is an [Astro](https://astro.build/) integration that generates images for [OpenGraph](https://ogp.me/) using [Satori](https://github.com/vercel/satori) and [resvg-js](https://github.com/yisibl/resvg-js).
3
+ [![astro-opengraph-images](https://img.shields.io/npm/v/astro-opengraph-images.svg)](https://www.npmjs.com/package/astro-opengraph-images)
4
4
 
5
- OpenGraph images are the link preview you see when linking a site to Slack, Discord, Twitter, Facebook, etc.
5
+ Generate fully customizable OpenGraph images with a few lines of code.
6
6
 
7
- ## Usage
7
+ ## Quick Start
8
8
 
9
- 1. Install this package:
9
+ 1. Add this integration:
10
10
 
11
11
  ```bash
12
- npm i astro-satori
12
+ npm i astro-opengraph-images
13
13
  ```
14
14
 
15
- 1. If you want to use React syntax (recommended), install `@types/react`:
16
-
17
- ```bash
18
- npm i -D @types/react
19
- ```
20
-
21
- 1. Define a render function:
22
-
23
- - Satori supports a [limited subset of CSS](https://github.com/vercel/satori?tab=readme-ov-file#css).
24
- - You can use the [Satori playground](https://og-playground.vercel.app/) to iterate on your render function
25
-
26
- - Using React:
27
-
28
- Create a `.tsx` file to define your render function. Note: Astro does not support `.tsx` files for a static file endpoint, so you must define the render function in a separate `.tsx` file.
29
-
30
- ```tsx
31
- import type { APIContext } from "astro";
32
- import type { ReactNode } from "react";
33
- import React from "react";
34
-
35
- export const render = ({ params, request }: APIContext): ReactNode => {
36
- const title = params.slug;
37
- const type = "blog";
38
-
39
- return (
40
- <div
41
- style={{
42
- height: "100%",
43
- width: "100%",
44
- display: "flex",
45
- flexDirection: "column",
46
- backgroundColor: "#000",
47
- padding: "55px 70px",
48
- color: "#fff",
49
- fontFamily: "Atkinson",
50
- fontSize: 72,
51
- }}
52
- >
53
- <div
54
- style={{
55
- marginTop: 96,
56
- }}
57
- >
58
- {title}
59
- </div>
60
- <div
61
- style={{
62
- fontSize: 40,
63
- }}
64
- >
65
- {type === "blog" ? "by Jerred Shepherd" : ""}
66
- </div>
67
- </div>
68
- );
69
- };
70
- ```
71
-
72
- - Using vanilla JavaScript:
73
-
74
- ```typescript
75
- export function render() ({
76
- type: "div",
77
- props: {
78
- style: {
79
- height: "100%",
80
- width: "100%",
81
- display: "flex",
82
- flexDirection: "column",
83
- backgroundColor: "#000",
84
- padding: "55px 70px",
85
- color: "#fff",
86
- fontFamily: "CommitMono",
87
- fontSize: 72,
88
- },
89
- children: [
90
- {
91
- type: "div",
92
- props: {
93
- style: {
94
- marginTop: 96,
95
- },
96
- children: title,
97
- },
98
- },
99
- {
100
- type: "div",
101
- props: {
102
- style: {
103
- fontSize: 40,
104
- },
105
- children: type === "blog" ? "by Jerred Shepherd" : "",
106
- },
107
- },
108
- ],
109
- },
110
- });
111
- ```
112
-
113
- 1. Add this integration to your Astro config
114
-
115
- ```typescript
116
- import satoriOpenGraph from "satro-satori";
117
-
118
- // You'll need to provide _every_ font that you use with Satori. Satori does not have any fonts by default.
119
- const commitMono = fs.readFileSync("public/fonts/CommitMono/CommitMono-450-Regular.otf");
120
-
121
- // https://astro.build/config
122
- export default defineConfig({
123
- integrations: [satoriOpenGraph(options: {
124
- fonts: [
125
- {
126
- name: "Commit Mono",
127
- data: commitMono,
128
- weight: 400,
129
- style: "normal",
130
- },
131
- ],
132
- }, render)],
133
- });
134
- ```
135
-
136
- The integration will generate a `openGraph.png` file next to every `.html` in your Astro site.
137
-
138
15
  1. Update your layout to add the appropriate `meta` tags. The [OpenGraph site](https://ogp.me/) has more information about valid tags. At a minimum, you should define the tags below.
139
16
 
140
17
  ```astro
@@ -150,13 +27,6 @@ const { url } = Astro;
150
27
 
151
28
  1. Confirm that your OpenGraph images are accessible. After you deploy these changes, navigate to [OpenGraph.xyz](https://www.opengraph.xyz/) and test your site.
152
29
 
153
- ## Resources
154
-
155
- I consulted these resources while building this library.
156
-
157
- - https://dietcode.io/p/astro-og/
158
- - https://github.com/sdnts/dietcode/blob/914e3970f6a0f555113768b12db3229dd822e6f1/astro.config.ts#L55
159
-
160
30
  ## Alternatives
161
31
 
162
32
  Here are some similar libraries using Satori and Astro. I haven't done a feature comparison.
@@ -168,24 +38,3 @@ Here are some similar libraries using Satori and Astro. I haven't done a feature
168
38
  - https://github.com/cijiugechu/astro-satori (Possibly dead, hasn't been updated in a year)
169
39
  - https://github.com/kevinzunigacuellar/astro-satori (Possibly dead, hasn't been updated in a year)
170
40
  - https://github.com/rumaan/astro-vercel-og (Possibly dead, hasn't been updated in a year)
171
-
172
- ## Related
173
-
174
- I didn't consult these resources, but they're relevant if you wanted to build your own version of this library.
175
-
176
- - https://www.knaap.dev/posts/dynamic-og-images-with-any-static-site-generator/
177
- - https://blog.otterlord.dev/posts/dynamic-opengraph/
178
- - https://egghead.io/lessons/astro-implement-dynamic-og-image-generation-with-astro-api-routes-and-satori
179
- - https://arne.me/blog/static-og-images-in-astro/
180
- - https://jafaraziz.com/blog/generate-open-graph-images-with-astro-and-satori/
181
- - https://rumaan.dev/blog/open-graph-images-using-satori
182
- - https://www.alperdogan.dev/blog/og-image-with-satori-and-astro/
183
- - https://techsquidtv.com/blog/generating-open-graph-images-for-astro/
184
- - https://blog.vhng.dev/posts/20230901-dynamically-generate-og-image
185
- - https://www.kozhuhds.com/blog/generating-static-open-graph-og-images-in-astro-using-vercel-og/
186
- - https://arne.me/blog/static-og-images-in-astro
187
- - https://aidankinzett.com/blog/astro-open-graph-image/
188
- - https://dev.to/jxd-dev/open-graph-image-generation-with-astro-gnp
189
- - https://www.omar45.com/blog/dynamic-og-images-with-astro
190
- - https://www.jafaraziz.com/blog/generate-open-graph-images-with-astro-and-satori/
191
- - https://www.merlinmason.co.uk/blog/generate-open-graph-images-with-astro
@@ -0,0 +1,2 @@
1
+ export declare const placeholder = "[[OPENGRAPH IMAGE]]";
2
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,wBAAwB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const placeholder = `[[OPENGRAPH IMAGE]]`;
2
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PageDetails } from "./types.js";
2
+ export declare function sanitizeHtml(html: string): string;
3
+ export declare function extract(html: string): PageDetails;
4
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAYjD"}
@@ -0,0 +1,23 @@
1
+ import * as jsdom from "jsdom";
2
+ // Astro CSS parsing fails: Error: Could not parse CSS stylesheet
3
+ // Remove CSS from the HTML
4
+ // https://github.com/jsdom/jsdom/issues/2005#issuecomment-1758940894
5
+ export function sanitizeHtml(html) {
6
+ return html
7
+ .replace(/<style([\S\s]*?)>([\S\s]*?)<\/style>/gim, "")
8
+ .replace(/<script([\S\s]*?)>([\S\s]*?)<\/script>/gim, "");
9
+ }
10
+ export function extract(html) {
11
+ const htmlDoc = new jsdom.JSDOM(sanitizeHtml(html)).window.document;
12
+ const title = htmlDoc.title;
13
+ const description = htmlDoc.querySelector("meta[name=description]")?.getAttribute("content");
14
+ const returnVal = {};
15
+ if (title) {
16
+ returnVal.title = title;
17
+ }
18
+ if (description) {
19
+ returnVal.description = description;
20
+ }
21
+ return returnVal;
22
+ }
23
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,iEAAiE;AACjE,2BAA2B;AAC3B,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI;SACR,OAAO,CAAC,yCAAyC,EAAE,EAAE,CAAC;SACtD,OAAO,CAAC,2CAA2C,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;IACpE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,MAAM,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,wBAAwB,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAC7F,MAAM,SAAS,GAAgB,EAAE,CAAC;IAClC,IAAI,KAAK,EAAE,CAAC;QACV,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC;IAC1B,CAAC;IACD,IAAI,WAAW,EAAE,CAAC;QAChB,SAAS,CAAC,WAAW,GAAG,WAAW,CAAC;IACtC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/dist/hook.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { AstroBuildDoneHookInput, IntegrationOptions, RenderFunction } from "./types.js";
2
+ export declare function buildDoneHook({ logger, pages, options, dir, render, }: AstroBuildDoneHookInput & {
3
+ options: IntegrationOptions;
4
+ render: RenderFunction;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAQ,cAAc,EAAE,MAAM,YAAY,CAAC;AAOpG,wBAAsB,aAAa,CAAC,EAClC,MAAM,EACN,KAAK,EACL,OAAO,EACP,GAAG,EACH,MAAM,GACP,EAAE,uBAAuB,GAAG;IAC3B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,cAAc,CAAC;CACxB,iBASA"}
package/dist/hook.js ADDED
@@ -0,0 +1,39 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+ import satori from "satori";
3
+ import * as fs from "fs";
4
+ import { extract } from "./extract.js";
5
+ import { getFilePath } from "./util.js";
6
+ import { placeholder } from "./constants.js";
7
+ export async function buildDoneHook({ logger, pages, options, dir, render, }) {
8
+ logger.info("Generating OpenGraph images");
9
+ for (const page of pages) {
10
+ try {
11
+ await handlePage({ page, options, render, dir, logger });
12
+ }
13
+ catch (e) {
14
+ logger.error(e);
15
+ }
16
+ }
17
+ }
18
+ async function handlePage({ page, options, render, dir, logger }) {
19
+ const file = getFilePath({ dir: dir.pathname, page: page.pathname });
20
+ const html = fs.readFileSync(file).toString();
21
+ const data = extract(html);
22
+ const svg = await satori(render({ ...page, ...data }), options);
23
+ const resvg = new Resvg(svg, {
24
+ fitTo: {
25
+ mode: "width",
26
+ value: options.width,
27
+ },
28
+ });
29
+ const target = file.replace(/\.html$/, ".png");
30
+ fs.writeFileSync(target, resvg.render().asPng());
31
+ // remove local filesystem pathname
32
+ let sitePath = target.replace(process.cwd(), "");
33
+ // remove leading dist/ from the path
34
+ sitePath = sitePath.replace("/dist/", "");
35
+ const content = fs.readFileSync(file).toString();
36
+ fs.writeFileSync(file, content.replace(placeholder, sitePath));
37
+ logger.info(`Generated ${sitePath} for ${page.pathname}`);
38
+ }
39
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAClC,MAAM,EACN,KAAK,EACL,OAAO,EACP,GAAG,EACH,MAAM,GAIP;IACC,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,CAAC,CAAW,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;AACH,CAAC;AAUD,KAAK,UAAU,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAmB;IAC/E,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;QAC3B,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB;KACF,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC/C,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;IAEjD,mCAAmC;IACnC,IAAI,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IACjD,qCAAqC;IACrC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE/D,MAAM,CAAC,IAAI,CAAC,aAAa,QAAQ,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC5D,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,21 +1,6 @@
1
- import type { APIContext, AstroIntegration } from "astro";
2
- import { type SatoriOptions } from "satori";
3
- import type { RenderFunction, RenderRouteFunction } from "./types.js";
4
- export type DefaultOptions = SatoriOptions & {
5
- width?: number;
6
- height?: number;
7
- };
8
- export type Options = DefaultOptions & {
9
- width: number;
10
- height: number;
11
- };
12
- export default function satoriOpenGraph({ options, render, }: {
13
- options: DefaultOptions;
14
- render: RenderFunction;
15
- }): AstroIntegration;
16
- export declare function handleRoute({ context, options, render, }: {
17
- context: APIContext;
18
- options: Options;
19
- render: RenderRouteFunction;
20
- }): Promise<Buffer>;
1
+ import { astroOpenGraphImages } from "./integration.js";
2
+ export { placeholder } from "./constants.js";
3
+ export * from "./presets/index.js";
4
+ export * from "./types.js";
5
+ export default astroOpenGraphImages;
21
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAG1D,OAAe,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,EAAQ,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAO5E,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,OAAO,EACP,MAAM,GACP,EAAE;IACD,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACxB,GAAG,gBAAgB,CAiBnB;AAuBD,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,OAAO,EACP,MAAM,GACP,EAAE;IACD,OAAO,EAAE,UAAU,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;CAC7B,mBASA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAE3B,eAAe,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,47 +1,6 @@
1
- import * as fs from "fs";
2
- import { Resvg } from "@resvg/resvg-js";
3
- import satori from "satori";
4
- const defaults = {
5
- width: 1200,
6
- height: 630,
7
- };
8
- export default function satoriOpenGraph({ options, render, }) {
9
- const optionsWithDefaults = { ...defaults, ...options };
10
- return {
11
- name: "astro-satori",
12
- hooks: {
13
- "astro:build:done": async (entry) => {
14
- try {
15
- for (const page of entry.pages) {
16
- await handlePage({ page, options: optionsWithDefaults, render, dir: entry.dir });
17
- entry.logger.info(`Generated OpenGraph image for ${page.pathname}`);
18
- }
19
- }
20
- catch (e) {
21
- entry.logger.error(e);
22
- }
23
- },
24
- },
25
- };
26
- }
27
- async function handlePage({ page, options, render, dir, }) {
28
- const svg = await satori(render(page), options);
29
- const resvg = new Resvg(svg, {
30
- fitTo: {
31
- mode: "width",
32
- value: options.width,
33
- },
34
- });
35
- fs.writeFileSync(`${dir.pathname}${page.pathname}openGraph.png`, resvg.render().asPng());
36
- }
37
- export async function handleRoute({ context, options, render, }) {
38
- const svg = await satori(render(context), options);
39
- const resvg = new Resvg(svg, {
40
- fitTo: {
41
- mode: "width",
42
- value: options.width,
43
- },
44
- });
45
- return resvg.render().asPng();
46
- }
1
+ import { astroOpenGraphImages } from "./integration.js";
2
+ export { placeholder } from "./constants.js";
3
+ export * from "./presets/index.js";
4
+ export * from "./types.js";
5
+ export default astroOpenGraphImages;
47
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,MAA8B,MAAM,QAAQ,CAAC;AAGpD,MAAM,QAAQ,GAAG;IACf,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,GAAG;CACZ,CAAC;AAYF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,OAAO,EACP,MAAM,GAIP;IACC,MAAM,mBAAmB,GAAY,EAAE,GAAG,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;IACjE,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE;YACL,kBAAkB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAClC,IAAI,CAAC;oBACH,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;wBAC/B,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;wBACjF,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;oBACtE,CAAC;gBACH,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAW,CAAC,CAAC;gBAClC,CAAC;YACH,CAAC;SACF;KACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,EACxB,IAAI,EACJ,OAAO,EACP,MAAM,EACN,GAAG,GAMJ;IACC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;QAC3B,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB;KACF,CAAC,CAAC;IACH,EAAE,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,eAAe,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,OAAO,EACP,OAAO,EACP,MAAM,GAKP;IACC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;QAC3B,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB;KACF,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;AAChC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAE3B,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { AstroIntegration } from "astro";
2
+ import type { IntegrationInput } from "./types.js";
3
+ export declare function astroOpenGraphImages({ options, render }: IntegrationInput): AstroIntegration;
4
+ //# sourceMappingURL=integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,YAAY,CAAC;AAQvE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAAG,gBAAgB,CAe5F"}
@@ -0,0 +1,21 @@
1
+ import { buildDoneHook } from "./hook.js";
2
+ const defaults = {
3
+ width: 1200,
4
+ height: 630,
5
+ };
6
+ export function astroOpenGraphImages({ options, render }) {
7
+ const optionsWithDefaults = { ...defaults, ...options };
8
+ return {
9
+ name: "astro-opengraph-images",
10
+ hooks: {
11
+ "astro:build:done": async (entry) => {
12
+ await buildDoneHook({
13
+ ...entry,
14
+ options: optionsWithDefaults,
15
+ render,
16
+ });
17
+ },
18
+ },
19
+ };
20
+ }
21
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,QAAQ,GAAG;IACf,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,GAAG;CACZ,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAoB;IACxE,MAAM,mBAAmB,GAAuB,EAAE,GAAG,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;IAE5E,OAAO;QACL,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE;YACL,kBAAkB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAClC,MAAM,aAAa,CAAC;oBAClB,GAAG,KAAK;oBACR,OAAO,EAAE,mBAAmB;oBAC5B,MAAM;iBACP,CAAC,CAAC;YACL,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import type { RenderFunctionInput } from "../types.js";
3
+ export declare function blackAndWhite({ title, description }: RenderFunctionInput): React.ReactNode;
4
+ //# sourceMappingURL=blackAndWhite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blackAndWhite.d.ts","sourceRoot":"","sources":["../../src/presets/blackAndWhite.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,mBAAmB,GAAG,KAAK,CAAC,SAAS,CA+B1F"}
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ export function blackAndWhite({ title, description }) {
3
+ return (<div style={{
4
+ height: "100%",
5
+ width: "100%",
6
+ display: "flex",
7
+ flexDirection: "column",
8
+ backgroundColor: "#000",
9
+ padding: "55px 70px",
10
+ color: "#fff",
11
+ fontFamily: "Commit Mono",
12
+ fontSize: 72,
13
+ }}>
14
+ <div style={{
15
+ marginTop: 96,
16
+ }}>
17
+ {title}
18
+ </div>
19
+ <div style={{
20
+ fontSize: 40,
21
+ }}>
22
+ {description ?? ""}
23
+ </div>
24
+ </div>);
25
+ }
26
+ //# sourceMappingURL=blackAndWhite.jsx.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blackAndWhite.jsx","sourceRoot":"","sources":["../../src/presets/blackAndWhite.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,WAAW,EAAuB;IACvE,OAAO,CACL,CAAC,GAAG,CACF,KAAK,CAAC,CAAC;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,MAAM;YACf,aAAa,EAAE,QAAQ;YACvB,eAAe,EAAE,MAAM;YACvB,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,MAAM;YACb,UAAU,EAAE,aAAa;YACzB,QAAQ,EAAE,EAAE;SACb,CAAC,CAEF;MAAA,CAAC,GAAG,CACF,KAAK,CAAC,CAAC;YACL,SAAS,EAAE,EAAE;SACd,CAAC,CAEF;QAAA,CAAC,KAAK,CACR;MAAA,EAAE,GAAG,CACL;MAAA,CAAC,GAAG,CACF,KAAK,CAAC,CAAC;YACL,QAAQ,EAAE,EAAE;SACb,CAAC,CAEF;QAAA,CAAC,WAAW,IAAI,EAAE,CACpB;MAAA,EAAE,GAAG,CACP;IAAA,EAAE,GAAG,CAAC,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { blackAndWhite } from "./blackAndWhite.jsx";
2
+ export declare const presets: {
3
+ blackAndWhite: typeof blackAndWhite;
4
+ };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/presets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,eAAO,MAAM,OAAO;;CAEnB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { blackAndWhite } from "./blackAndWhite.jsx";
2
+ export const presets = {
3
+ blackAndWhite: blackAndWhite,
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/presets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,aAAa,EAAE,aAAa;CAC7B,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,22 +1,55 @@
1
- import type { APIContext } from "astro";
1
+ import type { AstroIntegrationLogger, RouteData } from "astro";
2
2
  import type { ReactNode } from "react";
3
+ export interface IntegrationInput {
4
+ options: DefaultIntegrationOptions;
5
+ render: RenderFunction;
6
+ }
7
+ export type DefaultIntegrationOptions = SatoriOptions & {
8
+ width?: number;
9
+ height?: number;
10
+ };
11
+ export type IntegrationOptions = DefaultIntegrationOptions & {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ /** This is the page data passed in by Astro */
3
16
  export interface Page {
4
17
  pathname: string;
5
18
  }
6
- export type RenderFunction = (page: Page) => ReactNode;
7
- export type RenderRouteFunction = (context: APIContext) => ReactNode;
8
- export type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
9
- export type FontStyle = "normal" | "italic";
10
- export interface FontOptions {
19
+ /** The input Astro passes to the build done hook */
20
+ export interface AstroBuildDoneHookInput {
21
+ logger: AstroIntegrationLogger;
22
+ pages: {
23
+ pathname: string;
24
+ }[];
25
+ dir: URL;
26
+ routes: RouteData[];
27
+ cacheManifest: boolean;
28
+ }
29
+ /** The input arguments to a `RenderFunction` */
30
+ export type RenderFunctionInput = {
31
+ pathname: string;
32
+ } & PageDetails;
33
+ /** A function that renders some page input to React */
34
+ export type RenderFunction = (input: RenderFunctionInput) => ReactNode;
35
+ /** Basic information about a page */
36
+ export interface PageDetails {
37
+ title?: string;
38
+ description?: string;
39
+ }
40
+ /** Types copied from Satori */
41
+ export type SatoriWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
42
+ export type SatoriFontStyle = "normal" | "italic";
43
+ export interface SatoriFontOptions {
11
44
  data: Buffer | ArrayBuffer;
12
45
  name: string;
13
- weight?: Weight;
14
- style?: FontStyle;
46
+ weight?: SatoriWeight;
47
+ style?: SatoriFontStyle;
15
48
  lang?: string;
16
49
  }
17
50
  export interface SatoriOptions {
18
51
  width: number;
19
52
  height: number;
20
- fonts: FontOptions[];
53
+ fonts: SatoriFontOptions[];
21
54
  }
22
55
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC;AAEvD,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,UAAU,KAAK,SAAS,CAAC;AAGrE,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACzE,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC5C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,yBAAyB,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,MAAM,yBAAyB,GAAG,aAAa,GAAG;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,yBAAyB,GAAG;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,+CAA+C;AAC/C,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;KAClB,EAAE,CAAC;IACJ,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,WAAW,CAAC;AAEhB,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,mBAAmB,KAAK,SAAS,CAAC;AAEvE,qCAAqC;AACrC,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,+BAA+B;AAC/B,MAAM,MAAM,YAAY,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAC/E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAClD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function getFilePath({ dir, page }: {
2
+ dir: string;
3
+ page: string;
4
+ }): string;
5
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,UAWvE"}
package/dist/util.js ADDED
@@ -0,0 +1,18 @@
1
+ import * as fs from "fs";
2
+ // some files, e.g. index or 404 pages, are served without a folder
3
+ // other files, e.g. blog posts, are served from a folder
4
+ // I don't fully understand how Astro decides this, so:
5
+ // Check if `page.pathname` is a directory on disk
6
+ export function getFilePath({ dir, page }) {
7
+ let target;
8
+ if (fs.existsSync(`${dir}${page}`)) {
9
+ target = `${dir}${page}index.html`;
10
+ }
11
+ else {
12
+ target = `${dir}${page}`;
13
+ target = target.slice(0, -1);
14
+ target = target + ".html";
15
+ }
16
+ return target;
17
+ }
18
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,mEAAmE;AACnE,yDAAyD;AACzD,uDAAuD;AACvD,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,EAAE,GAAG,EAAE,IAAI,EAAiC;IACtE,IAAI,MAAc,CAAC;IACnB,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,YAAY,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;QACzB,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC5B,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "astro-opengraph-images",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.2.1",
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
7
  "lint": "eslint src",
@@ -11,27 +11,37 @@
11
11
  },
12
12
  "main": "dist/index.js",
13
13
  "types": "dist/index.d.ts",
14
+ "keywords": [
15
+ "astro-integration",
16
+ "withastro",
17
+ "seo",
18
+ "ui",
19
+ "renderer"
20
+ ],
14
21
  "dependencies": {
15
22
  "@resvg/resvg-js": "^2.6.2",
23
+ "jsdom": "^24.1.0",
24
+ "react": "^18.3.1",
16
25
  "satori": "^0.10.13"
17
26
  },
18
27
  "devDependencies": {
19
- "astro": "^4.11.3",
20
28
  "@commitlint/cli": "^19.3.0",
21
29
  "@commitlint/config-conventional": "^19.2.2",
22
- "@eslint/js": "^9.4.0",
30
+ "@eslint/js": "^9.6.0",
23
31
  "@tsconfig/node20": "^20.1.4",
24
32
  "@tsconfig/strictest": "^2.0.5",
33
+ "@types/jsdom": "^21.1.7",
25
34
  "@types/node": "^20.14.9",
26
35
  "@types/react": "^18.3.3",
27
- "@typescript-eslint/eslint-plugin": "^7.11.0",
28
- "@typescript-eslint/parser": "^7.11.0",
36
+ "astro": "^4.11.3",
37
+ "@typescript-eslint/eslint-plugin": "^7.14.1",
38
+ "@typescript-eslint/parser": "^7.14.1",
29
39
  "eslint": "^8.57.0",
30
40
  "husky": "^9.0.11",
31
- "lint-staged": "^15.2.5",
32
- "prettier": "^3.3.0",
33
- "typescript": "^5.4.5",
34
- "typescript-eslint": "^7.11.0",
41
+ "lint-staged": "^15.2.7",
42
+ "prettier": "^3.3.2",
43
+ "typescript": "^5.5.2",
44
+ "typescript-eslint": "^7.14.1",
35
45
  "vitest": "^1.6.0"
36
46
  },
37
47
  "lint-staged": {
@@ -0,0 +1 @@
1
+ export const placeholder = `[[OPENGRAPH IMAGE]]`;
package/src/extract.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { PageDetails } from "./types.js";
2
+ import * as jsdom from "jsdom";
3
+
4
+ // Astro CSS parsing fails: Error: Could not parse CSS stylesheet
5
+ // Remove CSS from the HTML
6
+ // https://github.com/jsdom/jsdom/issues/2005#issuecomment-1758940894
7
+ export function sanitizeHtml(html: string): string {
8
+ return html
9
+ .replace(/<style([\S\s]*?)>([\S\s]*?)<\/style>/gim, "")
10
+ .replace(/<script([\S\s]*?)>([\S\s]*?)<\/script>/gim, "");
11
+ }
12
+
13
+ export function extract(html: string): PageDetails {
14
+ const htmlDoc = new jsdom.JSDOM(sanitizeHtml(html)).window.document;
15
+ const title = htmlDoc.title;
16
+ const description = htmlDoc.querySelector("meta[name=description]")?.getAttribute("content");
17
+ const returnVal: PageDetails = {};
18
+ if (title) {
19
+ returnVal.title = title;
20
+ }
21
+ if (description) {
22
+ returnVal.description = description;
23
+ }
24
+ return returnVal;
25
+ }
package/src/hook.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+ import satori from "satori";
3
+ import type { AstroBuildDoneHookInput, IntegrationOptions, Page, RenderFunction } from "./types.js";
4
+ import * as fs from "fs";
5
+ import type { AstroIntegrationLogger } from "astro";
6
+ import { extract } from "./extract.js";
7
+ import { getFilePath } from "./util.js";
8
+ import { placeholder } from "./constants.js";
9
+
10
+ export async function buildDoneHook({
11
+ logger,
12
+ pages,
13
+ options,
14
+ dir,
15
+ render,
16
+ }: AstroBuildDoneHookInput & {
17
+ options: IntegrationOptions;
18
+ render: RenderFunction;
19
+ }) {
20
+ logger.info("Generating OpenGraph images");
21
+ for (const page of pages) {
22
+ try {
23
+ await handlePage({ page, options, render, dir, logger });
24
+ } catch (e) {
25
+ logger.error(e as string);
26
+ }
27
+ }
28
+ }
29
+
30
+ interface HandlePageInput {
31
+ page: Page;
32
+ options: IntegrationOptions;
33
+ render: RenderFunction;
34
+ dir: URL;
35
+ logger: AstroIntegrationLogger;
36
+ }
37
+
38
+ async function handlePage({ page, options, render, dir, logger }: HandlePageInput) {
39
+ const file = getFilePath({ dir: dir.pathname, page: page.pathname });
40
+ const html = fs.readFileSync(file).toString();
41
+ const data = extract(html);
42
+
43
+ const svg = await satori(render({ ...page, ...data }), options);
44
+ const resvg = new Resvg(svg, {
45
+ fitTo: {
46
+ mode: "width",
47
+ value: options.width,
48
+ },
49
+ });
50
+
51
+ const target = file.replace(/\.html$/, ".png");
52
+ fs.writeFileSync(target, resvg.render().asPng());
53
+
54
+ // remove local filesystem pathname
55
+ let sitePath = target.replace(process.cwd(), "");
56
+ // remove leading dist/ from the path
57
+ sitePath = sitePath.replace("/dist/", "");
58
+
59
+ const content = fs.readFileSync(file).toString();
60
+ fs.writeFileSync(file, content.replace(placeholder, sitePath));
61
+
62
+ logger.info(`Generated ${sitePath} for ${page.pathname}`);
63
+ }
package/src/index.ts CHANGED
@@ -1,85 +1,7 @@
1
- import type { APIContext, AstroIntegration } from "astro";
2
- import * as fs from "fs";
3
- import { Resvg } from "@resvg/resvg-js";
4
- import satori, { type SatoriOptions } from "satori";
5
- import type { Page, RenderFunction, RenderRouteFunction } from "./types.js";
1
+ import { astroOpenGraphImages } from "./integration.js";
6
2
 
7
- const defaults = {
8
- width: 1200,
9
- height: 630,
10
- };
3
+ export { placeholder } from "./constants.js";
4
+ export * from "./presets/index.js";
5
+ export * from "./types.js";
11
6
 
12
- export type DefaultOptions = SatoriOptions & {
13
- width?: number;
14
- height?: number;
15
- };
16
-
17
- export type Options = DefaultOptions & {
18
- width: number;
19
- height: number;
20
- };
21
-
22
- export default function satoriOpenGraph({
23
- options,
24
- render,
25
- }: {
26
- options: DefaultOptions;
27
- render: RenderFunction;
28
- }): AstroIntegration {
29
- const optionsWithDefaults: Options = { ...defaults, ...options };
30
- return {
31
- name: "astro-satori",
32
- hooks: {
33
- "astro:build:done": async (entry) => {
34
- try {
35
- for (const page of entry.pages) {
36
- await handlePage({ page, options: optionsWithDefaults, render, dir: entry.dir });
37
- entry.logger.info(`Generated OpenGraph image for ${page.pathname}`);
38
- }
39
- } catch (e) {
40
- entry.logger.error(e as string);
41
- }
42
- },
43
- },
44
- };
45
- }
46
-
47
- async function handlePage({
48
- page,
49
- options,
50
- render,
51
- dir,
52
- }: {
53
- page: Page;
54
- dir: URL;
55
- options: Options;
56
- render: RenderFunction;
57
- }) {
58
- const svg = await satori(render(page), options);
59
- const resvg = new Resvg(svg, {
60
- fitTo: {
61
- mode: "width",
62
- value: options.width,
63
- },
64
- });
65
- fs.writeFileSync(`${dir.pathname}${page.pathname}openGraph.png`, resvg.render().asPng());
66
- }
67
-
68
- export async function handleRoute({
69
- context,
70
- options,
71
- render,
72
- }: {
73
- context: APIContext;
74
- options: Options;
75
- render: RenderRouteFunction;
76
- }) {
77
- const svg = await satori(render(context), options);
78
- const resvg = new Resvg(svg, {
79
- fitTo: {
80
- mode: "width",
81
- value: options.width,
82
- },
83
- });
84
- return resvg.render().asPng();
85
- }
7
+ export default astroOpenGraphImages;
@@ -0,0 +1,25 @@
1
+ import type { AstroIntegration } from "astro";
2
+ import type { IntegrationInput, IntegrationOptions } from "./types.js";
3
+ import { buildDoneHook } from "./hook.js";
4
+
5
+ const defaults = {
6
+ width: 1200,
7
+ height: 630,
8
+ };
9
+
10
+ export function astroOpenGraphImages({ options, render }: IntegrationInput): AstroIntegration {
11
+ const optionsWithDefaults: IntegrationOptions = { ...defaults, ...options };
12
+
13
+ return {
14
+ name: "astro-opengraph-images",
15
+ hooks: {
16
+ "astro:build:done": async (entry) => {
17
+ await buildDoneHook({
18
+ ...entry,
19
+ options: optionsWithDefaults,
20
+ render,
21
+ });
22
+ },
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import type { RenderFunctionInput } from "../types.js";
3
+
4
+ export function blackAndWhite({ title, description }: RenderFunctionInput): React.ReactNode {
5
+ return (
6
+ <div
7
+ style={{
8
+ height: "100%",
9
+ width: "100%",
10
+ display: "flex",
11
+ flexDirection: "column",
12
+ backgroundColor: "#000",
13
+ padding: "55px 70px",
14
+ color: "#fff",
15
+ fontFamily: "Commit Mono",
16
+ fontSize: 72,
17
+ }}
18
+ >
19
+ <div
20
+ style={{
21
+ marginTop: 96,
22
+ }}
23
+ >
24
+ {title}
25
+ </div>
26
+ <div
27
+ style={{
28
+ fontSize: 40,
29
+ }}
30
+ >
31
+ {description ?? ""}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,5 @@
1
+ import { blackAndWhite } from "./blackAndWhite.jsx";
2
+
3
+ export const presets = {
4
+ blackAndWhite: blackAndWhite,
5
+ };
package/src/types.ts CHANGED
@@ -1,26 +1,63 @@
1
- import type { APIContext } from "astro";
1
+ import type { AstroIntegrationLogger, RouteData } from "astro";
2
2
  import type { ReactNode } from "react";
3
3
 
4
+ export interface IntegrationInput {
5
+ options: DefaultIntegrationOptions;
6
+ render: RenderFunction;
7
+ }
8
+
9
+ export type DefaultIntegrationOptions = SatoriOptions & {
10
+ width?: number;
11
+ height?: number;
12
+ };
13
+
14
+ export type IntegrationOptions = DefaultIntegrationOptions & {
15
+ width: number;
16
+ height: number;
17
+ };
18
+
19
+ /** This is the page data passed in by Astro */
4
20
  export interface Page {
5
21
  pathname: string;
6
22
  }
7
23
 
8
- export type RenderFunction = (page: Page) => ReactNode;
24
+ /** The input Astro passes to the build done hook */
25
+ export interface AstroBuildDoneHookInput {
26
+ logger: AstroIntegrationLogger;
27
+ pages: {
28
+ pathname: string;
29
+ }[];
30
+ dir: URL;
31
+ routes: RouteData[];
32
+ cacheManifest: boolean;
33
+ }
34
+
35
+ /** The input arguments to a `RenderFunction` */
36
+ export type RenderFunctionInput = {
37
+ pathname: string;
38
+ } & PageDetails;
9
39
 
10
- export type RenderRouteFunction = (context: APIContext) => ReactNode;
40
+ /** A function that renders some page input to React */
41
+ export type RenderFunction = (input: RenderFunctionInput) => ReactNode;
42
+
43
+ /** Basic information about a page */
44
+ export interface PageDetails {
45
+ title?: string;
46
+ description?: string;
47
+ }
11
48
 
12
- // copied from Satori
13
- export type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
14
- export type FontStyle = "normal" | "italic";
15
- export interface FontOptions {
49
+ /** Types copied from Satori */
50
+ export type SatoriWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
51
+ export type SatoriFontStyle = "normal" | "italic";
52
+ export interface SatoriFontOptions {
16
53
  data: Buffer | ArrayBuffer;
17
54
  name: string;
18
- weight?: Weight;
19
- style?: FontStyle;
55
+ weight?: SatoriWeight;
56
+ style?: SatoriFontStyle;
20
57
  lang?: string;
21
58
  }
22
59
  export interface SatoriOptions {
23
60
  width: number;
24
61
  height: number;
25
- fonts: FontOptions[];
62
+ fonts: SatoriFontOptions[];
26
63
  }
package/src/util.ts ADDED
@@ -0,0 +1,18 @@
1
+ import * as fs from "fs";
2
+
3
+ // some files, e.g. index or 404 pages, are served without a folder
4
+ // other files, e.g. blog posts, are served from a folder
5
+ // I don't fully understand how Astro decides this, so:
6
+ // Check if `page.pathname` is a directory on disk
7
+ export function getFilePath({ dir, page }: { dir: string; page: string }) {
8
+ let target: string;
9
+ if (fs.existsSync(`${dir}${page}`)) {
10
+ target = `${dir}${page}index.html`;
11
+ } else {
12
+ target = `${dir}${page}`;
13
+ target = target.slice(0, -1);
14
+ target = target + ".html";
15
+ }
16
+
17
+ return target;
18
+ }