@lacspace/og 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/README.md +94 -0
- package/dist/index.cjs +309 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence — a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/og
|
|
4
|
+
|
|
5
|
+
**Dynamic Open Graph images — configure the look once, call it per page.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/og)
|
|
8
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> Every page deserves a gorgeous share card. `@lacspace/og` gives you one design system that renders **two ways from the same options** — a `next/og` element tree (real PNG via Satori) and a standalone **SVG** (no browser, no deps) — with auto-fitting titles, eyebrows, badges, logos and gradients.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @lacspace/og
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use it with `next/og` (dynamic PNG)
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// app/og/route.tsx
|
|
24
|
+
import { ImageResponse } from "next/og";
|
|
25
|
+
import { ogCard } from "@lacspace/og";
|
|
26
|
+
|
|
27
|
+
export const runtime = "edge";
|
|
28
|
+
|
|
29
|
+
export function GET(req: Request) {
|
|
30
|
+
const title = new URL(req.url).searchParams.get("title") ?? "My site";
|
|
31
|
+
return new ImageResponse(
|
|
32
|
+
ogCard({
|
|
33
|
+
title,
|
|
34
|
+
eyebrow: "Guide",
|
|
35
|
+
subtitle: "my-site.com",
|
|
36
|
+
logo: "M",
|
|
37
|
+
from: "#22d3ee",
|
|
38
|
+
to: "#6366f1",
|
|
39
|
+
}) as any,
|
|
40
|
+
{ width: 1200, height: 630 },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then point your metadata at it: `openGraph.images = ["/og?title=" + encodeURIComponent(pageTitle)]`.
|
|
46
|
+
|
|
47
|
+
## Use it anywhere (zero-dependency SVG)
|
|
48
|
+
|
|
49
|
+
`ogSvg()` needs no browser and no Satori — perfect for previews, email headers, README banners, or a guaranteed fallback:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { ogSvg, ogSvgDataUri } from "@lacspace/og";
|
|
53
|
+
|
|
54
|
+
const svg = ogSvg({ title: "Launch day", badge: "NEW", from: "#f43f5e", to: "#f59e0b" });
|
|
55
|
+
// -> "<svg …>…</svg>" (write to a file, return as image/svg+xml, embed inline)
|
|
56
|
+
|
|
57
|
+
const uri = ogSvgDataUri({ title: "Hello" });
|
|
58
|
+
// -> "data:image/svg+xml;utf8,…" (drop straight into <img src> or background-image)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
| Option | Meaning |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `title` | Headline — **font size auto-fits** to length |
|
|
66
|
+
| `subtitle` | Secondary line (often your domain) |
|
|
67
|
+
| `eyebrow` | Small uppercase label above the title |
|
|
68
|
+
| `badge` | A pill (e.g. `"NEW"`, `"Guide"`) |
|
|
69
|
+
| `logo` | Initial / emoji for the corner mark |
|
|
70
|
+
| `footer` | Bottom-left line (e.g. your URL) |
|
|
71
|
+
| `from` / `to` | Accent gradient stops |
|
|
72
|
+
| `theme` | `"dark"` (default) or `"light"` |
|
|
73
|
+
| `preset` | `"gradient"` · `"split"` · `"minimal"` · `"terminal"` |
|
|
74
|
+
| `width` / `height` | Canvas — defaults to the OG standard `1200×630` |
|
|
75
|
+
|
|
76
|
+
Also exports `fitFontSize(title)` if you want the sizing math for your own layout.
|
|
77
|
+
|
|
78
|
+
Pairs with [`@lacspace/seo`](https://www.npmjs.com/package/@lacspace/seo) — feed the generated URL to `defineSite().meta({ image })`.
|
|
79
|
+
|
|
80
|
+
## Licensing
|
|
81
|
+
|
|
82
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice. See the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<div align="center">
|
|
87
|
+
|
|
88
|
+
**Part of the Lacspace ecosystem — zero-dependency, isomorphic TypeScript packages.**
|
|
89
|
+
|
|
90
|
+
[All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
|
|
91
|
+
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULTS = {
|
|
5
|
+
from: "#22d3ee",
|
|
6
|
+
to: "#6366f1",
|
|
7
|
+
theme: "dark",
|
|
8
|
+
preset: "gradient",
|
|
9
|
+
width: 1200,
|
|
10
|
+
height: 630
|
|
11
|
+
};
|
|
12
|
+
function resolve(o) {
|
|
13
|
+
return {
|
|
14
|
+
title: o.title,
|
|
15
|
+
subtitle: o.subtitle,
|
|
16
|
+
eyebrow: o.eyebrow,
|
|
17
|
+
badge: o.badge,
|
|
18
|
+
logo: o.logo,
|
|
19
|
+
footer: o.footer,
|
|
20
|
+
from: o.from ?? DEFAULTS.from,
|
|
21
|
+
to: o.to ?? DEFAULTS.to,
|
|
22
|
+
theme: o.theme ?? DEFAULTS.theme,
|
|
23
|
+
preset: o.preset ?? DEFAULTS.preset,
|
|
24
|
+
width: o.width ?? DEFAULTS.width,
|
|
25
|
+
height: o.height ?? DEFAULTS.height
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function fitFontSize(title, opts = {}) {
|
|
29
|
+
const max = opts.max ?? 78;
|
|
30
|
+
const min = opts.min ?? 42;
|
|
31
|
+
const len = title.length;
|
|
32
|
+
if (len <= 20) return max;
|
|
33
|
+
if (len >= 90) return min;
|
|
34
|
+
const t = (len - 20) / (90 - 20);
|
|
35
|
+
return Math.round(max - t * (max - min));
|
|
36
|
+
}
|
|
37
|
+
function palette(r) {
|
|
38
|
+
const dark = r.theme === "dark";
|
|
39
|
+
return {
|
|
40
|
+
bg: dark ? "#0a0a0f" : "#ffffff",
|
|
41
|
+
fg: dark ? "#ffffff" : "#0a0a0f",
|
|
42
|
+
muted: dark ? "rgba(255,255,255,0.68)" : "rgba(10,10,15,0.62)",
|
|
43
|
+
border: dark ? "rgba(255,255,255,0.14)" : "rgba(10,10,15,0.10)",
|
|
44
|
+
from: r.from,
|
|
45
|
+
to: r.to
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function el(type, style, children) {
|
|
49
|
+
return { type, key: null, props: { style, ...children === void 0 ? {} : { children } } };
|
|
50
|
+
}
|
|
51
|
+
function ogCard(options) {
|
|
52
|
+
const r = resolve(options);
|
|
53
|
+
const p = palette(r);
|
|
54
|
+
const pad = Math.round(r.width * 0.075);
|
|
55
|
+
const titleSize = fitFontSize(r.title);
|
|
56
|
+
const accentBar = el("div", {
|
|
57
|
+
position: "absolute",
|
|
58
|
+
top: "0",
|
|
59
|
+
left: "0",
|
|
60
|
+
width: "100%",
|
|
61
|
+
height: `${Math.round(r.height * 0.02)}px`,
|
|
62
|
+
background: `linear-gradient(90deg, ${p.from}, ${p.to})`
|
|
63
|
+
});
|
|
64
|
+
const glow = el("div", {
|
|
65
|
+
position: "absolute",
|
|
66
|
+
top: `-${Math.round(r.height * 0.35)}px`,
|
|
67
|
+
right: `-${Math.round(r.width * 0.18)}px`,
|
|
68
|
+
width: `${Math.round(r.width * 0.55)}px`,
|
|
69
|
+
height: `${Math.round(r.width * 0.55)}px`,
|
|
70
|
+
borderRadius: "9999px",
|
|
71
|
+
background: `linear-gradient(135deg, ${p.from}, ${p.to})`,
|
|
72
|
+
opacity: r.theme === "dark" ? "0.22" : "0.16",
|
|
73
|
+
filter: "blur(20px)"
|
|
74
|
+
});
|
|
75
|
+
const topRow = [];
|
|
76
|
+
if (r.eyebrow) {
|
|
77
|
+
topRow.push(
|
|
78
|
+
el(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
display: "flex",
|
|
82
|
+
fontSize: "26px",
|
|
83
|
+
fontWeight: 600,
|
|
84
|
+
letterSpacing: "0.12em",
|
|
85
|
+
textTransform: "uppercase",
|
|
86
|
+
color: p.from
|
|
87
|
+
},
|
|
88
|
+
r.eyebrow.toUpperCase()
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (r.badge) {
|
|
93
|
+
topRow.push(
|
|
94
|
+
el(
|
|
95
|
+
"div",
|
|
96
|
+
{
|
|
97
|
+
display: "flex",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
padding: "8px 20px",
|
|
100
|
+
borderRadius: "9999px",
|
|
101
|
+
fontSize: "24px",
|
|
102
|
+
fontWeight: 700,
|
|
103
|
+
color: p.fg,
|
|
104
|
+
background: `linear-gradient(90deg, ${p.from}33, ${p.to}33)`,
|
|
105
|
+
border: `1px solid ${p.border}`
|
|
106
|
+
},
|
|
107
|
+
r.badge
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const header = el(
|
|
112
|
+
"div",
|
|
113
|
+
{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" },
|
|
114
|
+
[
|
|
115
|
+
topRow.length ? el("div", { display: "flex", gap: "20px", alignItems: "center" }, topRow) : el("div", { display: "flex" }, ""),
|
|
116
|
+
r.logo ? el(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
display: "flex",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
width: "76px",
|
|
123
|
+
height: "76px",
|
|
124
|
+
borderRadius: "20px",
|
|
125
|
+
fontSize: "40px",
|
|
126
|
+
fontWeight: 800,
|
|
127
|
+
color: "#0a0a0f",
|
|
128
|
+
background: `linear-gradient(135deg, ${p.from}, ${p.to})`
|
|
129
|
+
},
|
|
130
|
+
r.logo
|
|
131
|
+
) : el("div", { display: "flex" }, "")
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
const body = [
|
|
135
|
+
el(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
display: "flex",
|
|
139
|
+
fontSize: `${titleSize}px`,
|
|
140
|
+
fontWeight: 800,
|
|
141
|
+
lineHeight: 1.05,
|
|
142
|
+
letterSpacing: "-0.02em",
|
|
143
|
+
color: p.fg
|
|
144
|
+
},
|
|
145
|
+
r.title
|
|
146
|
+
)
|
|
147
|
+
];
|
|
148
|
+
if (r.subtitle) {
|
|
149
|
+
body.push(
|
|
150
|
+
el(
|
|
151
|
+
"div",
|
|
152
|
+
{ display: "flex", marginTop: "28px", fontSize: "34px", fontWeight: 500, color: p.muted },
|
|
153
|
+
r.subtitle
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const children = [
|
|
158
|
+
accentBar,
|
|
159
|
+
glow,
|
|
160
|
+
header,
|
|
161
|
+
el("div", { display: "flex", flexDirection: "column" }, body)
|
|
162
|
+
];
|
|
163
|
+
if (r.footer) {
|
|
164
|
+
children.push(
|
|
165
|
+
el(
|
|
166
|
+
"div",
|
|
167
|
+
{ display: "flex", fontSize: "26px", fontWeight: 500, color: p.muted },
|
|
168
|
+
r.footer
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return el(
|
|
173
|
+
"div",
|
|
174
|
+
{
|
|
175
|
+
position: "relative",
|
|
176
|
+
display: "flex",
|
|
177
|
+
flexDirection: "column",
|
|
178
|
+
justifyContent: "space-between",
|
|
179
|
+
width: "100%",
|
|
180
|
+
height: "100%",
|
|
181
|
+
padding: `${pad}px`,
|
|
182
|
+
background: r.preset === "split" ? `linear-gradient(135deg, ${p.from}, ${p.to})` : p.bg,
|
|
183
|
+
color: p.fg,
|
|
184
|
+
fontFamily: "sans-serif"
|
|
185
|
+
},
|
|
186
|
+
children
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function esc(s) {
|
|
190
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
191
|
+
}
|
|
192
|
+
function wrap(text, fontSize, maxWidth, maxLines) {
|
|
193
|
+
const charW = fontSize * 0.56;
|
|
194
|
+
const perLine = Math.max(1, Math.floor(maxWidth / charW));
|
|
195
|
+
const words = text.split(/\s+/);
|
|
196
|
+
const lines = [];
|
|
197
|
+
let cur = "";
|
|
198
|
+
for (const w of words) {
|
|
199
|
+
const next = cur ? `${cur} ${w}` : w;
|
|
200
|
+
if (next.length > perLine && cur) {
|
|
201
|
+
lines.push(cur);
|
|
202
|
+
cur = w;
|
|
203
|
+
if (lines.length === maxLines - 1) break;
|
|
204
|
+
} else {
|
|
205
|
+
cur = next;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
209
|
+
const used = lines.join(" ").length;
|
|
210
|
+
if (used < text.length && lines.length) {
|
|
211
|
+
lines[lines.length - 1] = `${lines[lines.length - 1].replace(/\.*$/, "")}\u2026`;
|
|
212
|
+
}
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
function ogSvg(options) {
|
|
216
|
+
const r = resolve(options);
|
|
217
|
+
const p = palette(r);
|
|
218
|
+
const W = r.width;
|
|
219
|
+
const H = r.height;
|
|
220
|
+
const pad = Math.round(W * 0.075);
|
|
221
|
+
const titleSize = fitFontSize(r.title);
|
|
222
|
+
const contentW = W - pad * 2;
|
|
223
|
+
const split = r.preset === "split";
|
|
224
|
+
const titleLines = wrap(r.title, titleSize, contentW, 3);
|
|
225
|
+
const titleBlockH = titleLines.length * titleSize * 1.08;
|
|
226
|
+
const titleTop = H / 2 - titleBlockH / 2 + titleSize * 0.8;
|
|
227
|
+
const parts = [];
|
|
228
|
+
parts.push(
|
|
229
|
+
`<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" font-family="Inter, ui-sans-serif, system-ui, sans-serif">`
|
|
230
|
+
);
|
|
231
|
+
parts.push(`<defs>
|
|
232
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
|
233
|
+
<stop offset="0" stop-color="${esc(p.from)}"/><stop offset="1" stop-color="${esc(p.to)}"/>
|
|
234
|
+
</linearGradient>
|
|
235
|
+
<radialGradient id="glow" cx="0.85" cy="0.1" r="0.7">
|
|
236
|
+
<stop offset="0" stop-color="${esc(p.from)}" stop-opacity="${r.theme === "dark" ? 0.28 : 0.2}"/>
|
|
237
|
+
<stop offset="1" stop-color="${esc(p.to)}" stop-opacity="0"/>
|
|
238
|
+
</radialGradient>
|
|
239
|
+
</defs>`);
|
|
240
|
+
parts.push(`<rect width="${W}" height="${H}" fill="${split ? "url(#accent)" : esc(p.bg)}"/>`);
|
|
241
|
+
if (!split) parts.push(`<rect width="${W}" height="${H}" fill="url(#glow)"/>`);
|
|
242
|
+
parts.push(`<rect width="${W}" height="${Math.round(H * 0.02)}" fill="url(#accent)"/>`);
|
|
243
|
+
let topY = pad + 34;
|
|
244
|
+
if (r.eyebrow) {
|
|
245
|
+
parts.push(
|
|
246
|
+
`<text x="${pad}" y="${topY}" font-size="26" font-weight="600" letter-spacing="3" fill="${esc(p.from)}">${esc(
|
|
247
|
+
r.eyebrow.toUpperCase()
|
|
248
|
+
)}</text>`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (r.logo) {
|
|
252
|
+
const s = 76;
|
|
253
|
+
const lx = W - pad - s;
|
|
254
|
+
const ly = pad;
|
|
255
|
+
parts.push(`<rect x="${lx}" y="${ly}" width="${s}" height="${s}" rx="20" fill="url(#accent)"/>`);
|
|
256
|
+
parts.push(
|
|
257
|
+
`<text x="${lx + s / 2}" y="${ly + s / 2 + 14}" font-size="40" font-weight="800" text-anchor="middle" fill="#0a0a0f">${esc(
|
|
258
|
+
r.logo
|
|
259
|
+
)}</text>`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (r.badge) {
|
|
263
|
+
const bx = pad + (r.eyebrow ? 0 : 0);
|
|
264
|
+
parts.push(
|
|
265
|
+
`<rect x="${bx}" y="${topY + 14}" width="${r.badge.length * 16 + 40}" height="44" rx="22" fill="${esc(
|
|
266
|
+
p.from
|
|
267
|
+
)}" opacity="0.18"/>`
|
|
268
|
+
);
|
|
269
|
+
parts.push(
|
|
270
|
+
`<text x="${bx + 20}" y="${topY + 43}" font-size="24" font-weight="700" fill="${esc(p.fg)}">${esc(
|
|
271
|
+
r.badge
|
|
272
|
+
)}</text>`
|
|
273
|
+
);
|
|
274
|
+
topY += 60;
|
|
275
|
+
}
|
|
276
|
+
titleLines.forEach((line, i) => {
|
|
277
|
+
parts.push(
|
|
278
|
+
`<text x="${pad}" y="${titleTop + i * titleSize * 1.08}" font-size="${titleSize}" font-weight="800" letter-spacing="-1" fill="${esc(
|
|
279
|
+
p.fg
|
|
280
|
+
)}">${esc(line)}</text>`
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
if (r.subtitle) {
|
|
284
|
+
parts.push(
|
|
285
|
+
`<text x="${pad}" y="${titleTop + titleLines.length * titleSize * 1.08 + 46}" font-size="34" font-weight="500" fill="${esc(
|
|
286
|
+
p.muted
|
|
287
|
+
)}">${esc(r.subtitle)}</text>`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (r.footer) {
|
|
291
|
+
parts.push(
|
|
292
|
+
`<text x="${pad}" y="${H - pad}" font-size="26" font-weight="500" fill="${esc(p.muted)}">${esc(
|
|
293
|
+
r.footer
|
|
294
|
+
)}</text>`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
parts.push(`</svg>`);
|
|
298
|
+
return parts.join("");
|
|
299
|
+
}
|
|
300
|
+
function ogSvgDataUri(options) {
|
|
301
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(ogSvg(options))}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
exports.fitFontSize = fitFontSize;
|
|
305
|
+
exports.ogCard = ogCard;
|
|
306
|
+
exports.ogSvg = ogSvg;
|
|
307
|
+
exports.ogSvgDataUri = ogSvgDataUri;
|
|
308
|
+
//# sourceMappingURL=index.cjs.map
|
|
309
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAgEA,IAAM,QAAA,GAAW;AAAA,EACf,IAAA,EAAM,SAAA;AAAA,EACN,EAAA,EAAI,SAAA;AAAA,EACJ,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,UAAA;AAAA,EACR,KAAA,EAAO,IAAA;AAAA,EACP,MAAA,EAAQ;AACV,CAAA;AAEA,SAAS,QAAQ,CAAA,EAAwB;AACvC,EAAA,OAAO;AAAA,IACL,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,UAAU,CAAA,CAAE,QAAA;AAAA,IACZ,SAAS,CAAA,CAAE,OAAA;AAAA,IACX,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,QAAQ,CAAA,CAAE,MAAA;AAAA,IACV,IAAA,EAAM,CAAA,CAAE,IAAA,IAAQ,QAAA,CAAS,IAAA;AAAA,IACzB,EAAA,EAAI,CAAA,CAAE,EAAA,IAAM,QAAA,CAAS,EAAA;AAAA,IACrB,KAAA,EAAO,CAAA,CAAE,KAAA,IAAS,QAAA,CAAS,KAAA;AAAA,IAC3B,MAAA,EAAQ,CAAA,CAAE,MAAA,IAAU,QAAA,CAAS,MAAA;AAAA,IAC7B,KAAA,EAAO,CAAA,CAAE,KAAA,IAAS,QAAA,CAAS,KAAA;AAAA,IAC3B,MAAA,EAAQ,CAAA,CAAE,MAAA,IAAU,QAAA,CAAS;AAAA,GAC/B;AACF;AAOO,SAAS,WAAA,CACd,KAAA,EACA,IAAA,GAAuC,EAAC,EAChC;AACR,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,IAAO,EAAA;AACxB,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,IAAO,EAAA;AACxB,EAAA,MAAM,MAAM,KAAA,CAAM,MAAA;AAClB,EAAA,IAAI,GAAA,IAAO,IAAI,OAAO,GAAA;AACtB,EAAA,IAAI,GAAA,IAAO,IAAI,OAAO,GAAA;AAEtB,EAAA,MAAM,CAAA,GAAA,CAAK,GAAA,GAAM,EAAA,KAAO,EAAA,GAAK,EAAA,CAAA;AAC7B,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,CAAA,IAAK,MAAM,GAAA,CAAI,CAAA;AACzC;AAWA,SAAS,QAAQ,CAAA,EAAsB;AACrC,EAAA,MAAM,IAAA,GAAO,EAAE,KAAA,KAAU,MAAA;AACzB,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,OAAO,SAAA,GAAY,SAAA;AAAA,IACvB,EAAA,EAAI,OAAO,SAAA,GAAY,SAAA;AAAA,IACvB,KAAA,EAAO,OAAO,wBAAA,GAA2B,qBAAA;AAAA,IACzC,MAAA,EAAQ,OAAO,wBAAA,GAA2B,qBAAA;AAAA,IAC1C,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,IAAI,CAAA,CAAE;AAAA,GACR;AACF;AAaA,SAAS,EAAA,CAAG,IAAA,EAAc,KAAA,EAAgB,QAAA,EAA0D;AAClG,EAAA,OAAO,EAAE,IAAA,EAAM,GAAA,EAAK,IAAA,EAAM,OAAO,EAAE,KAAA,EAAO,GAAI,QAAA,KAAa,SAAY,EAAC,GAAI,EAAE,QAAA,IAAY,EAAE;AAC9F;AASO,SAAS,OAAO,OAAA,EAA4B;AACjD,EAAA,MAAM,CAAA,GAAI,QAAQ,OAAO,CAAA;AACzB,EAAA,MAAM,CAAA,GAAI,QAAQ,CAAC,CAAA;AACnB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,QAAQ,KAAK,CAAA;AACtC,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,CAAA,CAAE,KAAK,CAAA;AAErC,EAAA,MAAM,SAAA,GAAY,GAAG,KAAA,EAAO;AAAA,IAC1B,QAAA,EAAU,UAAA;AAAA,IACV,GAAA,EAAK,GAAA;AAAA,IACL,IAAA,EAAM,GAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,QAAQ,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAE,MAAA,GAAS,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,IACtC,YAAY,CAAA,uBAAA,EAA0B,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,EAAE,EAAE,CAAA,CAAA;AAAA,GACtD,CAAA;AAED,EAAA,MAAM,IAAA,GAAO,GAAG,KAAA,EAAO;AAAA,IACrB,QAAA,EAAU,UAAA;AAAA,IACV,KAAK,CAAA,CAAA,EAAI,IAAA,CAAK,MAAM,CAAA,CAAE,MAAA,GAAS,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,IACpC,OAAO,CAAA,CAAA,EAAI,IAAA,CAAK,MAAM,CAAA,CAAE,KAAA,GAAQ,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,IACrC,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAE,KAAA,GAAQ,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,IACpC,QAAQ,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAE,KAAA,GAAQ,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,IACrC,YAAA,EAAc,QAAA;AAAA,IACd,YAAY,CAAA,wBAAA,EAA2B,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,EAAE,EAAE,CAAA,CAAA,CAAA;AAAA,IACtD,OAAA,EAAS,CAAA,CAAE,KAAA,KAAU,MAAA,GAAS,MAAA,GAAS,MAAA;AAAA,IACvC,MAAA,EAAQ;AAAA,GACT,CAAA;AAED,EAAA,MAAM,SAA8B,EAAC;AACrC,EAAA,IAAI,EAAE,OAAA,EAAS;AACb,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,EAAA;AAAA,QACE,KAAA;AAAA,QACA;AAAA,UACE,OAAA,EAAS,MAAA;AAAA,UACT,QAAA,EAAU,MAAA;AAAA,UACV,UAAA,EAAY,GAAA;AAAA,UACZ,aAAA,EAAe,QAAA;AAAA,UACf,aAAA,EAAe,WAAA;AAAA,UACf,OAAO,CAAA,CAAE;AAAA,SACX;AAAA,QACA,CAAA,CAAE,QAAQ,WAAA;AAAY;AACxB,KACF;AAAA,EACF;AACA,EAAA,IAAI,EAAE,KAAA,EAAO;AACX,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,EAAA;AAAA,QACE,KAAA;AAAA,QACA;AAAA,UACE,OAAA,EAAS,MAAA;AAAA,UACT,UAAA,EAAY,QAAA;AAAA,UACZ,OAAA,EAAS,UAAA;AAAA,UACT,YAAA,EAAc,QAAA;AAAA,UACd,QAAA,EAAU,MAAA;AAAA,UACV,UAAA,EAAY,GAAA;AAAA,UACZ,OAAO,CAAA,CAAE,EAAA;AAAA,UACT,YAAY,CAAA,uBAAA,EAA0B,CAAA,CAAE,IAAI,CAAA,IAAA,EAAO,EAAE,EAAE,CAAA,GAAA,CAAA;AAAA,UACvD,MAAA,EAAQ,CAAA,UAAA,EAAa,CAAA,CAAE,MAAM,CAAA;AAAA,SAC/B;AAAA,QACA,CAAA,CAAE;AAAA;AACJ,KACF;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,EAAA;AAAA,IACb,KAAA;AAAA,IACA,EAAE,SAAS,MAAA,EAAQ,UAAA,EAAY,UAAU,cAAA,EAAgB,eAAA,EAAiB,OAAO,MAAA,EAAO;AAAA,IACxF;AAAA,MACE,MAAA,CAAO,SACH,EAAA,CAAG,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,KAAK,MAAA,EAAQ,UAAA,EAAY,UAAS,EAAG,MAAM,IACxE,EAAA,CAAG,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,IAAU,EAAE,CAAA;AAAA,MACrC,EAAE,IAAA,GACE,EAAA;AAAA,QACE,KAAA;AAAA,QACA;AAAA,UACE,OAAA,EAAS,MAAA;AAAA,UACT,UAAA,EAAY,QAAA;AAAA,UACZ,cAAA,EAAgB,QAAA;AAAA,UAChB,KAAA,EAAO,MAAA;AAAA,UACP,MAAA,EAAQ,MAAA;AAAA,UACR,YAAA,EAAc,MAAA;AAAA,UACd,QAAA,EAAU,MAAA;AAAA,UACV,UAAA,EAAY,GAAA;AAAA,UACZ,KAAA,EAAO,SAAA;AAAA,UACP,YAAY,CAAA,wBAAA,EAA2B,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,EAAE,EAAE,CAAA,CAAA;AAAA,SACxD;AAAA,QACA,CAAA,CAAE;AAAA,UAEJ,EAAA,CAAG,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,IAAU,EAAE;AAAA;AACvC,GACF;AAEA,EAAA,MAAM,IAAA,GAA4B;AAAA,IAChC,EAAA;AAAA,MACE,KAAA;AAAA,MACA;AAAA,QACE,OAAA,EAAS,MAAA;AAAA,QACT,QAAA,EAAU,GAAG,SAAS,CAAA,EAAA,CAAA;AAAA,QACtB,UAAA,EAAY,GAAA;AAAA,QACZ,UAAA,EAAY,IAAA;AAAA,QACZ,aAAA,EAAe,SAAA;AAAA,QACf,OAAO,CAAA,CAAE;AAAA,OACX;AAAA,MACA,CAAA,CAAE;AAAA;AACJ,GACF;AACA,EAAA,IAAI,EAAE,QAAA,EAAU;AACd,IAAA,IAAA,CAAK,IAAA;AAAA,MACH,EAAA;AAAA,QACE,KAAA;AAAA,QACA,EAAE,OAAA,EAAS,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,KAAA,EAAO,CAAA,CAAE,KAAA,EAAM;AAAA,QACxF,CAAA,CAAE;AAAA;AACJ,KACF;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAqB;AAAA,IACzB,SAAA;AAAA,IACA,IAAA;AAAA,IACA,MAAA;AAAA,IACA,EAAA,CAAG,OAAO,EAAE,OAAA,EAAS,QAAQ,aAAA,EAAe,QAAA,IAAY,IAAI;AAAA,GAC9D;AAEA,EAAA,IAAI,EAAE,MAAA,EAAQ;AACZ,IAAA,QAAA,CAAS,IAAA;AAAA,MACP,EAAA;AAAA,QACE,KAAA;AAAA,QACA,EAAE,SAAS,MAAA,EAAQ,QAAA,EAAU,QAAQ,UAAA,EAAY,GAAA,EAAK,KAAA,EAAO,CAAA,CAAE,KAAA,EAAM;AAAA,QACrE,CAAA,CAAE;AAAA;AACJ,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAA;AAAA,IACL,KAAA;AAAA,IACA;AAAA,MACE,QAAA,EAAU,UAAA;AAAA,MACV,OAAA,EAAS,MAAA;AAAA,MACT,aAAA,EAAe,QAAA;AAAA,MACf,cAAA,EAAgB,eAAA;AAAA,MAChB,KAAA,EAAO,MAAA;AAAA,MACP,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,GAAG,GAAG,CAAA,EAAA,CAAA;AAAA,MACf,UAAA,EAAY,CAAA,CAAE,MAAA,KAAW,OAAA,GAAU,CAAA,wBAAA,EAA2B,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,CAAA,CAAE,EAAE,CAAA,CAAA,CAAA,GAAM,CAAA,CAAE,EAAA;AAAA,MACrF,OAAO,CAAA,CAAE,EAAA;AAAA,MACT,UAAA,EAAY;AAAA,KACd;AAAA,IACA;AAAA,GACF;AACF;AAMA,SAAS,IAAI,CAAA,EAAmB;AAC9B,EAAA,OAAO,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,OAAO,EAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,QAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,OAAA,CAAQ,MAAM,QAAQ,CAAA;AACpG;AAGA,SAAS,IAAA,CAAK,IAAA,EAAc,QAAA,EAAkB,QAAA,EAAkB,QAAA,EAA4B;AAC1F,EAAA,MAAM,QAAQ,QAAA,GAAW,IAAA;AACzB,EAAA,MAAM,OAAA,GAAU,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,QAAA,GAAW,KAAK,CAAC,CAAA;AACxD,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC9B,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,MAAM,OAAO,GAAA,GAAM,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,GAAK,CAAA;AACnC,IAAA,IAAI,IAAA,CAAK,MAAA,GAAS,OAAA,IAAW,GAAA,EAAK;AAChC,MAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AACd,MAAA,GAAA,GAAM,CAAA;AACN,MAAA,IAAI,KAAA,CAAM,MAAA,KAAW,QAAA,GAAW,CAAA,EAAG;AAAA,IACrC,CAAA,MAAO;AACL,MAAA,GAAA,GAAM,IAAA;AAAA,IACR;AAAA,EACF;AACA,EAAA,IAAI,OAAO,KAAA,CAAM,MAAA,GAAS,QAAA,EAAU,KAAA,CAAM,KAAK,GAAG,CAAA;AAClD,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA,CAAE,MAAA;AAC7B,EAAA,IAAI,IAAA,GAAO,IAAA,CAAK,MAAA,IAAU,KAAA,CAAM,MAAA,EAAQ;AACtC,IAAA,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,GAAI,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,CAAG,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,MAAA,CAAA;AAAA,EAC3E;AACA,EAAA,OAAO,KAAA;AACT;AAMO,SAAS,MAAM,OAAA,EAA4B;AAChD,EAAA,MAAM,CAAA,GAAI,QAAQ,OAAO,CAAA;AACzB,EAAA,MAAM,CAAA,GAAI,QAAQ,CAAC,CAAA;AACnB,EAAA,MAAM,IAAI,CAAA,CAAE,KAAA;AACZ,EAAA,MAAM,IAAI,CAAA,CAAE,MAAA;AACZ,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,CAAA,GAAI,KAAK,CAAA;AAChC,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,CAAA,CAAE,KAAK,CAAA;AACrC,EAAA,MAAM,QAAA,GAAW,IAAI,GAAA,GAAM,CAAA;AAC3B,EAAA,MAAM,KAAA,GAAQ,EAAE,MAAA,KAAW,OAAA;AAE3B,EAAA,MAAM,aAAa,IAAA,CAAK,CAAA,CAAE,KAAA,EAAO,SAAA,EAAW,UAAU,CAAC,CAAA;AACvD,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,MAAA,GAAS,SAAA,GAAY,IAAA;AACpD,EAAA,MAAM,QAAA,GAAW,CAAA,GAAI,CAAA,GAAI,WAAA,GAAc,IAAI,SAAA,GAAY,GAAA;AAEvD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,CAAM,IAAA;AAAA,IACJ,eAAe,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,eAAA,EAAkB,CAAC,IAAI,CAAC,CAAA,+FAAA;AAAA,GACxD;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA;AAAA;AAAA,mCAAA,EAEwB,GAAA,CAAI,EAAE,IAAI,CAAC,mCAAmC,GAAA,CAAI,CAAA,CAAE,EAAE,CAAC,CAAA;AAAA;AAAA;AAAA,mCAAA,EAGvD,GAAA,CAAI,EAAE,IAAI,CAAC,mBAAmB,CAAA,CAAE,KAAA,KAAU,MAAA,GAAS,IAAA,GAAO,GAAG,CAAA;AAAA,mCAAA,EAC7D,GAAA,CAAI,CAAA,CAAE,EAAE,CAAC,CAAA;AAAA;AAAA,SAAA,CAEpC,CAAA;AAGR,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,aAAA,EAAgB,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,QAAA,EAAW,KAAA,GAAQ,cAAA,GAAiB,GAAA,CAAI,CAAA,CAAE,EAAE,CAAC,CAAA,GAAA,CAAK,CAAA;AAC5F,EAAA,IAAI,CAAC,OAAO,KAAA,CAAM,IAAA,CAAK,gBAAgB,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,qBAAA,CAAuB,CAAA;AAE7E,EAAA,KAAA,CAAM,IAAA,CAAK,gBAAgB,CAAC,CAAA,UAAA,EAAa,KAAK,KAAA,CAAM,CAAA,GAAI,IAAI,CAAC,CAAA,uBAAA,CAAyB,CAAA;AAGtF,EAAA,IAAI,OAAO,GAAA,GAAM,EAAA;AACjB,EAAA,IAAI,EAAE,OAAA,EAAS;AACb,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,GAAG,CAAA,KAAA,EAAQ,IAAI,+DAA+D,GAAA,CAAI,CAAA,CAAE,IAAI,CAAC,CAAA,EAAA,EAAK,GAAA;AAAA,QACxG,CAAA,CAAE,QAAQ,WAAA;AAAY,OACvB,CAAA,OAAA;AAAA,KACH;AAAA,EACF;AAEA,EAAA,IAAI,EAAE,IAAA,EAAM;AACV,IAAA,MAAM,CAAA,GAAI,EAAA;AACV,IAAA,MAAM,EAAA,GAAK,IAAI,GAAA,GAAM,CAAA;AACrB,IAAA,MAAM,EAAA,GAAK,GAAA;AACX,IAAA,KAAA,CAAM,IAAA,CAAK,YAAY,EAAE,CAAA,KAAA,EAAQ,EAAE,CAAA,SAAA,EAAY,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,+BAAA,CAAiC,CAAA;AAC/F,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,KAAK,CAAA,GAAI,CAAC,QAAQ,EAAA,GAAK,CAAA,GAAI,CAAA,GAAI,EAAE,CAAA,uEAAA,EAA0E,GAAA;AAAA,QACrH,CAAA,CAAE;AAAA,OACH,CAAA,OAAA;AAAA,KACH;AAAA,EACF;AAEA,EAAA,IAAI,EAAE,KAAA,EAAO;AACX,IAAA,MAAM,EAAA,GAAK,GAAA,IAAO,CAAA,CAAE,OAAA,GAAU,CAAA,GAAI,CAAA,CAAA;AAClC,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,EAAE,CAAA,KAAA,EAAQ,IAAA,GAAO,EAAE,CAAA,SAAA,EAAY,CAAA,CAAE,KAAA,CAAM,MAAA,GAAS,EAAA,GAAK,EAAE,CAAA,4BAAA,EAA+B,GAAA;AAAA,QAChG,CAAA,CAAE;AAAA,OACH,CAAA,kBAAA;AAAA,KACH;AACA,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,EAAA,GAAK,EAAE,CAAA,KAAA,EAAQ,IAAA,GAAO,EAAE,CAAA,yCAAA,EAA4C,GAAA,CAAI,CAAA,CAAE,EAAE,CAAC,CAAA,EAAA,EAAK,GAAA;AAAA,QAC5F,CAAA,CAAE;AAAA,OACH,CAAA,OAAA;AAAA,KACH;AACA,IAAA,IAAA,IAAQ,EAAA;AAAA,EACV;AAGA,EAAA,UAAA,CAAW,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AAC9B,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,GAAG,CAAA,KAAA,EAAQ,QAAA,GAAW,IAAI,SAAA,GAAY,IAAI,CAAA,aAAA,EAAgB,SAAS,CAAA,8CAAA,EAAiD,GAAA;AAAA,QAC9H,CAAA,CAAE;AAAA,OACH,CAAA,EAAA,EAAK,GAAA,CAAI,IAAI,CAAC,CAAA,OAAA;AAAA,KACjB;AAAA,EACF,CAAC,CAAA;AAED,EAAA,IAAI,EAAE,QAAA,EAAU;AACd,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,GAAG,CAAA,KAAA,EAAQ,QAAA,GAAW,WAAW,MAAA,GAAS,SAAA,GAAY,IAAA,GAAO,EAAE,CAAA,yCAAA,EAA4C,GAAA;AAAA,QACrH,CAAA,CAAE;AAAA,OACH,CAAA,EAAA,EAAK,GAAA,CAAI,CAAA,CAAE,QAAQ,CAAC,CAAA,OAAA;AAAA,KACvB;AAAA,EACF;AAEA,EAAA,IAAI,EAAE,MAAA,EAAQ;AACZ,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,SAAA,EAAY,GAAG,CAAA,KAAA,EAAQ,CAAA,GAAI,GAAG,4CAA4C,GAAA,CAAI,CAAA,CAAE,KAAK,CAAC,CAAA,EAAA,EAAK,GAAA;AAAA,QACzF,CAAA,CAAE;AAAA,OACH,CAAA,OAAA;AAAA,KACH;AAAA,EACF;AAEA,EAAA,KAAA,CAAM,KAAK,CAAA,MAAA,CAAQ,CAAA;AACnB,EAAA,OAAO,KAAA,CAAM,KAAK,EAAE,CAAA;AACtB;AAGO,SAAS,aAAa,OAAA,EAA4B;AACvD,EAAA,OAAO,CAAA,wBAAA,EAA2B,kBAAA,CAAmB,KAAA,CAAM,OAAO,CAAC,CAAC,CAAA,CAAA;AACtE","file":"index.cjs","sourcesContent":["/**\n * @lacspace/og\n * Beautiful, dynamic Open Graph images — a design system you configure once and\n * call per page. Produces a `next/og` element tree *and* a zero-dependency SVG\n * from the same options, with auto-fitting titles, presets and palettes.\n *\n * ```tsx\n * // app/og/route.tsx\n * import { ImageResponse } from \"next/og\";\n * import { ogCard } from \"@lacspace/og\";\n *\n * export const runtime = \"edge\";\n * export function GET(req: Request) {\n * const title = new URL(req.url).searchParams.get(\"title\") ?? \"My site\";\n * return new ImageResponse(\n * ogCard({ title, subtitle: \"my-site.com\", from: \"#22d3ee\", to: \"#6366f1\" }) as any,\n * { width: 1200, height: 630 },\n * );\n * }\n * ```\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\n/* ------------------------------------------------------------------ *\n * Options\n * ------------------------------------------------------------------ */\n\nexport type OgPreset = \"gradient\" | \"split\" | \"minimal\" | \"terminal\";\nexport type OgTheme = \"dark\" | \"light\";\n\nexport interface OgOptions {\n /** The headline. Font size auto-fits to length. */\n title: string;\n /** Secondary line under the title (often your domain). */\n subtitle?: string;\n /** Small uppercase label above the title (a section or category). */\n eyebrow?: string;\n /** A pill badge (e.g. \"NEW\", \"Guide\"). */\n badge?: string;\n /** Text/initial/emoji for the corner logo mark. */\n logo?: string;\n /** Footer line, bottom-left (e.g. your URL). */\n footer?: string;\n /** Accent gradient start / end. */\n from?: string;\n to?: string;\n /** \"dark\" (default) or \"light\" ground. */\n theme?: OgTheme;\n /** Layout preset. */\n preset?: OgPreset;\n /** Canvas size. Defaults to the OG standard 1200×630. */\n width?: number;\n height?: number;\n}\n\ninterface Resolved extends Required<Omit<OgOptions, \"subtitle\" | \"eyebrow\" | \"badge\" | \"logo\" | \"footer\">> {\n subtitle?: string;\n eyebrow?: string;\n badge?: string;\n logo?: string;\n footer?: string;\n}\n\nconst DEFAULTS = {\n from: \"#22d3ee\",\n to: \"#6366f1\",\n theme: \"dark\" as OgTheme,\n preset: \"gradient\" as OgPreset,\n width: 1200,\n height: 630,\n};\n\nfunction resolve(o: OgOptions): Resolved {\n return {\n title: o.title,\n subtitle: o.subtitle,\n eyebrow: o.eyebrow,\n badge: o.badge,\n logo: o.logo,\n footer: o.footer,\n from: o.from ?? DEFAULTS.from,\n to: o.to ?? DEFAULTS.to,\n theme: o.theme ?? DEFAULTS.theme,\n preset: o.preset ?? DEFAULTS.preset,\n width: o.width ?? DEFAULTS.width,\n height: o.height ?? DEFAULTS.height,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * Shared design math\n * ------------------------------------------------------------------ */\n\n/** Pick a title font size that keeps long headlines readable and short ones bold. */\nexport function fitFontSize(\n title: string,\n opts: { max?: number; min?: number } = {},\n): number {\n const max = opts.max ?? 78;\n const min = opts.min ?? 42;\n const len = title.length;\n if (len <= 20) return max;\n if (len >= 90) return min;\n // Linear falloff between the two anchors.\n const t = (len - 20) / (90 - 20);\n return Math.round(max - t * (max - min));\n}\n\ninterface Palette {\n bg: string;\n fg: string;\n muted: string;\n border: string;\n from: string;\n to: string;\n}\n\nfunction palette(r: Resolved): Palette {\n const dark = r.theme === \"dark\";\n return {\n bg: dark ? \"#0a0a0f\" : \"#ffffff\",\n fg: dark ? \"#ffffff\" : \"#0a0a0f\",\n muted: dark ? \"rgba(255,255,255,0.68)\" : \"rgba(10,10,15,0.62)\",\n border: dark ? \"rgba(255,255,255,0.14)\" : \"rgba(10,10,15,0.10)\",\n from: r.from,\n to: r.to,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * next/og element tree\n * ------------------------------------------------------------------ */\n\nexport type OgStyle = Record<string, string | number>;\nexport interface OgNode {\n type: string;\n props: { style?: OgStyle; children?: (OgNode | string)[] | OgNode | string; [k: string]: unknown };\n key?: null;\n}\n\nfunction el(type: string, style: OgStyle, children?: (OgNode | string)[] | OgNode | string): OgNode {\n return { type, key: null, props: { style, ...(children === undefined ? {} : { children }) } };\n}\n\n/**\n * Build a `next/og`-compatible element tree for a share card. Pass it straight\n * to `new ImageResponse(ogCard(opts) as any, { width, height })`.\n *\n * (The `as any` sidesteps a React type mismatch — the object is a valid Satori\n * node, which is what `next/og` renders under the hood.)\n */\nexport function ogCard(options: OgOptions): OgNode {\n const r = resolve(options);\n const p = palette(r);\n const pad = Math.round(r.width * 0.075);\n const titleSize = fitFontSize(r.title);\n\n const accentBar = el(\"div\", {\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"100%\",\n height: `${Math.round(r.height * 0.02)}px`,\n background: `linear-gradient(90deg, ${p.from}, ${p.to})`,\n });\n\n const glow = el(\"div\", {\n position: \"absolute\",\n top: `-${Math.round(r.height * 0.35)}px`,\n right: `-${Math.round(r.width * 0.18)}px`,\n width: `${Math.round(r.width * 0.55)}px`,\n height: `${Math.round(r.width * 0.55)}px`,\n borderRadius: \"9999px\",\n background: `linear-gradient(135deg, ${p.from}, ${p.to})`,\n opacity: r.theme === \"dark\" ? \"0.22\" : \"0.16\",\n filter: \"blur(20px)\",\n });\n\n const topRow: (OgNode | string)[] = [];\n if (r.eyebrow) {\n topRow.push(\n el(\n \"div\",\n {\n display: \"flex\",\n fontSize: \"26px\",\n fontWeight: 600,\n letterSpacing: \"0.12em\",\n textTransform: \"uppercase\",\n color: p.from,\n },\n r.eyebrow.toUpperCase(),\n ),\n );\n }\n if (r.badge) {\n topRow.push(\n el(\n \"div\",\n {\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 20px\",\n borderRadius: \"9999px\",\n fontSize: \"24px\",\n fontWeight: 700,\n color: p.fg,\n background: `linear-gradient(90deg, ${p.from}33, ${p.to}33)`,\n border: `1px solid ${p.border}`,\n },\n r.badge,\n ),\n );\n }\n\n const header = el(\n \"div\",\n { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", width: \"100%\" },\n [\n topRow.length\n ? el(\"div\", { display: \"flex\", gap: \"20px\", alignItems: \"center\" }, topRow)\n : el(\"div\", { display: \"flex\" }, \"\"),\n r.logo\n ? el(\n \"div\",\n {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"76px\",\n height: \"76px\",\n borderRadius: \"20px\",\n fontSize: \"40px\",\n fontWeight: 800,\n color: \"#0a0a0f\",\n background: `linear-gradient(135deg, ${p.from}, ${p.to})`,\n },\n r.logo,\n )\n : el(\"div\", { display: \"flex\" }, \"\"),\n ],\n );\n\n const body: (OgNode | string)[] = [\n el(\n \"div\",\n {\n display: \"flex\",\n fontSize: `${titleSize}px`,\n fontWeight: 800,\n lineHeight: 1.05,\n letterSpacing: \"-0.02em\",\n color: p.fg,\n },\n r.title,\n ),\n ];\n if (r.subtitle) {\n body.push(\n el(\n \"div\",\n { display: \"flex\", marginTop: \"28px\", fontSize: \"34px\", fontWeight: 500, color: p.muted },\n r.subtitle,\n ),\n );\n }\n\n const children: OgNode[] = [\n accentBar,\n glow,\n header,\n el(\"div\", { display: \"flex\", flexDirection: \"column\" }, body),\n ];\n\n if (r.footer) {\n children.push(\n el(\n \"div\",\n { display: \"flex\", fontSize: \"26px\", fontWeight: 500, color: p.muted },\n r.footer,\n ),\n );\n }\n\n return el(\n \"div\",\n {\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n width: \"100%\",\n height: \"100%\",\n padding: `${pad}px`,\n background: r.preset === \"split\" ? `linear-gradient(135deg, ${p.from}, ${p.to})` : p.bg,\n color: p.fg,\n fontFamily: \"sans-serif\",\n },\n children,\n );\n}\n\n/* ------------------------------------------------------------------ *\n * Zero-dependency SVG (previews, emails, icons, guaranteed fallback)\n * ------------------------------------------------------------------ */\n\nfunction esc(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\n/** Greedy word-wrap by estimated glyph width. */\nfunction wrap(text: string, fontSize: number, maxWidth: number, maxLines: number): string[] {\n const charW = fontSize * 0.56;\n const perLine = Math.max(1, Math.floor(maxWidth / charW));\n const words = text.split(/\\s+/);\n const lines: string[] = [];\n let cur = \"\";\n for (const w of words) {\n const next = cur ? `${cur} ${w}` : w;\n if (next.length > perLine && cur) {\n lines.push(cur);\n cur = w;\n if (lines.length === maxLines - 1) break;\n } else {\n cur = next;\n }\n }\n if (cur && lines.length < maxLines) lines.push(cur);\n const used = lines.join(\" \").length;\n if (used < text.length && lines.length) {\n lines[lines.length - 1] = `${lines[lines.length - 1]!.replace(/\\.*$/, \"\")}…`;\n }\n return lines;\n}\n\n/**\n * Render a share card as a standalone SVG string — no browser, no runtime deps.\n * Perfect for previews, email headers, README banners and icons.\n */\nexport function ogSvg(options: OgOptions): string {\n const r = resolve(options);\n const p = palette(r);\n const W = r.width;\n const H = r.height;\n const pad = Math.round(W * 0.075);\n const titleSize = fitFontSize(r.title);\n const contentW = W - pad * 2;\n const split = r.preset === \"split\";\n\n const titleLines = wrap(r.title, titleSize, contentW, 3);\n const titleBlockH = titleLines.length * titleSize * 1.08;\n const titleTop = H / 2 - titleBlockH / 2 + titleSize * 0.8;\n\n const parts: string[] = [];\n parts.push(\n `<svg width=\"${W}\" height=\"${H}\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter, ui-sans-serif, system-ui, sans-serif\">`,\n );\n parts.push(`<defs>\n <linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0\" stop-color=\"${esc(p.from)}\"/><stop offset=\"1\" stop-color=\"${esc(p.to)}\"/>\n </linearGradient>\n <radialGradient id=\"glow\" cx=\"0.85\" cy=\"0.1\" r=\"0.7\">\n <stop offset=\"0\" stop-color=\"${esc(p.from)}\" stop-opacity=\"${r.theme === \"dark\" ? 0.28 : 0.2}\"/>\n <stop offset=\"1\" stop-color=\"${esc(p.to)}\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>`);\n\n // background\n parts.push(`<rect width=\"${W}\" height=\"${H}\" fill=\"${split ? \"url(#accent)\" : esc(p.bg)}\"/>`);\n if (!split) parts.push(`<rect width=\"${W}\" height=\"${H}\" fill=\"url(#glow)\"/>`);\n // accent bar\n parts.push(`<rect width=\"${W}\" height=\"${Math.round(H * 0.02)}\" fill=\"url(#accent)\"/>`);\n\n // eyebrow\n let topY = pad + 34;\n if (r.eyebrow) {\n parts.push(\n `<text x=\"${pad}\" y=\"${topY}\" font-size=\"26\" font-weight=\"600\" letter-spacing=\"3\" fill=\"${esc(p.from)}\">${esc(\n r.eyebrow.toUpperCase(),\n )}</text>`,\n );\n }\n // logo mark (top-right)\n if (r.logo) {\n const s = 76;\n const lx = W - pad - s;\n const ly = pad;\n parts.push(`<rect x=\"${lx}\" y=\"${ly}\" width=\"${s}\" height=\"${s}\" rx=\"20\" fill=\"url(#accent)\"/>`);\n parts.push(\n `<text x=\"${lx + s / 2}\" y=\"${ly + s / 2 + 14}\" font-size=\"40\" font-weight=\"800\" text-anchor=\"middle\" fill=\"#0a0a0f\">${esc(\n r.logo,\n )}</text>`,\n );\n }\n // badge\n if (r.badge) {\n const bx = pad + (r.eyebrow ? 0 : 0);\n parts.push(\n `<rect x=\"${bx}\" y=\"${topY + 14}\" width=\"${r.badge.length * 16 + 40}\" height=\"44\" rx=\"22\" fill=\"${esc(\n p.from,\n )}\" opacity=\"0.18\"/>`,\n );\n parts.push(\n `<text x=\"${bx + 20}\" y=\"${topY + 43}\" font-size=\"24\" font-weight=\"700\" fill=\"${esc(p.fg)}\">${esc(\n r.badge,\n )}</text>`,\n );\n topY += 60;\n }\n\n // title (wrapped)\n titleLines.forEach((line, i) => {\n parts.push(\n `<text x=\"${pad}\" y=\"${titleTop + i * titleSize * 1.08}\" font-size=\"${titleSize}\" font-weight=\"800\" letter-spacing=\"-1\" fill=\"${esc(\n p.fg,\n )}\">${esc(line)}</text>`,\n );\n });\n // subtitle\n if (r.subtitle) {\n parts.push(\n `<text x=\"${pad}\" y=\"${titleTop + titleLines.length * titleSize * 1.08 + 46}\" font-size=\"34\" font-weight=\"500\" fill=\"${esc(\n p.muted,\n )}\">${esc(r.subtitle)}</text>`,\n );\n }\n // footer\n if (r.footer) {\n parts.push(\n `<text x=\"${pad}\" y=\"${H - pad}\" font-size=\"26\" font-weight=\"500\" fill=\"${esc(p.muted)}\">${esc(\n r.footer,\n )}</text>`,\n );\n }\n\n parts.push(`</svg>`);\n return parts.join(\"\");\n}\n\n/** SVG as a `data:` URI — drop straight into `src` / `background-image`. */\nexport function ogSvgDataUri(options: OgOptions): string {\n return `data:image/svg+xml;utf8,${encodeURIComponent(ogSvg(options))}`;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/og
|
|
3
|
+
* Beautiful, dynamic Open Graph images — a design system you configure once and
|
|
4
|
+
* call per page. Produces a `next/og` element tree *and* a zero-dependency SVG
|
|
5
|
+
* from the same options, with auto-fitting titles, presets and palettes.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // app/og/route.tsx
|
|
9
|
+
* import { ImageResponse } from "next/og";
|
|
10
|
+
* import { ogCard } from "@lacspace/og";
|
|
11
|
+
*
|
|
12
|
+
* export const runtime = "edge";
|
|
13
|
+
* export function GET(req: Request) {
|
|
14
|
+
* const title = new URL(req.url).searchParams.get("title") ?? "My site";
|
|
15
|
+
* return new ImageResponse(
|
|
16
|
+
* ogCard({ title, subtitle: "my-site.com", from: "#22d3ee", to: "#6366f1" }) as any,
|
|
17
|
+
* { width: 1200, height: 630 },
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Zero dependencies · isomorphic · fully typed.
|
|
23
|
+
*/
|
|
24
|
+
type OgPreset = "gradient" | "split" | "minimal" | "terminal";
|
|
25
|
+
type OgTheme = "dark" | "light";
|
|
26
|
+
interface OgOptions {
|
|
27
|
+
/** The headline. Font size auto-fits to length. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Secondary line under the title (often your domain). */
|
|
30
|
+
subtitle?: string;
|
|
31
|
+
/** Small uppercase label above the title (a section or category). */
|
|
32
|
+
eyebrow?: string;
|
|
33
|
+
/** A pill badge (e.g. "NEW", "Guide"). */
|
|
34
|
+
badge?: string;
|
|
35
|
+
/** Text/initial/emoji for the corner logo mark. */
|
|
36
|
+
logo?: string;
|
|
37
|
+
/** Footer line, bottom-left (e.g. your URL). */
|
|
38
|
+
footer?: string;
|
|
39
|
+
/** Accent gradient start / end. */
|
|
40
|
+
from?: string;
|
|
41
|
+
to?: string;
|
|
42
|
+
/** "dark" (default) or "light" ground. */
|
|
43
|
+
theme?: OgTheme;
|
|
44
|
+
/** Layout preset. */
|
|
45
|
+
preset?: OgPreset;
|
|
46
|
+
/** Canvas size. Defaults to the OG standard 1200×630. */
|
|
47
|
+
width?: number;
|
|
48
|
+
height?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Pick a title font size that keeps long headlines readable and short ones bold. */
|
|
51
|
+
declare function fitFontSize(title: string, opts?: {
|
|
52
|
+
max?: number;
|
|
53
|
+
min?: number;
|
|
54
|
+
}): number;
|
|
55
|
+
type OgStyle = Record<string, string | number>;
|
|
56
|
+
interface OgNode {
|
|
57
|
+
type: string;
|
|
58
|
+
props: {
|
|
59
|
+
style?: OgStyle;
|
|
60
|
+
children?: (OgNode | string)[] | OgNode | string;
|
|
61
|
+
[k: string]: unknown;
|
|
62
|
+
};
|
|
63
|
+
key?: null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a `next/og`-compatible element tree for a share card. Pass it straight
|
|
67
|
+
* to `new ImageResponse(ogCard(opts) as any, { width, height })`.
|
|
68
|
+
*
|
|
69
|
+
* (The `as any` sidesteps a React type mismatch — the object is a valid Satori
|
|
70
|
+
* node, which is what `next/og` renders under the hood.)
|
|
71
|
+
*/
|
|
72
|
+
declare function ogCard(options: OgOptions): OgNode;
|
|
73
|
+
/**
|
|
74
|
+
* Render a share card as a standalone SVG string — no browser, no runtime deps.
|
|
75
|
+
* Perfect for previews, email headers, README banners and icons.
|
|
76
|
+
*/
|
|
77
|
+
declare function ogSvg(options: OgOptions): string;
|
|
78
|
+
/** SVG as a `data:` URI — drop straight into `src` / `background-image`. */
|
|
79
|
+
declare function ogSvgDataUri(options: OgOptions): string;
|
|
80
|
+
|
|
81
|
+
export { type OgNode, type OgOptions, type OgPreset, type OgStyle, type OgTheme, fitFontSize, ogCard, ogSvg, ogSvgDataUri };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/og
|
|
3
|
+
* Beautiful, dynamic Open Graph images — a design system you configure once and
|
|
4
|
+
* call per page. Produces a `next/og` element tree *and* a zero-dependency SVG
|
|
5
|
+
* from the same options, with auto-fitting titles, presets and palettes.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // app/og/route.tsx
|
|
9
|
+
* import { ImageResponse } from "next/og";
|
|
10
|
+
* import { ogCard } from "@lacspace/og";
|
|
11
|
+
*
|
|
12
|
+
* export const runtime = "edge";
|
|
13
|
+
* export function GET(req: Request) {
|
|
14
|
+
* const title = new URL(req.url).searchParams.get("title") ?? "My site";
|
|
15
|
+
* return new ImageResponse(
|
|
16
|
+
* ogCard({ title, subtitle: "my-site.com", from: "#22d3ee", to: "#6366f1" }) as any,
|
|
17
|
+
* { width: 1200, height: 630 },
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Zero dependencies · isomorphic · fully typed.
|
|
23
|
+
*/
|
|
24
|
+
type OgPreset = "gradient" | "split" | "minimal" | "terminal";
|
|
25
|
+
type OgTheme = "dark" | "light";
|
|
26
|
+
interface OgOptions {
|
|
27
|
+
/** The headline. Font size auto-fits to length. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Secondary line under the title (often your domain). */
|
|
30
|
+
subtitle?: string;
|
|
31
|
+
/** Small uppercase label above the title (a section or category). */
|
|
32
|
+
eyebrow?: string;
|
|
33
|
+
/** A pill badge (e.g. "NEW", "Guide"). */
|
|
34
|
+
badge?: string;
|
|
35
|
+
/** Text/initial/emoji for the corner logo mark. */
|
|
36
|
+
logo?: string;
|
|
37
|
+
/** Footer line, bottom-left (e.g. your URL). */
|
|
38
|
+
footer?: string;
|
|
39
|
+
/** Accent gradient start / end. */
|
|
40
|
+
from?: string;
|
|
41
|
+
to?: string;
|
|
42
|
+
/** "dark" (default) or "light" ground. */
|
|
43
|
+
theme?: OgTheme;
|
|
44
|
+
/** Layout preset. */
|
|
45
|
+
preset?: OgPreset;
|
|
46
|
+
/** Canvas size. Defaults to the OG standard 1200×630. */
|
|
47
|
+
width?: number;
|
|
48
|
+
height?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Pick a title font size that keeps long headlines readable and short ones bold. */
|
|
51
|
+
declare function fitFontSize(title: string, opts?: {
|
|
52
|
+
max?: number;
|
|
53
|
+
min?: number;
|
|
54
|
+
}): number;
|
|
55
|
+
type OgStyle = Record<string, string | number>;
|
|
56
|
+
interface OgNode {
|
|
57
|
+
type: string;
|
|
58
|
+
props: {
|
|
59
|
+
style?: OgStyle;
|
|
60
|
+
children?: (OgNode | string)[] | OgNode | string;
|
|
61
|
+
[k: string]: unknown;
|
|
62
|
+
};
|
|
63
|
+
key?: null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a `next/og`-compatible element tree for a share card. Pass it straight
|
|
67
|
+
* to `new ImageResponse(ogCard(opts) as any, { width, height })`.
|
|
68
|
+
*
|
|
69
|
+
* (The `as any` sidesteps a React type mismatch — the object is a valid Satori
|
|
70
|
+
* node, which is what `next/og` renders under the hood.)
|
|
71
|
+
*/
|
|
72
|
+
declare function ogCard(options: OgOptions): OgNode;
|
|
73
|
+
/**
|
|
74
|
+
* Render a share card as a standalone SVG string — no browser, no runtime deps.
|
|
75
|
+
* Perfect for previews, email headers, README banners and icons.
|
|
76
|
+
*/
|
|
77
|
+
declare function ogSvg(options: OgOptions): string;
|
|
78
|
+
/** SVG as a `data:` URI — drop straight into `src` / `background-image`. */
|
|
79
|
+
declare function ogSvgDataUri(options: OgOptions): string;
|
|
80
|
+
|
|
81
|
+
export { type OgNode, type OgOptions, type OgPreset, type OgStyle, type OgTheme, fitFontSize, ogCard, ogSvg, ogSvgDataUri };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULTS = {
|
|
3
|
+
from: "#22d3ee",
|
|
4
|
+
to: "#6366f1",
|
|
5
|
+
theme: "dark",
|
|
6
|
+
preset: "gradient",
|
|
7
|
+
width: 1200,
|
|
8
|
+
height: 630
|
|
9
|
+
};
|
|
10
|
+
function resolve(o) {
|
|
11
|
+
return {
|
|
12
|
+
title: o.title,
|
|
13
|
+
subtitle: o.subtitle,
|
|
14
|
+
eyebrow: o.eyebrow,
|
|
15
|
+
badge: o.badge,
|
|
16
|
+
logo: o.logo,
|
|
17
|
+
footer: o.footer,
|
|
18
|
+
from: o.from ?? DEFAULTS.from,
|
|
19
|
+
to: o.to ?? DEFAULTS.to,
|
|
20
|
+
theme: o.theme ?? DEFAULTS.theme,
|
|
21
|
+
preset: o.preset ?? DEFAULTS.preset,
|
|
22
|
+
width: o.width ?? DEFAULTS.width,
|
|
23
|
+
height: o.height ?? DEFAULTS.height
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function fitFontSize(title, opts = {}) {
|
|
27
|
+
const max = opts.max ?? 78;
|
|
28
|
+
const min = opts.min ?? 42;
|
|
29
|
+
const len = title.length;
|
|
30
|
+
if (len <= 20) return max;
|
|
31
|
+
if (len >= 90) return min;
|
|
32
|
+
const t = (len - 20) / (90 - 20);
|
|
33
|
+
return Math.round(max - t * (max - min));
|
|
34
|
+
}
|
|
35
|
+
function palette(r) {
|
|
36
|
+
const dark = r.theme === "dark";
|
|
37
|
+
return {
|
|
38
|
+
bg: dark ? "#0a0a0f" : "#ffffff",
|
|
39
|
+
fg: dark ? "#ffffff" : "#0a0a0f",
|
|
40
|
+
muted: dark ? "rgba(255,255,255,0.68)" : "rgba(10,10,15,0.62)",
|
|
41
|
+
border: dark ? "rgba(255,255,255,0.14)" : "rgba(10,10,15,0.10)",
|
|
42
|
+
from: r.from,
|
|
43
|
+
to: r.to
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function el(type, style, children) {
|
|
47
|
+
return { type, key: null, props: { style, ...children === void 0 ? {} : { children } } };
|
|
48
|
+
}
|
|
49
|
+
function ogCard(options) {
|
|
50
|
+
const r = resolve(options);
|
|
51
|
+
const p = palette(r);
|
|
52
|
+
const pad = Math.round(r.width * 0.075);
|
|
53
|
+
const titleSize = fitFontSize(r.title);
|
|
54
|
+
const accentBar = el("div", {
|
|
55
|
+
position: "absolute",
|
|
56
|
+
top: "0",
|
|
57
|
+
left: "0",
|
|
58
|
+
width: "100%",
|
|
59
|
+
height: `${Math.round(r.height * 0.02)}px`,
|
|
60
|
+
background: `linear-gradient(90deg, ${p.from}, ${p.to})`
|
|
61
|
+
});
|
|
62
|
+
const glow = el("div", {
|
|
63
|
+
position: "absolute",
|
|
64
|
+
top: `-${Math.round(r.height * 0.35)}px`,
|
|
65
|
+
right: `-${Math.round(r.width * 0.18)}px`,
|
|
66
|
+
width: `${Math.round(r.width * 0.55)}px`,
|
|
67
|
+
height: `${Math.round(r.width * 0.55)}px`,
|
|
68
|
+
borderRadius: "9999px",
|
|
69
|
+
background: `linear-gradient(135deg, ${p.from}, ${p.to})`,
|
|
70
|
+
opacity: r.theme === "dark" ? "0.22" : "0.16",
|
|
71
|
+
filter: "blur(20px)"
|
|
72
|
+
});
|
|
73
|
+
const topRow = [];
|
|
74
|
+
if (r.eyebrow) {
|
|
75
|
+
topRow.push(
|
|
76
|
+
el(
|
|
77
|
+
"div",
|
|
78
|
+
{
|
|
79
|
+
display: "flex",
|
|
80
|
+
fontSize: "26px",
|
|
81
|
+
fontWeight: 600,
|
|
82
|
+
letterSpacing: "0.12em",
|
|
83
|
+
textTransform: "uppercase",
|
|
84
|
+
color: p.from
|
|
85
|
+
},
|
|
86
|
+
r.eyebrow.toUpperCase()
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (r.badge) {
|
|
91
|
+
topRow.push(
|
|
92
|
+
el(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
display: "flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
padding: "8px 20px",
|
|
98
|
+
borderRadius: "9999px",
|
|
99
|
+
fontSize: "24px",
|
|
100
|
+
fontWeight: 700,
|
|
101
|
+
color: p.fg,
|
|
102
|
+
background: `linear-gradient(90deg, ${p.from}33, ${p.to}33)`,
|
|
103
|
+
border: `1px solid ${p.border}`
|
|
104
|
+
},
|
|
105
|
+
r.badge
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const header = el(
|
|
110
|
+
"div",
|
|
111
|
+
{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" },
|
|
112
|
+
[
|
|
113
|
+
topRow.length ? el("div", { display: "flex", gap: "20px", alignItems: "center" }, topRow) : el("div", { display: "flex" }, ""),
|
|
114
|
+
r.logo ? el(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
display: "flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
width: "76px",
|
|
121
|
+
height: "76px",
|
|
122
|
+
borderRadius: "20px",
|
|
123
|
+
fontSize: "40px",
|
|
124
|
+
fontWeight: 800,
|
|
125
|
+
color: "#0a0a0f",
|
|
126
|
+
background: `linear-gradient(135deg, ${p.from}, ${p.to})`
|
|
127
|
+
},
|
|
128
|
+
r.logo
|
|
129
|
+
) : el("div", { display: "flex" }, "")
|
|
130
|
+
]
|
|
131
|
+
);
|
|
132
|
+
const body = [
|
|
133
|
+
el(
|
|
134
|
+
"div",
|
|
135
|
+
{
|
|
136
|
+
display: "flex",
|
|
137
|
+
fontSize: `${titleSize}px`,
|
|
138
|
+
fontWeight: 800,
|
|
139
|
+
lineHeight: 1.05,
|
|
140
|
+
letterSpacing: "-0.02em",
|
|
141
|
+
color: p.fg
|
|
142
|
+
},
|
|
143
|
+
r.title
|
|
144
|
+
)
|
|
145
|
+
];
|
|
146
|
+
if (r.subtitle) {
|
|
147
|
+
body.push(
|
|
148
|
+
el(
|
|
149
|
+
"div",
|
|
150
|
+
{ display: "flex", marginTop: "28px", fontSize: "34px", fontWeight: 500, color: p.muted },
|
|
151
|
+
r.subtitle
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const children = [
|
|
156
|
+
accentBar,
|
|
157
|
+
glow,
|
|
158
|
+
header,
|
|
159
|
+
el("div", { display: "flex", flexDirection: "column" }, body)
|
|
160
|
+
];
|
|
161
|
+
if (r.footer) {
|
|
162
|
+
children.push(
|
|
163
|
+
el(
|
|
164
|
+
"div",
|
|
165
|
+
{ display: "flex", fontSize: "26px", fontWeight: 500, color: p.muted },
|
|
166
|
+
r.footer
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return el(
|
|
171
|
+
"div",
|
|
172
|
+
{
|
|
173
|
+
position: "relative",
|
|
174
|
+
display: "flex",
|
|
175
|
+
flexDirection: "column",
|
|
176
|
+
justifyContent: "space-between",
|
|
177
|
+
width: "100%",
|
|
178
|
+
height: "100%",
|
|
179
|
+
padding: `${pad}px`,
|
|
180
|
+
background: r.preset === "split" ? `linear-gradient(135deg, ${p.from}, ${p.to})` : p.bg,
|
|
181
|
+
color: p.fg,
|
|
182
|
+
fontFamily: "sans-serif"
|
|
183
|
+
},
|
|
184
|
+
children
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
function esc(s) {
|
|
188
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
189
|
+
}
|
|
190
|
+
function wrap(text, fontSize, maxWidth, maxLines) {
|
|
191
|
+
const charW = fontSize * 0.56;
|
|
192
|
+
const perLine = Math.max(1, Math.floor(maxWidth / charW));
|
|
193
|
+
const words = text.split(/\s+/);
|
|
194
|
+
const lines = [];
|
|
195
|
+
let cur = "";
|
|
196
|
+
for (const w of words) {
|
|
197
|
+
const next = cur ? `${cur} ${w}` : w;
|
|
198
|
+
if (next.length > perLine && cur) {
|
|
199
|
+
lines.push(cur);
|
|
200
|
+
cur = w;
|
|
201
|
+
if (lines.length === maxLines - 1) break;
|
|
202
|
+
} else {
|
|
203
|
+
cur = next;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
207
|
+
const used = lines.join(" ").length;
|
|
208
|
+
if (used < text.length && lines.length) {
|
|
209
|
+
lines[lines.length - 1] = `${lines[lines.length - 1].replace(/\.*$/, "")}\u2026`;
|
|
210
|
+
}
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
function ogSvg(options) {
|
|
214
|
+
const r = resolve(options);
|
|
215
|
+
const p = palette(r);
|
|
216
|
+
const W = r.width;
|
|
217
|
+
const H = r.height;
|
|
218
|
+
const pad = Math.round(W * 0.075);
|
|
219
|
+
const titleSize = fitFontSize(r.title);
|
|
220
|
+
const contentW = W - pad * 2;
|
|
221
|
+
const split = r.preset === "split";
|
|
222
|
+
const titleLines = wrap(r.title, titleSize, contentW, 3);
|
|
223
|
+
const titleBlockH = titleLines.length * titleSize * 1.08;
|
|
224
|
+
const titleTop = H / 2 - titleBlockH / 2 + titleSize * 0.8;
|
|
225
|
+
const parts = [];
|
|
226
|
+
parts.push(
|
|
227
|
+
`<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" font-family="Inter, ui-sans-serif, system-ui, sans-serif">`
|
|
228
|
+
);
|
|
229
|
+
parts.push(`<defs>
|
|
230
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
|
231
|
+
<stop offset="0" stop-color="${esc(p.from)}"/><stop offset="1" stop-color="${esc(p.to)}"/>
|
|
232
|
+
</linearGradient>
|
|
233
|
+
<radialGradient id="glow" cx="0.85" cy="0.1" r="0.7">
|
|
234
|
+
<stop offset="0" stop-color="${esc(p.from)}" stop-opacity="${r.theme === "dark" ? 0.28 : 0.2}"/>
|
|
235
|
+
<stop offset="1" stop-color="${esc(p.to)}" stop-opacity="0"/>
|
|
236
|
+
</radialGradient>
|
|
237
|
+
</defs>`);
|
|
238
|
+
parts.push(`<rect width="${W}" height="${H}" fill="${split ? "url(#accent)" : esc(p.bg)}"/>`);
|
|
239
|
+
if (!split) parts.push(`<rect width="${W}" height="${H}" fill="url(#glow)"/>`);
|
|
240
|
+
parts.push(`<rect width="${W}" height="${Math.round(H * 0.02)}" fill="url(#accent)"/>`);
|
|
241
|
+
let topY = pad + 34;
|
|
242
|
+
if (r.eyebrow) {
|
|
243
|
+
parts.push(
|
|
244
|
+
`<text x="${pad}" y="${topY}" font-size="26" font-weight="600" letter-spacing="3" fill="${esc(p.from)}">${esc(
|
|
245
|
+
r.eyebrow.toUpperCase()
|
|
246
|
+
)}</text>`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (r.logo) {
|
|
250
|
+
const s = 76;
|
|
251
|
+
const lx = W - pad - s;
|
|
252
|
+
const ly = pad;
|
|
253
|
+
parts.push(`<rect x="${lx}" y="${ly}" width="${s}" height="${s}" rx="20" fill="url(#accent)"/>`);
|
|
254
|
+
parts.push(
|
|
255
|
+
`<text x="${lx + s / 2}" y="${ly + s / 2 + 14}" font-size="40" font-weight="800" text-anchor="middle" fill="#0a0a0f">${esc(
|
|
256
|
+
r.logo
|
|
257
|
+
)}</text>`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (r.badge) {
|
|
261
|
+
const bx = pad + (r.eyebrow ? 0 : 0);
|
|
262
|
+
parts.push(
|
|
263
|
+
`<rect x="${bx}" y="${topY + 14}" width="${r.badge.length * 16 + 40}" height="44" rx="22" fill="${esc(
|
|
264
|
+
p.from
|
|
265
|
+
)}" opacity="0.18"/>`
|
|
266
|
+
);
|
|
267
|
+
parts.push(
|
|
268
|
+
`<text x="${bx + 20}" y="${topY + 43}" font-size="24" font-weight="700" fill="${esc(p.fg)}">${esc(
|
|
269
|
+
r.badge
|
|
270
|
+
)}</text>`
|
|
271
|
+
);
|
|
272
|
+
topY += 60;
|
|
273
|
+
}
|
|
274
|
+
titleLines.forEach((line, i) => {
|
|
275
|
+
parts.push(
|
|
276
|
+
`<text x="${pad}" y="${titleTop + i * titleSize * 1.08}" font-size="${titleSize}" font-weight="800" letter-spacing="-1" fill="${esc(
|
|
277
|
+
p.fg
|
|
278
|
+
)}">${esc(line)}</text>`
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
if (r.subtitle) {
|
|
282
|
+
parts.push(
|
|
283
|
+
`<text x="${pad}" y="${titleTop + titleLines.length * titleSize * 1.08 + 46}" font-size="34" font-weight="500" fill="${esc(
|
|
284
|
+
p.muted
|
|
285
|
+
)}">${esc(r.subtitle)}</text>`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (r.footer) {
|
|
289
|
+
parts.push(
|
|
290
|
+
`<text x="${pad}" y="${H - pad}" font-size="26" font-weight="500" fill="${esc(p.muted)}">${esc(
|
|
291
|
+
r.footer
|
|
292
|
+
)}</text>`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
parts.push(`</svg>`);
|
|
296
|
+
return parts.join("");
|
|
297
|
+
}
|
|
298
|
+
function ogSvgDataUri(options) {
|
|
299
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(ogSvg(options))}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { fitFontSize, ogCard, ogSvg, ogSvgDataUri };
|
|
303
|
+
//# sourceMappingURL=index.js.map
|
|
304
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/og\n * Beautiful, dynamic Open Graph images — a design system you configure once and\n * call per page. Produces a `next/og` element tree *and* a zero-dependency SVG\n * from the same options, with auto-fitting titles, presets and palettes.\n *\n * ```tsx\n * // app/og/route.tsx\n * import { ImageResponse } from \"next/og\";\n * import { ogCard } from \"@lacspace/og\";\n *\n * export const runtime = \"edge\";\n * export function GET(req: Request) {\n * const title = new URL(req.url).searchParams.get(\"title\") ?? \"My site\";\n * return new ImageResponse(\n * ogCard({ title, subtitle: \"my-site.com\", from: \"#22d3ee\", to: \"#6366f1\" }) as any,\n * { width: 1200, height: 630 },\n * );\n * }\n * ```\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\n/* ------------------------------------------------------------------ *\n * Options\n * ------------------------------------------------------------------ */\n\nexport type OgPreset = \"gradient\" | \"split\" | \"minimal\" | \"terminal\";\nexport type OgTheme = \"dark\" | \"light\";\n\nexport interface OgOptions {\n /** The headline. Font size auto-fits to length. */\n title: string;\n /** Secondary line under the title (often your domain). */\n subtitle?: string;\n /** Small uppercase label above the title (a section or category). */\n eyebrow?: string;\n /** A pill badge (e.g. \"NEW\", \"Guide\"). */\n badge?: string;\n /** Text/initial/emoji for the corner logo mark. */\n logo?: string;\n /** Footer line, bottom-left (e.g. your URL). */\n footer?: string;\n /** Accent gradient start / end. */\n from?: string;\n to?: string;\n /** \"dark\" (default) or \"light\" ground. */\n theme?: OgTheme;\n /** Layout preset. */\n preset?: OgPreset;\n /** Canvas size. Defaults to the OG standard 1200×630. */\n width?: number;\n height?: number;\n}\n\ninterface Resolved extends Required<Omit<OgOptions, \"subtitle\" | \"eyebrow\" | \"badge\" | \"logo\" | \"footer\">> {\n subtitle?: string;\n eyebrow?: string;\n badge?: string;\n logo?: string;\n footer?: string;\n}\n\nconst DEFAULTS = {\n from: \"#22d3ee\",\n to: \"#6366f1\",\n theme: \"dark\" as OgTheme,\n preset: \"gradient\" as OgPreset,\n width: 1200,\n height: 630,\n};\n\nfunction resolve(o: OgOptions): Resolved {\n return {\n title: o.title,\n subtitle: o.subtitle,\n eyebrow: o.eyebrow,\n badge: o.badge,\n logo: o.logo,\n footer: o.footer,\n from: o.from ?? DEFAULTS.from,\n to: o.to ?? DEFAULTS.to,\n theme: o.theme ?? DEFAULTS.theme,\n preset: o.preset ?? DEFAULTS.preset,\n width: o.width ?? DEFAULTS.width,\n height: o.height ?? DEFAULTS.height,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * Shared design math\n * ------------------------------------------------------------------ */\n\n/** Pick a title font size that keeps long headlines readable and short ones bold. */\nexport function fitFontSize(\n title: string,\n opts: { max?: number; min?: number } = {},\n): number {\n const max = opts.max ?? 78;\n const min = opts.min ?? 42;\n const len = title.length;\n if (len <= 20) return max;\n if (len >= 90) return min;\n // Linear falloff between the two anchors.\n const t = (len - 20) / (90 - 20);\n return Math.round(max - t * (max - min));\n}\n\ninterface Palette {\n bg: string;\n fg: string;\n muted: string;\n border: string;\n from: string;\n to: string;\n}\n\nfunction palette(r: Resolved): Palette {\n const dark = r.theme === \"dark\";\n return {\n bg: dark ? \"#0a0a0f\" : \"#ffffff\",\n fg: dark ? \"#ffffff\" : \"#0a0a0f\",\n muted: dark ? \"rgba(255,255,255,0.68)\" : \"rgba(10,10,15,0.62)\",\n border: dark ? \"rgba(255,255,255,0.14)\" : \"rgba(10,10,15,0.10)\",\n from: r.from,\n to: r.to,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * next/og element tree\n * ------------------------------------------------------------------ */\n\nexport type OgStyle = Record<string, string | number>;\nexport interface OgNode {\n type: string;\n props: { style?: OgStyle; children?: (OgNode | string)[] | OgNode | string; [k: string]: unknown };\n key?: null;\n}\n\nfunction el(type: string, style: OgStyle, children?: (OgNode | string)[] | OgNode | string): OgNode {\n return { type, key: null, props: { style, ...(children === undefined ? {} : { children }) } };\n}\n\n/**\n * Build a `next/og`-compatible element tree for a share card. Pass it straight\n * to `new ImageResponse(ogCard(opts) as any, { width, height })`.\n *\n * (The `as any` sidesteps a React type mismatch — the object is a valid Satori\n * node, which is what `next/og` renders under the hood.)\n */\nexport function ogCard(options: OgOptions): OgNode {\n const r = resolve(options);\n const p = palette(r);\n const pad = Math.round(r.width * 0.075);\n const titleSize = fitFontSize(r.title);\n\n const accentBar = el(\"div\", {\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"100%\",\n height: `${Math.round(r.height * 0.02)}px`,\n background: `linear-gradient(90deg, ${p.from}, ${p.to})`,\n });\n\n const glow = el(\"div\", {\n position: \"absolute\",\n top: `-${Math.round(r.height * 0.35)}px`,\n right: `-${Math.round(r.width * 0.18)}px`,\n width: `${Math.round(r.width * 0.55)}px`,\n height: `${Math.round(r.width * 0.55)}px`,\n borderRadius: \"9999px\",\n background: `linear-gradient(135deg, ${p.from}, ${p.to})`,\n opacity: r.theme === \"dark\" ? \"0.22\" : \"0.16\",\n filter: \"blur(20px)\",\n });\n\n const topRow: (OgNode | string)[] = [];\n if (r.eyebrow) {\n topRow.push(\n el(\n \"div\",\n {\n display: \"flex\",\n fontSize: \"26px\",\n fontWeight: 600,\n letterSpacing: \"0.12em\",\n textTransform: \"uppercase\",\n color: p.from,\n },\n r.eyebrow.toUpperCase(),\n ),\n );\n }\n if (r.badge) {\n topRow.push(\n el(\n \"div\",\n {\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 20px\",\n borderRadius: \"9999px\",\n fontSize: \"24px\",\n fontWeight: 700,\n color: p.fg,\n background: `linear-gradient(90deg, ${p.from}33, ${p.to}33)`,\n border: `1px solid ${p.border}`,\n },\n r.badge,\n ),\n );\n }\n\n const header = el(\n \"div\",\n { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", width: \"100%\" },\n [\n topRow.length\n ? el(\"div\", { display: \"flex\", gap: \"20px\", alignItems: \"center\" }, topRow)\n : el(\"div\", { display: \"flex\" }, \"\"),\n r.logo\n ? el(\n \"div\",\n {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"76px\",\n height: \"76px\",\n borderRadius: \"20px\",\n fontSize: \"40px\",\n fontWeight: 800,\n color: \"#0a0a0f\",\n background: `linear-gradient(135deg, ${p.from}, ${p.to})`,\n },\n r.logo,\n )\n : el(\"div\", { display: \"flex\" }, \"\"),\n ],\n );\n\n const body: (OgNode | string)[] = [\n el(\n \"div\",\n {\n display: \"flex\",\n fontSize: `${titleSize}px`,\n fontWeight: 800,\n lineHeight: 1.05,\n letterSpacing: \"-0.02em\",\n color: p.fg,\n },\n r.title,\n ),\n ];\n if (r.subtitle) {\n body.push(\n el(\n \"div\",\n { display: \"flex\", marginTop: \"28px\", fontSize: \"34px\", fontWeight: 500, color: p.muted },\n r.subtitle,\n ),\n );\n }\n\n const children: OgNode[] = [\n accentBar,\n glow,\n header,\n el(\"div\", { display: \"flex\", flexDirection: \"column\" }, body),\n ];\n\n if (r.footer) {\n children.push(\n el(\n \"div\",\n { display: \"flex\", fontSize: \"26px\", fontWeight: 500, color: p.muted },\n r.footer,\n ),\n );\n }\n\n return el(\n \"div\",\n {\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n width: \"100%\",\n height: \"100%\",\n padding: `${pad}px`,\n background: r.preset === \"split\" ? `linear-gradient(135deg, ${p.from}, ${p.to})` : p.bg,\n color: p.fg,\n fontFamily: \"sans-serif\",\n },\n children,\n );\n}\n\n/* ------------------------------------------------------------------ *\n * Zero-dependency SVG (previews, emails, icons, guaranteed fallback)\n * ------------------------------------------------------------------ */\n\nfunction esc(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\n/** Greedy word-wrap by estimated glyph width. */\nfunction wrap(text: string, fontSize: number, maxWidth: number, maxLines: number): string[] {\n const charW = fontSize * 0.56;\n const perLine = Math.max(1, Math.floor(maxWidth / charW));\n const words = text.split(/\\s+/);\n const lines: string[] = [];\n let cur = \"\";\n for (const w of words) {\n const next = cur ? `${cur} ${w}` : w;\n if (next.length > perLine && cur) {\n lines.push(cur);\n cur = w;\n if (lines.length === maxLines - 1) break;\n } else {\n cur = next;\n }\n }\n if (cur && lines.length < maxLines) lines.push(cur);\n const used = lines.join(\" \").length;\n if (used < text.length && lines.length) {\n lines[lines.length - 1] = `${lines[lines.length - 1]!.replace(/\\.*$/, \"\")}…`;\n }\n return lines;\n}\n\n/**\n * Render a share card as a standalone SVG string — no browser, no runtime deps.\n * Perfect for previews, email headers, README banners and icons.\n */\nexport function ogSvg(options: OgOptions): string {\n const r = resolve(options);\n const p = palette(r);\n const W = r.width;\n const H = r.height;\n const pad = Math.round(W * 0.075);\n const titleSize = fitFontSize(r.title);\n const contentW = W - pad * 2;\n const split = r.preset === \"split\";\n\n const titleLines = wrap(r.title, titleSize, contentW, 3);\n const titleBlockH = titleLines.length * titleSize * 1.08;\n const titleTop = H / 2 - titleBlockH / 2 + titleSize * 0.8;\n\n const parts: string[] = [];\n parts.push(\n `<svg width=\"${W}\" height=\"${H}\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter, ui-sans-serif, system-ui, sans-serif\">`,\n );\n parts.push(`<defs>\n <linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0\" stop-color=\"${esc(p.from)}\"/><stop offset=\"1\" stop-color=\"${esc(p.to)}\"/>\n </linearGradient>\n <radialGradient id=\"glow\" cx=\"0.85\" cy=\"0.1\" r=\"0.7\">\n <stop offset=\"0\" stop-color=\"${esc(p.from)}\" stop-opacity=\"${r.theme === \"dark\" ? 0.28 : 0.2}\"/>\n <stop offset=\"1\" stop-color=\"${esc(p.to)}\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>`);\n\n // background\n parts.push(`<rect width=\"${W}\" height=\"${H}\" fill=\"${split ? \"url(#accent)\" : esc(p.bg)}\"/>`);\n if (!split) parts.push(`<rect width=\"${W}\" height=\"${H}\" fill=\"url(#glow)\"/>`);\n // accent bar\n parts.push(`<rect width=\"${W}\" height=\"${Math.round(H * 0.02)}\" fill=\"url(#accent)\"/>`);\n\n // eyebrow\n let topY = pad + 34;\n if (r.eyebrow) {\n parts.push(\n `<text x=\"${pad}\" y=\"${topY}\" font-size=\"26\" font-weight=\"600\" letter-spacing=\"3\" fill=\"${esc(p.from)}\">${esc(\n r.eyebrow.toUpperCase(),\n )}</text>`,\n );\n }\n // logo mark (top-right)\n if (r.logo) {\n const s = 76;\n const lx = W - pad - s;\n const ly = pad;\n parts.push(`<rect x=\"${lx}\" y=\"${ly}\" width=\"${s}\" height=\"${s}\" rx=\"20\" fill=\"url(#accent)\"/>`);\n parts.push(\n `<text x=\"${lx + s / 2}\" y=\"${ly + s / 2 + 14}\" font-size=\"40\" font-weight=\"800\" text-anchor=\"middle\" fill=\"#0a0a0f\">${esc(\n r.logo,\n )}</text>`,\n );\n }\n // badge\n if (r.badge) {\n const bx = pad + (r.eyebrow ? 0 : 0);\n parts.push(\n `<rect x=\"${bx}\" y=\"${topY + 14}\" width=\"${r.badge.length * 16 + 40}\" height=\"44\" rx=\"22\" fill=\"${esc(\n p.from,\n )}\" opacity=\"0.18\"/>`,\n );\n parts.push(\n `<text x=\"${bx + 20}\" y=\"${topY + 43}\" font-size=\"24\" font-weight=\"700\" fill=\"${esc(p.fg)}\">${esc(\n r.badge,\n )}</text>`,\n );\n topY += 60;\n }\n\n // title (wrapped)\n titleLines.forEach((line, i) => {\n parts.push(\n `<text x=\"${pad}\" y=\"${titleTop + i * titleSize * 1.08}\" font-size=\"${titleSize}\" font-weight=\"800\" letter-spacing=\"-1\" fill=\"${esc(\n p.fg,\n )}\">${esc(line)}</text>`,\n );\n });\n // subtitle\n if (r.subtitle) {\n parts.push(\n `<text x=\"${pad}\" y=\"${titleTop + titleLines.length * titleSize * 1.08 + 46}\" font-size=\"34\" font-weight=\"500\" fill=\"${esc(\n p.muted,\n )}\">${esc(r.subtitle)}</text>`,\n );\n }\n // footer\n if (r.footer) {\n parts.push(\n `<text x=\"${pad}\" y=\"${H - pad}\" font-size=\"26\" font-weight=\"500\" fill=\"${esc(p.muted)}\">${esc(\n r.footer,\n )}</text>`,\n );\n }\n\n parts.push(`</svg>`);\n return parts.join(\"\");\n}\n\n/** SVG as a `data:` URI — drop straight into `src` / `background-image`. */\nexport function ogSvgDataUri(options: OgOptions): string {\n return `data:image/svg+xml;utf8,${encodeURIComponent(ogSvg(options))}`;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/og",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dynamic Open Graph images — a share-card design system you configure once and call per page. Produces a next/og element tree AND a zero-dependency SVG from the same options, with auto-fitting titles, presets, badges and gradients. Zero-dependency, isomorphic.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"og-image",
|
|
31
|
+
"open-graph",
|
|
32
|
+
"opengraph",
|
|
33
|
+
"social-image",
|
|
34
|
+
"social-card",
|
|
35
|
+
"next-og",
|
|
36
|
+
"vercel-og",
|
|
37
|
+
"satori",
|
|
38
|
+
"twitter-card",
|
|
39
|
+
"seo",
|
|
40
|
+
"svg",
|
|
41
|
+
"dynamic-image",
|
|
42
|
+
"typescript",
|
|
43
|
+
"zero-dependency",
|
|
44
|
+
"isomorphic"
|
|
45
|
+
],
|
|
46
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
47
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
48
|
+
"homepage": "https://lacspace.com/packages",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
52
|
+
"directory": "og"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|