@ewanc26/og 0.1.4 → 0.1.5
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/dist/chunk-2I73D34T.js +368 -0
- package/dist/chunk-2I73D34T.js.map +1 -0
- package/dist/index.cjs +248 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +64 -20
- package/dist/index.js.map +1 -1
- package/dist/templates/index.cjs +179 -77
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.d.cts +208 -85
- package/dist/templates/index.d.ts +208 -85
- package/dist/templates/index.js +1 -1
- package/package.json +62 -62
- package/src/endpoint.ts +4 -2
- package/src/fonts-data.ts +61 -0
- package/src/fonts.ts +15 -15
- package/src/templates/blog.ts +70 -36
- package/src/templates/default.ts +67 -33
- package/src/templates/profile.ts +40 -6
- package/dist/chunk-EPPJ2HBS.js +0 -258
- package/dist/chunk-EPPJ2HBS.js.map +0 -1
|
@@ -5,10 +5,11 @@ import { f as OgTemplateProps, e as OgTemplate } from '../types-Bn2R50Vr.js';
|
|
|
5
5
|
* Clean centered layout.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
declare function blogTemplate({ title, description, siteName, colors, width, height, }: OgTemplateProps): {
|
|
8
|
+
declare function blogTemplate({ title, description, siteName, colors, noiseDataUrl, width, height, }: OgTemplateProps): {
|
|
9
9
|
type: string;
|
|
10
10
|
props: {
|
|
11
11
|
style: {
|
|
12
|
+
position: string;
|
|
12
13
|
display: string;
|
|
13
14
|
flexDirection: string;
|
|
14
15
|
alignItems: string;
|
|
@@ -20,56 +21,96 @@ declare function blogTemplate({ title, description, siteName, colors, width, hei
|
|
|
20
21
|
children: ({
|
|
21
22
|
type: string;
|
|
22
23
|
props: {
|
|
24
|
+
src: string;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
23
27
|
style: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
opacity?: undefined;
|
|
28
|
+
position: string;
|
|
29
|
+
top: number;
|
|
30
|
+
left: number;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
display?: undefined;
|
|
34
|
+
flexDirection?: undefined;
|
|
35
|
+
alignItems?: undefined;
|
|
36
|
+
justifyContent?: undefined;
|
|
37
|
+
padding?: undefined;
|
|
35
38
|
};
|
|
36
|
-
children
|
|
39
|
+
children?: undefined;
|
|
37
40
|
};
|
|
38
41
|
} | {
|
|
39
42
|
type: string;
|
|
40
43
|
props: {
|
|
41
44
|
style: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
opacity?: undefined;
|
|
45
|
+
position: string;
|
|
46
|
+
display: string;
|
|
47
|
+
flexDirection: string;
|
|
48
|
+
alignItems: string;
|
|
49
|
+
justifyContent: string;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
padding: string;
|
|
53
|
+
top?: undefined;
|
|
54
|
+
left?: undefined;
|
|
53
55
|
};
|
|
54
|
-
children:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
children: ({
|
|
57
|
+
type: string;
|
|
58
|
+
props: {
|
|
59
|
+
style: {
|
|
60
|
+
fontSize: number;
|
|
61
|
+
fontWeight: number;
|
|
62
|
+
color: string;
|
|
63
|
+
letterSpacing: string;
|
|
64
|
+
margin: number;
|
|
65
|
+
textAlign: string;
|
|
66
|
+
lineHeight: number;
|
|
67
|
+
maxWidth: number;
|
|
68
|
+
marginTop?: undefined;
|
|
69
|
+
marginBottom?: undefined;
|
|
70
|
+
opacity?: undefined;
|
|
71
|
+
};
|
|
72
|
+
children: string;
|
|
73
|
+
};
|
|
74
|
+
} | {
|
|
75
|
+
type: string;
|
|
76
|
+
props: {
|
|
77
|
+
style: {
|
|
78
|
+
fontSize: number;
|
|
79
|
+
fontWeight: number;
|
|
80
|
+
color: string;
|
|
81
|
+
marginTop: number;
|
|
82
|
+
marginBottom: number;
|
|
83
|
+
textAlign: string;
|
|
84
|
+
lineHeight: number;
|
|
85
|
+
maxWidth: number;
|
|
86
|
+
letterSpacing?: undefined;
|
|
87
|
+
margin?: undefined;
|
|
88
|
+
opacity?: undefined;
|
|
89
|
+
};
|
|
90
|
+
children: string;
|
|
91
|
+
};
|
|
92
|
+
} | {
|
|
93
|
+
type: string;
|
|
94
|
+
props: {
|
|
95
|
+
style: {
|
|
96
|
+
fontSize: number;
|
|
97
|
+
fontWeight: number;
|
|
98
|
+
color: string;
|
|
99
|
+
marginTop: number;
|
|
100
|
+
marginBottom: number;
|
|
101
|
+
textAlign: string;
|
|
102
|
+
opacity: number;
|
|
103
|
+
letterSpacing?: undefined;
|
|
104
|
+
margin?: undefined;
|
|
105
|
+
lineHeight?: undefined;
|
|
106
|
+
maxWidth?: undefined;
|
|
107
|
+
};
|
|
108
|
+
children: string;
|
|
109
|
+
};
|
|
110
|
+
} | null)[];
|
|
111
|
+
src?: undefined;
|
|
112
|
+
width?: undefined;
|
|
113
|
+
height?: undefined;
|
|
73
114
|
};
|
|
74
115
|
} | null)[];
|
|
75
116
|
};
|
|
@@ -80,10 +121,11 @@ declare function blogTemplate({ title, description, siteName, colors, width, hei
|
|
|
80
121
|
* Centered layout.
|
|
81
122
|
*/
|
|
82
123
|
|
|
83
|
-
declare function profileTemplate({ title, description, siteName, image, colors, width, height, }: OgTemplateProps): {
|
|
124
|
+
declare function profileTemplate({ title, description, siteName, image, colors, noiseDataUrl, width, height, }: OgTemplateProps): {
|
|
84
125
|
type: string;
|
|
85
126
|
props: {
|
|
86
127
|
style: {
|
|
128
|
+
position: string;
|
|
87
129
|
display: string;
|
|
88
130
|
flexDirection: string;
|
|
89
131
|
alignItems: string;
|
|
@@ -92,7 +134,47 @@ declare function profileTemplate({ title, description, siteName, image, colors,
|
|
|
92
134
|
height: number;
|
|
93
135
|
backgroundColor: string;
|
|
94
136
|
};
|
|
95
|
-
children:
|
|
137
|
+
children: ({
|
|
138
|
+
type: string;
|
|
139
|
+
props: {
|
|
140
|
+
src: string;
|
|
141
|
+
width: number;
|
|
142
|
+
height: number;
|
|
143
|
+
style: {
|
|
144
|
+
position: string;
|
|
145
|
+
top: number;
|
|
146
|
+
left: number;
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
display?: undefined;
|
|
150
|
+
flexDirection?: undefined;
|
|
151
|
+
alignItems?: undefined;
|
|
152
|
+
justifyContent?: undefined;
|
|
153
|
+
padding?: undefined;
|
|
154
|
+
};
|
|
155
|
+
children?: undefined;
|
|
156
|
+
};
|
|
157
|
+
} | {
|
|
158
|
+
type: string;
|
|
159
|
+
props: {
|
|
160
|
+
style: {
|
|
161
|
+
position: string;
|
|
162
|
+
display: string;
|
|
163
|
+
flexDirection: string;
|
|
164
|
+
alignItems: string;
|
|
165
|
+
justifyContent: string;
|
|
166
|
+
width: number;
|
|
167
|
+
height: number;
|
|
168
|
+
padding: string;
|
|
169
|
+
top?: undefined;
|
|
170
|
+
left?: undefined;
|
|
171
|
+
};
|
|
172
|
+
children: unknown[];
|
|
173
|
+
src?: undefined;
|
|
174
|
+
width?: undefined;
|
|
175
|
+
height?: undefined;
|
|
176
|
+
};
|
|
177
|
+
} | null)[];
|
|
96
178
|
};
|
|
97
179
|
};
|
|
98
180
|
|
|
@@ -101,10 +183,11 @@ declare function profileTemplate({ title, description, siteName, image, colors,
|
|
|
101
183
|
* Clean, centered layout.
|
|
102
184
|
*/
|
|
103
185
|
|
|
104
|
-
declare function defaultTemplate({ title, description, siteName, colors, width, height, }: OgTemplateProps): {
|
|
186
|
+
declare function defaultTemplate({ title, description, siteName, colors, noiseDataUrl, width, height, }: OgTemplateProps): {
|
|
105
187
|
type: string;
|
|
106
188
|
props: {
|
|
107
189
|
style: {
|
|
190
|
+
position: string;
|
|
108
191
|
display: string;
|
|
109
192
|
flexDirection: string;
|
|
110
193
|
alignItems: string;
|
|
@@ -116,53 +199,93 @@ declare function defaultTemplate({ title, description, siteName, colors, width,
|
|
|
116
199
|
children: ({
|
|
117
200
|
type: string;
|
|
118
201
|
props: {
|
|
202
|
+
src: string;
|
|
203
|
+
width: number;
|
|
204
|
+
height: number;
|
|
119
205
|
style: {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
children: string;
|
|
132
|
-
};
|
|
133
|
-
} | {
|
|
134
|
-
type: string;
|
|
135
|
-
props: {
|
|
136
|
-
style: {
|
|
137
|
-
fontSize: number;
|
|
138
|
-
fontWeight: number;
|
|
139
|
-
color: string;
|
|
140
|
-
marginTop: number;
|
|
141
|
-
marginBottom: number;
|
|
142
|
-
textAlign: string;
|
|
143
|
-
maxWidth: number;
|
|
144
|
-
letterSpacing?: undefined;
|
|
145
|
-
margin?: undefined;
|
|
146
|
-
opacity?: undefined;
|
|
206
|
+
position: string;
|
|
207
|
+
top: number;
|
|
208
|
+
left: number;
|
|
209
|
+
width: number;
|
|
210
|
+
height: number;
|
|
211
|
+
display?: undefined;
|
|
212
|
+
flexDirection?: undefined;
|
|
213
|
+
alignItems?: undefined;
|
|
214
|
+
justifyContent?: undefined;
|
|
215
|
+
padding?: undefined;
|
|
147
216
|
};
|
|
148
|
-
children
|
|
217
|
+
children?: undefined;
|
|
149
218
|
};
|
|
150
219
|
} | {
|
|
151
220
|
type: string;
|
|
152
221
|
props: {
|
|
153
222
|
style: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
223
|
+
position: string;
|
|
224
|
+
display: string;
|
|
225
|
+
flexDirection: string;
|
|
226
|
+
alignItems: string;
|
|
227
|
+
justifyContent: string;
|
|
228
|
+
width: number;
|
|
229
|
+
height: number;
|
|
230
|
+
padding: string;
|
|
231
|
+
top?: undefined;
|
|
232
|
+
left?: undefined;
|
|
164
233
|
};
|
|
165
|
-
children:
|
|
234
|
+
children: ({
|
|
235
|
+
type: string;
|
|
236
|
+
props: {
|
|
237
|
+
style: {
|
|
238
|
+
fontSize: number;
|
|
239
|
+
fontWeight: number;
|
|
240
|
+
color: string;
|
|
241
|
+
letterSpacing: string;
|
|
242
|
+
margin: number;
|
|
243
|
+
textAlign: string;
|
|
244
|
+
marginTop?: undefined;
|
|
245
|
+
marginBottom?: undefined;
|
|
246
|
+
maxWidth?: undefined;
|
|
247
|
+
opacity?: undefined;
|
|
248
|
+
};
|
|
249
|
+
children: string;
|
|
250
|
+
};
|
|
251
|
+
} | {
|
|
252
|
+
type: string;
|
|
253
|
+
props: {
|
|
254
|
+
style: {
|
|
255
|
+
fontSize: number;
|
|
256
|
+
fontWeight: number;
|
|
257
|
+
color: string;
|
|
258
|
+
marginTop: number;
|
|
259
|
+
marginBottom: number;
|
|
260
|
+
textAlign: string;
|
|
261
|
+
maxWidth: number;
|
|
262
|
+
letterSpacing?: undefined;
|
|
263
|
+
margin?: undefined;
|
|
264
|
+
opacity?: undefined;
|
|
265
|
+
};
|
|
266
|
+
children: string;
|
|
267
|
+
};
|
|
268
|
+
} | {
|
|
269
|
+
type: string;
|
|
270
|
+
props: {
|
|
271
|
+
style: {
|
|
272
|
+
fontSize: number;
|
|
273
|
+
fontWeight: number;
|
|
274
|
+
color: string;
|
|
275
|
+
marginTop: number;
|
|
276
|
+
marginBottom: number;
|
|
277
|
+
textAlign: string;
|
|
278
|
+
opacity: number;
|
|
279
|
+
letterSpacing?: undefined;
|
|
280
|
+
margin?: undefined;
|
|
281
|
+
maxWidth?: undefined;
|
|
282
|
+
};
|
|
283
|
+
children: string;
|
|
284
|
+
};
|
|
285
|
+
} | null)[];
|
|
286
|
+
src?: undefined;
|
|
287
|
+
width?: undefined;
|
|
288
|
+
height?: undefined;
|
|
166
289
|
};
|
|
167
290
|
} | null)[];
|
|
168
291
|
};
|
package/dist/templates/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
2
|
+
"name": "@ewanc26/og",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Dynamic OpenGraph image generator with noise backgrounds, bold typography, and Satori-based rendering. Works in SvelteKit endpoints, edge runtimes, and build scripts.",
|
|
5
|
+
"author": "Ewan Croft",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"og",
|
|
10
|
+
"opengraph",
|
|
11
|
+
"image",
|
|
12
|
+
"satori",
|
|
13
|
+
"sveltekit",
|
|
14
|
+
"seo",
|
|
15
|
+
"social"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ewanc26/pkgs.git",
|
|
20
|
+
"directory": "packages/og"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ewanc26/pkgs/tree/main/packages/og",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ewanc26/pkgs/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"fonts"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./templates": {
|
|
41
|
+
"types": "./dist/templates/index.d.ts",
|
|
42
|
+
"import": "./dist/templates/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"main": "./dist/index.cjs",
|
|
46
|
+
"module": "./dist/index.js",
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@ewanc26/noise": "^0.1.3",
|
|
50
|
+
"@resvg/resvg-js": "^2.6.0",
|
|
51
|
+
"satori": "^0.15.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^25.5.0",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"dev": "tsup --watch",
|
|
61
|
+
"check": "tsc --noEmit"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/endpoint.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { generateOgResponse } from './generate.js'
|
|
6
|
-
import type { OgEndpointOptions,
|
|
6
|
+
import type { OgEndpointOptions, OgGenerateOptions } from './types.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create a SvelteKit GET handler for OG image generation.
|
|
@@ -42,6 +42,8 @@ export function createOgEndpoint(options: OgEndpointOptions) {
|
|
|
42
42
|
const description = url.searchParams.get('description') ?? undefined
|
|
43
43
|
const image = url.searchParams.get('image') ?? undefined
|
|
44
44
|
const noiseSeed = url.searchParams.get('seed') ?? undefined
|
|
45
|
+
const templateParam = url.searchParams.get('template') as 'blog' | 'profile' | 'default' | null
|
|
46
|
+
const resolvedTemplate: OgGenerateOptions['template'] = templateParam ?? template
|
|
45
47
|
|
|
46
48
|
if (!title) {
|
|
47
49
|
return new Response('Missing title parameter', { status: 400 })
|
|
@@ -54,7 +56,7 @@ export function createOgEndpoint(options: OgEndpointOptions) {
|
|
|
54
56
|
description,
|
|
55
57
|
siteName,
|
|
56
58
|
image,
|
|
57
|
-
template:
|
|
59
|
+
template: resolvedTemplate,
|
|
58
60
|
colors,
|
|
59
61
|
fonts,
|
|
60
62
|
noise,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font loading helper for serverless environments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
import { dirname, resolve } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
// Declare __dirname for CJS contexts (injected by bundlers)
|
|
10
|
+
declare const __dirname: string | undefined
|
|
11
|
+
|
|
12
|
+
function getModuleDir(): string {
|
|
13
|
+
if (typeof import.meta !== 'undefined' && import.meta.url) {
|
|
14
|
+
return dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
}
|
|
16
|
+
if (typeof __dirname !== 'undefined') {
|
|
17
|
+
return __dirname
|
|
18
|
+
}
|
|
19
|
+
return resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FontData {
|
|
23
|
+
heading: ArrayBuffer
|
|
24
|
+
body: ArrayBuffer
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Try loading fonts from dist/fonts directory
|
|
29
|
+
*/
|
|
30
|
+
export async function loadEmbeddedFonts(): Promise<FontData | null> {
|
|
31
|
+
const moduleDir = getModuleDir()
|
|
32
|
+
|
|
33
|
+
const paths = [
|
|
34
|
+
{
|
|
35
|
+
heading: resolve(moduleDir, 'fonts/Inter-Bold.ttf'),
|
|
36
|
+
body: resolve(moduleDir, 'fonts/Inter-Regular.ttf'),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
heading: resolve(moduleDir, '../fonts/Inter-Bold.ttf'),
|
|
40
|
+
body: resolve(moduleDir, '../fonts/Inter-Regular.ttf'),
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for (const p of paths) {
|
|
45
|
+
try {
|
|
46
|
+
const [headingBuf, bodyBuf] = await Promise.all([
|
|
47
|
+
readFile(p.heading),
|
|
48
|
+
readFile(p.body),
|
|
49
|
+
])
|
|
50
|
+
// Convert Buffer to ArrayBuffer
|
|
51
|
+
return {
|
|
52
|
+
heading: headingBuf.buffer.slice(headingBuf.byteOffset, headingBuf.byteOffset + headingBuf.byteLength),
|
|
53
|
+
body: bodyBuf.buffer.slice(bodyBuf.byteOffset, bodyBuf.byteOffset + bodyBuf.byteLength),
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null
|
|
61
|
+
}
|
package/src/fonts.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync } from 'node:fs'
|
|
|
9
9
|
import { dirname, resolve } from 'node:path'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
import type { OgFontConfig } from './types.js'
|
|
12
|
+
import { loadEmbeddedFonts } from './fonts-data.js'
|
|
12
13
|
|
|
13
14
|
// Declare __dirname for CJS contexts (injected by bundlers)
|
|
14
15
|
declare const __dirname: string | undefined
|
|
@@ -75,9 +76,15 @@ export interface LoadedFonts {
|
|
|
75
76
|
body: ArrayBuffer
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Helper to convert Buffer to ArrayBuffer
|
|
81
|
+
*/
|
|
82
|
+
function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
83
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
/**
|
|
79
87
|
* Load fonts from config, falling back to bundled fonts.
|
|
80
|
-
* In serverless environments, falls back to fetching from upstream CDN.
|
|
81
88
|
*/
|
|
82
89
|
export async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {
|
|
83
90
|
const headingPath = config?.heading ?? BUNDLED_FONTS.heading
|
|
@@ -93,26 +100,19 @@ export async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {
|
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
102
|
* Load a font from file path.
|
|
96
|
-
* Falls back to
|
|
103
|
+
* Falls back to alternative locations if local file not found.
|
|
97
104
|
*/
|
|
98
105
|
async function loadFontFile(source: string): Promise<ArrayBuffer> {
|
|
99
106
|
try {
|
|
100
107
|
const buffer = await readFile(source)
|
|
101
|
-
return
|
|
108
|
+
return toArrayBuffer(buffer)
|
|
102
109
|
} catch (error) {
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const response = await fetch(cdnUrl)
|
|
109
|
-
if (!response.ok) {
|
|
110
|
-
throw new Error(`CDN fetch failed: ${response.status}`)
|
|
111
|
-
}
|
|
112
|
-
return response.arrayBuffer()
|
|
113
|
-
} catch (cdnError) {
|
|
114
|
-
throw new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`)
|
|
110
|
+
// Try embedded fonts (loaded from alternative locations)
|
|
111
|
+
const embedded = await loadEmbeddedFonts()
|
|
112
|
+
if (embedded) {
|
|
113
|
+
return source.includes('Bold') ? embedded.heading : embedded.body
|
|
115
114
|
}
|
|
115
|
+
throw new Error(`Failed to load font from ${source}`)
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|