@altano/astro-opengraph 0.0.1-alpha.1 → 0.0.1-alpha.2
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/README.md +1 -1
- package/assets/dev-toolbar.png +0 -0
- package/dist/config.d.ts +4 -0
- package/dist/endpoint.d.ts +14 -0
- package/dist/endpoint.js +137 -0
- package/dist/image.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/integration.d.ts +5 -0
- package/dist/toolbar/components/CopyImageUrlButton.d.ts +3 -0
- package/dist/toolbar/components/ExternalLinkicon.d.ts +7 -0
- package/dist/toolbar/components/Image.d.ts +5 -0
- package/dist/toolbar/components/ImageSection.d.ts +4 -0
- package/dist/toolbar/components/ImageZoomToolbar.d.ts +7 -0
- package/dist/toolbar/components/LinkOrText.d.ts +7 -0
- package/dist/toolbar/components/MetaTags.d.ts +3 -0
- package/dist/toolbar/components/Summary.d.ts +5 -0
- package/dist/toolbar/components/ToolbarApp.d.ts +3 -0
- package/dist/toolbar/components/ToolbarAppWindow.d.ts +3 -0
- package/dist/toolbar/components/ToolbarSection.d.ts +6 -0
- package/dist/toolbar/components/ZoomButton.d.ts +7 -0
- package/dist/toolbar/components/ZoomLabel.d.ts +4 -0
- package/dist/toolbar/hooks/useHoverState.d.ts +3 -0
- package/dist/toolbar/hooks/useImageURL.d.ts +3 -0
- package/dist/toolbar/hooks/useMetaTagInfo.d.ts +3 -0
- package/dist/toolbar/index.d.ts +3 -0
- package/dist/toolbar/index.js +472 -0
- package/dist/toolbar/styles.d.ts +2 -0
- package/dist/types.d.ts +67 -0
- package/dist/virtual-user-config.d.ts +8 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -399,7 +399,7 @@ This integration makes use of the [experimental container APIs](https://docs.ast
|
|
|
399
399
|
|
|
400
400
|
While in `astro dev` mode, you can use the included Astro dev toolbar app installed by this integration. It include a preview of the opengraph image as well as the `og:` meta tags on the page:
|
|
401
401
|
|
|
402
|
-

|
|
403
403
|
|
|
404
404
|
Once your site is live, you can use a tool like [opengraph.xyz](https://www.opengraph.xyz/) to preview the opengraph image/tags.
|
|
405
405
|
|
|
Binary file
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpengraphImageConfigDeserialized, OpengraphImageConfigResolved, OpengraphImageConfigSerializableMaybeMocked } from "./types.js";
|
|
2
|
+
export declare function deserializeVirtualConfig(configMaybeMocked: OpengraphImageConfigSerializableMaybeMocked): Promise<OpengraphImageConfigDeserialized>;
|
|
3
|
+
export declare function getResolvedConfig(): Promise<OpengraphImageConfigResolved>;
|
|
4
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import type { ImageOptions } from "./types.js";
|
|
3
|
+
type Options = {
|
|
4
|
+
template: unknown;
|
|
5
|
+
/**
|
|
6
|
+
* Custom "Content-Type" header, only used in SSR. Defaults to "image/png"
|
|
7
|
+
*/
|
|
8
|
+
contentType?: string;
|
|
9
|
+
imageOptionOverrides?: Partial<ImageOptions>;
|
|
10
|
+
};
|
|
11
|
+
export declare function makeOpengraphDevEndpoint({ template, contentType, }: Options): APIRoute;
|
|
12
|
+
export declare function makeOpengraphEndpoint({ template, imageOptionOverrides, contentType, }: Options): APIRoute;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=endpoint.d.ts.map
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/endpoint.ts
|
|
2
|
+
import { experimental_AstroContainer } from "astro/container";
|
|
3
|
+
import { isAstroComponentFactory } from "astro/runtime/server/render/astro/factory.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
async function deserializeVirtualConfig(configMaybeMocked) {
|
|
8
|
+
const config = typeof configMaybeMocked === "function" ? configMaybeMocked() : configMaybeMocked;
|
|
9
|
+
const fontsWithBuffers = await Promise.all(
|
|
10
|
+
config.imageOptions.fonts.map(async (font) => {
|
|
11
|
+
const data = await readFile(font.path);
|
|
12
|
+
return {
|
|
13
|
+
...font,
|
|
14
|
+
data
|
|
15
|
+
};
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
return {
|
|
19
|
+
...config,
|
|
20
|
+
imageOptions: {
|
|
21
|
+
...config.imageOptions,
|
|
22
|
+
fonts: fontsWithBuffers
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var imageOptionDefaults = {
|
|
27
|
+
width: 1200,
|
|
28
|
+
height: 630
|
|
29
|
+
};
|
|
30
|
+
var metaTagDefaults = {
|
|
31
|
+
"og:image": `/opengraph.png`
|
|
32
|
+
};
|
|
33
|
+
async function getResolvedConfig() {
|
|
34
|
+
const module = await import("virtual:astro-opengraph/user-config");
|
|
35
|
+
const lazyConfig = module.default;
|
|
36
|
+
const deserialized = await deserializeVirtualConfig(lazyConfig);
|
|
37
|
+
return {
|
|
38
|
+
...deserialized,
|
|
39
|
+
imageOptions: {
|
|
40
|
+
...imageOptionDefaults,
|
|
41
|
+
...deserialized.imageOptions
|
|
42
|
+
},
|
|
43
|
+
componentMetaTagFallbacks: {
|
|
44
|
+
...deserialized.componentMetaTagFallbacks,
|
|
45
|
+
...metaTagDefaults
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/image.ts
|
|
51
|
+
import { getConfiguredImageService, imageConfig } from "astro:assets";
|
|
52
|
+
import satori from "satori";
|
|
53
|
+
import { html as htmlToVNode } from "satori-html";
|
|
54
|
+
import he from "he";
|
|
55
|
+
async function htmlToPNG(html, satoriOptions) {
|
|
56
|
+
const responseTextWithDecodedHtmlEntities = he.decode(html);
|
|
57
|
+
const vnode = htmlToVNode(responseTextWithDecodedHtmlEntities);
|
|
58
|
+
const svg = await satori(vnode, satoriOptions);
|
|
59
|
+
const fileBuffer = Buffer.from(svg, "utf-8");
|
|
60
|
+
const uint8Array = new Uint8Array(
|
|
61
|
+
fileBuffer.buffer,
|
|
62
|
+
fileBuffer.byteOffset,
|
|
63
|
+
fileBuffer.byteLength
|
|
64
|
+
);
|
|
65
|
+
return bufferToTransformedPNG(uint8Array);
|
|
66
|
+
}
|
|
67
|
+
async function bufferToTransformedPNG(imageBuffer) {
|
|
68
|
+
const imageService = await getConfiguredImageService();
|
|
69
|
+
return imageService.transform(
|
|
70
|
+
imageBuffer,
|
|
71
|
+
{ src: "", format: "png" },
|
|
72
|
+
imageConfig
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/endpoint.ts
|
|
77
|
+
function makeOpengraphDevEndpoint({
|
|
78
|
+
template,
|
|
79
|
+
contentType = "text/html"
|
|
80
|
+
}) {
|
|
81
|
+
return async ({ locals, params, props, request }) => {
|
|
82
|
+
const config = await getResolvedConfig();
|
|
83
|
+
if (config.command !== "dev") {
|
|
84
|
+
return new Response(null, { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
if (!isAstroComponentFactory(template)) {
|
|
87
|
+
throw new Error(`Given is not an Astro template.`);
|
|
88
|
+
}
|
|
89
|
+
const container = await experimental_AstroContainer.create();
|
|
90
|
+
const templateStr = await container.renderToString(template, {
|
|
91
|
+
partial: false,
|
|
92
|
+
request,
|
|
93
|
+
params,
|
|
94
|
+
locals,
|
|
95
|
+
props
|
|
96
|
+
});
|
|
97
|
+
return new Response(templateStr, {
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": contentType
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function makeOpengraphEndpoint({
|
|
105
|
+
template,
|
|
106
|
+
imageOptionOverrides,
|
|
107
|
+
contentType = "image/png"
|
|
108
|
+
}) {
|
|
109
|
+
return async ({ locals, params, props, request }) => {
|
|
110
|
+
if (!isAstroComponentFactory(template)) {
|
|
111
|
+
throw new Error(`Given is not an Astro template.`);
|
|
112
|
+
}
|
|
113
|
+
const container = await experimental_AstroContainer.create();
|
|
114
|
+
const templateStr = await container.renderToString(template, {
|
|
115
|
+
partial: false,
|
|
116
|
+
request,
|
|
117
|
+
params,
|
|
118
|
+
locals,
|
|
119
|
+
props
|
|
120
|
+
});
|
|
121
|
+
const config = await getResolvedConfig();
|
|
122
|
+
const imageOptions = {
|
|
123
|
+
...config.imageOptions,
|
|
124
|
+
...imageOptionOverrides
|
|
125
|
+
};
|
|
126
|
+
const png = await htmlToPNG(templateStr, imageOptions);
|
|
127
|
+
return new Response(png.data, {
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": contentType
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
makeOpengraphDevEndpoint,
|
|
136
|
+
makeOpengraphEndpoint
|
|
137
|
+
};
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SatoriOptions } from "satori";
|
|
2
|
+
import type { LocalImageService } from "astro";
|
|
3
|
+
/**
|
|
4
|
+
* Convert an html string to an image
|
|
5
|
+
*/
|
|
6
|
+
export declare function htmlToPNG(html: string, satoriOptions: SatoriOptions): Promise<ReturnType<LocalImageService["transform"]>>;
|
|
7
|
+
//# sourceMappingURL=image.d.ts.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/virtual-user-config.ts
|
|
2
|
+
function resolveVirtualModuleId(id) {
|
|
3
|
+
return `\0${id}`;
|
|
4
|
+
}
|
|
5
|
+
function vitePluginOpengraphImageUserConfig(config) {
|
|
6
|
+
const modules = {
|
|
7
|
+
"virtual:astro-opengraph/user-config": `export default ${JSON.stringify(
|
|
8
|
+
config
|
|
9
|
+
)}`
|
|
10
|
+
};
|
|
11
|
+
const resolutionMap = Object.fromEntries(
|
|
12
|
+
Object.keys(modules).map((key) => [
|
|
13
|
+
resolveVirtualModuleId(key),
|
|
14
|
+
key
|
|
15
|
+
])
|
|
16
|
+
);
|
|
17
|
+
return {
|
|
18
|
+
name: "vite-plugin-astro-opengraph-user-config",
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id in modules) return resolveVirtualModuleId(id);
|
|
21
|
+
},
|
|
22
|
+
load(id) {
|
|
23
|
+
const resolution = resolutionMap[id];
|
|
24
|
+
if (resolution) return modules[resolution];
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/integration.ts
|
|
30
|
+
var integration_default = (config) => ({
|
|
31
|
+
name: "@altano/astro-opengraph",
|
|
32
|
+
hooks: {
|
|
33
|
+
async "astro:config:setup"({
|
|
34
|
+
updateConfig,
|
|
35
|
+
command,
|
|
36
|
+
addDevToolbarApp
|
|
37
|
+
}) {
|
|
38
|
+
const imageOptions = await config.getImageOptions();
|
|
39
|
+
const configSerializable = {
|
|
40
|
+
imageOptions,
|
|
41
|
+
command
|
|
42
|
+
};
|
|
43
|
+
updateConfig({
|
|
44
|
+
vite: {
|
|
45
|
+
// Shove the serializable config into the virtual module for later
|
|
46
|
+
// retrieval in the middleware
|
|
47
|
+
plugins: [vitePluginOpengraphImageUserConfig(configSerializable)]
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
addDevToolbarApp({
|
|
51
|
+
id: "@altano/astro-opengraph",
|
|
52
|
+
name: "Open Graph",
|
|
53
|
+
// icon: "🌠",
|
|
54
|
+
icon: "image",
|
|
55
|
+
// We don't use `@altano/astro-opengraph/toolbar` because that won't
|
|
56
|
+
// resolve when serving the test fixture for e2e runs (`pnpm
|
|
57
|
+
// test:e2e:server`)
|
|
58
|
+
entrypoint: new URL("./toolbar/index.js", import.meta.url)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
export {
|
|
64
|
+
integration_default as default
|
|
65
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
export declare function ImageZoomToolbar({ isHovered, imageSize, setImageSize, }: {
|
|
3
|
+
isHovered: boolean;
|
|
4
|
+
imageSize: number;
|
|
5
|
+
setImageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
//# sourceMappingURL=ImageZoomToolbar.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
export declare function ZoomButton({ value, imageSize, setImageSize, }: {
|
|
3
|
+
value: number;
|
|
4
|
+
imageSize: number;
|
|
5
|
+
setImageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
//# sourceMappingURL=ZoomButton.d.ts.map
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// src/toolbar/index.tsx
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { defineToolbarApp } from "astro/toolbar";
|
|
4
|
+
|
|
5
|
+
// src/toolbar/styles.tsx
|
|
6
|
+
var styles = (
|
|
7
|
+
/* css */
|
|
8
|
+
`
|
|
9
|
+
:host astro-dev-toolbar-window {
|
|
10
|
+
max-height: calc(100vh - 80px);
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
button {
|
|
15
|
+
background: none;
|
|
16
|
+
color: inherit;
|
|
17
|
+
border: none;
|
|
18
|
+
font: inherit;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
outline: inherit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
details {
|
|
24
|
+
margin-block-end: 10px;
|
|
25
|
+
}
|
|
26
|
+
details:last-of-type {
|
|
27
|
+
margin-block-end: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
details[open] summary, .image {
|
|
31
|
+
margin-block-end: 7px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.image img {
|
|
35
|
+
transition: max-width 0.1s;
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// src/toolbar/components/ToolbarApp.tsx
|
|
41
|
+
import { useState as useState4 } from "react";
|
|
42
|
+
|
|
43
|
+
// src/toolbar/components/Summary.tsx
|
|
44
|
+
import { jsx } from "react/jsx-runtime";
|
|
45
|
+
function Summary({ children }) {
|
|
46
|
+
return /* @__PURE__ */ jsx(
|
|
47
|
+
"summary",
|
|
48
|
+
{
|
|
49
|
+
style: {
|
|
50
|
+
cursor: "pointer",
|
|
51
|
+
fontWeight: 700,
|
|
52
|
+
color: "white"
|
|
53
|
+
},
|
|
54
|
+
children
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/toolbar/components/ExternalLinkicon.tsx
|
|
60
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
61
|
+
function ExternalLinkicon({
|
|
62
|
+
width = 27,
|
|
63
|
+
height = 27,
|
|
64
|
+
fill = "#020202"
|
|
65
|
+
}) {
|
|
66
|
+
return /* @__PURE__ */ jsx2(
|
|
67
|
+
"span",
|
|
68
|
+
{
|
|
69
|
+
style: {
|
|
70
|
+
display: "inline-block",
|
|
71
|
+
width,
|
|
72
|
+
height
|
|
73
|
+
},
|
|
74
|
+
children: /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 120 120", children: [
|
|
75
|
+
/* @__PURE__ */ jsx2("path", { d: "M120.09 16.696H60.256V.096L120.09.09z", fill }),
|
|
76
|
+
/* @__PURE__ */ jsx2(
|
|
77
|
+
"path",
|
|
78
|
+
{
|
|
79
|
+
d: "M57.99 48.64l42.573-42.574 13.475 13.475-42.574 42.58z",
|
|
80
|
+
fill
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
/* @__PURE__ */ jsx2(
|
|
84
|
+
"path",
|
|
85
|
+
{
|
|
86
|
+
d: "M119.98.107l.02 59.83-16.59.013-.02-59.846zM3 23.5h17v87H3zm83.49 52.56h17V113h-17z",
|
|
87
|
+
fill
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
/* @__PURE__ */ jsx2("path", { d: "M3 16.692h40.655v17H3zM3 96h100.49v17H3z", fill })
|
|
91
|
+
] })
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/toolbar/components/LinkOrText.tsx
|
|
97
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
98
|
+
function LinkOrText({
|
|
99
|
+
maybeLinkText
|
|
100
|
+
}) {
|
|
101
|
+
let parsedMaybeLink = null;
|
|
102
|
+
try {
|
|
103
|
+
if (maybeLinkText != null) {
|
|
104
|
+
const url = new URL(maybeLinkText);
|
|
105
|
+
parsedMaybeLink = /* @__PURE__ */ jsxs2("a", { href: url.toString(), target: "_blank", children: [
|
|
106
|
+
maybeLinkText,
|
|
107
|
+
" ",
|
|
108
|
+
/* @__PURE__ */ jsx3(ExternalLinkicon, { width: 10, height: 10, fill: "white" })
|
|
109
|
+
] });
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {
|
|
112
|
+
parsedMaybeLink = maybeLinkText;
|
|
113
|
+
}
|
|
114
|
+
return parsedMaybeLink;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/toolbar/hooks/useMetaTagInfo.tsx
|
|
118
|
+
function useMetaTagInfo() {
|
|
119
|
+
return Array.from(document.querySelectorAll("meta[property^='og:']")).map(
|
|
120
|
+
(tag) => [
|
|
121
|
+
tag.getAttribute("content"),
|
|
122
|
+
tag.getAttribute("property")
|
|
123
|
+
]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/toolbar/components/MetaTags.tsx
|
|
128
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
129
|
+
function MetaTags() {
|
|
130
|
+
const tags = useMetaTagInfo();
|
|
131
|
+
return /* @__PURE__ */ jsx4(
|
|
132
|
+
"dl",
|
|
133
|
+
{
|
|
134
|
+
"data-testid": "astro-opengraph-toolbar",
|
|
135
|
+
style: {
|
|
136
|
+
margin: 0,
|
|
137
|
+
display: "grid",
|
|
138
|
+
gridTemplateColumns: "max-content 1fr",
|
|
139
|
+
gap: 7,
|
|
140
|
+
columnGap: 13
|
|
141
|
+
},
|
|
142
|
+
children: tags.map((tag) => /* @__PURE__ */ jsx4(MetaTagContent, { tag }, tag[1]))
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
function MetaTagContent({ tag }) {
|
|
147
|
+
const [content, property] = tag;
|
|
148
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
149
|
+
/* @__PURE__ */ jsx4(
|
|
150
|
+
"dt",
|
|
151
|
+
{
|
|
152
|
+
style: {
|
|
153
|
+
color: "white",
|
|
154
|
+
textAlign: "right",
|
|
155
|
+
fontWeight: 500
|
|
156
|
+
},
|
|
157
|
+
children: property
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsx4("dd", { style: { margin: 0 }, children: /* @__PURE__ */ jsx4(LinkOrText, { maybeLinkText: content }) })
|
|
161
|
+
] });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/toolbar/hooks/useHoverState.tsx
|
|
165
|
+
import { useRef, useEffect } from "react";
|
|
166
|
+
function useHoverState(setHoverState) {
|
|
167
|
+
const ref = useRef(null);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
const { signal } = controller;
|
|
171
|
+
ref.current?.addEventListener("mouseenter", () => setHoverState(true), {
|
|
172
|
+
signal
|
|
173
|
+
});
|
|
174
|
+
ref.current?.addEventListener("mouseleave", () => setHoverState(false), {
|
|
175
|
+
signal
|
|
176
|
+
});
|
|
177
|
+
return () => controller.abort();
|
|
178
|
+
}, [setHoverState]);
|
|
179
|
+
return ref;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/toolbar/hooks/useImageURL.tsx
|
|
183
|
+
import { useState } from "react";
|
|
184
|
+
function useImageURL() {
|
|
185
|
+
const imageUrl = useImageURLIfAvailable();
|
|
186
|
+
if (imageUrl == null) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Shouldn't be rendering this component without an image URL`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return imageUrl;
|
|
192
|
+
}
|
|
193
|
+
function useImageURLIfAvailable() {
|
|
194
|
+
const [imageUrlRef] = useState(() => {
|
|
195
|
+
const meta = document.querySelector('meta[property="og:image"]');
|
|
196
|
+
return meta?.getAttribute("content") ?? null;
|
|
197
|
+
});
|
|
198
|
+
return imageUrlRef;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/toolbar/components/Image.tsx
|
|
202
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
203
|
+
function Image({ imageSize }) {
|
|
204
|
+
const imageUrl = useImageURL();
|
|
205
|
+
return /* @__PURE__ */ jsx5("div", { className: "image", children: /* @__PURE__ */ jsx5("a", { href: imageUrl, target: "_blank", title: "Open image in new tab", children: /* @__PURE__ */ jsx5(
|
|
206
|
+
"img",
|
|
207
|
+
{
|
|
208
|
+
alt: "Open Graph",
|
|
209
|
+
src: imageUrl,
|
|
210
|
+
style: {
|
|
211
|
+
maxHeight: "100%",
|
|
212
|
+
maxWidth: `${imageSize}%`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
) }) });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/toolbar/components/ZoomLabel.tsx
|
|
219
|
+
function ZoomLabel({ imageSize }) {
|
|
220
|
+
const formattedNumber = (imageSize / 100).toLocaleString(void 0, {
|
|
221
|
+
minimumFractionDigits: 2
|
|
222
|
+
});
|
|
223
|
+
return `${formattedNumber}x`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/toolbar/components/ZoomButton.tsx
|
|
227
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
228
|
+
function ZoomButton({
|
|
229
|
+
value,
|
|
230
|
+
imageSize,
|
|
231
|
+
setImageSize
|
|
232
|
+
}) {
|
|
233
|
+
const isSelected = value === imageSize;
|
|
234
|
+
return /* @__PURE__ */ jsx6(
|
|
235
|
+
"astro-dev-toolbar-button",
|
|
236
|
+
{
|
|
237
|
+
size: "small",
|
|
238
|
+
style: { padding: 0, margin: 0 },
|
|
239
|
+
buttonStyle: isSelected ? "gray" : "ghost",
|
|
240
|
+
className: "zoom-button",
|
|
241
|
+
role: "radio",
|
|
242
|
+
onClick: () => setImageSize(value),
|
|
243
|
+
children: /* @__PURE__ */ jsx6(ZoomLabel, { imageSize: value })
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/toolbar/components/ToolbarSection.tsx
|
|
249
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
250
|
+
function ToolbarSection({
|
|
251
|
+
icon,
|
|
252
|
+
children
|
|
253
|
+
}) {
|
|
254
|
+
return /* @__PURE__ */ jsxs4(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
style: {
|
|
258
|
+
fontSize: 12,
|
|
259
|
+
display: "flex",
|
|
260
|
+
alignItems: "center",
|
|
261
|
+
columnGap: 5
|
|
262
|
+
},
|
|
263
|
+
children: [
|
|
264
|
+
/* @__PURE__ */ jsx7(
|
|
265
|
+
"div",
|
|
266
|
+
{
|
|
267
|
+
style: {
|
|
268
|
+
display: "inline-block",
|
|
269
|
+
width: 12,
|
|
270
|
+
height: 12
|
|
271
|
+
},
|
|
272
|
+
children: icon
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
children
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/toolbar/components/ImageZoomToolbar.tsx
|
|
282
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
283
|
+
function ImageZoomToolbar({
|
|
284
|
+
isHovered,
|
|
285
|
+
imageSize,
|
|
286
|
+
setImageSize
|
|
287
|
+
}) {
|
|
288
|
+
return /* @__PURE__ */ jsx8(ToolbarSection, { icon: /* @__PURE__ */ jsx8("astro-dev-toolbar-icon", { icon: "gear" }), children: !isHovered ? /* @__PURE__ */ jsx8(
|
|
289
|
+
ZoomButton,
|
|
290
|
+
{
|
|
291
|
+
value: imageSize,
|
|
292
|
+
imageSize,
|
|
293
|
+
setImageSize
|
|
294
|
+
}
|
|
295
|
+
) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
296
|
+
/* @__PURE__ */ jsx8(
|
|
297
|
+
ZoomButton,
|
|
298
|
+
{
|
|
299
|
+
value: 25,
|
|
300
|
+
imageSize,
|
|
301
|
+
setImageSize
|
|
302
|
+
}
|
|
303
|
+
),
|
|
304
|
+
/* @__PURE__ */ jsx8(
|
|
305
|
+
ZoomButton,
|
|
306
|
+
{
|
|
307
|
+
value: 50,
|
|
308
|
+
imageSize,
|
|
309
|
+
setImageSize
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
/* @__PURE__ */ jsx8(
|
|
313
|
+
ZoomButton,
|
|
314
|
+
{
|
|
315
|
+
value: 75,
|
|
316
|
+
imageSize,
|
|
317
|
+
setImageSize
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
/* @__PURE__ */ jsx8(
|
|
321
|
+
ZoomButton,
|
|
322
|
+
{
|
|
323
|
+
value: 100,
|
|
324
|
+
imageSize,
|
|
325
|
+
setImageSize
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
] }) });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/toolbar/components/CopyImageUrlButton.tsx
|
|
332
|
+
import React2, {
|
|
333
|
+
useState as useState2,
|
|
334
|
+
useRef as useRef2,
|
|
335
|
+
useEffect as useEffect2
|
|
336
|
+
} from "react";
|
|
337
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
338
|
+
function CopyImageUrlButton() {
|
|
339
|
+
const imageUrl = useImageURL();
|
|
340
|
+
const [wasRecentlyClicked, setWasRecentlyClicked] = useState2(false);
|
|
341
|
+
const checkTimer = useRef2(null);
|
|
342
|
+
useEffect2(() => {
|
|
343
|
+
if (wasRecentlyClicked && checkTimer.current == null) {
|
|
344
|
+
checkTimer.current = setTimeout(() => {
|
|
345
|
+
setWasRecentlyClicked(false);
|
|
346
|
+
checkTimer.current = null;
|
|
347
|
+
}, 3e3);
|
|
348
|
+
}
|
|
349
|
+
}, [wasRecentlyClicked]);
|
|
350
|
+
const handleClick = React2.useCallback(
|
|
351
|
+
(evt) => {
|
|
352
|
+
void navigator.clipboard.writeText(imageUrl);
|
|
353
|
+
setWasRecentlyClicked(true);
|
|
354
|
+
evt.stopPropagation();
|
|
355
|
+
evt.preventDefault();
|
|
356
|
+
},
|
|
357
|
+
[imageUrl]
|
|
358
|
+
);
|
|
359
|
+
return /* @__PURE__ */ jsx9(ToolbarSection, { icon: /* @__PURE__ */ jsx9("astro-dev-toolbar-icon", { icon: "copy" }), children: /* @__PURE__ */ jsx9("div", { style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx9(
|
|
360
|
+
"astro-dev-toolbar-button",
|
|
361
|
+
{
|
|
362
|
+
size: "small",
|
|
363
|
+
buttonStyle: "gray",
|
|
364
|
+
onClick: handleClick,
|
|
365
|
+
children: wasRecentlyClicked ? "Copied to clipboard" : "Copy image URL"
|
|
366
|
+
}
|
|
367
|
+
) }) });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/toolbar/components/ImageSection.tsx
|
|
371
|
+
import { useState as useState3 } from "react";
|
|
372
|
+
import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
373
|
+
function ImageSection({
|
|
374
|
+
isHovered
|
|
375
|
+
}) {
|
|
376
|
+
const [imageSize, setImageSize] = useState3(100);
|
|
377
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
378
|
+
/* @__PURE__ */ jsx10(Image, { imageSize }),
|
|
379
|
+
/* @__PURE__ */ jsxs6(
|
|
380
|
+
"div",
|
|
381
|
+
{
|
|
382
|
+
"data-testid": "image-section-toolbar",
|
|
383
|
+
style: {
|
|
384
|
+
display: "flex",
|
|
385
|
+
flexDirection: "row",
|
|
386
|
+
alignItems: "center",
|
|
387
|
+
columnGap: 15
|
|
388
|
+
},
|
|
389
|
+
children: [
|
|
390
|
+
/* @__PURE__ */ jsx10(CopyImageUrlButton, {}),
|
|
391
|
+
/* @__PURE__ */ jsx10(
|
|
392
|
+
ImageZoomToolbar,
|
|
393
|
+
{
|
|
394
|
+
isHovered,
|
|
395
|
+
imageSize,
|
|
396
|
+
setImageSize
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
] });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/toolbar/components/ToolbarApp.tsx
|
|
406
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
407
|
+
function ToolbarApp() {
|
|
408
|
+
const [isImageDetailsHovered, setIsImageDetailsHovered] = useState4(false);
|
|
409
|
+
const imageDetailsRef = useHoverState(
|
|
410
|
+
setIsImageDetailsHovered
|
|
411
|
+
);
|
|
412
|
+
const imageUrl = useImageURLIfAvailable();
|
|
413
|
+
if (imageUrl == null) {
|
|
414
|
+
return /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: [
|
|
415
|
+
/* @__PURE__ */ jsx11(
|
|
416
|
+
"astro-dev-toolbar-icon",
|
|
417
|
+
{
|
|
418
|
+
style: { width: 24, height: 24 },
|
|
419
|
+
icon: "warning"
|
|
420
|
+
}
|
|
421
|
+
),
|
|
422
|
+
" ",
|
|
423
|
+
"No Open Graph image found on this page"
|
|
424
|
+
] });
|
|
425
|
+
}
|
|
426
|
+
return /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
427
|
+
/* @__PURE__ */ jsxs7("details", { open: true, className: "image", ref: imageDetailsRef, children: [
|
|
428
|
+
/* @__PURE__ */ jsx11(Summary, { children: "Image" }),
|
|
429
|
+
/* @__PURE__ */ jsx11(ImageSection, { isHovered: isImageDetailsHovered })
|
|
430
|
+
] }),
|
|
431
|
+
/* @__PURE__ */ jsxs7("details", { children: [
|
|
432
|
+
/* @__PURE__ */ jsx11(Summary, { children: "Meta Tags" }),
|
|
433
|
+
/* @__PURE__ */ jsx11(MetaTags, {})
|
|
434
|
+
] })
|
|
435
|
+
] });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/toolbar/components/ToolbarAppWindow.tsx
|
|
439
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
440
|
+
function ToolbarAppWindow() {
|
|
441
|
+
return /* @__PURE__ */ jsxs8("astro-dev-toolbar-window", { children: [
|
|
442
|
+
/* @__PURE__ */ jsx12("style", { children: styles }),
|
|
443
|
+
/* @__PURE__ */ jsx12(ToolbarApp, {})
|
|
444
|
+
] });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/toolbar/index.tsx
|
|
448
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
449
|
+
var SESSION_KEY_OPEN = "@altano/astro-opengraph/toolbar:is-open";
|
|
450
|
+
var toolbar_default = defineToolbarApp({
|
|
451
|
+
init(canvas, app) {
|
|
452
|
+
const root = createRoot(canvas);
|
|
453
|
+
app.onToggled((event) => {
|
|
454
|
+
if (event.state) {
|
|
455
|
+
sessionStorage.setItem(SESSION_KEY_OPEN, "true");
|
|
456
|
+
root.render(/* @__PURE__ */ jsx13(ToolbarAppWindow, {}));
|
|
457
|
+
} else {
|
|
458
|
+
sessionStorage.removeItem(SESSION_KEY_OPEN);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
app.toggleNotification({
|
|
462
|
+
state: document.querySelector('meta[property="og:image"]') != null,
|
|
463
|
+
level: "info"
|
|
464
|
+
});
|
|
465
|
+
if (sessionStorage.getItem(SESSION_KEY_OPEN) != null) {
|
|
466
|
+
app.toggleState({ state: true });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
export {
|
|
471
|
+
toolbar_default as default
|
|
472
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const styles = "\n :host astro-dev-toolbar-window {\n max-height: calc(100vh - 80px);\n overflow-y: auto;\n }\n\n button {\n background: none;\n color: inherit;\n border: none;\n font: inherit;\n cursor: pointer;\n outline: inherit;\n }\n\n details {\n margin-block-end: 10px;\n }\n details:last-of-type {\n margin-block-end: 0;\n }\n\n details[open] summary, .image {\n margin-block-end: 7px;\n }\n\n .image img {\n transition: max-width 0.1s;\n }\n";
|
|
2
|
+
//# sourceMappingURL=styles.d.ts.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SatoriOptions } from "satori";
|
|
2
|
+
export type ImageOptions = SatoriOptions;
|
|
3
|
+
export type FontWithBuffer = ImageOptions["fonts"][0];
|
|
4
|
+
export type FontWithPath = Omit<FontWithBuffer, "data"> & {
|
|
5
|
+
path: string;
|
|
6
|
+
};
|
|
7
|
+
type ImageOptionsBase = {
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type ImageOptionsBaseResolved = {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type ImageOptionsWithFontPaths = ImageOptionsBase & {
|
|
18
|
+
fonts: FontWithPath[];
|
|
19
|
+
};
|
|
20
|
+
export type ImageOptionsWithFontBuffers = ImageOptionsBase & {
|
|
21
|
+
fonts: FontWithBuffer[];
|
|
22
|
+
};
|
|
23
|
+
export type ImageOptionsWithFontBuffersResolved = ImageOptionsBaseResolved & {
|
|
24
|
+
fonts: FontWithBuffer[];
|
|
25
|
+
};
|
|
26
|
+
export type ComponentMetaTagFallbacks = {
|
|
27
|
+
"og:title"?: string;
|
|
28
|
+
"og:description"?: string;
|
|
29
|
+
"og:image"?: string;
|
|
30
|
+
};
|
|
31
|
+
export type MetaTagDefaults = {
|
|
32
|
+
"og:image": string;
|
|
33
|
+
};
|
|
34
|
+
export type OpengraphImageConfig = {
|
|
35
|
+
/**
|
|
36
|
+
* Options to use for the image file that is generated. Most importantly, fonts
|
|
37
|
+
* must be provided.
|
|
38
|
+
*/
|
|
39
|
+
getImageOptions(): Promise<ImageOptionsWithFontPaths>;
|
|
40
|
+
/**
|
|
41
|
+
* Defaults to use in the component for tags you don't manually set.
|
|
42
|
+
*/
|
|
43
|
+
componentMetaTagFallbacks?: ComponentMetaTagFallbacks;
|
|
44
|
+
};
|
|
45
|
+
type AstroConfigSetupHookOptions = Parameters<Astro.IntegrationHooks["astro:config:setup"]>[0];
|
|
46
|
+
type Command = AstroConfigSetupHookOptions["command"];
|
|
47
|
+
/**
|
|
48
|
+
* This must remain JSON-serializable!
|
|
49
|
+
*/
|
|
50
|
+
export type OpengraphImageConfigSerializable = {
|
|
51
|
+
imageOptions: ImageOptionsWithFontPaths;
|
|
52
|
+
componentMetaTagFallbacks?: ComponentMetaTagFallbacks;
|
|
53
|
+
command: Command;
|
|
54
|
+
};
|
|
55
|
+
export type OpengraphImageConfigSerializableMaybeMocked = OpengraphImageConfigSerializable | (() => OpengraphImageConfigSerializable);
|
|
56
|
+
export type OpengraphImageConfigDeserialized = {
|
|
57
|
+
imageOptions: ImageOptionsWithFontBuffers;
|
|
58
|
+
componentMetaTagFallbacks?: ComponentMetaTagFallbacks | undefined;
|
|
59
|
+
command: Command;
|
|
60
|
+
};
|
|
61
|
+
export type OpengraphImageConfigResolved = {
|
|
62
|
+
imageOptions: ImageOptionsWithFontBuffersResolved;
|
|
63
|
+
componentMetaTagFallbacks: ComponentMetaTagFallbacks & MetaTagDefaults;
|
|
64
|
+
command: Command;
|
|
65
|
+
};
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ViteUserConfig } from "astro";
|
|
2
|
+
import type { OpengraphImageConfigSerializable } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that exposes OpengraphImage user config and project context via
|
|
5
|
+
* virtual modules.
|
|
6
|
+
*/
|
|
7
|
+
export declare function vitePluginOpengraphImageUserConfig(config: OpengraphImageConfigSerializable): NonNullable<ViteUserConfig["plugins"]>[number];
|
|
8
|
+
//# sourceMappingURL=virtual-user-config.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altano/astro-opengraph",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.2",
|
|
4
4
|
"description": "Open Graph Tools for Astro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"./toolbar": "./dist/toolbar/index.js"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
|
+
"assets",
|
|
32
33
|
"components",
|
|
33
34
|
"dist/**/*.js",
|
|
34
35
|
"dist/**/*.d.ts"
|
|
@@ -57,11 +58,11 @@
|
|
|
57
58
|
"typescript": "^5.8.2",
|
|
58
59
|
"vite": "^6.2.5",
|
|
59
60
|
"vitest": "^3.1.1",
|
|
61
|
+
"@altano/assets": "0.0.0",
|
|
60
62
|
"@altano/testing": "0.0.0",
|
|
61
|
-
"@altano/
|
|
63
|
+
"@altano/vitest-plugins": "1.1.0",
|
|
62
64
|
"@altano/build-config": "0.0.0",
|
|
63
|
-
"@altano/
|
|
64
|
-
"@altano/vitest-plugins": "1.1.0"
|
|
65
|
+
"@altano/tsconfig": "0.0.0"
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@types/react": ">=18",
|