astro-opengraph-images 0.0.0-dev.706

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 (105) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/README.md +531 -0
  3. package/README.md.tmpl +273 -0
  4. package/bun.lock +1856 -0
  5. package/dist/extract.d.ts +4 -0
  6. package/dist/extract.d.ts.map +1 -0
  7. package/dist/extract.js +48 -0
  8. package/dist/extract.js.map +1 -0
  9. package/dist/hook.d.ts +6 -0
  10. package/dist/hook.d.ts.map +1 -0
  11. package/dist/hook.js +58 -0
  12. package/dist/hook.js.map +1 -0
  13. package/dist/index.d.ts +32 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/integration.d.ts +4 -0
  18. package/dist/integration.d.ts.map +1 -0
  19. package/dist/integration.js +22 -0
  20. package/dist/integration.js.map +1 -0
  21. package/dist/presets/background-image.d.ts +3 -0
  22. package/dist/presets/background-image.d.ts.map +1 -0
  23. package/dist/presets/background-image.js +11 -0
  24. package/dist/presets/background-image.js.map +1 -0
  25. package/dist/presets/black-and-white.d.ts +3 -0
  26. package/dist/presets/black-and-white.d.ts.map +1 -0
  27. package/dist/presets/black-and-white.js +19 -0
  28. package/dist/presets/black-and-white.js.map +1 -0
  29. package/dist/presets/branded-logo.d.ts +3 -0
  30. package/dist/presets/branded-logo.d.ts.map +1 -0
  31. package/dist/presets/branded-logo.js +10 -0
  32. package/dist/presets/branded-logo.js.map +1 -0
  33. package/dist/presets/custom-property.d.ts +3 -0
  34. package/dist/presets/custom-property.d.ts.map +1 -0
  35. package/dist/presets/custom-property.js +12 -0
  36. package/dist/presets/custom-property.js.map +1 -0
  37. package/dist/presets/gradients.d.ts +3 -0
  38. package/dist/presets/gradients.d.ts.map +1 -0
  39. package/dist/presets/gradients.js +25 -0
  40. package/dist/presets/gradients.js.map +1 -0
  41. package/dist/presets/index.d.ts +25 -0
  42. package/dist/presets/index.d.ts.map +1 -0
  43. package/dist/presets/index.js +25 -0
  44. package/dist/presets/index.js.map +1 -0
  45. package/dist/presets/podcast.d.ts +3 -0
  46. package/dist/presets/podcast.d.ts.map +1 -0
  47. package/dist/presets/podcast.js +11 -0
  48. package/dist/presets/podcast.js.map +1 -0
  49. package/dist/presets/rauchg.d.ts +3 -0
  50. package/dist/presets/rauchg.d.ts.map +1 -0
  51. package/dist/presets/rauchg.js +40 -0
  52. package/dist/presets/rauchg.js.map +1 -0
  53. package/dist/presets/render-examples.d.ts +2 -0
  54. package/dist/presets/render-examples.d.ts.map +1 -0
  55. package/dist/presets/render-examples.js +57 -0
  56. package/dist/presets/render-examples.js.map +1 -0
  57. package/dist/presets/simple-blog.d.ts +3 -0
  58. package/dist/presets/simple-blog.d.ts.map +1 -0
  59. package/dist/presets/simple-blog.js +7 -0
  60. package/dist/presets/simple-blog.js.map +1 -0
  61. package/dist/presets/tailwind.d.ts +3 -0
  62. package/dist/presets/tailwind.d.ts.map +1 -0
  63. package/dist/presets/tailwind.js +6 -0
  64. package/dist/presets/tailwind.js.map +1 -0
  65. package/dist/presets/vercel.d.ts +3 -0
  66. package/dist/presets/vercel.d.ts.map +1 -0
  67. package/dist/presets/vercel.js +29 -0
  68. package/dist/presets/vercel.js.map +1 -0
  69. package/dist/presets/wave-svg.d.ts +3 -0
  70. package/dist/presets/wave-svg.d.ts.map +1 -0
  71. package/dist/presets/wave-svg.js +7 -0
  72. package/dist/presets/wave-svg.js.map +1 -0
  73. package/dist/types.d.ts +64 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/util.d.ts +9 -0
  78. package/dist/util.d.ts.map +1 -0
  79. package/dist/util.js +43 -0
  80. package/dist/util.js.map +1 -0
  81. package/dist/util.test.d.ts +2 -0
  82. package/dist/util.test.d.ts.map +1 -0
  83. package/dist/util.test.js +46 -0
  84. package/dist/util.test.js.map +1 -0
  85. package/package.json +92 -0
  86. package/src/extract.ts +58 -0
  87. package/src/hook.ts +103 -0
  88. package/src/index.ts +36 -0
  89. package/src/integration.ts +34 -0
  90. package/src/presets/background-image.tsx +33 -0
  91. package/src/presets/black-and-white.tsx +37 -0
  92. package/src/presets/branded-logo.tsx +86 -0
  93. package/src/presets/custom-property.tsx +31 -0
  94. package/src/presets/gradients.tsx +45 -0
  95. package/src/presets/index.ts +25 -0
  96. package/src/presets/podcast.tsx +48 -0
  97. package/src/presets/rauchg.tsx +62 -0
  98. package/src/presets/render-examples.ts +66 -0
  99. package/src/presets/simple-blog.tsx +25 -0
  100. package/src/presets/tailwind.tsx +32 -0
  101. package/src/presets/vercel.tsx +52 -0
  102. package/src/presets/wave-svg.tsx +32 -0
  103. package/src/types.ts +85 -0
  104. package/src/util.test.ts +64 -0
  105. package/src/util.ts +64 -0
@@ -0,0 +1,57 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+ import satori from "satori";
3
+ import { presets } from "./index.js";
4
+ import * as fs from "node:fs/promises";
5
+ import { getFilePath } from "#src/util.js";
6
+ import * as jsdom from "jsdom";
7
+ import { sanitizeHtml } from "#src/extract.js";
8
+ import { fileURLToPath } from "node:url";
9
+ // Updates the examples for the README
10
+ // Run with `npx tsx src/presets/render-examples.ts`
11
+ async function renderExamples() {
12
+ const pathname = "dist/index/";
13
+ const dir = new URL("../../examples/preset", import.meta.url);
14
+ const htmlFile = await getFilePath({
15
+ dir: fileURLToPath(dir),
16
+ page: pathname,
17
+ });
18
+ const htmlBuffer = await fs.readFile(htmlFile);
19
+ const html = htmlBuffer.toString();
20
+ const document = new jsdom.JSDOM(sanitizeHtml(html)).window.document;
21
+ const page = {
22
+ title: "3D Graphics with OpenGL",
23
+ description: "An introduction to 3D graphics rendering and OpenGL.",
24
+ url: "https://example.com/3d-graphics",
25
+ type: "article",
26
+ image: "https://example.com/3d-graphics.png",
27
+ pathname: pathname,
28
+ dir,
29
+ document,
30
+ };
31
+ const options = {
32
+ width: 1200,
33
+ height: 630,
34
+ fonts: [
35
+ {
36
+ name: "Roboto",
37
+ weight: 400,
38
+ style: "normal",
39
+ data: await fs.readFile("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff"),
40
+ },
41
+ ],
42
+ };
43
+ const promises = Object.entries(presets).map(async ([name, preset]) => {
44
+ const node = await preset(page);
45
+ const svg = await satori(node, options);
46
+ const resvg = new Resvg(svg, {
47
+ font: { loadSystemFonts: false },
48
+ fitTo: { mode: "width", value: options.width },
49
+ });
50
+ const target = `assets/presets/${name}.png`;
51
+ await fs.writeFile(target, resvg.render().asPng());
52
+ console.warn(`Wrote ${target}`);
53
+ });
54
+ await Promise.all(promises);
55
+ }
56
+ await renderExamples();
57
+ //# sourceMappingURL=render-examples.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-examples.js","sourceRoot":"","sources":["../../src/presets/render-examples.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,MAA8B,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,sCAAsC;AACtC,oDAAoD;AACpD,KAAK,UAAU,cAAc;IAC3B,MAAM,QAAQ,GAAG,aAAa,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE9D,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC;QACjC,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC;QACvB,IAAI,EAAE,QAAQ;KACf,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;IAErE,MAAM,IAAI,GAAwB;QAChC,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,sDAAsD;QACnE,GAAG,EAAE,iCAAiC;QACtC,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,qCAAqC;QAC5C,QAAQ,EAAE,QAAQ;QAClB,GAAG;QACH,QAAQ;KACT,CAAC;IAEF,MAAM,OAAO,GAAkB;QAC7B,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,GAAG;QACX,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,QAAQ;gBACd,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,QAAQ;gBACf,IAAI,EAAE,MAAM,EAAE,CAAC,QAAQ,CACrB,oEAAoE,CACrE;aACF;SACF;KACF,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE;QACpE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;YAC3B,IAAI,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE;YAChC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;SAC/C,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,kBAAkB,IAAI,MAAM,CAAC;QAC5C,MAAM,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,cAAc,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RenderFunctionInput } from "#src/types.js";
2
+ export declare function simpleBlog({ title, description, }: RenderFunctionInput): React.ReactNode;
3
+ //# sourceMappingURL=simple-blog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-blog.d.ts","sourceRoot":"","sources":["../../src/presets/simple-blog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAIzD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,GACZ,EAAE,mBAAmB,GAAG,KAAK,CAAC,SAAS,CAiBvC"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const { twj } = await import("tw-to-css");
3
+ // from https://fullstackheroes.com/resources/vercel-og-templates/simple/
4
+ export function simpleBlog({ title, description, }) {
5
+ return (_jsx("div", { style: twj("h-full w-full flex items-start justify-start border border-blue-500 border-[12px] bg-gray-50"), children: _jsx("div", { style: twj("flex items-start justify-start h-full"), children: _jsxs("div", { style: twj("flex flex-col justify-between w-full h-full"), children: [_jsx("h1", { style: twj("text-[80px] p-20 font-black text-left"), children: title }), _jsx("div", { style: twj("text-2xl pb-10 px-20 font-bold mb-0"), children: description })] }) }) }));
6
+ }
7
+ //# sourceMappingURL=simple-blog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-blog.js","sourceRoot":"","sources":["../../src/presets/simple-blog.tsx"],"names":[],"mappings":";AACA,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;AAE1C,yEAAyE;AACzE,MAAM,UAAU,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,GACS;IACpB,OAAO,CACL,cACE,KAAK,EAAE,GAAG,CACR,8FAA8F,CAC/F,YAED,cAAK,KAAK,EAAE,GAAG,CAAC,uCAAuC,CAAC,YACtD,eAAK,KAAK,EAAE,GAAG,CAAC,6CAA6C,CAAC,aAC5D,aAAI,KAAK,EAAE,GAAG,CAAC,uCAAuC,CAAC,YAAG,KAAK,GAAM,EACrE,cAAK,KAAK,EAAE,GAAG,CAAC,qCAAqC,CAAC,YACnD,WAAW,GACR,IACF,GACF,GACF,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RenderFunctionInput } from "#src/types.js";
2
+ export declare function tailwind({ title, description, }: RenderFunctionInput): React.ReactNode;
3
+ //# sourceMappingURL=tailwind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailwind.d.ts","sourceRoot":"","sources":["../../src/presets/tailwind.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGzD,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,WAAW,GACZ,EAAE,mBAAmB,GAAG,KAAK,CAAC,SAAS,CAyBvC"}
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const { twj } = await import("tw-to-css");
3
+ export function tailwind({ title, description, }) {
4
+ return (_jsx("div", { style: twj("flex flex-col w-full h-full items-center justify-center bg-white"), children: _jsx("div", { style: twj("bg-gray-50 flex w-full"), children: _jsx("div", { style: twj("flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8"), children: _jsxs("h2", { style: twj("flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left"), children: [_jsx("span", { children: title }), _jsx("span", { style: twj("text-indigo-600"), children: description })] }) }) }) }));
5
+ }
6
+ //# sourceMappingURL=tailwind.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailwind.js","sourceRoot":"","sources":["../../src/presets/tailwind.tsx"],"names":[],"mappings":";AACA,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;AAE1C,MAAM,UAAU,QAAQ,CAAC,EACvB,KAAK,EACL,WAAW,GACS;IACpB,OAAO,CACL,cACE,KAAK,EAAE,GAAG,CACR,kEAAkE,CACnE,YAED,cAAK,KAAK,EAAE,GAAG,CAAC,wBAAwB,CAAC,YACvC,cACE,KAAK,EAAE,GAAG,CACR,iFAAiF,CAClF,YAED,cACE,KAAK,EAAE,GAAG,CACR,qFAAqF,CACtF,aAED,yBAAO,KAAK,GAAQ,EACpB,eAAM,KAAK,EAAE,GAAG,CAAC,iBAAiB,CAAC,YAAG,WAAW,GAAQ,IACtD,GACD,GACF,GACF,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RenderFunctionInput } from "#src/types.js";
2
+ export declare function vercel({ title }: RenderFunctionInput): React.ReactNode;
3
+ //# sourceMappingURL=vercel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../src/presets/vercel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,mBAAmB,GAAG,KAAK,CAAC,SAAS,CAiDtE"}
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function vercel({ title }) {
3
+ return (_jsxs("div", { style: {
4
+ height: "100%",
5
+ width: "100%",
6
+ display: "flex",
7
+ textAlign: "center",
8
+ alignItems: "center",
9
+ justifyContent: "center",
10
+ flexDirection: "column",
11
+ flexWrap: "nowrap",
12
+ backgroundColor: "white",
13
+ backgroundImage: "radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)",
14
+ backgroundSize: "100px 100px",
15
+ }, children: [_jsx("div", { style: {
16
+ display: "flex",
17
+ alignItems: "center",
18
+ justifyContent: "center",
19
+ }, children: _jsx("svg", { height: 80, viewBox: "0 0 75 65", fill: "black", style: { margin: "0 75px" }, children: _jsx("path", { d: "M37.59.25l36.95 64H.64l36.95-64z" }) }) }), _jsx("div", { style: {
20
+ display: "flex",
21
+ fontSize: 40,
22
+ fontStyle: "normal",
23
+ color: "black",
24
+ marginTop: 30,
25
+ lineHeight: 1.8,
26
+ whiteSpace: "pre-wrap",
27
+ }, children: _jsx("b", { children: title }) })] }));
28
+ }
29
+ //# sourceMappingURL=vercel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vercel.js","sourceRoot":"","sources":["../../src/presets/vercel.tsx"],"names":[],"mappings":";AAEA,MAAM,UAAU,MAAM,CAAC,EAAE,KAAK,EAAuB;IACnD,OAAO,CACL,eACE,KAAK,EAAE;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,MAAM;YACf,SAAS,EAAE,QAAQ;YACnB,UAAU,EAAE,QAAQ;YACpB,cAAc,EAAE,QAAQ;YACxB,aAAa,EAAE,QAAQ;YACvB,QAAQ,EAAE,QAAQ;YAClB,eAAe,EAAE,OAAO;YACxB,eAAe,EACb,wIAAwI;YAC1I,cAAc,EAAE,aAAa;SAC9B,aAED,cACE,KAAK,EAAE;oBACL,OAAO,EAAE,MAAM;oBACf,UAAU,EAAE,QAAQ;oBACpB,cAAc,EAAE,QAAQ;iBACzB,YAED,cACE,MAAM,EAAE,EAAE,EACV,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,OAAO,EACZ,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,YAE3B,eAAM,CAAC,EAAC,kCAAkC,GAAQ,GAC9C,GACF,EACN,cACE,KAAK,EAAE;oBACL,OAAO,EAAE,MAAM;oBACf,QAAQ,EAAE,EAAE;oBACZ,SAAS,EAAE,QAAQ;oBACnB,KAAK,EAAE,OAAO;oBACd,SAAS,EAAE,EAAE;oBACb,UAAU,EAAE,GAAG;oBACf,UAAU,EAAE,UAAU;iBACvB,YAED,sBAAI,KAAK,GAAK,GACV,IACF,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RenderFunctionInput } from "#src/types.js";
2
+ export declare function waveSvg({ title }: RenderFunctionInput): React.ReactNode;
3
+ //# sourceMappingURL=wave-svg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wave-svg.d.ts","sourceRoot":"","sources":["../../src/presets/wave-svg.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAIzD,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,mBAAmB,GAAG,KAAK,CAAC,SAAS,CA2BvE"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const { twj } = await import("tw-to-css");
3
+ // from https://fullstackheroes.com/resources/vercel-og-templates/wave/
4
+ export function waveSvg({ title }) {
5
+ return (_jsxs("div", { style: twj("h-full w-full flex items-start justify-start bg-yellow-50 relative"), children: [_jsx("h1", { style: twj("text-7xl p-20 font-bold text-left text-gray-900"), children: title }), _jsx("svg", { style: twj("absolute bottom-0 left-0 right-0"), viewBox: "0 0 1200 627", width: "1200", height: "627", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M0 513L28.5 509.8C57 506.7 114 500.3 171.2 484.5C228.3 468.7 285.7 443.3 342.8 435C400 426.7 457 435.3 514.2 447.3C571.3 459.3 628.7 474.7 685.8 490.7C743 506.7 800 523.3 857.2 522.5C914.3 521.7 971.7 503.3 1028.8 491.8C1086 480.3 1143 475.7 1171.5 473.3L1200 471L1200 628L1171.5 628C1143 628 1086 628 1028.8 628C971.7 628 914.3 628 857.2 628C800 628 743 628 685.8 628C628.7 628 571.3 628 514.2 628C457 628 400 628 342.8 628C285.7 628 228.3 628 171.2 628C114 628 57 628 28.5 628L0 628Z", fill: "#fbbf24", strokeLinecap: "round", strokeLinejoin: "miter" }) })] }));
6
+ }
7
+ //# sourceMappingURL=wave-svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wave-svg.js","sourceRoot":"","sources":["../../src/presets/wave-svg.tsx"],"names":[],"mappings":";AACA,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;AAE1C,uEAAuE;AACvE,MAAM,UAAU,OAAO,CAAC,EAAE,KAAK,EAAuB;IACpD,OAAO,CACL,eACE,KAAK,EAAE,GAAG,CACR,oEAAoE,CACrE,aAED,aAAI,KAAK,EAAE,GAAG,CAAC,iDAAiD,CAAC,YAC9D,KAAK,GACH,EAEL,cACE,KAAK,EAAE,GAAG,CAAC,kCAAkC,CAAC,EAC9C,OAAO,EAAC,cAAc,EACtB,KAAK,EAAC,MAAM,EACZ,MAAM,EAAC,KAAK,EACZ,KAAK,EAAC,4BAA4B,YAElC,eACE,CAAC,EAAC,ueAAue,EACze,IAAI,EAAC,SAAS,EACd,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,GAChB,GACJ,IACF,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,64 @@
1
+ import type { BaseIntegrationHooks } from "astro";
2
+ import type { ReactNode } from "react";
3
+ export type IntegrationInput = {
4
+ options: PartialIntegrationOptions;
5
+ render: RenderFunction;
6
+ };
7
+ /** When applied to PartialIntegrationOptions this type equals IntegrationOptions */
8
+ export type IntegrationDefaults = {
9
+ width: number;
10
+ height: number;
11
+ verbose: boolean;
12
+ };
13
+ /**
14
+ * IntegrationOptions with some optional properties. This is what we expose to the user. It allows us to
15
+ * merge the defaults with the user's options and ensure that all required properties are present.
16
+ */
17
+ export type PartialIntegrationOptions = Omit<Omit<SatoriOptions, "width">, "height"> & Partial<IntegrationDefaults>;
18
+ /**
19
+ * The options that we use internally. This ensures that all options are configured, either with something
20
+ * the user provided or with a default value.
21
+ */
22
+ export type IntegrationOptions = PartialIntegrationOptions & IntegrationDefaults;
23
+ /** This is the page data passed in by Astro */
24
+ export type Page = {
25
+ pathname: string;
26
+ };
27
+ /** The input Astro passes to the build done hook */
28
+ export type AstroBuildDoneHookInput = Parameters<BaseIntegrationHooks["astro:build:done"]>[0];
29
+ /** The input arguments to a `RenderFunction` */
30
+ export type RenderFunctionInput = {
31
+ pathname: string;
32
+ dir: URL;
33
+ document: Document;
34
+ } & PageDetails;
35
+ /** A function that renders some page input to React */
36
+ export type RenderFunction = (input: RenderFunctionInput) => ReactNode | Promise<ReactNode>;
37
+ /** Basic information about a page */
38
+ export type PageDetails = {
39
+ title: string;
40
+ description?: string;
41
+ url: string;
42
+ type: string;
43
+ image: string;
44
+ };
45
+ type NonEmptyArray<T> = [T, ...T[]];
46
+ /** Types copied from Satori */
47
+ export type SatoriWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
48
+ export type SatoriFontStyle = "normal" | "italic";
49
+ export type SatoriFontOptions = {
50
+ data: Buffer | ArrayBuffer;
51
+ name: string;
52
+ weight?: SatoriWeight;
53
+ style?: SatoriFontStyle;
54
+ lang?: string;
55
+ };
56
+ export type SatoriOptions = {
57
+ width: number;
58
+ height: number;
59
+ fonts: NonEmptyArray<SatoriFontOptions>;
60
+ /** Callback to load additional assets like emoji images or fallback fonts */
61
+ loadAdditionalAsset?: (languageCode: string, segment: string) => Promise<string | SatoriFontOptions[]>;
62
+ };
63
+ export {};
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,yBAAyB,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC;CACxB,CAAC;AAEF,oFAAoF;AACpF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,EAC5B,QAAQ,CACT,GACC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,yBAAyB,GACxD,mBAAmB,CAAC;AAEtB,+CAA+C;AAC/C,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,oDAAoD;AACpD,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAC9C,oBAAoB,CAAC,kBAAkB,CAAC,CACzC,CAAC,CAAC,CAAC,CAAC;AAEL,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,QAAQ,CAAC;CACpB,GAAG,WAAW,CAAC;AAEhB,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,CAC3B,KAAK,EAAE,mBAAmB,KACvB,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;AAEpC,qCAAqC;AACrC,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;AAEpC,+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,MAAM,iBAAiB,GAAG;IAC9B,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,CAAC;AACF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACxC,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,CACpB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC,CAAC;CAC5C,CAAC"}
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":""}
package/dist/util.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export declare function getFilePath({ dir, page, }: {
2
+ dir: string;
3
+ page: string;
4
+ }): Promise<string>;
5
+ export declare function getImagePath({ url, site, }: {
6
+ url: URL;
7
+ site: URL | undefined;
8
+ }): string;
9
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAeA,wBAAsB,WAAW,CAAC,EAChC,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,mBAQA;AAED,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,GAAG,GAAG,SAAS,CAAC;CACvB,GAAG,MAAM,CA0BT"}
package/dist/util.js ADDED
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { access } from "node:fs/promises";
3
+ async function fileExists(filePath) {
4
+ try {
5
+ await access(filePath);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ // some files, e.g. index or 404 pages, are served without a folder
13
+ // other files, e.g. blog posts, are served from a folder
14
+ // I don't fully understand how Astro decides this, so:
15
+ export async function getFilePath({ dir, page, }) {
16
+ let target = path.join(dir, page, "index.html");
17
+ if (!(await fileExists(target))) {
18
+ target = path.join(dir, page.slice(0, -1) + ".html");
19
+ }
20
+ return target;
21
+ }
22
+ export function getImagePath({ url, site, }) {
23
+ if (site === undefined) {
24
+ throw new Error("`site` must be set in your Astro configuration: https://docs.astro.build/en/reference/configuration-reference/#site");
25
+ }
26
+ let target = url.pathname;
27
+ // if url ends with a slash, it's a directory
28
+ // add index.png to the end
29
+ target = target.endsWith("/") ? target + "index.png" : target + ".png";
30
+ // Astro creates these as top-level files rather than in a folder
31
+ if (target === "/404/index.png") {
32
+ return site.toString() + "404.png";
33
+ }
34
+ else if (target === "/500/index.png") {
35
+ return site.toString() + "500.png";
36
+ }
37
+ // remove leading slash
38
+ target = target.slice(1);
39
+ // add site URL
40
+ target = site.toString() + target;
41
+ return target;
42
+ }
43
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,KAAK,UAAU,UAAU,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,yDAAyD;AACzD,uDAAuD;AACvD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,GAAG,EACH,IAAI,GAIL;IACC,IAAI,MAAM,GAAW,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;IAExD,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAChC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAC3B,GAAG,EACH,IAAI,GAIL;IACC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,qHAAqH,CACtH,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC;IAE1B,6CAA6C;IAC7C,2BAA2B;IAC3B,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC;IAEvE,iEAAiE;IACjE,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC;IACrC,CAAC;SAAM,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC;IACrC,CAAC;IAED,uBAAuB;IACvB,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,eAAe;IACf,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=util.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.test.d.ts","sourceRoot":"","sources":["../src/util.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { expect, test } from "vitest";
2
+ import { getFilePath } from "./util.js";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
6
+ test("getFilePath index", async () => {
7
+ const tmpDir = await createTempDir();
8
+ // change the current working directory to the temp dir
9
+ process.chdir(tmpDir);
10
+ // create a folder named blog inside the temp dir
11
+ await writeFile(path.join(tmpDir, "index.html"), "");
12
+ const result = await getFilePath({ dir: "", page: "index/" });
13
+ // change the current working directory back to the original
14
+ process.chdir(import.meta.dirname);
15
+ expect(path.normalize(result)).toBe(path.normalize("index.html"));
16
+ });
17
+ test("getFilePath 404", async () => {
18
+ const tmpDir = await createTempDir();
19
+ // change the current working directory to the temp dir
20
+ process.chdir(tmpDir);
21
+ // create a folder named blog inside the temp dir
22
+ await writeFile(path.join(tmpDir, "404.html"), "");
23
+ const result = await getFilePath({ dir: "", page: "404/" });
24
+ // change the current working directory back to the original
25
+ process.chdir(import.meta.dirname);
26
+ expect(path.normalize(result)).toBe(path.normalize("404.html"));
27
+ });
28
+ test("getFilePath blog", async () => {
29
+ const tmpDir = await createTempDir();
30
+ // change the current working directory to the temp dir
31
+ process.chdir(tmpDir);
32
+ // create a folder named blog inside the temp dir
33
+ await mkdir(path.join(tmpDir, "blog"));
34
+ await writeFile(path.join(tmpDir, "blog", "index.html"), "");
35
+ const result = await getFilePath({ dir: "", page: "blog/" });
36
+ // change the current working directory back to the original
37
+ process.chdir(import.meta.dirname);
38
+ expect(path.normalize(result)).toBe(path.normalize("blog/index.html"));
39
+ });
40
+ // https://sdorra.dev/posts/2024-02-12-vitest-tmpdir
41
+ async function createTempDir() {
42
+ const ostmpdir = tmpdir();
43
+ const dir = path.join(ostmpdir, "unit-test-");
44
+ return await mkdtemp(dir);
45
+ }
46
+ //# sourceMappingURL=util.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.test.js","sourceRoot":"","sources":["../src/util.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "astro-opengraph-images",
3
+ "description": "Generate Open Graph images for your Astro site.",
4
+ "author": {
5
+ "name": "Jerred Shepherd",
6
+ "email": "npm@sjer.red",
7
+ "url": "https://sjer.red"
8
+ },
9
+ "homepage": "https://github.com/shepherdjerred/monorepo/tree/main/packages/astro-opengraph-images",
10
+ "license": "GPL-3.0-only",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/shepherdjerred/monorepo.git",
14
+ "directory": "packages/astro-opengraph-images"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/shepherdjerred/monorepo/issues"
18
+ },
19
+ "type": "module",
20
+ "version": "0.0.0-dev.706",
21
+ "imports": {
22
+ "#src/*": "./src/*"
23
+ },
24
+ "scripts": {
25
+ "lint": "eslint src --cache",
26
+ "build": "tsc",
27
+ "watch": "tsc -w",
28
+ "test": "vitest",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "main": "dist/index.js",
32
+ "types": "dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./types.js": {
39
+ "types": "./dist/types.d.ts",
40
+ "default": "./dist/types.js"
41
+ },
42
+ "./util.js": {
43
+ "types": "./dist/util.d.ts",
44
+ "default": "./dist/util.js"
45
+ },
46
+ "./extract.js": {
47
+ "types": "./dist/extract.d.ts",
48
+ "default": "./dist/extract.js"
49
+ }
50
+ },
51
+ "keywords": [
52
+ "astro-integration",
53
+ "withastro",
54
+ "seo",
55
+ "ui",
56
+ "renderer"
57
+ ],
58
+ "dependencies": {
59
+ "@resvg/resvg-js": "^2.6.2",
60
+ "jsdom": "^28.0.0",
61
+ "react": "^19.0.0",
62
+ "satori": "^0.19.0"
63
+ },
64
+ "peerDependencies": {
65
+ "tw-to-css": "^0.0.12"
66
+ },
67
+ "devDependencies": {
68
+ "@fontsource/roboto": "^5.2.5",
69
+ "@shepherdjerred/eslint-config": "^0.2.0",
70
+ "@tsconfig/node24": "^24.0.0",
71
+ "@tsconfig/strictest": "^2.0.5",
72
+ "@types/bun": "^1.3.9",
73
+ "@types/jsdom": "^27.0.0",
74
+ "@types/node": "^25.0.0",
75
+ "@types/react": "^19.0.10",
76
+ "astro": "^5.4.3",
77
+ "eslint": "^9.22.0",
78
+ "jiti": "^2.6.1",
79
+ "typescript": "^5.8.2",
80
+ "vitest": "^4.0.0"
81
+ },
82
+ "files": [
83
+ "dist",
84
+ "src",
85
+ "package.json",
86
+ "README.md",
87
+ "LICENSE",
88
+ "bun.lock",
89
+ "CHANGELOG.md"
90
+ ],
91
+ "patchedDependencies": {}
92
+ }
package/src/extract.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { PageDetails } from "./types.ts";
2
+
3
+ // Astro CSS parsing fails: Error: Could not parse CSS stylesheet
4
+ // Remove CSS from the HTML
5
+ // https://github.com/jsdom/jsdom/issues/2005#issuecomment-1758940894
6
+ export function sanitizeHtml(html: string): string {
7
+ return html
8
+ .replaceAll(/<style[^>]*>[^<]*<\/style>/gi, "")
9
+ .replaceAll(/<script[^>]*>[^<]*<\/script>/gi, "");
10
+ }
11
+
12
+ function getMetaContent(document: Document, property: string): string | null {
13
+ const content = document
14
+ .querySelector(`meta[property='${property}']`)
15
+ ?.getAttribute("content");
16
+ if (content === undefined || content === null || content === "") {
17
+ return null;
18
+ }
19
+ return content;
20
+ }
21
+
22
+ export function extract(document: Document): PageDetails {
23
+ const title = getMetaContent(document, "og:title");
24
+ const description = getMetaContent(document, "og:description");
25
+ const url = getMetaContent(document, "og:url");
26
+ const type = getMetaContent(document, "og:type");
27
+ const image = getMetaContent(document, "og:image");
28
+
29
+ const required = { title, url, type, image };
30
+ const missing = Object.entries(required)
31
+ .filter(([, value]) => value === null)
32
+ .map(([key]) => `og:${key}`);
33
+
34
+ if (missing.length > 0) {
35
+ const html = missing.map(
36
+ (tag) => `<meta property="${tag}" content="some value"/>`,
37
+ );
38
+ throw new Error(
39
+ `Missing required meta tags: ${missing.join(", ")}. Add the following to your page:\n${html.join("\n")}`,
40
+ );
41
+ }
42
+
43
+ // After the check above, these values are guaranteed to be non-null strings
44
+ const safeTitle = title ?? "";
45
+ const safeUrl = url ?? "";
46
+ const safeType = type ?? "";
47
+ const safeImage = image ?? "";
48
+ const returnVal: PageDetails = {
49
+ title: safeTitle,
50
+ url: safeUrl,
51
+ type: safeType,
52
+ image: safeImage,
53
+ };
54
+ if (description !== null && description !== title) {
55
+ returnVal.description = description;
56
+ }
57
+ return returnVal;
58
+ }
package/src/hook.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+ import satori from "satori";
3
+ import type {
4
+ AstroBuildDoneHookInput,
5
+ IntegrationOptions,
6
+ Page,
7
+ RenderFunction,
8
+ } from "./types.ts";
9
+ import * as fs from "node:fs/promises";
10
+ import type { AstroIntegrationLogger } from "astro";
11
+ import { extract, sanitizeHtml } from "./extract.ts";
12
+ import { getFilePath } from "./util.ts";
13
+ import { fileURLToPath } from "node:url";
14
+ import * as jsdom from "jsdom";
15
+ import path from "node:path";
16
+
17
+ export async function buildDoneHook({
18
+ logger,
19
+ pages,
20
+ options,
21
+ dir,
22
+ render,
23
+ }: AstroBuildDoneHookInput & {
24
+ options: IntegrationOptions;
25
+ render: RenderFunction;
26
+ }) {
27
+ logger.info("Generating Open Graph images");
28
+ const promises = pages.map((page) =>
29
+ handlePage({ page, options, render, dir, logger }),
30
+ );
31
+ await Promise.all(promises);
32
+ }
33
+
34
+ type HandlePageInput = {
35
+ page: Page;
36
+ options: IntegrationOptions;
37
+ render: RenderFunction;
38
+ dir: URL;
39
+ logger: AstroIntegrationLogger;
40
+ };
41
+
42
+ async function handlePage({
43
+ page,
44
+ options,
45
+ render,
46
+ dir,
47
+ logger,
48
+ }: HandlePageInput) {
49
+ // gets the absolute path to the HTML file. E.g. /home/user/project/dist/blog/index.html
50
+ // fileURLToPath() converts the URL to a file path. Without it, the path would start with a leading slash on Windows
51
+ // systems, resulting in an invalid path.
52
+ const htmlFile = await getFilePath({
53
+ dir: fileURLToPath(dir),
54
+ page: page.pathname,
55
+ });
56
+
57
+ // read the HTML file and parse it with jsdom
58
+ const htmlBuffer = await fs.readFile(htmlFile);
59
+ const html = htmlBuffer.toString();
60
+ const document = new jsdom.JSDOM(sanitizeHtml(html)).window.document;
61
+
62
+ // extract the OpenGraph properties from the HTML file
63
+ const pageDetails = extract(document);
64
+
65
+ // render the image using Satori and Resvg
66
+ const reactNode = await render({ ...page, ...pageDetails, dir, document });
67
+ const svg = await satori(reactNode, options);
68
+ const resvg = new Resvg(svg, {
69
+ font: {
70
+ loadSystemFonts: false,
71
+ },
72
+ fitTo: {
73
+ mode: "width",
74
+ value: options.width,
75
+ },
76
+ });
77
+
78
+ // save the image as a PNG file. The file name is the same as the HTML file, but with a .png extension.
79
+ const pngFile = htmlFile.replace(/\.html$/, ".png");
80
+ await fs.writeFile(pngFile, resvg.render().asPng());
81
+
82
+ // get the relative filesystem path to the PNG file from the output directory. E.g. blog/index.png
83
+ // path.relative() returns the relative path from the first argument to the second argument.
84
+ const relativePngFile = path
85
+ .relative(fileURLToPath(dir), pngFile)
86
+ .replaceAll("\\", "/");
87
+
88
+ // convert the image path to a URL, decode URL-encoded characters, and remove the leading slash
89
+ const imageUrl = decodeURIComponent(
90
+ new URL(pageDetails.image).pathname.slice(1),
91
+ );
92
+
93
+ // check that the og:image property matches the sitePath
94
+ if (imageUrl !== relativePngFile) {
95
+ throw new Error(
96
+ `The og:image property in ${htmlFile} (${imageUrl}) does not match the generated image (${relativePngFile}).`,
97
+ );
98
+ }
99
+
100
+ if (options.verbose) {
101
+ logger.info(`Generated ${relativePngFile} for ${htmlFile}.`);
102
+ }
103
+ }