@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.
@@ -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
- fontSize: number;
25
- fontWeight: number;
26
- color: string;
27
- letterSpacing: string;
28
- margin: number;
29
- textAlign: string;
30
- lineHeight: number;
31
- maxWidth: number;
32
- marginTop?: undefined;
33
- marginBottom?: undefined;
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: string;
39
+ children?: undefined;
37
40
  };
38
41
  } | {
39
42
  type: string;
40
43
  props: {
41
44
  style: {
42
- fontSize: number;
43
- fontWeight: number;
44
- color: string;
45
- marginTop: number;
46
- marginBottom: number;
47
- textAlign: string;
48
- lineHeight: number;
49
- maxWidth: number;
50
- letterSpacing?: undefined;
51
- margin?: undefined;
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: string;
55
- };
56
- } | {
57
- type: string;
58
- props: {
59
- style: {
60
- fontSize: number;
61
- fontWeight: number;
62
- color: string;
63
- marginTop: number;
64
- marginBottom: number;
65
- textAlign: string;
66
- opacity: number;
67
- letterSpacing?: undefined;
68
- margin?: undefined;
69
- lineHeight?: undefined;
70
- maxWidth?: undefined;
71
- };
72
- children: string;
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: unknown[];
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
- fontSize: number;
121
- fontWeight: number;
122
- color: string;
123
- letterSpacing: string;
124
- margin: number;
125
- textAlign: string;
126
- marginTop?: undefined;
127
- marginBottom?: undefined;
128
- maxWidth?: undefined;
129
- opacity?: undefined;
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: string;
217
+ children?: undefined;
149
218
  };
150
219
  } | {
151
220
  type: string;
152
221
  props: {
153
222
  style: {
154
- fontSize: number;
155
- fontWeight: number;
156
- color: string;
157
- marginTop: number;
158
- marginBottom: number;
159
- textAlign: string;
160
- opacity: number;
161
- letterSpacing?: undefined;
162
- margin?: undefined;
163
- maxWidth?: undefined;
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: string;
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
  };
@@ -4,7 +4,7 @@ import {
4
4
  getTemplate,
5
5
  profileTemplate,
6
6
  templates
7
- } from "../chunk-EPPJ2HBS.js";
7
+ } from "../chunk-2I73D34T.js";
8
8
  export {
9
9
  blogTemplate,
10
10
  defaultTemplate,
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
1
  {
2
- "name": "@ewanc26/og",
3
- "version": "0.1.4",
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
- "scripts": {
49
- "build": "tsup",
50
- "dev": "tsup --watch",
51
- "check": "tsc --noEmit"
52
- },
53
- "dependencies": {
54
- "@ewanc26/noise": "^0.1.3",
55
- "@resvg/resvg-js": "^2.6.0",
56
- "satori": "^0.15.2"
57
- },
58
- "devDependencies": {
59
- "@types/node": "^25.5.0",
60
- "tsup": "^8.5.1",
61
- "typescript": "^5.9.3"
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, OgTemplate } from './types.js'
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: template as OgTemplate,
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 fetching from github raw if local file not found.
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 buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
108
+ return toArrayBuffer(buffer)
102
109
  } catch (error) {
103
- // In serverless, fonts might not be at expected path - fetch from CDN
104
- const filename = source.split('/').pop()
105
- const cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`
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