@ewanc26/og 0.1.3 → 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/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
@@ -68,12 +69,6 @@ export const BUNDLED_FONTS = {
68
69
  },
69
70
  } as const
70
71
 
71
- // Google Fonts CDN fallback URLs
72
- const FONT_FALLBACKS = {
73
- heading: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Bold.ttf',
74
- body: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Regular.ttf',
75
- }
76
-
77
72
  // ─── Font Loading ──────────────────────────────────────────────────────────────
78
73
 
79
74
  export interface LoadedFonts {
@@ -82,55 +77,45 @@ export interface LoadedFonts {
82
77
  }
83
78
 
84
79
  /**
85
- * Load fonts from config, falling back to bundled fonts,
86
- * with CDN fallback for serverless environments.
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
+
86
+ /**
87
+ * Load fonts from config, falling back to bundled fonts.
87
88
  */
88
89
  export async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {
89
90
  const headingPath = config?.heading ?? BUNDLED_FONTS.heading
90
91
  const bodyPath = config?.body ?? BUNDLED_FONTS.body
91
92
 
92
93
  const [heading, body] = await Promise.all([
93
- loadFontFileWithFallback(headingPath, FONT_FALLBACKS.heading),
94
- loadFontFileWithFallback(bodyPath, FONT_FALLBACKS.body),
94
+ loadFontFile(headingPath),
95
+ loadFontFile(bodyPath),
95
96
  ])
96
97
 
97
98
  return { heading, body }
98
99
  }
99
100
 
100
101
  /**
101
- * Load a font file, falling back to CDN if local file fails.
102
+ * Load a font from file path.
103
+ * Falls back to alternative locations if local file not found.
102
104
  */
103
- async function loadFontFileWithFallback(path: string, fallbackUrl: string): Promise<ArrayBuffer> {
105
+ async function loadFontFile(source: string): Promise<ArrayBuffer> {
104
106
  try {
105
- return await loadFontFile(path)
107
+ const buffer = await readFile(source)
108
+ return toArrayBuffer(buffer)
106
109
  } catch (error) {
107
- console.warn(`Failed to load local font at ${path}, trying CDN fallback:`, error)
108
- try {
109
- return await loadFontFile(fallbackUrl)
110
- } catch (fallbackError) {
111
- throw new Error(`Failed to load font from both local path (${path}) and CDN (${fallbackUrl}): ${fallbackError}`)
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
112
114
  }
115
+ throw new Error(`Failed to load font from ${source}`)
113
116
  }
114
117
  }
115
118
 
116
- /**
117
- * Load a font from file path or URL.
118
- */
119
- async function loadFontFile(source: string): Promise<ArrayBuffer> {
120
- // Handle URLs
121
- if (source.startsWith('http://') || source.startsWith('https://')) {
122
- const response = await fetch(source)
123
- if (!response.ok) {
124
- throw new Error(`Failed to load font from URL: ${source}`)
125
- }
126
- return response.arrayBuffer()
127
- }
128
-
129
- // Handle file paths
130
- const buffer = await readFile(source)
131
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
132
- }
133
-
134
119
  // ─── Font Registration for Satori ─────────────────────────────────────────────
135
120
 
136
121
  export type SatoriFontConfig = {
@@ -10,6 +10,7 @@ export function blogTemplate({
10
10
  description,
11
11
  siteName,
12
12
  colors,
13
+ noiseDataUrl,
13
14
  width,
14
15
  height,
15
16
  }: OgTemplateProps) {
@@ -17,6 +18,7 @@ export function blogTemplate({
17
18
  type: 'div',
18
19
  props: {
19
20
  style: {
21
+ position: 'relative',
20
22
  display: 'flex',
21
23
  flexDirection: 'column',
22
24
  alignItems: 'center',
@@ -26,51 +28,83 @@ export function blogTemplate({
26
28
  backgroundColor: colors.background,
27
29
  },
28
30
  children: [
29
- {
30
- type: 'h1',
31
- props: {
32
- style: {
33
- fontSize: 64,
34
- fontWeight: 700,
35
- color: colors.text,
36
- letterSpacing: '-0.02em',
37
- margin: 0,
38
- textAlign: 'center',
39
- lineHeight: 1.1,
40
- maxWidth: 1000,
41
- },
42
- children: title,
43
- },
44
- },
45
- description ? {
46
- type: 'p',
31
+ noiseDataUrl ? {
32
+ type: 'img',
47
33
  props: {
34
+ src: noiseDataUrl,
35
+ width,
36
+ height,
48
37
  style: {
49
- fontSize: 28,
50
- fontWeight: 400,
51
- color: colors.accent,
52
- marginTop: 28,
53
- marginBottom: 0,
54
- textAlign: 'center',
55
- lineHeight: 1.4,
56
- maxWidth: 900,
38
+ position: 'absolute',
39
+ top: 0,
40
+ left: 0,
41
+ width,
42
+ height,
57
43
  },
58
- children: description,
59
44
  },
60
45
  } : null,
61
46
  {
62
- type: 'p',
47
+ type: 'div',
63
48
  props: {
64
49
  style: {
65
- fontSize: 24,
66
- fontWeight: 400,
67
- color: colors.accent,
68
- marginTop: 56,
69
- marginBottom: 0,
70
- textAlign: 'center',
71
- opacity: 0.7,
50
+ position: 'relative',
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ width,
56
+ height,
57
+ padding: '0 60px',
72
58
  },
73
- children: siteName,
59
+ children: [
60
+ {
61
+ type: 'h1',
62
+ props: {
63
+ style: {
64
+ fontSize: 64,
65
+ fontWeight: 700,
66
+ color: colors.text,
67
+ letterSpacing: '-0.02em',
68
+ margin: 0,
69
+ textAlign: 'center',
70
+ lineHeight: 1.1,
71
+ maxWidth: 1000,
72
+ },
73
+ children: title,
74
+ },
75
+ },
76
+ description ? {
77
+ type: 'p',
78
+ props: {
79
+ style: {
80
+ fontSize: 28,
81
+ fontWeight: 400,
82
+ color: colors.accent,
83
+ marginTop: 28,
84
+ marginBottom: 0,
85
+ textAlign: 'center',
86
+ lineHeight: 1.4,
87
+ maxWidth: 900,
88
+ },
89
+ children: description,
90
+ },
91
+ } : null,
92
+ {
93
+ type: 'p',
94
+ props: {
95
+ style: {
96
+ fontSize: 24,
97
+ fontWeight: 400,
98
+ color: colors.accent,
99
+ marginTop: 56,
100
+ marginBottom: 0,
101
+ textAlign: 'center',
102
+ opacity: 0.7,
103
+ },
104
+ children: siteName,
105
+ },
106
+ },
107
+ ].filter(Boolean),
74
108
  },
75
109
  },
76
110
  ].filter(Boolean),
@@ -10,6 +10,7 @@ export function defaultTemplate({
10
10
  description,
11
11
  siteName,
12
12
  colors,
13
+ noiseDataUrl,
13
14
  width,
14
15
  height,
15
16
  }: OgTemplateProps) {
@@ -17,6 +18,7 @@ export function defaultTemplate({
17
18
  type: 'div',
18
19
  props: {
19
20
  style: {
21
+ position: 'relative',
20
22
  display: 'flex',
21
23
  flexDirection: 'column',
22
24
  alignItems: 'center',
@@ -26,48 +28,80 @@ export function defaultTemplate({
26
28
  backgroundColor: colors.background,
27
29
  },
28
30
  children: [
29
- {
30
- type: 'h1',
31
- props: {
32
- style: {
33
- fontSize: 72,
34
- fontWeight: 700,
35
- color: colors.text,
36
- letterSpacing: '-0.02em',
37
- margin: 0,
38
- textAlign: 'center',
39
- },
40
- children: title,
41
- },
42
- },
43
- description ? {
44
- type: 'p',
31
+ noiseDataUrl ? {
32
+ type: 'img',
45
33
  props: {
34
+ src: noiseDataUrl,
35
+ width,
36
+ height,
46
37
  style: {
47
- fontSize: 32,
48
- fontWeight: 400,
49
- color: colors.accent,
50
- marginTop: 24,
51
- marginBottom: 0,
52
- textAlign: 'center',
53
- maxWidth: 900,
38
+ position: 'absolute',
39
+ top: 0,
40
+ left: 0,
41
+ width,
42
+ height,
54
43
  },
55
- children: description,
56
44
  },
57
45
  } : null,
58
46
  {
59
- type: 'p',
47
+ type: 'div',
60
48
  props: {
61
49
  style: {
62
- fontSize: 28,
63
- fontWeight: 400,
64
- color: colors.accent,
65
- marginTop: 64,
66
- marginBottom: 0,
67
- textAlign: 'center',
68
- opacity: 0.7,
50
+ position: 'relative',
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ width,
56
+ height,
57
+ padding: '0 60px',
69
58
  },
70
- children: siteName,
59
+ children: [
60
+ {
61
+ type: 'h1',
62
+ props: {
63
+ style: {
64
+ fontSize: 72,
65
+ fontWeight: 700,
66
+ color: colors.text,
67
+ letterSpacing: '-0.02em',
68
+ margin: 0,
69
+ textAlign: 'center',
70
+ },
71
+ children: title,
72
+ },
73
+ },
74
+ description ? {
75
+ type: 'p',
76
+ props: {
77
+ style: {
78
+ fontSize: 32,
79
+ fontWeight: 400,
80
+ color: colors.accent,
81
+ marginTop: 24,
82
+ marginBottom: 0,
83
+ textAlign: 'center',
84
+ maxWidth: 900,
85
+ },
86
+ children: description,
87
+ },
88
+ } : null,
89
+ {
90
+ type: 'p',
91
+ props: {
92
+ style: {
93
+ fontSize: 28,
94
+ fontWeight: 400,
95
+ color: colors.accent,
96
+ marginTop: 64,
97
+ marginBottom: 0,
98
+ textAlign: 'center',
99
+ opacity: 0.7,
100
+ },
101
+ children: siteName,
102
+ },
103
+ },
104
+ ].filter(Boolean),
71
105
  },
72
106
  },
73
107
  ].filter(Boolean),
@@ -11,13 +11,14 @@ export function profileTemplate({
11
11
  siteName,
12
12
  image,
13
13
  colors,
14
+ noiseDataUrl,
14
15
  width,
15
16
  height,
16
17
  }: OgTemplateProps) {
17
- const children: unknown[] = []
18
+ const contentChildren: unknown[] = []
18
19
 
19
20
  if (image) {
20
- children.push({
21
+ contentChildren.push({
21
22
  type: 'img',
22
23
  props: {
23
24
  src: image,
@@ -32,7 +33,7 @@ export function profileTemplate({
32
33
  })
33
34
  }
34
35
 
35
- children.push({
36
+ contentChildren.push({
36
37
  type: 'h1',
37
38
  props: {
38
39
  style: {
@@ -50,7 +51,7 @@ export function profileTemplate({
50
51
  })
51
52
 
52
53
  if (description) {
53
- children.push({
54
+ contentChildren.push({
54
55
  type: 'p',
55
56
  props: {
56
57
  style: {
@@ -68,7 +69,7 @@ export function profileTemplate({
68
69
  })
69
70
  }
70
71
 
71
- children.push({
72
+ contentChildren.push({
72
73
  type: 'p',
73
74
  props: {
74
75
  style: {
@@ -88,6 +89,7 @@ export function profileTemplate({
88
89
  type: 'div',
89
90
  props: {
90
91
  style: {
92
+ position: 'relative',
91
93
  display: 'flex',
92
94
  flexDirection: 'column',
93
95
  alignItems: 'center',
@@ -96,7 +98,39 @@ export function profileTemplate({
96
98
  height,
97
99
  backgroundColor: colors.background,
98
100
  },
99
- children,
101
+ children: [
102
+ noiseDataUrl ? {
103
+ type: 'img',
104
+ props: {
105
+ src: noiseDataUrl,
106
+ width,
107
+ height,
108
+ style: {
109
+ position: 'absolute',
110
+ top: 0,
111
+ left: 0,
112
+ width,
113
+ height,
114
+ },
115
+ },
116
+ } : null,
117
+ {
118
+ type: 'div',
119
+ props: {
120
+ style: {
121
+ position: 'relative',
122
+ display: 'flex',
123
+ flexDirection: 'column',
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ width,
127
+ height,
128
+ padding: '0 60px',
129
+ },
130
+ children: contentChildren,
131
+ },
132
+ },
133
+ ].filter(Boolean),
100
134
  },
101
135
  }
102
136
  }
@@ -1,258 +0,0 @@
1
- // src/templates/blog.ts
2
- function blogTemplate({
3
- title,
4
- description,
5
- siteName,
6
- colors,
7
- width,
8
- height
9
- }) {
10
- return {
11
- type: "div",
12
- props: {
13
- style: {
14
- display: "flex",
15
- flexDirection: "column",
16
- alignItems: "center",
17
- justifyContent: "center",
18
- width,
19
- height,
20
- backgroundColor: colors.background
21
- },
22
- children: [
23
- {
24
- type: "h1",
25
- props: {
26
- style: {
27
- fontSize: 64,
28
- fontWeight: 700,
29
- color: colors.text,
30
- letterSpacing: "-0.02em",
31
- margin: 0,
32
- textAlign: "center",
33
- lineHeight: 1.1,
34
- maxWidth: 1e3
35
- },
36
- children: title
37
- }
38
- },
39
- description ? {
40
- type: "p",
41
- props: {
42
- style: {
43
- fontSize: 28,
44
- fontWeight: 400,
45
- color: colors.accent,
46
- marginTop: 28,
47
- marginBottom: 0,
48
- textAlign: "center",
49
- lineHeight: 1.4,
50
- maxWidth: 900
51
- },
52
- children: description
53
- }
54
- } : null,
55
- {
56
- type: "p",
57
- props: {
58
- style: {
59
- fontSize: 24,
60
- fontWeight: 400,
61
- color: colors.accent,
62
- marginTop: 56,
63
- marginBottom: 0,
64
- textAlign: "center",
65
- opacity: 0.7
66
- },
67
- children: siteName
68
- }
69
- }
70
- ].filter(Boolean)
71
- }
72
- };
73
- }
74
-
75
- // src/templates/profile.ts
76
- function profileTemplate({
77
- title,
78
- description,
79
- siteName,
80
- image,
81
- colors,
82
- width,
83
- height
84
- }) {
85
- const children = [];
86
- if (image) {
87
- children.push({
88
- type: "img",
89
- props: {
90
- src: image,
91
- width: 120,
92
- height: 120,
93
- style: {
94
- borderRadius: "50%",
95
- marginBottom: 32,
96
- objectFit: "cover"
97
- }
98
- }
99
- });
100
- }
101
- children.push({
102
- type: "h1",
103
- props: {
104
- style: {
105
- fontSize: 56,
106
- fontWeight: 700,
107
- color: colors.text,
108
- letterSpacing: "-0.02em",
109
- margin: 0,
110
- textAlign: "center",
111
- lineHeight: 1.1,
112
- maxWidth: 900
113
- },
114
- children: title
115
- }
116
- });
117
- if (description) {
118
- children.push({
119
- type: "p",
120
- props: {
121
- style: {
122
- fontSize: 26,
123
- fontWeight: 400,
124
- color: colors.accent,
125
- marginTop: 20,
126
- marginBottom: 0,
127
- textAlign: "center",
128
- lineHeight: 1.4,
129
- maxWidth: 700
130
- },
131
- children: description
132
- }
133
- });
134
- }
135
- children.push({
136
- type: "p",
137
- props: {
138
- style: {
139
- fontSize: 24,
140
- fontWeight: 400,
141
- color: colors.accent,
142
- marginTop: 48,
143
- marginBottom: 0,
144
- textAlign: "center",
145
- opacity: 0.7
146
- },
147
- children: siteName
148
- }
149
- });
150
- return {
151
- type: "div",
152
- props: {
153
- style: {
154
- display: "flex",
155
- flexDirection: "column",
156
- alignItems: "center",
157
- justifyContent: "center",
158
- width,
159
- height,
160
- backgroundColor: colors.background
161
- },
162
- children
163
- }
164
- };
165
- }
166
-
167
- // src/templates/default.ts
168
- function defaultTemplate({
169
- title,
170
- description,
171
- siteName,
172
- colors,
173
- width,
174
- height
175
- }) {
176
- return {
177
- type: "div",
178
- props: {
179
- style: {
180
- display: "flex",
181
- flexDirection: "column",
182
- alignItems: "center",
183
- justifyContent: "center",
184
- width,
185
- height,
186
- backgroundColor: colors.background
187
- },
188
- children: [
189
- {
190
- type: "h1",
191
- props: {
192
- style: {
193
- fontSize: 72,
194
- fontWeight: 700,
195
- color: colors.text,
196
- letterSpacing: "-0.02em",
197
- margin: 0,
198
- textAlign: "center"
199
- },
200
- children: title
201
- }
202
- },
203
- description ? {
204
- type: "p",
205
- props: {
206
- style: {
207
- fontSize: 32,
208
- fontWeight: 400,
209
- color: colors.accent,
210
- marginTop: 24,
211
- marginBottom: 0,
212
- textAlign: "center",
213
- maxWidth: 900
214
- },
215
- children: description
216
- }
217
- } : null,
218
- {
219
- type: "p",
220
- props: {
221
- style: {
222
- fontSize: 28,
223
- fontWeight: 400,
224
- color: colors.accent,
225
- marginTop: 64,
226
- marginBottom: 0,
227
- textAlign: "center",
228
- opacity: 0.7
229
- },
230
- children: siteName
231
- }
232
- }
233
- ].filter(Boolean)
234
- }
235
- };
236
- }
237
-
238
- // src/templates/index.ts
239
- var templates = {
240
- blog: blogTemplate,
241
- profile: profileTemplate,
242
- default: defaultTemplate
243
- };
244
- function getTemplate(name) {
245
- if (typeof name === "function") {
246
- return name;
247
- }
248
- return templates[name];
249
- }
250
-
251
- export {
252
- blogTemplate,
253
- profileTemplate,
254
- defaultTemplate,
255
- templates,
256
- getTemplate
257
- };
258
- //# sourceMappingURL=chunk-EPPJ2HBS.js.map