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.
- package/CHANGELOG.md +27 -0
- package/README.md +5 -156
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/extract.d.ts +4 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +23 -0
- package/dist/extract.js.map +1 -0
- package/dist/hook.d.ts +6 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +39 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +5 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -46
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +4 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +21 -0
- package/dist/integration.js.map +1 -0
- package/dist/presets/blackAndWhite.d.ts +4 -0
- package/dist/presets/blackAndWhite.d.ts.map +1 -0
- package/dist/presets/blackAndWhite.jsx +26 -0
- package/dist/presets/blackAndWhite.jsx.map +1 -0
- package/dist/presets/index.d.ts +5 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +5 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/types.d.ts +42 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +18 -0
- package/dist/util.js.map +1 -0
- package/package.json +19 -9
- package/src/constants.ts +1 -0
- package/src/extract.ts +25 -0
- package/src/hook.ts +63 -0
- package/src/index.ts +5 -83
- package/src/integration.ts +25 -0
- package/src/presets/blackAndWhite.tsx +35 -0
- package/src/presets/index.ts +5 -0
- package/src/types.ts +47 -10
- 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
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/astro-opengraph-images)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Generate fully customizable OpenGraph images with a few lines of code.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Quick Start
|
|
8
8
|
|
|
9
|
-
1.
|
|
9
|
+
1. Add this integration:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm i astro-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,wBAAwB,CAAC"}
|
|
@@ -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 @@
|
|
|
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"}
|
package/dist/extract.js
ADDED
|
@@ -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
|
package/dist/hook.js.map
ADDED
|
@@ -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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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":"
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 {
|
|
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
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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?:
|
|
14
|
-
style?:
|
|
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:
|
|
53
|
+
fonts: SatoriFontOptions[];
|
|
21
54
|
}
|
|
22
55
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
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 @@
|
|
|
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
|
package/dist/util.js.map
ADDED
|
@@ -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.
|
|
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.
|
|
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
|
-
"
|
|
28
|
-
"@typescript-eslint/
|
|
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.
|
|
32
|
-
"prettier": "^3.3.
|
|
33
|
-
"typescript": "^5.
|
|
34
|
-
"typescript-eslint": "^7.
|
|
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": {
|
package/src/constants.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
3
|
+
export { placeholder } from "./constants.js";
|
|
4
|
+
export * from "./presets/index.js";
|
|
5
|
+
export * from "./types.js";
|
|
11
6
|
|
|
12
|
-
export
|
|
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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
export type
|
|
14
|
-
export type
|
|
15
|
-
export interface
|
|
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?:
|
|
19
|
-
style?:
|
|
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:
|
|
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
|
+
}
|