@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,120 @@
1
+ export { OGPipeClient, OGPipeClientOptions, RenderError, RenderOutcome, RenderRequest, RenderResponse, RenderResult } from './client/index.js';
2
+
3
+ /**
4
+ * OGPipe configuration schema.
5
+ *
6
+ * Developers define this in ogpipe.config.ts at their project root.
7
+ * The CLI reads this to know which templates to use for which routes.
8
+ */
9
+ interface OGPipeTemplate {
10
+ /** Inline HTML string with {{variable}} placeholders */
11
+ html?: string;
12
+ /** Path to an HTML template file (relative to config file) */
13
+ file?: string;
14
+ /** Image width (default: 1200) */
15
+ width?: number;
16
+ /** Image height (default: 630) */
17
+ height?: number;
18
+ /** Output format (default: png) */
19
+ format?: "png" | "jpeg" | "webp";
20
+ }
21
+ interface RouteConfig {
22
+ /** Template ID to use for this route */
23
+ template: string;
24
+ /** Function to extract variables from page metadata */
25
+ vars?: (metadata: RouteMetadata) => Record<string, string>;
26
+ }
27
+ interface RouteMetadata {
28
+ /** Page title from metadata export */
29
+ title?: string;
30
+ /** Page description from metadata export */
31
+ description?: string;
32
+ /** Route path (e.g., /blog/my-post) */
33
+ path: string;
34
+ /** Route params (e.g., { slug: 'my-post' }) */
35
+ params?: Record<string, string>;
36
+ /** Any additional metadata from the page */
37
+ [key: string]: unknown;
38
+ }
39
+ interface OnDemandConfig {
40
+ /** Cache duration in seconds (default: 86400 = 24h) */
41
+ revalidate?: number;
42
+ /** Fallback image path if API is unavailable */
43
+ fallback?: string;
44
+ }
45
+ interface OGPipeConfig {
46
+ /** API key. Defaults to OGPIPE_API_KEY env var. */
47
+ apiKey?: string;
48
+ /** API base URL. Defaults to https://api.ogpipe.dev */
49
+ baseUrl?: string;
50
+ /** Named templates */
51
+ templates: Record<string, OGPipeTemplate>;
52
+ /** Route-to-template mapping. Supports glob patterns. */
53
+ routes: Record<string, RouteConfig>;
54
+ /** On-demand rendering config (for dynamic routes) */
55
+ onDemand?: OnDemandConfig;
56
+ /** Output directory for generated images (default: public/og) */
57
+ outDir?: string;
58
+ }
59
+ /**
60
+ * Helper to define a type-safe OGPipe config.
61
+ *
62
+ * Usage in ogpipe.config.ts:
63
+ * ```ts
64
+ * import { defineConfig } from '@ogpipe/next'
65
+ *
66
+ * export default defineConfig({
67
+ * templates: { ... },
68
+ * routes: { ... },
69
+ * })
70
+ * ```
71
+ */
72
+ declare function defineConfig(config: OGPipeConfig): OGPipeConfig;
73
+
74
+ /**
75
+ * On-demand OG Image handler for Next.js App Router.
76
+ *
77
+ * Use this for dynamic routes where build-time generation isn't possible.
78
+ * The handler calls the OGPipe API on first request, then CDN-caches the result.
79
+ *
80
+ * Usage in app/blog/[slug]/opengraph-image.ts:
81
+ * ```ts
82
+ * import { OGImageHandler } from '@ogpipe/next'
83
+ *
84
+ * export default OGImageHandler({
85
+ * template: 'blog',
86
+ * revalidate: 86400,
87
+ * fallback: '/og-fallback.png',
88
+ * })
89
+ * ```
90
+ */
91
+ interface OGImageHandlerOptions {
92
+ /** Template ID (must exist in ogpipe.config.ts templates) */
93
+ template?: string;
94
+ /** Inline HTML (alternative to template) */
95
+ html?: string;
96
+ /** Variables to inject into the template */
97
+ vars?: Record<string, string> | ((params: Record<string, string>) => Record<string, string>);
98
+ /** Cache duration in seconds (default: 86400 = 24h) */
99
+ revalidate?: number;
100
+ /** Fallback image path if API is unavailable */
101
+ fallback?: string;
102
+ /** Image width (default: 1200) */
103
+ width?: number;
104
+ /** Image height (default: 630) */
105
+ height?: number;
106
+ }
107
+ /**
108
+ * Create an on-demand OG image route handler.
109
+ *
110
+ * Returns a Next.js-compatible route handler that:
111
+ * 1. Resolves the template HTML with variables
112
+ * 2. Calls OGPipe API to render
113
+ * 3. Returns the image with cache headers
114
+ * 4. Falls back to a static image if API is down
115
+ */
116
+ declare function OGImageHandler(options: OGImageHandlerOptions): (request: Request, context: {
117
+ params?: Record<string, string | string[]>;
118
+ }) => Promise<Response>;
119
+
120
+ export { OGImageHandler, type OGImageHandlerOptions, type OGPipeConfig, type OGPipeTemplate, type OnDemandConfig, type RouteConfig, type RouteMetadata, defineConfig };
package/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ OGImageHandler: () => OGImageHandler,
24
+ OGPipeClient: () => OGPipeClient,
25
+ defineConfig: () => defineConfig
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/next/config.ts
30
+ function defineConfig(config) {
31
+ return config;
32
+ }
33
+ function injectVariables(html, variables) {
34
+ return html.replace(/\{\{(\w+)\}\}/g, (_, key) => {
35
+ return variables[key] ?? "";
36
+ });
37
+ }
38
+
39
+ // src/client/index.ts
40
+ var OGPipeClient = class {
41
+ apiKey;
42
+ baseUrl;
43
+ timeout;
44
+ constructor(options = {}) {
45
+ this.apiKey = options.apiKey || process.env.OGPIPE_API_KEY || "";
46
+ this.baseUrl = options.baseUrl || process.env.OGPIPE_BASE_URL || "https://api.ogpipe.dev";
47
+ this.timeout = options.timeout || 3e4;
48
+ if (!this.apiKey) {
49
+ throw new Error(
50
+ "[OGPipe] Missing API key. Set OGPIPE_API_KEY environment variable or pass apiKey option."
51
+ );
52
+ }
53
+ }
54
+ /**
55
+ * Render HTML to an image. Returns the CDN URL.
56
+ */
57
+ async render(request) {
58
+ const controller = new AbortController();
59
+ const timer = setTimeout(() => controller.abort(), this.timeout);
60
+ try {
61
+ const res = await fetch(`${this.baseUrl}/images`, {
62
+ method: "POST",
63
+ headers: {
64
+ Authorization: `Bearer ${this.apiKey}`,
65
+ "Content-Type": "application/json"
66
+ },
67
+ body: JSON.stringify({
68
+ html: request.html,
69
+ width: request.width || 1200,
70
+ height: request.height || 630,
71
+ format: request.format || "png"
72
+ }),
73
+ signal: controller.signal
74
+ });
75
+ const body = await res.json();
76
+ if (!res.ok) {
77
+ return {
78
+ success: false,
79
+ error: body.error || `HTTP ${res.status}`,
80
+ statusCode: res.status
81
+ };
82
+ }
83
+ return {
84
+ success: true,
85
+ data: body
86
+ };
87
+ } catch (err) {
88
+ if (err instanceof Error && err.name === "AbortError") {
89
+ return { success: false, error: "Request timed out", statusCode: 408 };
90
+ }
91
+ return {
92
+ success: false,
93
+ error: err instanceof Error ? err.message : "Unknown error",
94
+ statusCode: 500
95
+ };
96
+ } finally {
97
+ clearTimeout(timer);
98
+ }
99
+ }
100
+ /**
101
+ * Render HTML and return the raw image buffer (for writing to disk).
102
+ */
103
+ async renderToBuffer(request) {
104
+ const result = await this.render(request);
105
+ if (!result.success) return null;
106
+ const res = await fetch(result.data.url);
107
+ if (!res.ok) return null;
108
+ return Buffer.from(await res.arrayBuffer());
109
+ }
110
+ };
111
+
112
+ // src/next/handler.ts
113
+ function OGImageHandler(options) {
114
+ const {
115
+ revalidate = 86400,
116
+ fallback,
117
+ width = 1200,
118
+ height = 630
119
+ } = options;
120
+ return async function handler(request, context) {
121
+ try {
122
+ const client = new OGPipeClient();
123
+ let html;
124
+ if (options.html) {
125
+ html = options.html;
126
+ } else {
127
+ html = options.html || "<div>{{title}}</div>";
128
+ }
129
+ const params = flattenParams(context.params || {});
130
+ const vars = typeof options.vars === "function" ? options.vars(params) : options.vars || {};
131
+ html = injectVariables(html, vars);
132
+ const result = await client.render({ html, width, height });
133
+ if (!result.success) {
134
+ if (fallback) {
135
+ return Response.redirect(new URL(fallback, request.url), 302);
136
+ }
137
+ return new Response("OG image generation failed", { status: 500 });
138
+ }
139
+ const imageRes = await fetch(result.data.url);
140
+ const imageBuffer = await imageRes.arrayBuffer();
141
+ return new Response(imageBuffer, {
142
+ headers: {
143
+ "Content-Type": `image/${result.data.format}`,
144
+ "Cache-Control": `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate * 2}`
145
+ }
146
+ });
147
+ } catch (err) {
148
+ if (fallback) {
149
+ return Response.redirect(new URL(fallback, request.url), 302);
150
+ }
151
+ return new Response("OG image generation failed", { status: 500 });
152
+ }
153
+ };
154
+ }
155
+ function flattenParams(params) {
156
+ const flat = {};
157
+ for (const [key, value] of Object.entries(params)) {
158
+ flat[key] = Array.isArray(value) ? value.join("/") : value;
159
+ }
160
+ return flat;
161
+ }
162
+ // Annotate the CommonJS export names for ESM import in node:
163
+ 0 && (module.exports = {
164
+ OGImageHandler,
165
+ OGPipeClient,
166
+ defineConfig
167
+ });
168
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/next/config.ts","../src/client/index.ts","../src/next/handler.ts"],"sourcesContent":["/**\n * @ogpipe/next — Pixel-perfect OG images for Next.js\n *\n * Full CSS support. Any font. Any hosting platform.\n * Framework-agnostic alternative to @vercel/og.\n *\n * @example\n * ```ts\n * // ogpipe.config.ts\n * import { defineConfig } from '@ogpipe/next'\n *\n * export default defineConfig({\n * templates: {\n * blog: { file: './og-templates/blog.html' },\n * },\n * routes: {\n * '/blog/[slug]': { template: 'blog', vars: (meta) => ({ title: meta.title }) },\n * '*': { template: 'default' },\n * },\n * })\n * ```\n *\n * @example\n * ```ts\n * // app/blog/[slug]/opengraph-image.ts\n * import { OGImageHandler } from '@ogpipe/next'\n *\n * export default OGImageHandler({\n * html: '<div class=\"...\">{{title}}</div>',\n * vars: (params) => ({ title: params.slug.replace(/-/g, ' ') }),\n * revalidate: 86400,\n * })\n * ```\n */\n\n// Config\nexport { defineConfig } from \"./next/config.js\";\nexport type {\n OGPipeConfig,\n OGPipeTemplate,\n RouteConfig,\n RouteMetadata,\n OnDemandConfig,\n} from \"./next/config.js\";\n\n// On-demand handler\nexport { OGImageHandler } from \"./next/handler.js\";\nexport type { OGImageHandlerOptions } from \"./next/handler.js\";\n\n// Client (also available via @ogpipe/next/client)\nexport { OGPipeClient } from \"./client/index.js\";\nexport type {\n OGPipeClientOptions,\n RenderRequest,\n RenderResponse,\n RenderResult,\n RenderError,\n RenderOutcome,\n} from \"./client/index.js\";\n","/**\n * OGPipe configuration schema.\n *\n * Developers define this in ogpipe.config.ts at their project root.\n * The CLI reads this to know which templates to use for which routes.\n */\n\nimport { readFileSync } from \"fs\";\nimport { resolve, dirname } from \"path\";\n\n// ─────────────────────────────────────────────\n// Config Types\n// ─────────────────────────────────────────────\n\nexport interface OGPipeTemplate {\n /** Inline HTML string with {{variable}} placeholders */\n html?: string;\n /** Path to an HTML template file (relative to config file) */\n file?: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n /** Output format (default: png) */\n format?: \"png\" | \"jpeg\" | \"webp\";\n}\n\nexport interface RouteConfig {\n /** Template ID to use for this route */\n template: string;\n /** Function to extract variables from page metadata */\n vars?: (metadata: RouteMetadata) => Record<string, string>;\n}\n\nexport interface RouteMetadata {\n /** Page title from metadata export */\n title?: string;\n /** Page description from metadata export */\n description?: string;\n /** Route path (e.g., /blog/my-post) */\n path: string;\n /** Route params (e.g., { slug: 'my-post' }) */\n params?: Record<string, string>;\n /** Any additional metadata from the page */\n [key: string]: unknown;\n}\n\nexport interface OnDemandConfig {\n /** Cache duration in seconds (default: 86400 = 24h) */\n revalidate?: number;\n /** Fallback image path if API is unavailable */\n fallback?: string;\n}\n\nexport interface OGPipeConfig {\n /** API key. Defaults to OGPIPE_API_KEY env var. */\n apiKey?: string;\n /** API base URL. Defaults to https://api.ogpipe.dev */\n baseUrl?: string;\n /** Named templates */\n templates: Record<string, OGPipeTemplate>;\n /** Route-to-template mapping. Supports glob patterns. */\n routes: Record<string, RouteConfig>;\n /** On-demand rendering config (for dynamic routes) */\n onDemand?: OnDemandConfig;\n /** Output directory for generated images (default: public/og) */\n outDir?: string;\n}\n\n// ─────────────────────────────────────────────\n// Config Helper\n// ─────────────────────────────────────────────\n\n/**\n * Helper to define a type-safe OGPipe config.\n *\n * Usage in ogpipe.config.ts:\n * ```ts\n * import { defineConfig } from '@ogpipe/next'\n *\n * export default defineConfig({\n * templates: { ... },\n * routes: { ... },\n * })\n * ```\n */\nexport function defineConfig(config: OGPipeConfig): OGPipeConfig {\n return config;\n}\n\n// ─────────────────────────────────────────────\n// Template Resolution\n// ─────────────────────────────────────────────\n\n/**\n * Resolve a template to its final HTML string.\n * If the template uses a `file` path, reads the file from disk.\n */\nexport function resolveTemplateHtml(\n template: OGPipeTemplate,\n configDir: string\n): string {\n if (template.html) {\n return template.html;\n }\n\n if (template.file) {\n const filePath = resolve(configDir, template.file);\n try {\n return readFileSync(filePath, \"utf-8\");\n } catch (err) {\n throw new Error(`[OGPipe] Template file not found: ${filePath}`);\n }\n }\n\n throw new Error(\"[OGPipe] Template must have either 'html' or 'file' property.\");\n}\n\n/**\n * Inject variables into an HTML template string.\n * Replaces {{variable}} placeholders with values.\n */\nexport function injectVariables(\n html: string,\n variables: Record<string, string>\n): string {\n return html.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n return variables[key] ?? \"\";\n });\n}\n\n/**\n * Match a route path against a glob pattern.\n * Supports:\n * /blog/[slug] → matches /blog/anything\n * /blog/* → matches /blog/anything\n * * → matches everything (default fallback)\n */\nexport function matchRoute(\n path: string,\n pattern: string\n): boolean {\n if (pattern === \"*\") return true;\n\n // Convert Next.js dynamic route pattern to regex\n const regexStr = pattern\n .replace(/\\[\\.\\.\\.[\\w]+\\]/g, \".*\") // [...slug] → .*\n .replace(/\\[[\\w]+\\]/g, \"[^/]+\") // [slug] → [^/]+\n .replace(/\\*/g, \"[^/]+\"); // * → [^/]+\n\n const regex = new RegExp(`^${regexStr}$`);\n return regex.test(path);\n}\n\n/**\n * Find the best matching route config for a given path.\n * More specific patterns take priority over wildcards.\n */\nexport function findRouteConfig(\n path: string,\n routes: Record<string, RouteConfig>\n): RouteConfig | null {\n // Sort routes by specificity (longer patterns first, * last)\n const sortedPatterns = Object.keys(routes).sort((a, b) => {\n if (a === \"*\") return 1;\n if (b === \"*\") return -1;\n return b.length - a.length;\n });\n\n for (const pattern of sortedPatterns) {\n if (matchRoute(path, pattern)) {\n return routes[pattern];\n }\n }\n\n return null;\n}\n","/**\n * @ogpipe/next/client — Framework-agnostic API client for OGPipe\n *\n * This is the thin HTTP client that communicates with the OGPipe rendering API.\n * Can be used standalone (Python, Go, curl equivalent) or via the Next.js integration.\n */\n\nexport interface OGPipeClientOptions {\n /** API key (og_live_xxx). Defaults to OGPIPE_API_KEY env var. */\n apiKey?: string;\n /** API base URL. Defaults to https://api.ogpipe.dev */\n baseUrl?: string;\n /** Request timeout in ms. Defaults to 30000. */\n timeout?: number;\n}\n\nexport interface RenderRequest {\n /** HTML content to render */\n html: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n /** Output format (default: png) */\n format?: \"png\" | \"jpeg\" | \"webp\";\n}\n\nexport interface RenderResponse {\n /** CDN URL of the rendered image */\n url: string;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** Output format */\n format: string;\n /** Whether served from cache */\n cached: boolean;\n}\n\nexport interface RenderResult {\n success: true;\n data: RenderResponse;\n}\n\nexport interface RenderError {\n success: false;\n error: string;\n statusCode: number;\n}\n\nexport type RenderOutcome = RenderResult | RenderError;\n\n/**\n * OGPipe API client.\n *\n * Usage:\n * ```ts\n * import { OGPipeClient } from '@ogpipe/next/client'\n *\n * const client = new OGPipeClient({ apiKey: 'og_live_xxx' })\n * const result = await client.render({ html: '<div>Hello</div>' })\n * ```\n */\nexport class OGPipeClient {\n private apiKey: string;\n private baseUrl: string;\n private timeout: number;\n\n constructor(options: OGPipeClientOptions = {}) {\n this.apiKey = options.apiKey || process.env.OGPIPE_API_KEY || \"\";\n this.baseUrl = options.baseUrl || process.env.OGPIPE_BASE_URL || \"https://api.ogpipe.dev\";\n this.timeout = options.timeout || 30_000;\n\n if (!this.apiKey) {\n throw new Error(\n \"[OGPipe] Missing API key. Set OGPIPE_API_KEY environment variable or pass apiKey option.\"\n );\n }\n }\n\n /**\n * Render HTML to an image. Returns the CDN URL.\n */\n async render(request: RenderRequest): Promise<RenderOutcome> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(`${this.baseUrl}/images`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n html: request.html,\n width: request.width || 1200,\n height: request.height || 630,\n format: request.format || \"png\",\n }),\n signal: controller.signal,\n });\n\n const body = await res.json() as { error?: string };\n\n if (!res.ok) {\n return {\n success: false,\n error: body.error || `HTTP ${res.status}`,\n statusCode: res.status,\n };\n }\n\n return {\n success: true,\n data: body as RenderResponse,\n };\n } catch (err) {\n if (err instanceof Error && err.name === \"AbortError\") {\n return { success: false, error: \"Request timed out\", statusCode: 408 };\n }\n return {\n success: false,\n error: err instanceof Error ? err.message : \"Unknown error\",\n statusCode: 500,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n\n /**\n * Render HTML and return the raw image buffer (for writing to disk).\n */\n async renderToBuffer(request: RenderRequest): Promise<Buffer | null> {\n const result = await this.render(request);\n if (!result.success) return null;\n\n // Fetch the image from CDN URL\n const res = await fetch(result.data.url);\n if (!res.ok) return null;\n\n return Buffer.from(await res.arrayBuffer());\n }\n}\n","/**\n * On-demand OG Image handler for Next.js App Router.\n *\n * Use this for dynamic routes where build-time generation isn't possible.\n * The handler calls the OGPipe API on first request, then CDN-caches the result.\n *\n * Usage in app/blog/[slug]/opengraph-image.ts:\n * ```ts\n * import { OGImageHandler } from '@ogpipe/next'\n *\n * export default OGImageHandler({\n * template: 'blog',\n * revalidate: 86400,\n * fallback: '/og-fallback.png',\n * })\n * ```\n */\n\nimport { OGPipeClient } from \"../client/index.js\";\nimport {\n OGPipeConfig,\n OGPipeTemplate,\n resolveTemplateHtml,\n injectVariables,\n} from \"./config.js\";\n\nexport interface OGImageHandlerOptions {\n /** Template ID (must exist in ogpipe.config.ts templates) */\n template?: string;\n /** Inline HTML (alternative to template) */\n html?: string;\n /** Variables to inject into the template */\n vars?: Record<string, string> | ((params: Record<string, string>) => Record<string, string>);\n /** Cache duration in seconds (default: 86400 = 24h) */\n revalidate?: number;\n /** Fallback image path if API is unavailable */\n fallback?: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n}\n\n/**\n * Create an on-demand OG image route handler.\n *\n * Returns a Next.js-compatible route handler that:\n * 1. Resolves the template HTML with variables\n * 2. Calls OGPipe API to render\n * 3. Returns the image with cache headers\n * 4. Falls back to a static image if API is down\n */\nexport function OGImageHandler(options: OGImageHandlerOptions) {\n const {\n revalidate = 86400,\n fallback,\n width = 1200,\n height = 630,\n } = options;\n\n return async function handler(\n request: Request,\n context: { params?: Record<string, string | string[]> }\n ): Promise<Response> {\n try {\n const client = new OGPipeClient();\n\n // Resolve HTML\n let html: string;\n if (options.html) {\n html = options.html;\n } else {\n // Template-based — caller must provide resolved HTML\n // In practice, the developer passes vars directly\n html = options.html || \"<div>{{title}}</div>\";\n }\n\n // Resolve variables\n const params = flattenParams(context.params || {});\n const vars =\n typeof options.vars === \"function\"\n ? options.vars(params)\n : options.vars || {};\n\n html = injectVariables(html, vars);\n\n // Render via API\n const result = await client.render({ html, width, height });\n\n if (!result.success) {\n // Fallback to static image\n if (fallback) {\n return Response.redirect(new URL(fallback, request.url), 302);\n }\n return new Response(\"OG image generation failed\", { status: 500 });\n }\n\n // Fetch the rendered image and return it\n const imageRes = await fetch(result.data.url);\n const imageBuffer = await imageRes.arrayBuffer();\n\n return new Response(imageBuffer, {\n headers: {\n \"Content-Type\": `image/${result.data.format}`,\n \"Cache-Control\": `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate * 2}`,\n },\n });\n } catch (err) {\n // On any error, try fallback\n if (fallback) {\n return Response.redirect(new URL(fallback, request.url), 302);\n }\n return new Response(\"OG image generation failed\", { status: 500 });\n }\n };\n}\n\n/**\n * Flatten Next.js route params (which can be string | string[]) to Record<string, string>.\n */\nfunction flattenParams(params: Record<string, string | string[]>): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n flat[key] = Array.isArray(value) ? value.join(\"/\") : value;\n }\n return flat;\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,139 @@
1
+ // src/next/config.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+ function injectVariables(html, variables) {
6
+ return html.replace(/\{\{(\w+)\}\}/g, (_, key) => {
7
+ return variables[key] ?? "";
8
+ });
9
+ }
10
+
11
+ // src/client/index.ts
12
+ var OGPipeClient = class {
13
+ apiKey;
14
+ baseUrl;
15
+ timeout;
16
+ constructor(options = {}) {
17
+ this.apiKey = options.apiKey || process.env.OGPIPE_API_KEY || "";
18
+ this.baseUrl = options.baseUrl || process.env.OGPIPE_BASE_URL || "https://api.ogpipe.dev";
19
+ this.timeout = options.timeout || 3e4;
20
+ if (!this.apiKey) {
21
+ throw new Error(
22
+ "[OGPipe] Missing API key. Set OGPIPE_API_KEY environment variable or pass apiKey option."
23
+ );
24
+ }
25
+ }
26
+ /**
27
+ * Render HTML to an image. Returns the CDN URL.
28
+ */
29
+ async render(request) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), this.timeout);
32
+ try {
33
+ const res = await fetch(`${this.baseUrl}/images`, {
34
+ method: "POST",
35
+ headers: {
36
+ Authorization: `Bearer ${this.apiKey}`,
37
+ "Content-Type": "application/json"
38
+ },
39
+ body: JSON.stringify({
40
+ html: request.html,
41
+ width: request.width || 1200,
42
+ height: request.height || 630,
43
+ format: request.format || "png"
44
+ }),
45
+ signal: controller.signal
46
+ });
47
+ const body = await res.json();
48
+ if (!res.ok) {
49
+ return {
50
+ success: false,
51
+ error: body.error || `HTTP ${res.status}`,
52
+ statusCode: res.status
53
+ };
54
+ }
55
+ return {
56
+ success: true,
57
+ data: body
58
+ };
59
+ } catch (err) {
60
+ if (err instanceof Error && err.name === "AbortError") {
61
+ return { success: false, error: "Request timed out", statusCode: 408 };
62
+ }
63
+ return {
64
+ success: false,
65
+ error: err instanceof Error ? err.message : "Unknown error",
66
+ statusCode: 500
67
+ };
68
+ } finally {
69
+ clearTimeout(timer);
70
+ }
71
+ }
72
+ /**
73
+ * Render HTML and return the raw image buffer (for writing to disk).
74
+ */
75
+ async renderToBuffer(request) {
76
+ const result = await this.render(request);
77
+ if (!result.success) return null;
78
+ const res = await fetch(result.data.url);
79
+ if (!res.ok) return null;
80
+ return Buffer.from(await res.arrayBuffer());
81
+ }
82
+ };
83
+
84
+ // src/next/handler.ts
85
+ function OGImageHandler(options) {
86
+ const {
87
+ revalidate = 86400,
88
+ fallback,
89
+ width = 1200,
90
+ height = 630
91
+ } = options;
92
+ return async function handler(request, context) {
93
+ try {
94
+ const client = new OGPipeClient();
95
+ let html;
96
+ if (options.html) {
97
+ html = options.html;
98
+ } else {
99
+ html = options.html || "<div>{{title}}</div>";
100
+ }
101
+ const params = flattenParams(context.params || {});
102
+ const vars = typeof options.vars === "function" ? options.vars(params) : options.vars || {};
103
+ html = injectVariables(html, vars);
104
+ const result = await client.render({ html, width, height });
105
+ if (!result.success) {
106
+ if (fallback) {
107
+ return Response.redirect(new URL(fallback, request.url), 302);
108
+ }
109
+ return new Response("OG image generation failed", { status: 500 });
110
+ }
111
+ const imageRes = await fetch(result.data.url);
112
+ const imageBuffer = await imageRes.arrayBuffer();
113
+ return new Response(imageBuffer, {
114
+ headers: {
115
+ "Content-Type": `image/${result.data.format}`,
116
+ "Cache-Control": `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate * 2}`
117
+ }
118
+ });
119
+ } catch (err) {
120
+ if (fallback) {
121
+ return Response.redirect(new URL(fallback, request.url), 302);
122
+ }
123
+ return new Response("OG image generation failed", { status: 500 });
124
+ }
125
+ };
126
+ }
127
+ function flattenParams(params) {
128
+ const flat = {};
129
+ for (const [key, value] of Object.entries(params)) {
130
+ flat[key] = Array.isArray(value) ? value.join("/") : value;
131
+ }
132
+ return flat;
133
+ }
134
+ export {
135
+ OGImageHandler,
136
+ OGPipeClient,
137
+ defineConfig
138
+ };
139
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next/config.ts","../src/client/index.ts","../src/next/handler.ts"],"sourcesContent":["/**\n * OGPipe configuration schema.\n *\n * Developers define this in ogpipe.config.ts at their project root.\n * The CLI reads this to know which templates to use for which routes.\n */\n\nimport { readFileSync } from \"fs\";\nimport { resolve, dirname } from \"path\";\n\n// ─────────────────────────────────────────────\n// Config Types\n// ─────────────────────────────────────────────\n\nexport interface OGPipeTemplate {\n /** Inline HTML string with {{variable}} placeholders */\n html?: string;\n /** Path to an HTML template file (relative to config file) */\n file?: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n /** Output format (default: png) */\n format?: \"png\" | \"jpeg\" | \"webp\";\n}\n\nexport interface RouteConfig {\n /** Template ID to use for this route */\n template: string;\n /** Function to extract variables from page metadata */\n vars?: (metadata: RouteMetadata) => Record<string, string>;\n}\n\nexport interface RouteMetadata {\n /** Page title from metadata export */\n title?: string;\n /** Page description from metadata export */\n description?: string;\n /** Route path (e.g., /blog/my-post) */\n path: string;\n /** Route params (e.g., { slug: 'my-post' }) */\n params?: Record<string, string>;\n /** Any additional metadata from the page */\n [key: string]: unknown;\n}\n\nexport interface OnDemandConfig {\n /** Cache duration in seconds (default: 86400 = 24h) */\n revalidate?: number;\n /** Fallback image path if API is unavailable */\n fallback?: string;\n}\n\nexport interface OGPipeConfig {\n /** API key. Defaults to OGPIPE_API_KEY env var. */\n apiKey?: string;\n /** API base URL. Defaults to https://api.ogpipe.dev */\n baseUrl?: string;\n /** Named templates */\n templates: Record<string, OGPipeTemplate>;\n /** Route-to-template mapping. Supports glob patterns. */\n routes: Record<string, RouteConfig>;\n /** On-demand rendering config (for dynamic routes) */\n onDemand?: OnDemandConfig;\n /** Output directory for generated images (default: public/og) */\n outDir?: string;\n}\n\n// ─────────────────────────────────────────────\n// Config Helper\n// ─────────────────────────────────────────────\n\n/**\n * Helper to define a type-safe OGPipe config.\n *\n * Usage in ogpipe.config.ts:\n * ```ts\n * import { defineConfig } from '@ogpipe/next'\n *\n * export default defineConfig({\n * templates: { ... },\n * routes: { ... },\n * })\n * ```\n */\nexport function defineConfig(config: OGPipeConfig): OGPipeConfig {\n return config;\n}\n\n// ─────────────────────────────────────────────\n// Template Resolution\n// ─────────────────────────────────────────────\n\n/**\n * Resolve a template to its final HTML string.\n * If the template uses a `file` path, reads the file from disk.\n */\nexport function resolveTemplateHtml(\n template: OGPipeTemplate,\n configDir: string\n): string {\n if (template.html) {\n return template.html;\n }\n\n if (template.file) {\n const filePath = resolve(configDir, template.file);\n try {\n return readFileSync(filePath, \"utf-8\");\n } catch (err) {\n throw new Error(`[OGPipe] Template file not found: ${filePath}`);\n }\n }\n\n throw new Error(\"[OGPipe] Template must have either 'html' or 'file' property.\");\n}\n\n/**\n * Inject variables into an HTML template string.\n * Replaces {{variable}} placeholders with values.\n */\nexport function injectVariables(\n html: string,\n variables: Record<string, string>\n): string {\n return html.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n return variables[key] ?? \"\";\n });\n}\n\n/**\n * Match a route path against a glob pattern.\n * Supports:\n * /blog/[slug] → matches /blog/anything\n * /blog/* → matches /blog/anything\n * * → matches everything (default fallback)\n */\nexport function matchRoute(\n path: string,\n pattern: string\n): boolean {\n if (pattern === \"*\") return true;\n\n // Convert Next.js dynamic route pattern to regex\n const regexStr = pattern\n .replace(/\\[\\.\\.\\.[\\w]+\\]/g, \".*\") // [...slug] → .*\n .replace(/\\[[\\w]+\\]/g, \"[^/]+\") // [slug] → [^/]+\n .replace(/\\*/g, \"[^/]+\"); // * → [^/]+\n\n const regex = new RegExp(`^${regexStr}$`);\n return regex.test(path);\n}\n\n/**\n * Find the best matching route config for a given path.\n * More specific patterns take priority over wildcards.\n */\nexport function findRouteConfig(\n path: string,\n routes: Record<string, RouteConfig>\n): RouteConfig | null {\n // Sort routes by specificity (longer patterns first, * last)\n const sortedPatterns = Object.keys(routes).sort((a, b) => {\n if (a === \"*\") return 1;\n if (b === \"*\") return -1;\n return b.length - a.length;\n });\n\n for (const pattern of sortedPatterns) {\n if (matchRoute(path, pattern)) {\n return routes[pattern];\n }\n }\n\n return null;\n}\n","/**\n * @ogpipe/next/client — Framework-agnostic API client for OGPipe\n *\n * This is the thin HTTP client that communicates with the OGPipe rendering API.\n * Can be used standalone (Python, Go, curl equivalent) or via the Next.js integration.\n */\n\nexport interface OGPipeClientOptions {\n /** API key (og_live_xxx). Defaults to OGPIPE_API_KEY env var. */\n apiKey?: string;\n /** API base URL. Defaults to https://api.ogpipe.dev */\n baseUrl?: string;\n /** Request timeout in ms. Defaults to 30000. */\n timeout?: number;\n}\n\nexport interface RenderRequest {\n /** HTML content to render */\n html: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n /** Output format (default: png) */\n format?: \"png\" | \"jpeg\" | \"webp\";\n}\n\nexport interface RenderResponse {\n /** CDN URL of the rendered image */\n url: string;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** Output format */\n format: string;\n /** Whether served from cache */\n cached: boolean;\n}\n\nexport interface RenderResult {\n success: true;\n data: RenderResponse;\n}\n\nexport interface RenderError {\n success: false;\n error: string;\n statusCode: number;\n}\n\nexport type RenderOutcome = RenderResult | RenderError;\n\n/**\n * OGPipe API client.\n *\n * Usage:\n * ```ts\n * import { OGPipeClient } from '@ogpipe/next/client'\n *\n * const client = new OGPipeClient({ apiKey: 'og_live_xxx' })\n * const result = await client.render({ html: '<div>Hello</div>' })\n * ```\n */\nexport class OGPipeClient {\n private apiKey: string;\n private baseUrl: string;\n private timeout: number;\n\n constructor(options: OGPipeClientOptions = {}) {\n this.apiKey = options.apiKey || process.env.OGPIPE_API_KEY || \"\";\n this.baseUrl = options.baseUrl || process.env.OGPIPE_BASE_URL || \"https://api.ogpipe.dev\";\n this.timeout = options.timeout || 30_000;\n\n if (!this.apiKey) {\n throw new Error(\n \"[OGPipe] Missing API key. Set OGPIPE_API_KEY environment variable or pass apiKey option.\"\n );\n }\n }\n\n /**\n * Render HTML to an image. Returns the CDN URL.\n */\n async render(request: RenderRequest): Promise<RenderOutcome> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(`${this.baseUrl}/images`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n html: request.html,\n width: request.width || 1200,\n height: request.height || 630,\n format: request.format || \"png\",\n }),\n signal: controller.signal,\n });\n\n const body = await res.json() as { error?: string };\n\n if (!res.ok) {\n return {\n success: false,\n error: body.error || `HTTP ${res.status}`,\n statusCode: res.status,\n };\n }\n\n return {\n success: true,\n data: body as RenderResponse,\n };\n } catch (err) {\n if (err instanceof Error && err.name === \"AbortError\") {\n return { success: false, error: \"Request timed out\", statusCode: 408 };\n }\n return {\n success: false,\n error: err instanceof Error ? err.message : \"Unknown error\",\n statusCode: 500,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n\n /**\n * Render HTML and return the raw image buffer (for writing to disk).\n */\n async renderToBuffer(request: RenderRequest): Promise<Buffer | null> {\n const result = await this.render(request);\n if (!result.success) return null;\n\n // Fetch the image from CDN URL\n const res = await fetch(result.data.url);\n if (!res.ok) return null;\n\n return Buffer.from(await res.arrayBuffer());\n }\n}\n","/**\n * On-demand OG Image handler for Next.js App Router.\n *\n * Use this for dynamic routes where build-time generation isn't possible.\n * The handler calls the OGPipe API on first request, then CDN-caches the result.\n *\n * Usage in app/blog/[slug]/opengraph-image.ts:\n * ```ts\n * import { OGImageHandler } from '@ogpipe/next'\n *\n * export default OGImageHandler({\n * template: 'blog',\n * revalidate: 86400,\n * fallback: '/og-fallback.png',\n * })\n * ```\n */\n\nimport { OGPipeClient } from \"../client/index.js\";\nimport {\n OGPipeConfig,\n OGPipeTemplate,\n resolveTemplateHtml,\n injectVariables,\n} from \"./config.js\";\n\nexport interface OGImageHandlerOptions {\n /** Template ID (must exist in ogpipe.config.ts templates) */\n template?: string;\n /** Inline HTML (alternative to template) */\n html?: string;\n /** Variables to inject into the template */\n vars?: Record<string, string> | ((params: Record<string, string>) => Record<string, string>);\n /** Cache duration in seconds (default: 86400 = 24h) */\n revalidate?: number;\n /** Fallback image path if API is unavailable */\n fallback?: string;\n /** Image width (default: 1200) */\n width?: number;\n /** Image height (default: 630) */\n height?: number;\n}\n\n/**\n * Create an on-demand OG image route handler.\n *\n * Returns a Next.js-compatible route handler that:\n * 1. Resolves the template HTML with variables\n * 2. Calls OGPipe API to render\n * 3. Returns the image with cache headers\n * 4. Falls back to a static image if API is down\n */\nexport function OGImageHandler(options: OGImageHandlerOptions) {\n const {\n revalidate = 86400,\n fallback,\n width = 1200,\n height = 630,\n } = options;\n\n return async function handler(\n request: Request,\n context: { params?: Record<string, string | string[]> }\n ): Promise<Response> {\n try {\n const client = new OGPipeClient();\n\n // Resolve HTML\n let html: string;\n if (options.html) {\n html = options.html;\n } else {\n // Template-based — caller must provide resolved HTML\n // In practice, the developer passes vars directly\n html = options.html || \"<div>{{title}}</div>\";\n }\n\n // Resolve variables\n const params = flattenParams(context.params || {});\n const vars =\n typeof options.vars === \"function\"\n ? options.vars(params)\n : options.vars || {};\n\n html = injectVariables(html, vars);\n\n // Render via API\n const result = await client.render({ html, width, height });\n\n if (!result.success) {\n // Fallback to static image\n if (fallback) {\n return Response.redirect(new URL(fallback, request.url), 302);\n }\n return new Response(\"OG image generation failed\", { status: 500 });\n }\n\n // Fetch the rendered image and return it\n const imageRes = await fetch(result.data.url);\n const imageBuffer = await imageRes.arrayBuffer();\n\n return new Response(imageBuffer, {\n headers: {\n \"Content-Type\": `image/${result.data.format}`,\n \"Cache-Control\": `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate * 2}`,\n },\n });\n } catch (err) {\n // On any error, try fallback\n if (fallback) {\n return Response.redirect(new URL(fallback, request.url), 302);\n }\n return new Response(\"OG image generation failed\", { status: 500 });\n }\n };\n}\n\n/**\n * Flatten Next.js route params (which can be string | string[]) to Record<string, string>.\n */\nfunction flattenParams(params: Record<string, string | string[]>): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n flat[key] = Array.isArray(value) ? value.join(\"/\") : value;\n }\n return flat;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@ogpipe/next",
3
+ "version": "0.1.0",
4
+ "description": "Pixel-perfect OG images for Next.js — full CSS, any font, any hosting platform",
5
+ "keywords": ["og-image", "open-graph", "next.js", "social-cards", "seo", "chromium"],
6
+ "license": "MIT",
7
+ "author": "Order Udye Pvt Ltd",
8
+ "homepage": "https://ogpipe.dev",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ogpipe/ogpipe-next"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ },
21
+ "./client": {
22
+ "types": "./dist/client/index.d.ts",
23
+ "import": "./dist/client/index.js",
24
+ "require": "./dist/client/index.cjs"
25
+ }
26
+ },
27
+ "bin": {
28
+ "ogpipe": "./dist/bin/ogpipe.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "templates",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run"
40
+ },
41
+ "dependencies": {
42
+ "chalk": "^5.3.0",
43
+ "commander": "^12.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "next": ">=13.0.0",
47
+ "react": ">=18.0.0",
48
+ "react-dom": ">=18.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "next": { "optional": false },
52
+ "react": { "optional": false },
53
+ "react-dom": { "optional": false }
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.11.0",
57
+ "tsup": "^8.3.0",
58
+ "typescript": "^5.4.0",
59
+ "vitest": "^2.0.0"
60
+ }
61
+ }
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <script>tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter', 'sans-serif'] } } } }</script>
8
+ </head>
9
+ <body class="w-[1200px] h-[630px] flex flex-col justify-between p-16 font-sans bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative overflow-hidden">
10
+ <!-- Subtle glow -->
11
+ <div class="absolute top-[-200px] right-[-100px] w-[500px] h-[500px] rounded-full bg-blue-500/10 blur-3xl"></div>
12
+ <div class="absolute bottom-[-200px] left-[-100px] w-[400px] h-[400px] rounded-full bg-purple-500/8 blur-3xl"></div>
13
+
14
+ <!-- Content -->
15
+ <div class="relative z-10 flex-1 flex flex-col justify-center">
16
+ <h1 class="text-[56px] font-bold text-white leading-[1.15] tracking-tight max-w-[900px]">{{title}}</h1>
17
+ <p class="text-[22px] text-slate-400 mt-5 max-w-[700px] leading-relaxed">{{description}}</p>
18
+ </div>
19
+
20
+ <!-- Footer -->
21
+ <div class="relative z-10 flex items-center justify-between">
22
+ <div class="flex items-center gap-3">
23
+ <div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold text-sm">{{author_initial}}</div>
24
+ <div>
25
+ <p class="text-white font-medium text-base">{{author}}</p>
26
+ <p class="text-slate-500 text-sm">{{date}}</p>
27
+ </div>
28
+ </div>
29
+ <p class="text-slate-600 text-base">{{site}}</p>
30
+ </div>
31
+ </body>
32
+ </html>
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <script>tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] } } } }</script>
8
+ </head>
9
+ <body class="w-[1200px] h-[630px] flex flex-col justify-center p-16 font-sans bg-[#0a0a0f] relative overflow-hidden">
10
+ <!-- Gradient accent -->
11
+ <div class="absolute top-0 left-0 w-full h-[3px] bg-gradient-to-r from-emerald-400 via-cyan-400 to-blue-500"></div>
12
+ <div class="absolute top-0 right-16 w-[300px] h-[300px] rounded-full bg-emerald-500/5 blur-3xl"></div>
13
+
14
+ <!-- Version badge -->
15
+ <div class="relative z-10 flex items-center gap-3 mb-6">
16
+ <span class="px-3 py-1.5 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 font-mono text-sm font-medium">{{version}}</span>
17
+ <span class="text-slate-600 text-sm">{{date}}</span>
18
+ </div>
19
+
20
+ <!-- Headline -->
21
+ <h1 class="relative z-10 text-[52px] font-bold text-white leading-[1.15] tracking-tight max-w-[900px]">{{title}}</h1>
22
+
23
+ <!-- Summary -->
24
+ <p class="relative z-10 text-[20px] text-slate-400 mt-5 max-w-[700px] leading-relaxed">{{description}}</p>
25
+
26
+ <!-- Footer -->
27
+ <div class="relative z-10 mt-auto flex items-center gap-2">
28
+ <div class="w-2 h-2 rounded-full bg-emerald-400"></div>
29
+ <span class="text-slate-600 text-sm font-medium">{{site}}</span>
30
+ </div>
31
+ </body>
32
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <script>tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] } } } }</script>
8
+ </head>
9
+ <body class="w-[1200px] h-[630px] flex flex-col justify-center p-16 font-sans bg-[#fafafa] relative">
10
+ <!-- Top accent line -->
11
+ <div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 via-cyan-500 to-teal-500"></div>
12
+
13
+ <!-- Category badge -->
14
+ <span class="inline-flex items-center w-fit px-3 py-1.5 rounded-md bg-blue-50 border border-blue-100 text-blue-700 font-mono text-sm font-medium mb-6">{{category}}</span>
15
+
16
+ <!-- Title -->
17
+ <h1 class="text-[48px] font-semibold text-gray-900 leading-[1.2] tracking-tight max-w-[900px]">{{title}}</h1>
18
+
19
+ <!-- Description -->
20
+ <p class="text-[22px] text-gray-500 mt-4 max-w-[750px] leading-relaxed">{{description}}</p>
21
+
22
+ <!-- Brand -->
23
+ <p class="absolute bottom-14 right-16 text-[20px] font-medium text-gray-400">{{site}}</p>
24
+ </body>
25
+ </html>