@ogpipe/next 0.1.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.
@@ -0,0 +1,665 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/client/index.ts
13
+ var OGPipeClient;
14
+ var init_client = __esm({
15
+ "src/client/index.ts"() {
16
+ "use strict";
17
+ OGPipeClient = class {
18
+ apiKey;
19
+ baseUrl;
20
+ timeout;
21
+ constructor(options = {}) {
22
+ this.apiKey = options.apiKey || process.env.OGPIPE_API_KEY || "";
23
+ this.baseUrl = options.baseUrl || process.env.OGPIPE_BASE_URL || "https://api.ogpipe.dev";
24
+ this.timeout = options.timeout || 3e4;
25
+ if (!this.apiKey) {
26
+ throw new Error(
27
+ "[OGPipe] Missing API key. Set OGPIPE_API_KEY environment variable or pass apiKey option."
28
+ );
29
+ }
30
+ }
31
+ /**
32
+ * Render HTML to an image. Returns the CDN URL.
33
+ */
34
+ async render(request) {
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), this.timeout);
37
+ try {
38
+ const res = await fetch(`${this.baseUrl}/images`, {
39
+ method: "POST",
40
+ headers: {
41
+ Authorization: `Bearer ${this.apiKey}`,
42
+ "Content-Type": "application/json"
43
+ },
44
+ body: JSON.stringify({
45
+ html: request.html,
46
+ width: request.width || 1200,
47
+ height: request.height || 630,
48
+ format: request.format || "png"
49
+ }),
50
+ signal: controller.signal
51
+ });
52
+ const body = await res.json();
53
+ if (!res.ok) {
54
+ return {
55
+ success: false,
56
+ error: body.error || `HTTP ${res.status}`,
57
+ statusCode: res.status
58
+ };
59
+ }
60
+ return {
61
+ success: true,
62
+ data: body
63
+ };
64
+ } catch (err) {
65
+ if (err instanceof Error && err.name === "AbortError") {
66
+ return { success: false, error: "Request timed out", statusCode: 408 };
67
+ }
68
+ return {
69
+ success: false,
70
+ error: err instanceof Error ? err.message : "Unknown error",
71
+ statusCode: 500
72
+ };
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+ /**
78
+ * Render HTML and return the raw image buffer (for writing to disk).
79
+ */
80
+ async renderToBuffer(request) {
81
+ const result = await this.render(request);
82
+ if (!result.success) return null;
83
+ const res = await fetch(result.data.url);
84
+ if (!res.ok) return null;
85
+ return Buffer.from(await res.arrayBuffer());
86
+ }
87
+ };
88
+ }
89
+ });
90
+
91
+ // src/next/config.ts
92
+ import { readFileSync } from "fs";
93
+ import { resolve } from "path";
94
+ function resolveTemplateHtml(template, configDir) {
95
+ if (template.html) {
96
+ return template.html;
97
+ }
98
+ if (template.file) {
99
+ const filePath = resolve(configDir, template.file);
100
+ try {
101
+ return readFileSync(filePath, "utf-8");
102
+ } catch (err) {
103
+ throw new Error(`[OGPipe] Template file not found: ${filePath}`);
104
+ }
105
+ }
106
+ throw new Error("[OGPipe] Template must have either 'html' or 'file' property.");
107
+ }
108
+ function injectVariables(html, variables) {
109
+ return html.replace(/\{\{(\w+)\}\}/g, (_, key) => {
110
+ return variables[key] ?? "";
111
+ });
112
+ }
113
+ function matchRoute(path, pattern) {
114
+ if (pattern === "*") return true;
115
+ const regexStr = pattern.replace(/\[\.\.\.[\w]+\]/g, ".*").replace(/\[[\w]+\]/g, "[^/]+").replace(/\*/g, "[^/]+");
116
+ const regex = new RegExp(`^${regexStr}$`);
117
+ return regex.test(path);
118
+ }
119
+ function findRouteConfig(path, routes) {
120
+ const sortedPatterns = Object.keys(routes).sort((a, b) => {
121
+ if (a === "*") return 1;
122
+ if (b === "*") return -1;
123
+ return b.length - a.length;
124
+ });
125
+ for (const pattern of sortedPatterns) {
126
+ if (matchRoute(path, pattern)) {
127
+ return routes[pattern];
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ var init_config = __esm({
133
+ "src/next/config.ts"() {
134
+ "use strict";
135
+ }
136
+ });
137
+
138
+ // src/preview/server.ts
139
+ var server_exports = {};
140
+ __export(server_exports, {
141
+ startPreviewServer: () => startPreviewServer
142
+ });
143
+ import { createServer } from "http";
144
+ import { watch, existsSync as existsSync2 } from "fs";
145
+ import { resolve as resolve3 } from "path";
146
+ async function startPreviewServer(options) {
147
+ const { config, configDir, port = DEFAULT_PORT } = options;
148
+ const routes = buildPreviewRoutes(config);
149
+ const clients = [];
150
+ watchTemplateFiles(config, configDir, () => {
151
+ for (const client of clients) {
152
+ client.write("data: reload\n\n");
153
+ }
154
+ });
155
+ const server = createServer(async (req, res) => {
156
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
157
+ const pathname = url.pathname;
158
+ if (pathname === "/__ogpipe/events") {
159
+ res.writeHead(200, {
160
+ "Content-Type": "text/event-stream",
161
+ "Cache-Control": "no-cache",
162
+ Connection: "keep-alive",
163
+ "Access-Control-Allow-Origin": "*"
164
+ });
165
+ clients.push(res);
166
+ req.on("close", () => {
167
+ const idx = clients.indexOf(res);
168
+ if (idx >= 0) clients.splice(idx, 1);
169
+ });
170
+ return;
171
+ }
172
+ if (pathname === "/__ogpipe/render") {
173
+ const routePath = url.searchParams.get("route") || "/";
174
+ const templateId = url.searchParams.get("template");
175
+ await handleRender(req, res, config, configDir, routePath, templateId);
176
+ return;
177
+ }
178
+ if (pathname === "/__ogpipe/routes") {
179
+ res.writeHead(200, { "Content-Type": "application/json" });
180
+ res.end(JSON.stringify(routes));
181
+ return;
182
+ }
183
+ res.writeHead(200, { "Content-Type": "text/html" });
184
+ res.end(generateDashboardHtml(routes, port));
185
+ });
186
+ server.listen(port, () => {
187
+ console.log(`
188
+ \u26A1 OGPipe Dev Preview`);
189
+ console.log(` \u2192 http://localhost:${port}
190
+ `);
191
+ console.log(` ${routes.length} routes configured`);
192
+ console.log(` Watching templates for changes...
193
+ `);
194
+ });
195
+ }
196
+ function buildPreviewRoutes(config) {
197
+ const routes = [];
198
+ for (const [pattern, routeConfig] of Object.entries(config.routes)) {
199
+ const samplePath = patternToSamplePath(pattern);
200
+ const vars = typeof routeConfig.vars === "function" ? routeConfig.vars({ path: samplePath, title: "Sample Title", description: "Sample description", params: {} }) : { title: "Sample Title", description: "Sample description" };
201
+ routes.push({
202
+ path: samplePath,
203
+ template: routeConfig.template,
204
+ vars
205
+ });
206
+ }
207
+ return routes;
208
+ }
209
+ function patternToSamplePath(pattern) {
210
+ if (pattern === "*") return "/";
211
+ return pattern.replace(/\[\.\.\.(\w+)\]/g, "example-$1").replace(/\[(\w+)\]/g, "example-$1").replace(/\*/g, "example");
212
+ }
213
+ async function handleRender(req, res, config, configDir, routePath, templateId) {
214
+ try {
215
+ const tplId = templateId || Object.keys(config.templates)[0];
216
+ const template = config.templates[tplId];
217
+ if (!template) {
218
+ res.writeHead(404, { "Content-Type": "application/json" });
219
+ res.end(JSON.stringify({ error: `Template '${tplId}' not found` }));
220
+ return;
221
+ }
222
+ let html = resolveTemplateHtml(template, configDir);
223
+ const vars = { title: "Sample Blog Post Title", description: "A sample description for preview", author: "Developer", date: "June 2026", site: "mysite.dev", category: "Guide" };
224
+ html = injectVariables(html, vars);
225
+ const apiKey = config.apiKey || process.env.OGPIPE_API_KEY;
226
+ if (apiKey) {
227
+ const client = new OGPipeClient({ apiKey, baseUrl: config.baseUrl });
228
+ const result = await client.render({ html, width: template.width || 1200, height: template.height || 630 });
229
+ if (result.success) {
230
+ const imgRes = await fetch(result.data.url);
231
+ const buffer = Buffer.from(await imgRes.arrayBuffer());
232
+ res.writeHead(200, {
233
+ "Content-Type": `image/${result.data.format}`,
234
+ "Cache-Control": "no-cache"
235
+ });
236
+ res.end(buffer);
237
+ return;
238
+ }
239
+ }
240
+ res.writeHead(200, { "Content-Type": "text/html" });
241
+ res.end(html);
242
+ } catch (err) {
243
+ res.writeHead(500, { "Content-Type": "application/json" });
244
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Render failed" }));
245
+ }
246
+ }
247
+ function watchTemplateFiles(config, configDir, onChange) {
248
+ const filesToWatch = [];
249
+ for (const template of Object.values(config.templates)) {
250
+ if (template.file) {
251
+ const fullPath = resolve3(configDir, template.file);
252
+ if (existsSync2(fullPath)) {
253
+ filesToWatch.push(fullPath);
254
+ }
255
+ }
256
+ }
257
+ for (const filePath of filesToWatch) {
258
+ watch(filePath, { persistent: false }, (eventType) => {
259
+ if (eventType === "change") {
260
+ console.log(` \u21BB Template changed: ${filePath}`);
261
+ onChange();
262
+ }
263
+ });
264
+ }
265
+ }
266
+ function generateDashboardHtml(routes, port) {
267
+ return `<!DOCTYPE html>
268
+ <html lang="en">
269
+ <head>
270
+ <meta charset="utf-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1">
272
+ <title>OGPipe Dev Preview</title>
273
+ <style>
274
+ :root { --bg: #09090b; --surface: #18181b; --surface-2: #27272a; --border: #3f3f46; --text: #fafafa; --text-muted: #a1a1aa; --accent: #3b82f6; }
275
+ * { margin: 0; padding: 0; box-sizing: border-box; }
276
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); padding: 32px; }
277
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
278
+ .subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 32px; }
279
+ .platforms { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
280
+ .platform-btn { padding: 8px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); cursor: pointer; font-size: 13px; transition: all 0.15s; }
281
+ .platform-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(59,130,246,0.1); }
282
+ .preview-container { display: grid; grid-template-columns: 1fr; gap: 24px; }
283
+ .preview-card { background: var(--surface); border: 1px solid var(--surface-2); border-radius: 12px; padding: 24px; }
284
+ .preview-card h3 { font-size: 14px; color: var(--text-muted); margin-bottom: 12px; }
285
+ .route-info { font-family: monospace; font-size: 12px; color: var(--accent); margin-bottom: 16px; }
286
+
287
+ /* Platform mockups */
288
+ .mockup { border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
289
+ .mockup-twitter { max-width: 506px; }
290
+ .mockup-linkedin { max-width: 552px; }
291
+ .mockup-slack { max-width: 400px; }
292
+ .mockup-discord { max-width: 432px; }
293
+
294
+ .mockup img, .mockup iframe { width: 100%; aspect-ratio: 1200/630; display: block; border: none; background: var(--surface-2); }
295
+
296
+ .mockup-frame { padding: 12px; background: var(--surface-2); border-radius: 8px; }
297
+ .mockup-meta { padding: 8px 12px; font-size: 12px; color: var(--text-muted); }
298
+ .mockup-meta .title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 2px; }
299
+ .mockup-meta .desc { font-size: 12px; color: var(--text-muted); }
300
+ .mockup-meta .domain { font-size: 11px; color: var(--text-muted); margin-top: 4px; opacity: 0.7; }
301
+
302
+ .routes-list { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
303
+ .route-pill { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); cursor: pointer; font-size: 13px; font-family: monospace; }
304
+ .route-pill.active { border-color: var(--accent); color: var(--accent); }
305
+
306
+ .reload-badge { position: fixed; top: 16px; right: 16px; background: var(--accent); color: white; padding: 6px 12px; border-radius: 6px; font-size: 12px; display: none; animation: fadeIn 0.2s; }
307
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div class="reload-badge" id="reload-badge">\u21BB Reloading...</div>
312
+ <h1>\u26A1 OGPipe Dev Preview</h1>
313
+ <p class="subtitle">Live preview of your OG images across social platforms. Edit templates \u2192 see changes instantly.</p>
314
+
315
+ <div class="routes-list" id="routes-list">
316
+ ${routes.map((r, i) => `<button class="route-pill ${i === 0 ? "active" : ""}" data-route="${r.path}" data-template="${r.template}">${r.path} (${r.template})</button>`).join("\n ")}
317
+ </div>
318
+
319
+ <div class="platforms">
320
+ <button class="platform-btn active" data-platform="twitter">\u{1D54F} / Twitter</button>
321
+ <button class="platform-btn" data-platform="linkedin">LinkedIn</button>
322
+ <button class="platform-btn" data-platform="slack">Slack</button>
323
+ <button class="platform-btn" data-platform="discord">Discord</button>
324
+ </div>
325
+
326
+ <div class="preview-container" id="preview-container">
327
+ <div class="preview-card">
328
+ <div class="mockup mockup-twitter">
329
+ <div class="mockup-frame">
330
+ <img id="og-preview" src="/__ogpipe/render?route=/&template=${routes[0]?.template || "default"}" alt="OG Image Preview" />
331
+ <div class="mockup-meta">
332
+ <div class="title">Sample Blog Post Title</div>
333
+ <div class="desc">A sample description for preview</div>
334
+ <div class="domain">mysite.dev</div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <script>
342
+ const preview = document.getElementById('og-preview');
343
+ const reloadBadge = document.getElementById('reload-badge');
344
+
345
+ // Route selection
346
+ document.querySelectorAll('.route-pill').forEach(btn => {
347
+ btn.addEventListener('click', () => {
348
+ document.querySelectorAll('.route-pill').forEach(b => b.classList.remove('active'));
349
+ btn.classList.add('active');
350
+ const route = btn.dataset.route;
351
+ const template = btn.dataset.template;
352
+ preview.src = '/__ogpipe/render?route=' + encodeURIComponent(route) + '&template=' + encodeURIComponent(template) + '&t=' + Date.now();
353
+ });
354
+ });
355
+
356
+ // Platform selection (visual only for now \u2014 frame styling)
357
+ document.querySelectorAll('.platform-btn').forEach(btn => {
358
+ btn.addEventListener('click', () => {
359
+ document.querySelectorAll('.platform-btn').forEach(b => b.classList.remove('active'));
360
+ btn.classList.add('active');
361
+ });
362
+ });
363
+
364
+ // SSE hot-reload
365
+ const evtSource = new EventSource('/__ogpipe/events');
366
+ evtSource.onmessage = (event) => {
367
+ if (event.data === 'reload') {
368
+ reloadBadge.style.display = 'block';
369
+ // Cache-bust the image
370
+ preview.src = preview.src.replace(/[&?]t=\\d+/, '') + '&t=' + Date.now();
371
+ setTimeout(() => { reloadBadge.style.display = 'none'; }, 1500);
372
+ }
373
+ };
374
+ </script>
375
+ </body>
376
+ </html>`;
377
+ }
378
+ var DEFAULT_PORT;
379
+ var init_server = __esm({
380
+ "src/preview/server.ts"() {
381
+ "use strict";
382
+ init_client();
383
+ init_config();
384
+ DEFAULT_PORT = 3010;
385
+ }
386
+ });
387
+
388
+ // src/bin/ogpipe.ts
389
+ import { resolve as resolve4 } from "path";
390
+ import { existsSync as existsSync3 } from "fs";
391
+
392
+ // src/next/generator.ts
393
+ init_client();
394
+ init_config();
395
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync } from "fs";
396
+ import { resolve as resolve2, join, dirname as dirname2 } from "path";
397
+ async function generateOGImages(options) {
398
+ const { projectDir, config, configDir, concurrency = 5 } = options;
399
+ const startTime = Date.now();
400
+ const manifestPath = join(projectDir, ".next", "prerender-manifest.json");
401
+ if (!existsSync(manifestPath)) {
402
+ throw new Error(
403
+ `[OGPipe] Cannot find .next/prerender-manifest.json. Run 'next build' first.`
404
+ );
405
+ }
406
+ const manifest = JSON.parse(
407
+ readFileSync2(manifestPath, "utf-8")
408
+ );
409
+ const routes = Object.keys(manifest.routes).filter(
410
+ (route) => route !== "/_not-found" && !route.startsWith("/_")
411
+ );
412
+ if (routes.length === 0) {
413
+ return { success: [], failed: [], totalDurationMs: 0 };
414
+ }
415
+ const outDir = resolve2(projectDir, config.outDir || "public/og");
416
+ mkdirSync(outDir, { recursive: true });
417
+ const client = new OGPipeClient({
418
+ apiKey: config.apiKey,
419
+ baseUrl: config.baseUrl
420
+ });
421
+ const results = [];
422
+ const failures = [];
423
+ for (let i = 0; i < routes.length; i += concurrency) {
424
+ const batch = routes.slice(i, i + concurrency);
425
+ const batchPromises = batch.map(
426
+ (route) => generateSingleImage({ route, config, configDir, client, outDir })
427
+ );
428
+ const batchResults = await Promise.allSettled(batchPromises);
429
+ for (let j = 0; j < batchResults.length; j++) {
430
+ const result = batchResults[j];
431
+ const route = batch[j];
432
+ if (result.status === "fulfilled" && result.value) {
433
+ results.push(result.value);
434
+ } else if (result.status === "rejected") {
435
+ failures.push({ route, error: result.reason?.message || "Unknown error" });
436
+ } else if (result.status === "fulfilled" && result.value === null) {
437
+ }
438
+ }
439
+ }
440
+ const ogManifest = Object.fromEntries(
441
+ results.map((r) => [r.route, { path: r.outputPath, url: r.imageUrl }])
442
+ );
443
+ writeFileSync(
444
+ join(outDir, "og-manifest.json"),
445
+ JSON.stringify(ogManifest, null, 2)
446
+ );
447
+ return {
448
+ success: results,
449
+ failed: failures,
450
+ totalDurationMs: Date.now() - startTime
451
+ };
452
+ }
453
+ async function generateSingleImage(options) {
454
+ const { route, config, configDir, client, outDir } = options;
455
+ const startTime = Date.now();
456
+ const routeConfig = findRouteConfig(route, config.routes);
457
+ if (!routeConfig) return null;
458
+ const template = config.templates[routeConfig.template];
459
+ if (!template) {
460
+ throw new Error(`[OGPipe] Template '${routeConfig.template}' not found for route ${route}`);
461
+ }
462
+ let html = resolveTemplateHtml(template, configDir);
463
+ const metadata = {
464
+ path: route,
465
+ title: extractTitleFromRoute(route),
466
+ description: "",
467
+ params: extractParamsFromRoute(route)
468
+ };
469
+ const vars = routeConfig.vars ? routeConfig.vars(metadata) : { title: metadata.title || "", description: metadata.description || "" };
470
+ html = injectVariables(html, vars);
471
+ const result = await client.render({
472
+ html,
473
+ width: template.width || 1200,
474
+ height: template.height || 630,
475
+ format: template.format || "png"
476
+ });
477
+ if (!result.success) {
478
+ throw new Error(`API error for ${route}: ${result.error}`);
479
+ }
480
+ const filename = routeToFilename(route, template.format || "png");
481
+ const outputPath = join(outDir, filename);
482
+ const imageBuffer = await client.renderToBuffer({
483
+ html,
484
+ width: template.width || 1200,
485
+ height: template.height || 630,
486
+ format: template.format || "png"
487
+ });
488
+ if (imageBuffer) {
489
+ mkdirSync(dirname2(outputPath), { recursive: true });
490
+ writeFileSync(outputPath, imageBuffer);
491
+ }
492
+ return {
493
+ route,
494
+ outputPath: `/og/${filename}`,
495
+ imageUrl: result.data.url,
496
+ durationMs: Date.now() - startTime
497
+ };
498
+ }
499
+ function routeToFilename(route, format) {
500
+ if (route === "/") return `index.${format}`;
501
+ const clean = route.replace(/^\//, "");
502
+ return `${clean}.${format}`;
503
+ }
504
+ function extractTitleFromRoute(route) {
505
+ const segments = route.split("/").filter(Boolean);
506
+ const last = segments[segments.length - 1] || "Home";
507
+ return last.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
508
+ }
509
+ function extractParamsFromRoute(route) {
510
+ const segments = route.split("/").filter(Boolean);
511
+ if (segments.length >= 2) {
512
+ return { slug: segments[segments.length - 1] };
513
+ }
514
+ return {};
515
+ }
516
+
517
+ // src/bin/ogpipe.ts
518
+ var args = process.argv.slice(2);
519
+ var command = args[0];
520
+ async function main() {
521
+ switch (command) {
522
+ case "generate":
523
+ await runGenerate();
524
+ break;
525
+ case "dev":
526
+ await runDev();
527
+ break;
528
+ case "--help":
529
+ case "-h":
530
+ case void 0:
531
+ printHelp();
532
+ break;
533
+ default:
534
+ console.error(`Unknown command: ${command}`);
535
+ printHelp();
536
+ process.exit(1);
537
+ }
538
+ }
539
+ async function runGenerate() {
540
+ const isDry = args.includes("--dry");
541
+ const projectDir = process.cwd();
542
+ console.log("\n\u26A1 OGPipe \u2014 Generating OG images...\n");
543
+ const config = await loadConfig(projectDir);
544
+ if (!config) {
545
+ console.error("\u274C No ogpipe.config.ts found in project root.");
546
+ console.error(" Create one with: import { defineConfig } from '@ogpipe/next'");
547
+ process.exit(1);
548
+ }
549
+ if (isDry) {
550
+ console.log("\u{1F50D} Dry run \u2014 showing what would be generated:\n");
551
+ console.log(" (dry run not yet implemented \u2014 run without --dry to generate)");
552
+ return;
553
+ }
554
+ if (!existsSync3(resolve4(projectDir, ".next"))) {
555
+ console.error("\u274C No .next directory found. Run 'next build' first.");
556
+ process.exit(1);
557
+ }
558
+ try {
559
+ const configDir = projectDir;
560
+ const report = await generateOGImages({
561
+ projectDir,
562
+ config,
563
+ configDir
564
+ });
565
+ console.log(`\u2705 Generated ${report.success.length} OG images in ${report.totalDurationMs}ms
566
+ `);
567
+ for (const result of report.success) {
568
+ console.log(` ${result.route} \u2192 ${result.outputPath} (${result.durationMs}ms)`);
569
+ }
570
+ if (report.failed.length > 0) {
571
+ console.log(`
572
+ \u26A0\uFE0F ${report.failed.length} failed:
573
+ `);
574
+ for (const failure of report.failed) {
575
+ console.log(` ${failure.route}: ${failure.error}`);
576
+ }
577
+ }
578
+ console.log(`
579
+ \u{1F4C1} Output: ${config.outDir || "public/og/"}`);
580
+ console.log(`\u{1F4CB} Manifest: ${config.outDir || "public/og"}/og-manifest.json
581
+ `);
582
+ } catch (err) {
583
+ console.error(`
584
+ \u274C Generation failed: ${err instanceof Error ? err.message : err}`);
585
+ process.exit(1);
586
+ }
587
+ }
588
+ async function runDev() {
589
+ const projectDir = process.cwd();
590
+ console.log("\n\u26A1 OGPipe Dev Preview \u2014 starting...\n");
591
+ const config = await loadConfig(projectDir);
592
+ if (!config) {
593
+ console.error("\u274C No ogpipe.config.ts found in project root.");
594
+ console.error(" Create one with: import { defineConfig } from '@ogpipe/next'");
595
+ process.exit(1);
596
+ }
597
+ const { startPreviewServer: startPreviewServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
598
+ await startPreviewServer2({
599
+ config,
600
+ configDir: projectDir,
601
+ port: parseInt(process.env.OGPIPE_PORT || "3010")
602
+ });
603
+ }
604
+ async function loadConfig(projectDir) {
605
+ const configPaths = [
606
+ resolve4(projectDir, "ogpipe.config.ts"),
607
+ resolve4(projectDir, "ogpipe.config.js"),
608
+ resolve4(projectDir, "ogpipe.config.mjs")
609
+ ];
610
+ for (const configPath of configPaths) {
611
+ if (existsSync3(configPath)) {
612
+ try {
613
+ const module = await import(configPath);
614
+ return module.default || module;
615
+ } catch (err) {
616
+ try {
617
+ const module = await import(`file://${configPath}`);
618
+ return module.default || module;
619
+ } catch {
620
+ console.error(`\u274C Failed to load config: ${configPath}`);
621
+ console.error(` ${err instanceof Error ? err.message : err}`);
622
+ return null;
623
+ }
624
+ }
625
+ }
626
+ }
627
+ return null;
628
+ }
629
+ function printHelp() {
630
+ console.log(`
631
+ \u26A1 OGPipe CLI \u2014 Pixel-perfect OG images for Next.js
632
+
633
+ COMMANDS:
634
+ generate Generate OG images for all static routes (run after next build)
635
+ generate --dry Show what would be generated without calling the API
636
+ dev Start local preview server with hot-reload
637
+
638
+ SETUP:
639
+ 1. Create ogpipe.config.ts in your project root
640
+ 2. Add to package.json scripts: "build": "next build && ogpipe generate"
641
+ 3. Set OGPIPE_API_KEY environment variable
642
+
643
+ DOCS:
644
+ https://ogpipe.dev/docs.html
645
+
646
+ EXAMPLE CONFIG:
647
+ import { defineConfig } from '@ogpipe/next'
648
+
649
+ export default defineConfig({
650
+ templates: {
651
+ blog: { file: './og-templates/blog.html' },
652
+ default: { html: '<div style="...">{{title}}</div>' },
653
+ },
654
+ routes: {
655
+ '/blog/[slug]': { template: 'blog', vars: (meta) => ({ title: meta.title }) },
656
+ '*': { template: 'default' },
657
+ },
658
+ })
659
+ `);
660
+ }
661
+ main().catch((err) => {
662
+ console.error("Fatal error:", err);
663
+ process.exit(1);
664
+ });
665
+ //# sourceMappingURL=ogpipe.mjs.map