diff --git a/packages/app/src/app/api/og-preview/route.tsx b/packages/app/src/app/api/og-preview/route.tsx index df2f539..b82d041 100644 --- a/packages/app/src/app/api/og-preview/route.tsx +++ b/packages/app/src/app/api/og-preview/route.tsx @@ -28,6 +28,156 @@ import { renderOgImage as v22 } from '@/app/blog/[slug]/og-variants/v22-gold-tit import { renderOgImage as v23 } from '@/app/blog/[slug]/og-variants/v23-bold-circuit'; import { renderOgImage as v24 } from '@/app/blog/[slug]/og-variants/v24-gold-split-bold'; import { renderOgImage as v25 } from '@/app/blog/[slug]/og-variants/v25-gold-accent-stripe'; +import { renderOgImage as v26 } from '@/app/blog/[slug]/og-variants/v26-halftone-dots'; +import { renderOgImage as v27 } from '@/app/blog/[slug]/og-variants/v27-dot-grid'; +import { renderOgImage as v28 } from '@/app/blog/[slug]/og-variants/v28-constellation'; +import { renderOgImage as v29 } from '@/app/blog/[slug]/og-variants/v29-particle-burst'; +import { renderOgImage as v30 } from '@/app/blog/[slug]/og-variants/v30-dot-border'; +import { renderOgImage as v31 } from '@/app/blog/[slug]/og-variants/v31-concentric-rings'; +import { renderOgImage as v32 } from '@/app/blog/[slug]/og-variants/v32-venn-overlap'; +import { renderOgImage as v33 } from '@/app/blog/[slug]/og-variants/v33-floating-bubbles'; +import { renderOgImage as v34 } from '@/app/blog/[slug]/og-variants/v34-ripple-waves'; +import { renderOgImage as v35 } from '@/app/blog/[slug]/og-variants/v35-corner-arcs'; +import { renderOgImage as v36 } from '@/app/blog/[slug]/og-variants/v36-scan-lines'; +import { renderOgImage as v37 } from '@/app/blog/[slug]/og-variants/v37-vertical-blinds'; +import { renderOgImage as v38 } from '@/app/blog/[slug]/og-variants/v38-crosshatch'; +import { renderOgImage as v39 } from '@/app/blog/[slug]/og-variants/v39-sound-wave'; +import { renderOgImage as v40 } from '@/app/blog/[slug]/og-variants/v40-radial-rays'; +import { renderOgImage as v41 } from '@/app/blog/[slug]/og-variants/v41-isometric-grid'; +import { renderOgImage as v42 } from '@/app/blog/[slug]/og-variants/v42-blueprint'; +import { renderOgImage as v43 } from '@/app/blog/[slug]/og-variants/v43-glitch-grid'; +import { renderOgImage as v44 } from '@/app/blog/[slug]/og-variants/v44-perspective-lines'; +import { renderOgImage as v45 } from '@/app/blog/[slug]/og-variants/v45-topographic'; +import { renderOgImage as v46 } from '@/app/blog/[slug]/og-variants/v46-ocean-depths'; +import { renderOgImage as v47 } from '@/app/blog/[slug]/og-variants/v47-sunset-fire'; +import { renderOgImage as v48 } from '@/app/blog/[slug]/og-variants/v48-forest-canopy'; +import { renderOgImage as v49 } from '@/app/blog/[slug]/og-variants/v49-arctic-frost'; +import { renderOgImage as v50 } from '@/app/blog/[slug]/og-variants/v50-volcanic-ember'; +import { renderOgImage as v51 } from '@/app/blog/[slug]/og-variants/v51-royal-purple'; +import { renderOgImage as v52 } from '@/app/blog/[slug]/og-variants/v52-copper-patina'; +import { renderOgImage as v53 } from '@/app/blog/[slug]/og-variants/v53-neon-night'; +import { renderOgImage as v54 } from '@/app/blog/[slug]/og-variants/v54-sandstone'; +import { renderOgImage as v55 } from '@/app/blog/[slug]/og-variants/v55-monochrome-steel'; +import { renderOgImage as v56 } from '@/app/blog/[slug]/og-variants/v56-vertical-split'; +import { renderOgImage as v57 } from '@/app/blog/[slug]/og-variants/v57-top-banner'; +import { renderOgImage as v58 } from '@/app/blog/[slug]/og-variants/v58-right-sidebar'; +import { renderOgImage as v59 } from '@/app/blog/[slug]/og-variants/v59-left-accent-bar'; +import { renderOgImage as v60 } from '@/app/blog/[slug]/og-variants/v60-bottom-dock'; +import { renderOgImage as v61 } from '@/app/blog/[slug]/og-variants/v61-z-layout'; +import { renderOgImage as v62 } from '@/app/blog/[slug]/og-variants/v62-card-in-card'; +import { renderOgImage as v63 } from '@/app/blog/[slug]/og-variants/v63-three-column'; +import { renderOgImage as v64 } from '@/app/blog/[slug]/og-variants/v64-ruled-sections'; +import { renderOgImage as v65 } from '@/app/blog/[slug]/og-variants/v65-staircase-blocks'; +import { renderOgImage as v66 } from '@/app/blog/[slug]/og-variants/v66-drop-cap'; +import { renderOgImage as v67 } from '@/app/blog/[slug]/og-variants/v67-all-caps-impact'; +import { renderOgImage as v68 } from '@/app/blog/[slug]/og-variants/v68-serif-elegant'; +import { renderOgImage as v69 } from '@/app/blog/[slug]/og-variants/v69-stacked-words'; +import { renderOgImage as v70 } from '@/app/blog/[slug]/og-variants/v70-underline-accent'; +import { renderOgImage as v71 } from '@/app/blog/[slug]/og-variants/v71-highlight-marker'; +import { renderOgImage as v72 } from '@/app/blog/[slug]/og-variants/v72-title-badge'; +import { renderOgImage as v73 } from '@/app/blog/[slug]/og-variants/v73-ultra-tall'; +import { renderOgImage as v74 } from '@/app/blog/[slug]/og-variants/v74-two-size'; +import { renderOgImage as v75 } from '@/app/blog/[slug]/og-variants/v75-centered-zen'; +import { renderOgImage as v76 } from '@/app/blog/[slug]/og-variants/v76-terminal'; +import { renderOgImage as v77 } from '@/app/blog/[slug]/og-variants/v77-code-editor'; +import { renderOgImage as v78 } from '@/app/blog/[slug]/og-variants/v78-matrix-rain'; +import { renderOgImage as v79 } from '@/app/blog/[slug]/og-variants/v79-binary-accent'; +import { renderOgImage as v80 } from '@/app/blog/[slug]/og-variants/v80-network-nodes'; +import { renderOgImage as v81 } from '@/app/blog/[slug]/og-variants/v81-dashboard'; +import { renderOgImage as v82 } from '@/app/blog/[slug]/og-variants/v82-chip-layout'; +import { renderOgImage as v83 } from '@/app/blog/[slug]/og-variants/v83-data-table'; +import { renderOgImage as v84 } from '@/app/blog/[slug]/og-variants/v84-progress-bar'; +import { renderOgImage as v85 } from '@/app/blog/[slug]/og-variants/v85-api-docs'; +import { renderOgImage as v86 } from '@/app/blog/[slug]/og-variants/v86-hexagon-cells'; +import { renderOgImage as v87 } from '@/app/blog/[slug]/og-variants/v87-triangle-mosaic'; +import { renderOgImage as v88 } from '@/app/blog/[slug]/og-variants/v88-diamond-lattice'; +import { renderOgImage as v89 } from '@/app/blog/[slug]/og-variants/v89-chevron-pattern'; +import { renderOgImage as v90 } from '@/app/blog/[slug]/og-variants/v90-zigzag-border'; +import { renderOgImage as v91 } from '@/app/blog/[slug]/og-variants/v91-corner-ornaments'; +import { renderOgImage as v92 } from '@/app/blog/[slug]/og-variants/v92-ribbon-banner'; +import { renderOgImage as v93 } from '@/app/blog/[slug]/og-variants/v93-badge-seal'; +import { renderOgImage as v94 } from '@/app/blog/[slug]/og-variants/v94-bracket-frame'; +import { renderOgImage as v95 } from '@/app/blog/[slug]/og-variants/v95-arrow-accent'; +import { renderOgImage as v96 } from '@/app/blog/[slug]/og-variants/v96-magazine-cover'; +import { renderOgImage as v97 } from '@/app/blog/[slug]/og-variants/v97-newspaper'; +import { renderOgImage as v98 } from '@/app/blog/[slug]/og-variants/v98-book-cover'; +import { renderOgImage as v99 } from '@/app/blog/[slug]/og-variants/v99-academic-paper'; +import { renderOgImage as v100 } from '@/app/blog/[slug]/og-variants/v100-postcard'; +import { renderOgImage as v101 } from '@/app/blog/[slug]/og-variants/v101-playbill'; +import { renderOgImage as v102 } from '@/app/blog/[slug]/og-variants/v102-trading-card'; +import { renderOgImage as v103 } from '@/app/blog/[slug]/og-variants/v103-ticket'; +import { renderOgImage as v104 } from '@/app/blog/[slug]/og-variants/v104-album-cover'; +import { renderOgImage as v105 } from '@/app/blog/[slug]/og-variants/v105-movie-poster'; +import { renderOgImage as v106 } from '@/app/blog/[slug]/og-variants/v106-scattered-rects'; +import { renderOgImage as v107 } from '@/app/blog/[slug]/og-variants/v107-stacked-cards'; +import { renderOgImage as v108 } from '@/app/blog/[slug]/og-variants/v108-waveform-edge'; +import { renderOgImage as v109 } from '@/app/blog/[slug]/og-variants/v109-mountain-silhouette'; +import { renderOgImage as v110 } from '@/app/blog/[slug]/og-variants/v110-pixel-blocks'; +import { renderOgImage as v111 } from '@/app/blog/[slug]/og-variants/v111-layered-panels'; +import { renderOgImage as v112 } from '@/app/blog/[slug]/og-variants/v112-spiral-dots'; +import { renderOgImage as v113 } from '@/app/blog/[slug]/og-variants/v113-slash-marks'; +import { renderOgImage as v114 } from '@/app/blog/[slug]/og-variants/v114-noise-dots'; +import { renderOgImage as v115 } from '@/app/blog/[slug]/og-variants/v115-organic-blobs'; +import { renderOgImage as v116 } from '@/app/blog/[slug]/og-variants/v116-monogram-watermark'; +import { renderOgImage as v117 } from '@/app/blog/[slug]/og-variants/v117-timeline'; +import { renderOgImage as v118 } from '@/app/blog/[slug]/og-variants/v118-quote-marks'; +import { renderOgImage as v119 } from '@/app/blog/[slug]/og-variants/v119-classified'; +import { renderOgImage as v120 } from '@/app/blog/[slug]/og-variants/v120-retro-vhs'; +import { renderOgImage as v121 } from '@/app/blog/[slug]/og-variants/v121-barcode'; +import { renderOgImage as v122 } from '@/app/blog/[slug]/og-variants/v122-postmark'; +import { renderOgImage as v123 } from '@/app/blog/[slug]/og-variants/v123-breaking-news'; +import { renderOgImage as v124 } from '@/app/blog/[slug]/og-variants/v124-japanese-minimal'; +import { renderOgImage as v125 } from '@/app/blog/[slug]/og-variants/v125-brutalist'; +import { renderOgImage as v126 } from '@/app/blog/[slug]/og-variants/v126-ancient-scroll'; +import { renderOgImage as v127 } from '@/app/blog/[slug]/og-variants/v127-cave-painting'; +import { renderOgImage as v128 } from '@/app/blog/[slug]/og-variants/v128-art-deco'; +import { renderOgImage as v129 } from '@/app/blog/[slug]/og-variants/v129-constructivist'; +import { renderOgImage as v130 } from '@/app/blog/[slug]/og-variants/v130-ukiyo-e'; +import { renderOgImage as v131 } from '@/app/blog/[slug]/og-variants/v131-stained-glass'; +import { renderOgImage as v132 } from '@/app/blog/[slug]/og-variants/v132-hieroglyphs'; +import { renderOgImage as v133 } from '@/app/blog/[slug]/og-variants/v133-illuminated'; +import { renderOgImage as v134 } from '@/app/blog/[slug]/og-variants/v134-pop-art'; +import { renderOgImage as v135 } from '@/app/blog/[slug]/og-variants/v135-bauhaus'; +import { renderOgImage as v136 } from '@/app/blog/[slug]/og-variants/v136-chalkboard'; +import { renderOgImage as v137 } from '@/app/blog/[slug]/og-variants/v137-neon-sign'; +import { renderOgImage as v138 } from '@/app/blog/[slug]/og-variants/v138-leather-book'; +import { renderOgImage as v139 } from '@/app/blog/[slug]/og-variants/v139-shipping-label'; +import { renderOgImage as v140 } from '@/app/blog/[slug]/og-variants/v140-polaroid'; +import { renderOgImage as v141 } from '@/app/blog/[slug]/og-variants/v141-cassette'; +import { renderOgImage as v142 } from '@/app/blog/[slug]/og-variants/v142-license-plate'; +import { renderOgImage as v143 } from '@/app/blog/[slug]/og-variants/v143-credit-card'; +import { renderOgImage as v144 } from '@/app/blog/[slug]/og-variants/v144-prescription'; +import { renderOgImage as v145 } from '@/app/blog/[slug]/og-variants/v145-billboard'; +import { renderOgImage as v146 } from '@/app/blog/[slug]/og-variants/v146-dna-helix'; +import { renderOgImage as v147 } from '@/app/blog/[slug]/og-variants/v147-star-chart'; +import { renderOgImage as v148 } from '@/app/blog/[slug]/og-variants/v148-weather-map'; +import { renderOgImage as v149 } from '@/app/blog/[slug]/og-variants/v149-periodic-element'; +import { renderOgImage as v150 } from '@/app/blog/[slug]/og-variants/v150-microscope'; +import { renderOgImage as v151 } from '@/app/blog/[slug]/og-variants/v151-seismograph'; +import { renderOgImage as v152 } from '@/app/blog/[slug]/og-variants/v152-aurora'; +import { renderOgImage as v153 } from '@/app/blog/[slug]/og-variants/v153-coral-reef'; +import { renderOgImage as v154 } from '@/app/blog/[slug]/og-variants/v154-crystal'; +import { renderOgImage as v155 } from '@/app/blog/[slug]/og-variants/v155-telescope'; +import { renderOgImage as v156 } from '@/app/blog/[slug]/og-variants/v156-receipt'; +import { renderOgImage as v157 } from '@/app/blog/[slug]/og-variants/v157-passport'; +import { renderOgImage as v158 } from '@/app/blog/[slug]/og-variants/v158-ransom-note'; +import { renderOgImage as v159 } from '@/app/blog/[slug]/og-variants/v159-typewriter'; +import { renderOgImage as v160 } from '@/app/blog/[slug]/og-variants/v160-sticky-note'; +import { renderOgImage as v161 } from '@/app/blog/[slug]/og-variants/v161-safety-card'; +import { renderOgImage as v162 } from '@/app/blog/[slug]/og-variants/v162-nutrition-label'; +import { renderOgImage as v163 } from '@/app/blog/[slug]/og-variants/v163-warning-sign'; +import { renderOgImage as v164 } from '@/app/blog/[slug]/og-variants/v164-test-pattern'; +import { renderOgImage as v165 } from '@/app/blog/[slug]/og-variants/v165-boot-screen'; +import { renderOgImage as v166 } from '@/app/blog/[slug]/og-variants/v166-mondrian'; +import { renderOgImage as v167 } from '@/app/blog/[slug]/og-variants/v167-rothko'; +import { renderOgImage as v168 } from '@/app/blog/[slug]/og-variants/v168-kandinsky'; +import { renderOgImage as v169 } from '@/app/blog/[slug]/og-variants/v169-impossible'; +import { renderOgImage as v170 } from '@/app/blog/[slug]/og-variants/v170-op-art'; +import { renderOgImage as v171 } from '@/app/blog/[slug]/og-variants/v171-data-mosh'; +import { renderOgImage as v172 } from '@/app/blog/[slug]/og-variants/v172-risograph'; +import { renderOgImage as v173 } from '@/app/blog/[slug]/og-variants/v173-linocut'; +import { renderOgImage as v174 } from '@/app/blog/[slug]/og-variants/v174-psychedelic'; +import { renderOgImage as v175 } from '@/app/blog/[slug]/og-variants/v175-hologram'; const variants: Record Promise> = { v1, @@ -55,6 +205,156 @@ const variants: Record Promise> = { v23, v24, v25, + v26, + v27, + v28, + v29, + v30, + v31, + v32, + v33, + v34, + v35, + v36, + v37, + v38, + v39, + v40, + v41, + v42, + v43, + v44, + v45, + v46, + v47, + v48, + v49, + v50, + v51, + v52, + v53, + v54, + v55, + v56, + v57, + v58, + v59, + v60, + v61, + v62, + v63, + v64, + v65, + v66, + v67, + v68, + v69, + v70, + v71, + v72, + v73, + v74, + v75, + v76, + v77, + v78, + v79, + v80, + v81, + v82, + v83, + v84, + v85, + v86, + v87, + v88, + v89, + v90, + v91, + v92, + v93, + v94, + v95, + v96, + v97, + v98, + v99, + v100, + v101, + v102, + v103, + v104, + v105, + v106, + v107, + v108, + v109, + v110, + v111, + v112, + v113, + v114, + v115, + v116, + v117, + v118, + v119, + v120, + v121, + v122, + v123, + v124, + v125, + v126, + v127, + v128, + v129, + v130, + v131, + v132, + v133, + v134, + v135, + v136, + v137, + v138, + v139, + v140, + v141, + v142, + v143, + v144, + v145, + v146, + v147, + v148, + v149, + v150, + v151, + v152, + v153, + v154, + v155, + v156, + v157, + v158, + v159, + v160, + v161, + v162, + v163, + v164, + v165, + v166, + v167, + v168, + v169, + v170, + v171, + v172, + v173, + v174, + v175, }; export async function GET(request: NextRequest) { diff --git a/packages/app/src/app/blog/[slug]/og-variants/v100-postcard.tsx b/packages/app/src/app/blog/[slug]/og-variants/v100-postcard.tsx new file mode 100644 index 0000000..a2c4b1f --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v100-postcard.tsx @@ -0,0 +1,196 @@ +/** + * V100: Postcard — Right side content, left side decorative stamp area, address-line formatting. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Outer border — postcard edge */} +
+ {/* Left side — decorative */} +
+ {/* Stamp area — top-left bordered square */} +
+ + {meta.readingTime} + + + MIN READ + +
+ + {/* Logo at bottom-left of left panel */} +
+ + + INFERENCEX + +
+
+ + {/* Vertical divider */} +
+ + {/* Right side — content */} +
+ {/* Date */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Title */} +
+ + {meta.title} + +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Author — address-line style with underlines */} +
+ + TO: + +
+ {meta.author} +
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v101-playbill.tsx b/packages/app/src/app/blog/[slug]/og-variants/v101-playbill.tsx new file mode 100644 index 0000000..a7a96f4 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v101-playbill.tsx @@ -0,0 +1,155 @@ +/** + * V101: Playbill — Dramatic theater playbill with elegant presentation and cast listing. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Decorative top border */} +
+
+ + {/* "InferenceX PRESENTS" */} +
+ + + InferenceX PRESENTS + +
+ + {/* Main title — huge and dramatic */} +
+ + {meta.title} + +
+ + {/* Written by */} +
+ + Written by {meta.author} + +
+ + {/* Opening date */} +
+ + Opening{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Tags as "Starring" */} +
+ + Starring: {meta.tags ? meta.tags.join(' \u00b7 ') : ''} + +
+ + {/* Decorative bottom border */} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v102-trading-card.tsx b/packages/app/src/app/blog/[slug]/og-variants/v102-trading-card.tsx new file mode 100644 index 0000000..dea32c4 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v102-trading-card.tsx @@ -0,0 +1,191 @@ +/** + * V102: Trading Card — Bordered card with name bar, stats section, and holographic-hint border. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Holographic-hint outer border */} +
+ {/* Inner card */} +
+ {/* Name bar at top */} +
+ + + INFERENCEX + +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Main content area */} +
+ + {meta.title} + + + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Stats section at bottom */} +
+ {/* Author stat */} +
+ AUTHOR + + {meta.author} + +
+ + {/* Reading time stat */} +
+ + READ TIME + + + {meta.readingTime} MIN + +
+ + {/* Tags stat */} +
+ TAGS + + {meta.tags ? meta.tags.slice(0, 3).join(' \u00b7 ') : ''} + +
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v103-ticket.tsx b/packages/app/src/app/blog/[slug]/og-variants/v103-ticket.tsx new file mode 100644 index 0000000..d682b6e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v103-ticket.tsx @@ -0,0 +1,244 @@ +/** + * V103: Ticket — Landscape event ticket with tear-off stub separated by dashed line. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Ticket body */} +
+ {/* Left portion — event details */} +
+ {/* Event branding */} +
+ + + INFERENCEX EVENT + +
+ + {/* Title */} +
+ + {meta.title} + +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Date and author */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + {meta.author} +
+
+ + {/* Dashed vertical divider — perforated edge */} +
+ {Array.from({ length: 22 }).map((_, i) => ( +
+ ))} +
+ + {/* Right portion — tear-off stub */} +
+ {/* Reading time */} +
+ + {meta.readingTime} + + + MIN READ + +
+ + {/* Divider */} +
+ + {/* ADMIT ONE */} +
+ + ADMIT + + + ONE + +
+ + {/* Tags */} +
+ + {meta.tags ? meta.tags.slice(0, 3).join(' \u00b7 ') : ''} + +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v104-album-cover.tsx b/packages/app/src/app/blog/[slug]/og-variants/v104-album-cover.tsx new file mode 100644 index 0000000..6139509 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v104-album-cover.tsx @@ -0,0 +1,147 @@ +/** + * V104: Album Cover — Vinyl record cover aesthetic with centered square-ish content region. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Centered square-ish album cover region */} +
+ {/* Record label / logo top-left */} +
+ + + INFERENCEX RECORDS + +
+ + {/* Spacer */} +
+ + {/* Album title */} +
+ + {meta.title} + +
+ + {/* Artist name */} +
+ + {meta.author} + +
+ + {/* Thin separator */} +
+ + {/* Track listing hint (tags) */} +
+ + {meta.tags ? meta.tags.join(' / ') : ''} + +
+
+ + {/* Year on spine — far left */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Duration — far right */} +
+ {meta.readingTime} min +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v105-movie-poster.tsx b/packages/app/src/app/blog/[slug]/og-variants/v105-movie-poster.tsx new file mode 100644 index 0000000..cbdd8a7 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v105-movie-poster.tsx @@ -0,0 +1,229 @@ +/** + * V105: Movie Poster — Cinematic poster with massive title, colored glow, and genre labels. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Subtle colored glow behind title — layered translucent divs */} +
+
+
+ + {/* Date / Coming soon at top */} +
+ + {new Date(meta.date + 'T00:00:00Z') + .toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }) + .toUpperCase()} + +
+ + {/* Logo */} +
+ +
+ + {/* Title — MASSIVE */} +
+ + {meta.title} + +
+ + {/* "A film by" author */} +
+ + A film by {meta.author} + +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Genre labels (tags) at bottom */} +
+ {meta.tags && + meta.tags.map((tag, i) => ( +
0 ? '10px' : '0px', + }} + > + + {tag.toUpperCase()} + +
+ ))} +
+ + {/* Reading time bottom-right */} +
+ {meta.readingTime} MIN +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v106-scattered-rects.tsx b/packages/app/src/app/blog/[slug]/og-variants/v106-scattered-rects.tsx new file mode 100644 index 0000000..261aaae --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v106-scattered-rects.tsx @@ -0,0 +1,144 @@ +/** + * V106: Scattered Rects — 15-20 randomly positioned rectangles of varying sizes and colors at low opacity creating an abstract art background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const rects = [ + { top: 30, left: 50, w: 80, h: 60, color: 'rgba(0,200,180,0.12)', border: false }, + { top: 100, left: 900, w: 120, h: 40, color: 'rgba(255,180,0,0.10)', border: false }, + { top: 200, left: 150, w: 50, h: 100, color: 'rgba(100,100,255,0.08)', border: true }, + { top: 350, left: 1000, w: 90, h: 90, color: 'rgba(0,200,180,0.15)', border: false }, + { top: 450, left: 60, w: 40, h: 40, color: 'rgba(255,100,100,0.10)', border: false }, + { top: 500, left: 800, w: 110, h: 30, color: 'rgba(0,150,255,0.12)', border: true }, + { top: 80, left: 400, w: 70, h: 70, color: 'rgba(200,50,200,0.08)', border: false }, + { top: 250, left: 700, w: 60, h: 80, color: 'rgba(255,220,0,0.10)', border: true }, + { top: 150, left: 1050, w: 100, h: 50, color: 'rgba(0,200,180,0.10)', border: false }, + { top: 400, left: 300, w: 30, h: 120, color: 'rgba(100,200,100,0.12)', border: false }, + { top: 520, left: 500, w: 80, h: 25, color: 'rgba(0,100,255,0.08)', border: true }, + { top: 60, left: 700, w: 45, h: 45, color: 'rgba(255,150,50,0.15)', border: false }, + { top: 300, left: 450, w: 100, h: 60, color: 'rgba(50,50,200,0.10)', border: true }, + { top: 550, left: 200, w: 55, h: 55, color: 'rgba(0,200,180,0.08)', border: false }, + { top: 180, left: 550, w: 35, h: 90, color: 'rgba(200,200,0,0.12)', border: false }, + { top: 420, left: 650, w: 75, h: 35, color: 'rgba(255,80,80,0.10)', border: true }, + { top: 10, left: 250, w: 20, h: 20, color: 'rgba(0,200,180,0.18)', border: false }, + { top: 480, left: 1100, w: 60, h: 60, color: 'rgba(100,50,200,0.10)', border: false }, + ]; + + return new ImageResponse( +
+ {/* Scattered rectangles */} + {rects.map((r, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v107-stacked-cards.tsx b/packages/app/src/app/blog/[slug]/og-variants/v107-stacked-cards.tsx new file mode 100644 index 0000000..403fe73 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v107-stacked-cards.tsx @@ -0,0 +1,158 @@ +/** + * V107: Stacked Cards — 3 layered card rectangles slightly offset behind the main content card creating a paper-stack depth illusion. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Background card 3 (deepest) */} +
+ + {/* Background card 2 */} +
+ + {/* Background card 1 */} +
+ + {/* Main content card */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v108-waveform-edge.tsx b/packages/app/src/app/blog/[slug]/og-variants/v108-waveform-edge.tsx new file mode 100644 index 0000000..2e11b4b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v108-waveform-edge.tsx @@ -0,0 +1,140 @@ +/** + * V108: Waveform Edge — Bottom of the image has a waveform made of vertical bars of varying heights like an audio visualizer frozen in time. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Generate waveform bars with varying heights + const barHeights = [ + 35, 55, 28, 72, 40, 60, 25, 80, 45, 32, 68, 50, 38, 75, 30, 58, 42, 65, 35, 48, 70, 28, 55, 62, + 33, 78, 44, 52, 36, 67, 40, 58, 25, 73, 46, 54, 30, 64, 38, 50, 72, 32, 60, 45, 35, 76, 42, 56, + 28, 68, 48, 34, 62, 40, 70, 30, 54, 44, 66, 36, 58, 26, 74, 46, 52, 32, 80, 38, 60, 42, 50, 28, + 72, 34, 56, 44, 64, 36, 48, 30, 68, 40, 55, 32, 76, 46, 58, 26, 70, 38, 62, 42, 52, 34, 78, 44, + 54, 30, 66, 48, 60, 28, 74, 36, 50, 40, 72, 32, 56, 46, 64, 34, 80, 38, 52, 42, 68, 30, 58, 44, + ]; + + return new ImageResponse( +
+ {/* Content area */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+ + {/* Waveform at bottom */} +
+ {barHeights.map((h, i) => ( +
+ ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v109-mountain-silhouette.tsx b/packages/app/src/app/blog/[slug]/og-variants/v109-mountain-silhouette.tsx new file mode 100644 index 0000000..0852636 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v109-mountain-silhouette.tsx @@ -0,0 +1,195 @@ +/** + * V109: Mountain Silhouette — Bottom portion has layered horizontal strips creating a mountain range silhouette with a dark blue/purple palette. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Mountain layers from back (tallest, darkest) to front (shortest, lightest) + const mountains = [ + { + color: '#1a1040', + peaks: [ + { left: 0, width: 300, height: 200 }, + { left: 250, width: 400, height: 240 }, + { left: 600, width: 350, height: 180 }, + { left: 900, width: 300, height: 220 }, + ], + bottom: 430, + }, + { + color: '#241858', + peaks: [ + { left: 100, width: 350, height: 170 }, + { left: 400, width: 300, height: 200 }, + { left: 650, width: 400, height: 160 }, + { left: 1000, width: 200, height: 190 }, + ], + bottom: 470, + }, + { + color: '#2e2068', + peaks: [ + { left: 0, width: 250, height: 130 }, + { left: 200, width: 350, height: 150 }, + { left: 500, width: 300, height: 120 }, + { left: 750, width: 450, height: 140 }, + ], + bottom: 510, + }, + { + color: '#382878', + peaks: [ + { left: 50, width: 300, height: 90 }, + { left: 300, width: 250, height: 110 }, + { left: 550, width: 350, height: 80 }, + { left: 850, width: 350, height: 100 }, + ], + bottom: 550, + }, + ]; + + return new ImageResponse( +
+ {/* Mountain layers */} + {mountains.map((layer, li) => ( +
+ {/* Base fill for the layer */} +
+ {/* Peak blocks */} + {layer.peaks.map((peak, pi) => ( +
+ ))} +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v110-pixel-blocks.tsx b/packages/app/src/app/blog/[slug]/og-variants/v110-pixel-blocks.tsx new file mode 100644 index 0000000..d690983 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v110-pixel-blocks.tsx @@ -0,0 +1,187 @@ +/** + * V110: Pixel Blocks — Scattered small squares in a few colors creating a pixel-art dissolve effect along one edge with a retro gaming aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const colors = [ + 'rgba(0,220,180,0.7)', + 'rgba(0,160,255,0.6)', + 'rgba(255,100,200,0.5)', + 'rgba(255,200,0,0.6)', + 'rgba(100,255,150,0.5)', + ]; + + // Dense pixels on right edge, becoming sparse toward center + const pixels = [ + // Dense cluster - right edge + { top: 20, left: 1180, s: 12, c: 0 }, + { top: 40, left: 1168, s: 10, c: 1 }, + { top: 20, left: 1155, s: 14, c: 2 }, + { top: 60, left: 1175, s: 10, c: 0 }, + { top: 55, left: 1150, s: 12, c: 3 }, + { top: 80, left: 1185, s: 8, c: 1 }, + { top: 100, left: 1170, s: 14, c: 4 }, + { top: 90, left: 1145, s: 10, c: 0 }, + { top: 120, left: 1180, s: 12, c: 2 }, + { top: 140, left: 1160, s: 8, c: 1 }, + { top: 135, left: 1140, s: 16, c: 3 }, + { top: 160, left: 1175, s: 10, c: 0 }, + { top: 180, left: 1150, s: 14, c: 4 }, + { top: 200, left: 1185, s: 8, c: 2 }, + { top: 210, left: 1160, s: 12, c: 1 }, + { top: 230, left: 1170, s: 10, c: 0 }, + { top: 250, left: 1145, s: 14, c: 3 }, + { top: 270, left: 1180, s: 8, c: 2 }, + { top: 290, left: 1155, s: 12, c: 4 }, + { top: 310, left: 1175, s: 10, c: 1 }, + { top: 330, left: 1140, s: 16, c: 0 }, + { top: 350, left: 1165, s: 8, c: 3 }, + { top: 370, left: 1185, s: 12, c: 2 }, + { top: 390, left: 1150, s: 10, c: 4 }, + { top: 410, left: 1175, s: 14, c: 1 }, + { top: 430, left: 1160, s: 8, c: 0 }, + { top: 450, left: 1180, s: 12, c: 3 }, + { top: 470, left: 1145, s: 10, c: 2 }, + { top: 490, left: 1170, s: 14, c: 4 }, + { top: 510, left: 1185, s: 8, c: 1 }, + { top: 530, left: 1155, s: 12, c: 0 }, + { top: 550, left: 1175, s: 10, c: 3 }, + { top: 570, left: 1140, s: 16, c: 2 }, + { top: 590, left: 1165, s: 8, c: 4 }, + { top: 610, left: 1185, s: 12, c: 1 }, + // Medium density - mid-right + { top: 50, left: 1110, s: 10, c: 0 }, + { top: 130, left: 1100, s: 12, c: 2 }, + { top: 220, left: 1115, s: 8, c: 1 }, + { top: 300, left: 1105, s: 14, c: 3 }, + { top: 380, left: 1120, s: 10, c: 4 }, + { top: 460, left: 1095, s: 12, c: 0 }, + { top: 540, left: 1110, s: 8, c: 2 }, + // Sparse - further left + { top: 80, left: 1060, s: 8, c: 1 }, + { top: 200, left: 1050, s: 10, c: 3 }, + { top: 340, left: 1070, s: 8, c: 0 }, + { top: 480, left: 1055, s: 10, c: 4 }, + // Very sparse - scattered + { top: 150, left: 1000, s: 8, c: 2 }, + { top: 400, left: 1010, s: 8, c: 1 }, + { top: 560, left: 990, s: 8, c: 3 }, + ]; + + return new ImageResponse( +
+ {/* Pixel blocks */} + {pixels.map((p, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v111-layered-panels.tsx b/packages/app/src/app/blog/[slug]/og-variants/v111-layered-panels.tsx new file mode 100644 index 0000000..354288a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v111-layered-panels.tsx @@ -0,0 +1,132 @@ +/** + * V111: Layered Panels — 3-4 large semi-transparent rectangles overlapping at different positions creating a frosted glass layering effect. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const panels = [ + { top: -80, left: -100, w: 700, h: 500, color: 'rgba(0,180,200,0.10)', radius: 24 }, + { top: 200, left: 500, w: 600, h: 450, color: 'rgba(120,80,220,0.12)', radius: 20 }, + { top: -40, left: 700, w: 550, h: 400, color: 'rgba(0,120,255,0.10)', radius: 28 }, + { top: 300, left: -50, w: 500, h: 380, color: 'rgba(220,180,0,0.08)', radius: 22 }, + ]; + + return new ImageResponse( +
+ {/* Overlapping panels */} + {panels.map((p, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v112-spiral-dots.tsx b/packages/app/src/app/blog/[slug]/og-variants/v112-spiral-dots.tsx new file mode 100644 index 0000000..869f926 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v112-spiral-dots.tsx @@ -0,0 +1,149 @@ +/** + * V112: Spiral Dots — Dots arranged in a rough spiral pattern emanating from center-right, getting smaller outward, creating a galaxy/vortex feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Generate spiral dots from center-right + const cx = 900; + const cy = 315; + const dots: { top: number; left: number; s: number; opacity: number; blue: boolean }[] = []; + + for (let i = 0; i < 55; i++) { + const angle = i * 0.45; + const radius = 20 + i * 5.5; + const x = cx + Math.cos(angle) * radius; + const y = cy + Math.sin(angle) * radius; + const dotSize = Math.max(3, 12 - i * 0.18); + const opacity = Math.max(0.1, 0.7 - i * 0.01); + + if (x >= 0 && x <= 1200 && y >= 0 && y <= 630) { + dots.push({ + top: Math.round(y), + left: Math.round(x), + s: Math.round(dotSize), + opacity, + blue: i % 3 !== 0, + }); + } + } + + return new ImageResponse( +
+ {/* Spiral dots */} + {dots.map((d, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v113-slash-marks.tsx b/packages/app/src/app/blog/[slug]/og-variants/v113-slash-marks.tsx new file mode 100644 index 0000000..b716955 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v113-slash-marks.tsx @@ -0,0 +1,171 @@ +/** + * V113: Slash Marks — Multiple "/" characters scattered across the background like rain or hash marks at low opacity creating subtle texture. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const slashes = [ + { top: 15, left: 80, size: 28, opacity: 0.08 }, + { top: 45, left: 320, size: 22, opacity: 0.06 }, + { top: 30, left: 550, size: 32, opacity: 0.1 }, + { top: 20, left: 780, size: 24, opacity: 0.07 }, + { top: 50, left: 1000, size: 26, opacity: 0.09 }, + { top: 90, left: 180, size: 20, opacity: 0.05 }, + { top: 110, left: 450, size: 30, opacity: 0.08 }, + { top: 85, left: 700, size: 24, opacity: 0.06 }, + { top: 100, left: 920, size: 28, opacity: 0.1 }, + { top: 130, left: 1100, size: 22, opacity: 0.07 }, + { top: 170, left: 60, size: 26, opacity: 0.09 }, + { top: 160, left: 280, size: 32, opacity: 0.06 }, + { top: 180, left: 520, size: 20, opacity: 0.08 }, + { top: 155, left: 750, size: 28, opacity: 0.05 }, + { top: 190, left: 980, size: 24, opacity: 0.1 }, + { top: 230, left: 150, size: 22, opacity: 0.07 }, + { top: 250, left: 400, size: 30, opacity: 0.06 }, + { top: 240, left: 640, size: 26, opacity: 0.09 }, + { top: 260, left: 860, size: 20, opacity: 0.08 }, + { top: 220, left: 1050, size: 28, opacity: 0.05 }, + { top: 310, left: 100, size: 24, opacity: 0.1 }, + { top: 300, left: 350, size: 32, opacity: 0.06 }, + { top: 330, left: 580, size: 22, opacity: 0.08 }, + { top: 320, left: 810, size: 26, opacity: 0.07 }, + { top: 340, left: 1020, size: 28, opacity: 0.09 }, + { top: 380, left: 200, size: 20, opacity: 0.05 }, + { top: 400, left: 470, size: 30, opacity: 0.1 }, + { top: 390, left: 700, size: 24, opacity: 0.06 }, + { top: 410, left: 930, size: 26, opacity: 0.08 }, + { top: 370, left: 1130, size: 22, opacity: 0.07 }, + { top: 460, left: 70, size: 28, opacity: 0.09 }, + { top: 450, left: 310, size: 32, opacity: 0.05 }, + { top: 470, left: 540, size: 20, opacity: 0.08 }, + { top: 440, left: 780, size: 26, opacity: 0.1 }, + { top: 480, left: 1000, size: 24, opacity: 0.06 }, + { top: 520, left: 160, size: 22, opacity: 0.07 }, + { top: 540, left: 420, size: 28, opacity: 0.09 }, + { top: 530, left: 660, size: 30, opacity: 0.05 }, + { top: 550, left: 880, size: 24, opacity: 0.08 }, + { top: 510, left: 1080, size: 26, opacity: 0.1 }, + { top: 580, left: 240, size: 20, opacity: 0.06 }, + { top: 600, left: 500, size: 28, opacity: 0.07 }, + { top: 590, left: 740, size: 22, opacity: 0.09 }, + { top: 610, left: 960, size: 26, opacity: 0.05 }, + ]; + + return new ImageResponse( +
+ {/* Slash marks */} + {slashes.map((s, i) => ( +
+ / +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v114-noise-dots.tsx b/packages/app/src/app/blog/[slug]/og-variants/v114-noise-dots.tsx new file mode 100644 index 0000000..80a960c --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v114-noise-dots.tsx @@ -0,0 +1,186 @@ +/** + * V114: Noise Dots — Many tiny dots randomly positioned across the background like film grain, a mix of white and gold dots at 10-20% opacity. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const noiseDots = [ + { top: 12, left: 45, s: 4, gold: false, o: 0.12 }, + { top: 28, left: 180, s: 3, gold: true, o: 0.15 }, + { top: 8, left: 340, s: 5, gold: false, o: 0.1 }, + { top: 35, left: 500, s: 3, gold: true, o: 0.18 }, + { top: 15, left: 650, s: 4, gold: false, o: 0.14 }, + { top: 40, left: 810, s: 6, gold: true, o: 0.11 }, + { top: 22, left: 960, s: 3, gold: false, o: 0.16 }, + { top: 5, left: 1100, s: 5, gold: true, o: 0.13 }, + { top: 70, left: 90, s: 4, gold: true, o: 0.17 }, + { top: 85, left: 260, s: 3, gold: false, o: 0.12 }, + { top: 65, left: 420, s: 5, gold: true, o: 0.19 }, + { top: 95, left: 580, s: 4, gold: false, o: 0.1 }, + { top: 78, left: 730, s: 3, gold: true, o: 0.15 }, + { top: 60, left: 890, s: 6, gold: false, o: 0.13 }, + { top: 88, left: 1040, s: 4, gold: true, o: 0.11 }, + { top: 130, left: 30, s: 3, gold: false, o: 0.18 }, + { top: 145, left: 200, s: 5, gold: true, o: 0.14 }, + { top: 120, left: 370, s: 4, gold: false, o: 0.16 }, + { top: 155, left: 530, s: 3, gold: true, o: 0.12 }, + { top: 135, left: 690, s: 5, gold: false, o: 0.2 }, + { top: 160, left: 850, s: 4, gold: true, o: 0.1 }, + { top: 125, left: 1010, s: 3, gold: false, o: 0.17 }, + { top: 150, left: 1150, s: 5, gold: true, o: 0.13 }, + { top: 200, left: 110, s: 4, gold: false, o: 0.15 }, + { top: 215, left: 290, s: 3, gold: true, o: 0.11 }, + { top: 195, left: 450, s: 6, gold: false, o: 0.19 }, + { top: 225, left: 610, s: 4, gold: true, o: 0.14 }, + { top: 210, left: 770, s: 3, gold: false, o: 0.16 }, + { top: 190, left: 930, s: 5, gold: true, o: 0.12 }, + { top: 230, left: 1080, s: 4, gold: false, o: 0.18 }, + { top: 280, left: 60, s: 3, gold: true, o: 0.1 }, + { top: 295, left: 230, s: 5, gold: false, o: 0.15 }, + { top: 270, left: 400, s: 4, gold: true, o: 0.2 }, + { top: 305, left: 560, s: 3, gold: false, o: 0.13 }, + { top: 285, left: 720, s: 6, gold: true, o: 0.11 }, + { top: 310, left: 880, s: 4, gold: false, o: 0.17 }, + { top: 275, left: 1040, s: 3, gold: true, o: 0.14 }, + { top: 360, left: 140, s: 5, gold: false, o: 0.12 }, + { top: 375, left: 320, s: 4, gold: true, o: 0.19 }, + { top: 350, left: 480, s: 3, gold: false, o: 0.16 }, + { top: 385, left: 640, s: 5, gold: true, o: 0.1 }, + { top: 365, left: 800, s: 4, gold: false, o: 0.15 }, + { top: 390, left: 950, s: 3, gold: true, o: 0.18 }, + { top: 355, left: 1120, s: 6, gold: false, o: 0.13 }, + { top: 440, left: 50, s: 4, gold: true, o: 0.11 }, + { top: 455, left: 210, s: 3, gold: false, o: 0.2 }, + { top: 430, left: 380, s: 5, gold: true, o: 0.14 }, + { top: 465, left: 540, s: 4, gold: false, o: 0.12 }, + { top: 445, left: 700, s: 3, gold: true, o: 0.17 }, + { top: 470, left: 860, s: 5, gold: false, o: 0.1 }, + { top: 435, left: 1020, s: 4, gold: true, o: 0.16 }, + { top: 520, left: 130, s: 3, gold: false, o: 0.19 }, + { top: 535, left: 300, s: 5, gold: true, o: 0.13 }, + { top: 510, left: 460, s: 4, gold: false, o: 0.15 }, + { top: 545, left: 620, s: 3, gold: true, o: 0.11 }, + { top: 525, left: 780, s: 6, gold: false, o: 0.18 }, + { top: 550, left: 940, s: 4, gold: true, o: 0.14 }, + { top: 515, left: 1100, s: 3, gold: false, o: 0.12 }, + { top: 590, left: 80, s: 5, gold: true, o: 0.16 }, + { top: 605, left: 250, s: 4, gold: false, o: 0.1 }, + ]; + + return new ImageResponse( +
+ {/* Noise dots */} + {noiseDots.map((d, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v115-organic-blobs.tsx b/packages/app/src/app/blog/[slug]/og-variants/v115-organic-blobs.tsx new file mode 100644 index 0000000..9f15f1e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v115-organic-blobs.tsx @@ -0,0 +1,133 @@ +/** + * V115: Organic Blobs — 4-6 large circles with colored backgrounds at very low opacity positioned to partially overlap, creating soft organic color regions. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const blobs = [ + { top: -60, left: -40, size: 200, color: 'rgba(0,200,180,0.10)' }, + { top: 100, left: 350, size: 180, color: 'rgba(120,80,220,0.12)' }, + { top: -30, left: 800, size: 160, color: 'rgba(0,140,255,0.08)' }, + { top: 350, left: 150, size: 190, color: 'rgba(255,180,0,0.10)' }, + { top: 300, left: 700, size: 170, color: 'rgba(220,60,120,0.09)' }, + { top: 400, left: 1000, size: 200, color: 'rgba(0,200,180,0.12)' }, + ]; + + return new ImageResponse( +
+ {/* Organic blobs */} + {blobs.map((b, i) => ( +
+ ))} + + {/* Content overlay */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title and excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v116-monogram-watermark.tsx b/packages/app/src/app/blog/[slug]/og-variants/v116-monogram-watermark.tsx new file mode 100644 index 0000000..91f3fad --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v116-monogram-watermark.tsx @@ -0,0 +1,135 @@ +/** + * V116: Monogram Watermark — huge "IX" letters in very low opacity centered as a watermark behind content. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Monogram watermark */} +
+
+ IX +
+
+ + {/* Content overlay */} +
+ {/* Logo */} +
+ +
+ InferenceX +
+
+ + {/* Title + Excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+
{meta.author}
+
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v117-timeline.tsx b/packages/app/src/app/blog/[slug]/og-variants/v117-timeline.tsx new file mode 100644 index 0000000..23dafea --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v117-timeline.tsx @@ -0,0 +1,165 @@ +/** + * V117: Timeline — horizontal gold line across the middle with date marker dot. Title above, author and excerpt below. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top section: Logo + Title */} +
+ {/* Logo */} +
+ +
+ InferenceX +
+
+ + {/* Title */} +
+ {meta.title} +
+
+ + {/* Timeline bar */} +
+ {/* Line */} +
+ {/* Dot on the line */} +
+ {/* Date label near the dot */} +
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+ + {/* Bottom section: Excerpt + Author */} +
+ {/* Excerpt */} +
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+ + {/* Author + reading time */} +
+
{meta.author}
+
+ {meta.readingTime} min read +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v118-quote-marks.tsx b/packages/app/src/app/blog/[slug]/og-variants/v118-quote-marks.tsx new file mode 100644 index 0000000..ef50418 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v118-quote-marks.tsx @@ -0,0 +1,150 @@ +/** + * V118: Giant Quote Marks — huge low-opacity gold quotation characters framing the title for a literary feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Opening quote mark */} +
+ {'\u201C'} +
+ + {/* Closing quote mark */} +
+ {'\u201D'} +
+ + {/* Content */} +
+ {/* Logo */} +
+ +
+ InferenceX +
+
+ + {/* Title — centered for the quote feel */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v119-classified.tsx b/packages/app/src/app/blog/[slug]/og-variants/v119-classified.tsx new file mode 100644 index 0000000..ab17491 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v119-classified.tsx @@ -0,0 +1,212 @@ +/** + * V119: Classified — styled like a declassified dossier with stamp-style "CLASSIFIED" text and structured metadata fields. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* "CLASSIFIED" stamp in background */} +
+ CLASSIFIED +
+ + {/* "DECLASSIFIED" diagonal stamp */} +
+ DECLASSIFIED +
+ + {/* Content */} +
+ {/* Logo */} +
+ +
+ INFERENCEX +
+
+ + {/* Document body */} +
+ {/* Title as subject */} +
+
+ SUBJECT: +
+
+ {meta.title} +
+
+ + {/* Excerpt as brief */} +
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Dossier footer */} +
+
+
+ AUTHOR: +
+
{meta.author}
+
+
+
+ DATE: +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
+ CLEARANCE: +
+
+ TOP SECRET +
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v120-retro-vhs.tsx b/packages/app/src/app/blog/[slug]/og-variants/v120-retro-vhs.tsx new file mode 100644 index 0000000..704e9fd --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v120-retro-vhs.tsx @@ -0,0 +1,212 @@ +/** + * V120: Retro VHS — VHS tracking distortion with offset colored text layers and scan lines in pink/purple/cyan palette. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Generate scan line elements + const scanLines = Array.from({ length: 63 }, (_, i) => ( +
+ )); + + return new ImageResponse( +
+ {/* Scan lines overlay */} +
+ {scanLines} +
+ + {/* Cyan offset title layer */} +
+ {meta.title} +
+ + {/* Red offset title layer */} +
+ {meta.title} +
+ + {/* Main content */} +
+ {/* Top: Logo + REC indicator */} +
+
+ +
+ InferenceX +
+
+
+
+
+ REC +
+
+
+ + {/* Middle: Title (primary layer) */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Bottom: Author + Date + VHS style timestamp */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v121-barcode.tsx b/packages/app/src/app/blog/[slug]/og-variants/v121-barcode.tsx new file mode 100644 index 0000000..ce3890f --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v121-barcode.tsx @@ -0,0 +1,188 @@ +/** + * V121: Barcode Accent — barcode-like vertical lines at the bottom with product-info-styled date and reading time. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Generate barcode bars with varying widths + const barPattern = [ + 3, 1, 2, 1, 3, 2, 1, 1, 3, 1, 2, 1, 1, 3, 1, 2, 3, 1, 1, 2, 1, 3, 1, 2, 1, 1, 3, 2, 1, 3, 1, 2, + 1, 1, 2, 3, 1, 2, 1, 3, 1, 1, 2, 1, 3, 2, 1, 1, 3, 1, 2, 1, 3, 1, 2, 1, + ]; + let xOffset = 0; + const bars = barPattern.map((width, i) => { + const isBar = i % 2 === 0; + const el = isBar ? ( +
+ ) : ( +
+ ); + xOffset += width * 2; + return el; + }); + + return new ImageResponse( +
+ {/* Main content area */} +
+ {/* Logo */} +
+ +
+ InferenceX +
+
+ + {/* Title + Excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Author */} +
{meta.author}
+
+ + {/* Barcode section */} +
+ {/* Barcode bars */} +
+ {bars} +
+ + {/* Product-info style text below barcode */} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+ {meta.readingTime} MIN READ +
+
+ IX-BLOG-{meta.date.replace(/-/g, '')} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v122-postmark.tsx b/packages/app/src/app/blog/[slug]/og-variants/v122-postmark.tsx new file mode 100644 index 0000000..efbbbde --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v122-postmark.tsx @@ -0,0 +1,265 @@ +/** + * V122: Stamp/Postmark — large circular postmark element with date inside, vintage red/brown ink color, mailed letter aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + return new ImageResponse( +
+ {/* Postmark circle — top right */} +
+ {/* Inner circle */} +
+
+ INFERENCEX +
+ {/* Horizontal line */} +
+
+ {formattedDate} +
+ {/* Horizontal line */} +
+
+ BLOG POST +
+
+
+ + {/* Horizontal postmark lines through the circle */} +
+
+
+
+
+
+
+
+ + {/* Content */} +
+ {/* Logo */} +
+ +
+ InferenceX +
+
+ + {/* Title + Excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
{meta.author}
+
{formattedDate}
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v123-breaking-news.tsx b/packages/app/src/app/blog/[slug]/og-variants/v123-breaking-news.tsx new file mode 100644 index 0000000..b0fcf86 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v123-breaking-news.tsx @@ -0,0 +1,216 @@ +/** + * V123: Breaking News — TV news chyron/lower third with red "BREAKING" banner, broadcast urgent aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Upper area: Logo + Author info */} +
+ {/* Top bar */} +
+
+ +
+ InferenceX +
+
+
+
+
+ LIVE +
+
+
+ + {/* Author + Date + Excerpt above the chyron */} +
+
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+
+ + {/* Chyron / Lower Third */} +
+ {/* BREAKING label bar */} +
+
+ BREAKING +
+
+
+ + {/* Title banner */} +
+
+ {meta.title} +
+
+ + {/* Ticker bar at very bottom */} +
+
+ INFERENCEX NEWS NETWORK +
+
+ {meta.readingTime} MIN READ +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v124-japanese-minimal.tsx b/packages/app/src/app/blog/[slug]/og-variants/v124-japanese-minimal.tsx new file mode 100644 index 0000000..c344b11 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v124-japanese-minimal.tsx @@ -0,0 +1,134 @@ +/** + * V124: Minimal Japanese — extreme negative darkspace with small text in the corner and a single thin accent line. Zen, meditative. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + + return new ImageResponse( +
+ {/* Single thin vertical accent line */} +
+ + {/* Logo — small, top right */} +
+ +
+ InferenceX +
+
+ + {/* Content cluster — bottom left, small text */} +
+ {/* Title — intentionally small */} +
+ {meta.title} +
+ + {/* Excerpt — very small */} +
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+ + {/* Author + Date — tiny */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v125-brutalist.tsx b/packages/app/src/app/blog/[slug]/og-variants/v125-brutalist.tsx new file mode 100644 index 0000000..9251dcb --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v125-brutalist.tsx @@ -0,0 +1,181 @@ +/** + * V125: Brutalist — harsh, raw design with thick borders, high contrast black/white, loud red accent. Anti-design aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Inner container with thick border */} +
+ {/* Red accent bar — left side */} +
+ + {/* Content area */} +
+ {/* Top: Logo + BLOG label */} +
+
+ +
+ INFERENCEX +
+
+
+
+ BLOG +
+
+
+ + {/* Middle: Title — large and bold */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Bottom: Author + Date with harsh styling */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v126-ancient-scroll.tsx b/packages/app/src/app/blog/[slug]/og-variants/v126-ancient-scroll.tsx new file mode 100644 index 0000000..90289e3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v126-ancient-scroll.tsx @@ -0,0 +1,360 @@ +/** + * V126: Ancient Scroll — Warm parchment papyrus with rolled scroll edges, aged stains, and formal ancient typography. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + // Generate scattered stain positions + const stains = [ + { top: 80, left: 200, size: 12, opacity: 0.15 }, + { top: 150, left: 900, size: 18, opacity: 0.1 }, + { top: 300, left: 150, size: 10, opacity: 0.12 }, + { top: 400, left: 850, size: 15, opacity: 0.08 }, + { top: 500, left: 400, size: 20, opacity: 0.1 }, + { top: 120, left: 600, size: 8, opacity: 0.14 }, + { top: 450, left: 700, size: 14, opacity: 0.09 }, + { top: 250, left: 350, size: 11, opacity: 0.13 }, + { top: 550, left: 250, size: 16, opacity: 0.07 }, + { top: 350, left: 1000, size: 9, opacity: 0.11 }, + ]; + + // Scroll roller segments for left and right edges + const rollerSegments = Array.from({ length: 14 }, (_, i) => i); + + return new ImageResponse( +
+ {/* Parchment background area */} +
+ + {/* Aged inner parchment layer */} +
+ + {/* Scattered stain dots */} + {stains.map((stain, i) => ( +
+ ))} + + {/* Left scroll roller */} +
+ {rollerSegments.map((i) => ( +
+ ))} +
+ + {/* Right scroll roller */} +
+ {rollerSegments.map((i) => ( +
+ ))} +
+ + {/* Left roller cap - top */} +
+ + {/* Left roller cap - bottom */} +
+ + {/* Right roller cap - top */} +
+ + {/* Right roller cap - bottom */} +
+ + {/* Content area */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Decorative line */} +
+
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Bottom decorative line */} +
+
+ + {/* Footer */} +
+
+ Written in the year of {formattedDate} +
+
+ by {meta.author} · {meta.readingTime} min read +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v127-cave-painting.tsx b/packages/app/src/app/blog/[slug]/og-variants/v127-cave-painting.tsx new file mode 100644 index 0000000..bb98f24 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v127-cave-painting.tsx @@ -0,0 +1,296 @@ +/** + * V127: Cave Painting — Rough stone-brown background with ochre and red oxide hand-prints, stick figures, and primal energy. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + // Hand-print-like circles (ochre and red oxide) + const handPrints = [ + { top: 50, left: 80, size: 45, color: '#c4883a', opacity: 0.35 }, + { top: 120, left: 1050, size: 55, color: '#8b3a2a', opacity: 0.3 }, + { top: 400, left: 60, size: 50, color: '#c4883a', opacity: 0.25 }, + { top: 500, left: 1000, size: 40, color: '#8b3a2a', opacity: 0.35 }, + { top: 280, left: 1100, size: 35, color: '#c4883a', opacity: 0.2 }, + { top: 550, left: 500, size: 30, color: '#8b3a2a', opacity: 0.15 }, + { top: 100, left: 700, size: 25, color: '#c4883a', opacity: 0.12 }, + ]; + + // Finger marks radiating from hand prints + const fingerMarks = [ + { top: 25, left: 70, w: 8, h: 22, color: '#c4883a', opacity: 0.25 }, + { top: 30, left: 90, w: 7, h: 20, color: '#c4883a', opacity: 0.22 }, + { top: 28, left: 108, w: 8, h: 18, color: '#c4883a', opacity: 0.2 }, + { top: 32, left: 60, w: 7, h: 19, color: '#c4883a', opacity: 0.18 }, + { top: 95, left: 1040, w: 9, h: 24, color: '#8b3a2a', opacity: 0.25 }, + { top: 93, left: 1060, w: 8, h: 22, color: '#8b3a2a', opacity: 0.22 }, + { top: 98, left: 1080, w: 7, h: 20, color: '#8b3a2a', opacity: 0.2 }, + { top: 96, left: 1100, w: 8, h: 23, color: '#8b3a2a', opacity: 0.18 }, + ]; + + // Stick figure elements (simple lines using thin tall/wide divs) + const stickFigures = [ + // Figure 1 - body + { top: 150, left: 1020, w: 4, h: 50, color: '#c4883a', opacity: 0.3 }, + // Figure 1 - head + { top: 135, left: 1013, w: 18, h: 18, color: '#c4883a', opacity: 0.3, round: true }, + // Figure 1 - left arm + { top: 165, left: 1000, w: 22, h: 4, color: '#c4883a', opacity: 0.25 }, + // Figure 1 - right arm + { top: 165, left: 1022, w: 22, h: 4, color: '#c4883a', opacity: 0.25 }, + // Figure 1 - left leg + { top: 198, left: 1005, w: 4, h: 30, color: '#c4883a', opacity: 0.25 }, + // Figure 1 - right leg + { top: 198, left: 1030, w: 4, h: 30, color: '#c4883a', opacity: 0.25 }, + + // Figure 2 - body + { top: 420, left: 1080, w: 4, h: 45, color: '#8b3a2a', opacity: 0.3 }, + // Figure 2 - head + { top: 407, left: 1074, w: 16, h: 16, color: '#8b3a2a', opacity: 0.3, round: true }, + // Figure 2 - arms (raised) + { top: 425, left: 1060, w: 20, h: 4, color: '#8b3a2a', opacity: 0.25 }, + { top: 425, left: 1082, w: 20, h: 4, color: '#8b3a2a', opacity: 0.25 }, + ]; + + // Scattered dots mimicking stone texture + const stoneDots = Array.from({ length: 25 }, (_, i) => ({ + top: (i * 137 + 50) % 600, + left: (i * 211 + 30) % 1170, + size: 3 + (i % 4), + opacity: 0.06 + (i % 5) * 0.02, + })); + + return new ImageResponse( +
+ {/* Stone texture base layer */} +
+ + {/* Stone texture dots */} + {stoneDots.map((dot, i) => ( +
+ ))} + + {/* Hand-print circles */} + {handPrints.map((hp, i) => ( +
+ ))} + + {/* Finger marks */} + {fingerMarks.map((fm, i) => ( +
+ ))} + + {/* Stick figures and lines */} + {stickFigures.map((sf, i) => ( +
+ ))} + + {/* Horizontal primitive line across middle area */} +
+ + {/* Another primitive line */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title — large and primitive */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} +
+
+ {meta.readingTime} min read +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v128-art-deco.tsx b/packages/app/src/app/blog/[slug]/og-variants/v128-art-deco.tsx new file mode 100644 index 0000000..3ab903d --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v128-art-deco.tsx @@ -0,0 +1,407 @@ +/** + * V128: Art Deco — Black background with gold geometric sunburst rays, symmetrical layout, Gatsby-era elegance. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + // Sunburst rays from top center - using tall narrow divs rotated via positioning + // Since we can't use transform, we'll create rays as angled positioned rectangles + // We'll simulate rays by placing thin tall divs at various horizontal positions + const rays = Array.from({ length: 24 }, (_, i) => { + const angle = (i / 24) * Math.PI; + const spreadX = Math.cos(angle) * 800; + const _spreadY = Math.sin(angle) * 600; + return { + left: 600 + spreadX * 0.5 - 1, + top: -20, + width: 2, + height: 650, + opacity: 0.06 + (i % 3) * 0.02, + }; + }); + + // Parallel framing lines + const frameLines = [ + { top: 130, left: 100, right: 100, height: 1 }, + { top: 134, left: 120, right: 120, height: 2 }, + { top: 140, left: 100, right: 100, height: 1 }, + { top: 470, left: 100, right: 100, height: 1 }, + { top: 474, left: 120, right: 120, height: 2 }, + { top: 480, left: 100, right: 100, height: 1 }, + ]; + + // Vertical framing lines + const vertLines = [ + { top: 130, left: 98, width: 1, height: 352 }, + { top: 130, left: 102, width: 2, height: 352 }, + { top: 130, right: 98, width: 1, height: 352 }, + { top: 130, right: 102, width: 2, height: 352 }, + ]; + + // Decorative corner diamonds + const corners = [ + { top: 126, left: 92 }, + { top: 126, left: 1092 }, + { top: 472, left: 92 }, + { top: 472, left: 1092 }, + ]; + + return new ImageResponse( +
+ {/* Sunburst rays */} + {rays.map((ray, i) => ( +
+ ))} + + {/* Central gold glow circle */} +
+
+ + {/* Horizontal frame lines */} + {frameLines.map((line, i) => ( +
+ ))} + + {/* Vertical frame lines */} + {vertLines.map((line, i) => ( +
+ ))} + + {/* Corner diamonds */} + {corners.map((corner, i) => ( +
+ ))} + + {/* Top center ornament */} +
+
+ + {/* Logo centered at top */} +
+ + + INFERENCEX + +
+ + {/* Top decorative chevrons */} +
+
+
+
+
+ + {/* Title — centered, all caps, wide spacing */} +
+
+ {meta.title.toUpperCase()} +
+ + {/* Excerpt */} +
+ {excerpt} +
+
+ + {/* Bottom section */} +
+ {/* Bottom ornament */} +
+
+
+
+
+ + {/* Author and date */} +
+ {meta.author.toUpperCase()} · {formattedDate.toUpperCase()} ·{' '} + {meta.readingTime} MIN +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v129-constructivist.tsx b/packages/app/src/app/blog/[slug]/og-variants/v129-constructivist.tsx new file mode 100644 index 0000000..9e213a5 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v129-constructivist.tsx @@ -0,0 +1,369 @@ +/** + * V129: Constructivist — Soviet constructivist propaganda with bold red and black diagonal split, heavy caps, geometric shapes, and angular layout. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + return new ImageResponse( +
+ {/* Diagonal red zone — simulated with overlapping rectangles */} +
+ + {/* Black diagonal cut — large black rectangle positioned to create diagonal */} +
+ + {/* Secondary black wedge for sharper diagonal effect */} +
+ + {/* Red triangular accent in the black zone */} +
+ + {/* Large geometric circle — top right */} +
+ + {/* Smaller circle inside */} +
+ + {/* Triangle shape — using bordered div */} +
+ + {/* Geometric square */} +
+ + {/* Horizontal aggressive lines */} +
+
+ + {/* "READ NOW" propaganda banner */} +
+ + READ NOW + +
+ + {/* Blog post number badge */} +
+ + BLOG POST No. {meta.readingTime} + +
+ + {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title — heavy bold caps */} +
+
+ {meta.title.toUpperCase()} +
+
+ + {/* Excerpt */} +
+
+ {excerpt} +
+
+ + {/* Bottom stripe */} +
+ + {/* Footer */} +
+
+ {meta.author.toUpperCase()} · {formattedDate.toUpperCase()} +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v130-ukiyo-e.tsx b/packages/app/src/app/blog/[slug]/og-variants/v130-ukiyo-e.tsx new file mode 100644 index 0000000..1ca2dc8 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v130-ukiyo-e.tsx @@ -0,0 +1,309 @@ +/** + * V130: Ukiyo-e — Japanese woodblock print with deep indigo background, stylized wave patterns, cartouche box, and cream text. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + // Wave layers from bottom — each wave is a curved div + const waves = [ + { bottom: 0, height: 80, color: '#1a2a4a', opacity: 1, borderRadius: '50% 50% 0 0' }, + { bottom: 20, height: 70, color: '#1a3050', opacity: 0.9, borderRadius: '60% 40% 0 0' }, + { bottom: 40, height: 65, color: '#1a3560', opacity: 0.8, borderRadius: '40% 60% 0 0' }, + { bottom: 55, height: 60, color: '#1a3a6a', opacity: 0.7, borderRadius: '55% 45% 0 0' }, + { bottom: 70, height: 55, color: '#1a4070', opacity: 0.6, borderRadius: '45% 55% 0 0' }, + { bottom: 82, height: 50, color: '#1a4578', opacity: 0.5, borderRadius: '50% 50% 0 0' }, + ]; + + // Wave foam/crest details — small white curved elements on wave tops + const foamDots = [ + { bottom: 115, left: 150, w: 30, h: 10, opacity: 0.15 }, + { bottom: 120, left: 400, w: 25, h: 8, opacity: 0.12 }, + { bottom: 110, left: 650, w: 35, h: 12, opacity: 0.18 }, + { bottom: 118, left: 900, w: 28, h: 9, opacity: 0.14 }, + { bottom: 95, left: 250, w: 20, h: 7, opacity: 0.1 }, + { bottom: 100, left: 750, w: 22, h: 8, opacity: 0.13 }, + { bottom: 90, left: 1050, w: 18, h: 6, opacity: 0.11 }, + ]; + + return new ImageResponse( +
+ {/* Subtle background texture — scattered dots */} + {Array.from({ length: 15 }, (_, i) => ( +
+ ))} + + {/* Wave layers */} + {waves.map((wave, i) => ( +
+ ))} + + {/* Foam/crest details */} + {foamDots.map((foam, i) => ( +
+ ))} + + {/* Cartouche box — top right */} +
+
+ + INFERENCE + + + X + +
+
+ + {/* Thin vertical red accent line near cartouche */} +
+ + {/* Logo — top left */} +
+ +
+ + {/* Content area — vertical feeling layout */} +
+ {/* Thin horizontal divider */} +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer — above waves */} +
+
+ {meta.author} · {formattedDate} +
+
+ {meta.readingTime} min read +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+ + {/* Decorative horizontal bar above waves */} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v131-stained-glass.tsx b/packages/app/src/app/blog/[slug]/og-variants/v131-stained-glass.tsx new file mode 100644 index 0000000..64db7b6 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v131-stained-glass.tsx @@ -0,0 +1,282 @@ +/** + * V131: Stained Glass — Cathedral stained glass window with jewel-toned panes separated by thick dark lead borders, sacred geometry feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + // Jewel tone colors + const ruby = '#9b1b30'; + const sapphire = '#1a3a7a'; + const emerald = '#1a6b3a'; + const amber = '#c4881a'; + const amethyst = '#5a1a6b'; + const topaz = '#b8860b'; + + // Stained glass pane grid — arranged as irregular mosaic + const panes = [ + // Top row + { top: 0, left: 0, w: 150, h: 120, color: sapphire, opacity: 0.7 }, + { top: 0, left: 155, w: 120, h: 80, color: ruby, opacity: 0.65 }, + { top: 0, left: 280, w: 180, h: 120, color: emerald, opacity: 0.6 }, + { top: 0, left: 465, w: 100, h: 80, color: amber, opacity: 0.7 }, + { top: 0, left: 570, w: 160, h: 120, color: amethyst, opacity: 0.55 }, + { top: 0, left: 735, w: 130, h: 80, color: ruby, opacity: 0.6 }, + { top: 0, left: 870, w: 170, h: 120, color: sapphire, opacity: 0.65 }, + { top: 0, left: 1045, w: 155, h: 80, color: emerald, opacity: 0.7 }, + + // Second row fragments + { top: 85, left: 155, w: 120, h: 40, color: topaz, opacity: 0.5 }, + { top: 85, left: 465, w: 100, h: 40, color: sapphire, opacity: 0.5 }, + { top: 85, left: 735, w: 130, h: 40, color: amber, opacity: 0.55 }, + { top: 85, left: 1045, w: 155, h: 40, color: ruby, opacity: 0.5 }, + + // Middle rows — around the content area, creating a frame + { top: 125, left: 0, w: 80, h: 100, color: amber, opacity: 0.6 }, + { top: 125, left: 1120, w: 80, h: 100, color: emerald, opacity: 0.6 }, + { top: 230, left: 0, w: 80, h: 110, color: ruby, opacity: 0.55 }, + { top: 230, left: 1120, w: 80, h: 110, color: amethyst, opacity: 0.55 }, + { top: 345, left: 0, w: 80, h: 100, color: emerald, opacity: 0.6 }, + { top: 345, left: 1120, w: 80, h: 100, color: amber, opacity: 0.6 }, + + // Bottom rows + { top: 450, left: 0, w: 80, h: 180, color: sapphire, opacity: 0.65 }, + { top: 450, left: 1120, w: 80, h: 180, color: ruby, opacity: 0.65 }, + + { top: 530, left: 85, w: 130, h: 100, color: amethyst, opacity: 0.5 }, + { top: 530, left: 220, w: 160, h: 100, color: amber, opacity: 0.55 }, + { top: 530, left: 385, w: 120, h: 100, color: sapphire, opacity: 0.6 }, + { top: 530, left: 510, w: 180, h: 100, color: ruby, opacity: 0.5 }, + { top: 530, left: 695, w: 140, h: 100, color: emerald, opacity: 0.6 }, + { top: 530, left: 840, w: 130, h: 100, color: topaz, opacity: 0.55 }, + { top: 530, left: 975, w: 140, h: 100, color: amethyst, opacity: 0.5 }, + ]; + + // Rose window circle elements (sacred geometry at center-top) + const roseCircles = [ + { top: 125, left: 520, size: 160, border: 4, color: amber, opacity: 0.25 }, + { top: 155, left: 550, size: 100, border: 3, color: ruby, opacity: 0.2 }, + { top: 180, left: 575, size: 50, border: 2, color: sapphire, opacity: 0.3 }, + { top: 195, left: 590, size: 20, border: 0, color: amber, opacity: 0.4 }, + ]; + + return new ImageResponse( +
+ {/* Lead frame background */} +
+ + {/* Stained glass panes */} + {panes.map((pane, i) => ( +
+ ))} + + {/* Inner glow on select panes */} + {panes.slice(0, 8).map((pane, i) => ( +
+ ))} + + {/* Rose window / sacred geometry circles */} + {roseCircles.map((circle, i) => ( +
0 ? `${circle.border}px solid ${circle.color}` : 'none', + opacity: circle.opacity, + }} + /> + ))} + + {/* Content overlay panel */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} +
+
+
+ {meta.readingTime} min read +
+ {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( +
+ {tag} +
+ ))} +
+
+
+ + {/* Gothic arch suggestion at the very top — curved border */} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v132-hieroglyphs.tsx b/packages/app/src/app/blog/[slug]/og-variants/v132-hieroglyphs.tsx new file mode 100644 index 0000000..b76af8c --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v132-hieroglyphs.tsx @@ -0,0 +1,420 @@ +/** + * V132: Hieroglyphs — Egyptian hieroglyphic design with dark sand background, geometric glyph bands, cartouche oval, and Eye of Horus motif. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + const gold = '#c8a84e'; + const lapis = '#1e3a5f'; + const darkSand = '#1c1a14'; + + // Top hieroglyphic band — repeating geometric glyphs + const topGlyphs = Array.from({ length: 20 }, (_, i) => { + const shapes = ['circle', 'triangle', 'rect', 'diamond', 'circle', 'rect']; + return { left: i * 60, shape: shapes[i % shapes.length] }; + }); + + // Bottom hieroglyphic band + const bottomGlyphs = Array.from({ length: 20 }, (_, i) => { + const shapes = ['rect', 'circle', 'diamond', 'triangle', 'rect', 'circle']; + return { left: i * 60, shape: shapes[i % shapes.length] }; + }); + + // Eye of Horus components (geometric approximation in top-right) + const eyeComponents = [ + // Eye outline — oval + { + top: 120, + left: 1010, + w: 100, + h: 50, + borderRadius: '50%', + bg: 'transparent', + border: `2px solid ${gold}`, + opacity: 0.5, + }, + // Pupil + { + top: 133, + left: 1042, + w: 36, + h: 36, + borderRadius: '50%', + bg: lapis, + border: `2px solid ${gold}`, + opacity: 0.6, + }, + // Inner pupil + { + top: 141, + left: 1050, + w: 20, + h: 20, + borderRadius: '50%', + bg: gold, + border: 'none', + opacity: 0.4, + }, + // Teardrop line below eye + { + top: 168, + left: 1055, + w: 3, + h: 30, + borderRadius: '2px', + bg: gold, + border: 'none', + opacity: 0.35, + }, + // Eyebrow line + { + top: 112, + left: 1000, + w: 120, + h: 3, + borderRadius: '2px', + bg: gold, + border: 'none', + opacity: 0.3, + }, + // Tail swirl (horizontal line going right from eye) + { + top: 145, + left: 1110, + w: 40, + h: 2, + borderRadius: '1px', + bg: gold, + border: 'none', + opacity: 0.3, + }, + // Spiral end + { + top: 145, + left: 1148, + w: 10, + h: 10, + borderRadius: '50%', + bg: 'transparent', + border: `1px solid ${gold}`, + opacity: 0.25, + }, + ]; + + return new ImageResponse( +
+ {/* Subtle papyrus texture layer */} +
+ + {/* Top border line */} +
+ + {/* Top hieroglyphic band background */} +
+ + {/* Top glyph shapes */} + {topGlyphs.map((g, i) => ( +
+ ))} + + {/* Top border line bottom */} +
+ + {/* Bottom border line top */} +
+ + {/* Bottom hieroglyphic band background */} +
+ + {/* Bottom glyph shapes */} + {bottomGlyphs.map((g, i) => ( +
+ ))} + + {/* Bottom border line */} +
+ + {/* Eye of Horus */} + {eyeComponents.map((ec, i) => ( +
+ ))} + + {/* Cartouche oval around title area */} +
+ + {/* Cartouche inner line */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title inside cartouche */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} +
+
+
+ {meta.readingTime} min read +
+
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v133-illuminated.tsx b/packages/app/src/app/blog/[slug]/og-variants/v133-illuminated.tsx new file mode 100644 index 0000000..65bdd1b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v133-illuminated.tsx @@ -0,0 +1,418 @@ +/** + * V133: Illuminated Manuscript — Medieval vellum background with ornate initial letter, decorative vine margins, red and gold accents. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + const gold = '#c8a84e'; + const deepRed = '#8b1a1a'; + const vellum = '#1a1815'; + + const firstLetter = meta.title.charAt(0).toUpperCase(); + const restOfTitle = meta.title.slice(1); + + // Vine pattern along left margin — circles and connecting lines + const vineNodes = Array.from({ length: 12 }, (_, i) => ({ + top: 70 + i * 45, + left: 38, + size: 8 + (i % 3) * 3, + isLeaf: i % 3 === 0, + })); + + // Vine connecting stems (vertical lines between nodes) + const vineStems = Array.from({ length: 11 }, (_, i) => ({ + top: 70 + i * 45 + 10, + left: 42, + height: 35, + })); + + // Small decorative branch offshoots + const branches = [ + { top: 115, left: 50, w: 15, h: 2 }, + { top: 205, left: 50, w: 20, h: 2 }, + { top: 295, left: 50, w: 12, h: 2 }, + { top: 385, left: 50, w: 18, h: 2 }, + { top: 475, left: 50, w: 14, h: 2 }, + { top: 160, left: 50, w: 10, h: 2 }, + { top: 340, left: 50, w: 16, h: 2 }, + ]; + + // Corner ornaments + const cornerSize = 30; + + return new ImageResponse( +
+ {/* Subtle vellum texture overlay */} +
+ + {/* Outer border */} +
+ + {/* Inner border */} +
+ + {/* Corner ornaments — top left */} +
+ {/* Corner ornament — top right */} +
+ {/* Corner ornament — bottom left */} +
+ {/* Corner ornament — bottom right */} +
+ + {/* Corner inner dots */} + {[ + { top: 22, left: 22 }, + { top: 22, right: 22 }, + { bottom: 22, left: 22 }, + { bottom: 22, right: 22 }, + ].map((pos, i) => ( +
+ ))} + + {/* Left margin vine — stems */} + {vineStems.map((stem, i) => ( +
+ ))} + + {/* Left margin vine — nodes */} + {vineNodes.map((node, i) => ( +
+ ))} + + {/* Vine branches */} + {branches.map((branch, i) => ( +
+ ))} + + {/* Content area */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title with illuminated initial */} +
+ {/* Illuminated initial letter */} +
+ {/* Inner decorative border */} +
+ + {firstLetter} + +
+
+ + {/* Rest of title */} +
+ {restOfTitle} +
+
+ + {/* Decorative line under title */} +
+
+
+
+
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer */} +
+
+ Scribed by {meta.author} · {formattedDate} +
+
+ {meta.readingTime} min read +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v134-pop-art.tsx b/packages/app/src/app/blog/[slug]/og-variants/v134-pop-art.tsx new file mode 100644 index 0000000..052e614 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v134-pop-art.tsx @@ -0,0 +1,259 @@ +/** + * V134: Pop Art — Four-quadrant Warhol-inspired layout with hot pink, electric blue, lime, and yellow panels. Title repeated in different colors with halftone dots. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Truncate title for quadrants + const shortTitle = meta.title.length > 50 ? meta.title.slice(0, 47) + '...' : meta.title; + const quadrantTitleSize = shortTitle.length > 40 ? 24 : shortTitle.length > 25 ? 28 : 32; + + // Quadrant configs + const quadrants = [ + { bg: '#ff1493', textColor: '#ffd700', x: 0, y: 0, dotColor: '#ffffff' }, + { bg: '#00bfff', textColor: '#ff1493', x: 600, y: 0, dotColor: '#000000' }, + { bg: '#32cd32', textColor: '#00bfff', x: 0, y: 315, dotColor: '#ffffff' }, + { bg: '#ffd700', textColor: '#32cd32', x: 600, y: 315, dotColor: '#000000' }, + ]; + + // Halftone dot patterns for each corner + const halftonePositions = [ + // Top-left quadrant corner dots + [ + { x: 10, y: 10 }, + { x: 30, y: 10 }, + { x: 50, y: 10 }, + { x: 70, y: 10 }, + { x: 10, y: 30 }, + { x: 30, y: 30 }, + { x: 50, y: 30 }, + { x: 10, y: 50 }, + { x: 30, y: 50 }, + { x: 10, y: 70 }, + ], + // Top-right quadrant corner dots + [ + { x: 520, y: 10 }, + { x: 540, y: 10 }, + { x: 560, y: 10 }, + { x: 580, y: 10 }, + { x: 540, y: 30 }, + { x: 560, y: 30 }, + { x: 580, y: 30 }, + { x: 560, y: 50 }, + { x: 580, y: 50 }, + { x: 580, y: 70 }, + ], + // Bottom-left quadrant corner dots + [ + { x: 10, y: 245 }, + { x: 30, y: 245 }, + { x: 50, y: 245 }, + { x: 70, y: 245 }, + { x: 10, y: 265 }, + { x: 30, y: 265 }, + { x: 50, y: 265 }, + { x: 10, y: 285 }, + { x: 30, y: 285 }, + { x: 10, y: 305 }, + ], + // Bottom-right quadrant corner dots + [ + { x: 520, y: 245 }, + { x: 540, y: 245 }, + { x: 560, y: 245 }, + { x: 580, y: 245 }, + { x: 540, y: 265 }, + { x: 560, y: 265 }, + { x: 580, y: 265 }, + { x: 560, y: 285 }, + { x: 580, y: 285 }, + { x: 580, y: 305 }, + ], + ]; + + return new ImageResponse( +
+ {/* Four quadrants */} + {quadrants.map((q, qi) => ( +
+ {/* Halftone dots in corner */} + {halftonePositions[qi].map((dot, di) => ( +
+ ))} + + {/* Title in this quadrant */} +
+ {shortTitle} +
+ + {/* Spacer */} +
+ + {/* Quadrant label */} +
+ {qi === 0 + ? 'INFERENCEX' + : qi === 1 + ? meta.author.toUpperCase() + : qi === 2 + ? formattedDate.toUpperCase() + : `${meta.readingTime} MIN READ`} +
+
+ ))} + + {/* Thick black cross dividers */} + {/* Vertical divider */} +
+ {/* Horizontal divider */} +
+ + {/* Center logo badge */} +
+ +
+ + {/* Tags across the bottom center */} + {meta.tags && ( +
+ {meta.tags.slice(0, 4).map((tag, i) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v135-bauhaus.tsx b/packages/app/src/app/blog/[slug]/og-variants/v135-bauhaus.tsx new file mode 100644 index 0000000..2c34aa0 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v135-bauhaus.tsx @@ -0,0 +1,338 @@ +/** + * V135: Bauhaus — Stark white background with primary color geometric shapes (circle, square, triangle), heavy black typography, Dessau school aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + const excerpt = meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt; + + const red = '#e63946'; + const blue = '#1d3557'; + const yellow = '#f1c40f'; + const black = '#000000'; + const bg = '#f5f5f5'; + + return new ImageResponse( +
+ {/* Large red circle — behind and to the right */} +
+ + {/* Medium blue square — center-left area */} +
+ + {/* Yellow triangle — using CSS border trick */} +
+ + {/* Secondary smaller circle — accent */} +
+ + {/* Small black circle — accent */} +
+ + {/* Thin black horizontal line — Bauhaus compositional line */} +
+ + {/* Thin black vertical line */} +
+ + {/* Another compositional line — horizontal */} +
+ + {/* Small red square accent */} +
+ + {/* Small blue circle accent on bottom line */} +
+ + {/* Content */} +
+ {/* Logo row */} +
+ + + INFERENCEX + + + {/* Colored dots after name */} +
+
+
+
+
+
+ + {/* Spacing */} +
+ + {/* Title — heavy black typography */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {excerpt} +
+ + {/* Spacer */} +
+ + {/* Footer row */} +
+
+ {/* Red vertical bar accent */} +
+
+ {meta.author} +
+ {/* Blue dot separator */} +
+
{formattedDate}
+ {/* Yellow dot separator */} +
+
+ {meta.readingTime} min +
+
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag, i) => { + const tagColors = [red, blue, yellow]; + return ( +
+ {tag.toUpperCase()} +
+ ); + })} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v136-chalkboard.tsx b/packages/app/src/app/blog/[slug]/og-variants/v136-chalkboard.tsx new file mode 100644 index 0000000..ba10bc4 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v136-chalkboard.tsx @@ -0,0 +1,254 @@ +/** + * V136: Chalkboard — Dark green board with chalk dust dots, white chalk text, eraser, and dashed underlines. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + /* Chalk dust dots — scattered across the board */ + const dustDots = Array.from({ length: 40 }, (_, i) => ({ + left: ((i * 97 + 31) % 1160) + 20, + top: ((i * 53 + 17) % 590) + 20, + size: (i % 3) + 1, + opacity: 0.08 + (i % 5) * 0.03, + })); + + return new ImageResponse( +
+ {/* Chalk dust particles */} + {dustDots.map((dot, i) => ( +
+ ))} + + {/* Wooden frame — top */} +
+ + {/* Board content area */} +
+ {/* Top chalk line */} +
+ + {/* Date — written in chalk */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Title — chalk text */} +
+ + {meta.title} + + {/* Dashed underline under title */} +
+
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Bottom chalk line */} +
+ + {/* Author and reading time */} +
+ + {meta.author} · {meta.readingTime} min read + +
+ + {/* Eraser — bottom right */} +
+ {/* Eraser felt */} +
+ {/* Eraser body */} +
+ ERASER +
+
+ + {/* Logo — top right */} +
+ + + INFERENCEX + +
+
+ + {/* Wooden frame — bottom */} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v137-neon-sign.tsx b/packages/app/src/app/blog/[slug]/og-variants/v137-neon-sign.tsx new file mode 100644 index 0000000..7f9a006 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v137-neon-sign.tsx @@ -0,0 +1,305 @@ +/** + * V137: Neon Sign — Pure black bg with glowing neon tube text, double-line offset effect, and OPEN badge. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Subtle brick wall texture — rows of rectangles */} + {Array.from({ length: 8 }, (_, row) => + Array.from({ length: 12 }, (_, col) => ( +
+ )), + )} + + {/* Glow backdrop for title — large blurred area */} +
+ + {/* Secondary glow — cyan */} +
+ + {/* Neon title — back layer (offset for double-line effect) */} +
+ + {meta.title} + +
+ + {/* Neon title — front layer */} +
+ + {meta.title} + +
+ + {/* Neon underline bar */} +
+ {/* Glow behind underline */} +
+ + {/* Author and date — dim neon */} +
+ {meta.author} + + | + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* "OPEN" neon badge — top right */} +
+ {/* Glow behind badge */} +
+
+ + OPEN + +
+
+ + {/* Reading time — neon clock */} +
+ + {meta.readingTime} MIN READ + +
+ + {/* Logo — bottom left */} +
+ + + INFERENCEX + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v138-leather-book.tsx b/packages/app/src/app/blog/[slug]/og-variants/v138-leather-book.tsx new file mode 100644 index 0000000..9546b6e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v138-leather-book.tsx @@ -0,0 +1,295 @@ +/** + * V138: Leather Book — Rich dark brown embossed cover with gold double-line frame, diamond corners, and spine title. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Leather texture — subtle grain dots */} + {Array.from({ length: 60 }, (_, i) => ( +
+ ))} + + {/* Outer frame — gold */} +
+ + {/* Inner frame — gold with gap */} +
+ + {/* Corner diamonds — top-left */} +
+ {/* Corner diamond — top-right */} +
+ {/* Corner diamond — bottom-left */} +
+ {/* Corner diamond — bottom-right */} +
+ + {/* Spine line — left side */} +
+ + {/* Content area */} +
+ {/* Decorative top rule */} +
+
+
+
+
+ + {/* Publisher */} +
+ + INFERENCEX PUBLISHING + +
+ + {/* Title — centered like book cover */} +
+ + {meta.title} + +
+ + {/* Decorative middle rule */} +
+
+
+ + {/* Author */} +
+ + By {meta.author} + +
+ + {/* Date */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ + {tag} + +
+ ))} +
+ )} + + {/* Decorative bottom rule */} +
+
+
+
+
+
+ + {/* Logo — bottom center */} +
+ + + {meta.readingTime} MIN + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v139-shipping-label.tsx b/packages/app/src/app/blog/[slug]/og-variants/v139-shipping-label.tsx new file mode 100644 index 0000000..14f10db --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v139-shipping-label.tsx @@ -0,0 +1,257 @@ +/** + * V139: Shipping Label — Kraft brown package bg with white label, FRAGILE stamp, barcode lines, and tape strips. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + /* Barcode lines */ + const barcodeLines = Array.from({ length: 30 }, (_, i) => ({ + width: i % 3 === 0 ? 4 : 2, + marginRight: i % 4 === 0 ? 3 : 1, + })); + + return new ImageResponse( +
+ {/* Kraft paper texture dots */} + {Array.from({ length: 30 }, (_, i) => ( +
+ ))} + + {/* Tape strip — top */} +
+ + {/* Tape strip — bottom */} +
+ + {/* White label */} +
+ {/* FROM address */} +
+
+ FROM: +
+
+ + + InferenceX + + + · {meta.author} + +
+
+ + {/* Divider */} +
+ + {/* TO address */} +
+
+ TO: +
+
+ Reader +
+
+ + {/* Title — main content of label */} +
+ + {meta.title} + +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} + +
+ + {/* Date and reading time */} +
+ + Ship Date:{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + + | + + Weight: {meta.readingTime} min +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Barcode at bottom of label */} +
+ {barcodeLines.map((line, i) => ( +
+ ))} +
+ + {/* FRAGILE stamp — rotated via text positioning */} +
+ + FRAGILE + +
+ + {/* Handle with care */} +
+ + HANDLE WITH CARE + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v140-polaroid.tsx b/packages/app/src/app/blog/[slug]/og-variants/v140-polaroid.tsx new file mode 100644 index 0000000..2dbdb5b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v140-polaroid.tsx @@ -0,0 +1,215 @@ +/** + * V140: Polaroid — Medium grey bg with a centered polaroid frame, thick white border, title as photo, handwritten author. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Subtle surface texture */} + {Array.from({ length: 20 }, (_, i) => ( +
+ ))} + + {/* Shadow behind polaroid — offset darker div */} +
+ + {/* Polaroid frame — white border */} +
+ {/* Top border */} +
+ + {/* Left/Right borders wrap the photo area */} +
+ {/* Photo area — dark with title */} +
+ {/* Date — top left of photo */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Logo — top right of photo */} +
+ + + INFERENCEX + +
+ + {/* Title as the "photo" content */} +
+ + {meta.title} + +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 100 ? meta.excerpt.slice(0, 100) + '\u2026' : meta.excerpt} + +
+ + {/* Tags — bottom of photo */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Reading time — bottom right of photo */} +
+ + {meta.readingTime} min + +
+
+
+ + {/* Wide bottom margin — author "handwritten" */} +
+ + {meta.author} + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v141-cassette.tsx b/packages/app/src/app/blog/[slug]/og-variants/v141-cassette.tsx new file mode 100644 index 0000000..d40abdb --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v141-cassette.tsx @@ -0,0 +1,350 @@ +/** + * V141: Cassette Tape — Dark bg with cassette label design, tape reels, SIDE A, retro 80s/90s orange-brown aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Cassette shell body */} +
+ {/* Screw holes — four corners */} +
+
+
+
+ + {/* Main label area — center sticker */} +
+ {/* Label top row — SIDE A and record label */} +
+
+ + SIDE A + +
+
+ + + INFERENCEX RECORDS + +
+
+ + {/* Horizontal line */} +
+ + {/* Title — album/mix name */} +
+ + {meta.title} + +
+ + {/* Artist line */} +
+ Performed by {meta.author} +
+ + {/* Horizontal line */} +
+ + {/* Bottom row — date and duration */} +
+ + Recorded:{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + Duration: {meta.readingTime} min +
+ + {/* Tags row */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} +
+ + {/* Tape window area */} +
+ {/* Left reel */} +
+ {/* Inner hub */} +
+
+
+
+ + {/* Tape window — between reels */} +
+ {/* Tape lines visible through window */} + {Array.from({ length: 5 }, (_, i) => ( +
+ ))} + + NORMAL POSITION + +
+ + {/* Right reel */} +
+ {/* Inner hub */} +
+
+
+
+
+ + {/* Bottom edge detail */} +
+ + TYPE I · 60 MIN · HIGH OUTPUT + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v142-license-plate.tsx b/packages/app/src/app/blog/[slug]/og-variants/v142-license-plate.tsx new file mode 100644 index 0000000..aa2c470 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v142-license-plate.tsx @@ -0,0 +1,324 @@ +/** + * V142: License Plate — Dark bg with centered embossed-style plate, state label, registration stickers, reflective dot border. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + /* Truncate title for plate — plates have limited space */ + const plateText = meta.title.length > 35 ? meta.title.slice(0, 35) + '\u2026' : meta.title; + + /* Reflective border dots */ + const topDots = Array.from({ length: 44 }, (_, i) => i); + const sideDots = Array.from({ length: 20 }, (_, i) => i); + + return new ImageResponse( +
+ {/* License plate */} +
+ {/* Reflective dot border — top row */} +
+ {topDots.map((_, i) => ( +
+ ))} +
+ + {/* Reflective dot border — bottom row */} +
+ {topDots.map((_, i) => ( +
+ ))} +
+ + {/* Reflective dot border — left column */} +
+ {sideDots.map((_, i) => ( +
+ ))} +
+ + {/* Reflective dot border — right column */} +
+ {sideDots.map((_, i) => ( +
+ ))} +
+ + {/* Bolt holes */} +
+
+ + {/* State label — top */} +
+ + INFERENCEX + +
+ + {/* Subtitle — state motto style */} +
+ + THE BENCHMARK STATE + +
+ + {/* Main plate text — embossed title */} +
+ + {plateText} + +
+ + {/* Excerpt — smaller text below */} +
+ + {meta.excerpt.length > 100 ? meta.excerpt.slice(0, 100) + '\u2026' : meta.excerpt} + +
+ + {/* Bottom row — author, date */} +
+ {meta.author} + + · + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Registration sticker — top left corner */} +
+ + {meta.readingTime} + +
+ + {/* Registration sticker — top right corner */} +
+ + {meta.date.slice(0, 4)} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Logo — bottom center above tags */} +
+ +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v143-credit-card.tsx b/packages/app/src/app/blog/[slug]/og-variants/v143-credit-card.tsx new file mode 100644 index 0000000..9d49f57 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v143-credit-card.tsx @@ -0,0 +1,328 @@ +/** + * V143: Credit Card — Dark bg with centered credit card, chip, holographic stripe, card number-style reading time. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + /* Card number formatting from reading time */ + const cardNumber = `${String(meta.readingTime).padStart(4, '0')} XXXX XXXX ${meta.date.replace(/-/g, '').slice(4)}`; + + return new ImageResponse( +
+ {/* Ambient glow behind card */} +
+ + {/* Credit card body */} +
+ {/* Top row — bank logo and card network */} +
+
+ + + INFERENCEX + +
+
+ + PLATINUM + +
+
+ + {/* Chip */} +
+ {/* Chip internal lines */} +
+
+
+
+ + {/* Card number */} +
+ + {cardNumber} + +
+ + {/* Title as cardholder name */} +
+ + {meta.title} + +
+ + {/* Cardholder and expiry row */} +
+
+ + CARDHOLDER NAME + + + {meta.author.toUpperCase()} + +
+
+ + VALID THRU + + + {meta.date.slice(5, 7)}/{meta.date.slice(2, 4)} + +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Holographic stripe — colored bar at bottom */} +
+
+
+
+
+
+
+ + {/* Date formatted */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Contactless symbol — three arcs approximated */} +
+
+
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v144-prescription.tsx b/packages/app/src/app/blog/[slug]/og-variants/v144-prescription.tsx new file mode 100644 index 0000000..950abeb --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v144-prescription.tsx @@ -0,0 +1,274 @@ +/** + * V144: Prescription — White bg with pharmaceutical Rx label layout, large Rx symbol, warning stripes, dosage instructions. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Prescription label container */} +
+ {/* Pharmacy header bar */} +
+
+ + + INFERENCEX PHARMACY + +
+
+ + RX# {meta.date.replace(/-/g, '')} + +
+
+ + {/* Main label body */} +
+ {/* Rx symbol — large, top left */} +
+ + Rx + +
+ + {/* Patient and doctor info row */} +
+
+ + PRESCRIBED BY + + + Dr. {meta.author} + +
+
+ + FILL DATE + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ + {/* Divider */} +
+ + {/* Medication name — title */} +
+ + {meta.title} + +
+ + {/* Dosage instructions */} +
+ + TAKE {meta.readingTime} MIN DAILY + + + — Read with focus, no distractions + +
+ + {/* Description / excerpt */} +
+ + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} + + {/* Refills remaining */} +
+ REFILLS: UNLIMITED +
+
+ + {/* Warning stripes at bottom */} +
+ {Array.from({ length: 20 }, (_, i) => ( +
+ ))} +
+ + {/* Auxiliary warning label overlay */} +
+ + CAUTION: May cause deep thinking + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v145-billboard.tsx b/packages/app/src/app/blog/[slug]/og-variants/v145-billboard.tsx new file mode 100644 index 0000000..e814a56 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v145-billboard.tsx @@ -0,0 +1,325 @@ +/** + * V145: Billboard — Dark sky bg with green highway sign, white border, exit number, arrow, reflective dot pattern. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + /* Reflective dots on the sign surface */ + const reflectiveDots = Array.from({ length: 50 }, (_, i) => ({ + left: ((i * 79 + 13) % 900) + 40, + top: ((i * 43 + 29) % 350) + 40, + opacity: 0.04 + (i % 4) * 0.02, + })); + + return new ImageResponse( +
+ {/* Stars in the night sky */} + {Array.from({ length: 15 }, (_, i) => ( +
+ ))} + + {/* Sign support posts — left */} +
+ {/* Sign support posts — right */} +
+ + {/* Highway sign */} +
+ {/* Reflective dot pattern on sign surface */} + {reflectiveDots.map((dot, i) => ( +
+ ))} + + {/* Top row — Interstate shield and route info */} +
+ {/* Interstate shield shape approximation */} +
+ IX +
+ +
+ + INFERENCEX HIGHWAY + +
+ + {/* Logo — top right */} +
+ +
+
+ + {/* Divider line */} +
+ + {/* Main destination — title */} +
+ + {meta.title} + +
+ + {/* Excerpt as secondary destination */} +
+ + {meta.excerpt.length > 110 ? meta.excerpt.slice(0, 110) + '\u2026' : meta.excerpt} + +
+ + {/* Bottom row — EXIT info and arrow */} +
+ {/* Exit badge */} +
+
+ + EXIT {meta.readingTime} + +
+ {meta.author} + + | + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Arrow pointing right — constructed from rectangles */} +
+ {/* Arrow shaft */} +
+ {/* Arrow head — stacked bars narrowing */} +
+
+
+
+
+
+
+
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v146-dna-helix.tsx b/packages/app/src/app/blog/[slug]/og-variants/v146-dna-helix.tsx new file mode 100644 index 0000000..852002d --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v146-dna-helix.tsx @@ -0,0 +1,221 @@ +/** + * V146: DNA Helix — Dark bg with two intertwined strands of positioned dots + * connected by horizontal rungs, alternating cyan and magenta. Bio-tech aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0c14'; +const CYAN = '#00d4ff'; +const MAGENTA = '#ff00aa'; +const RUNG = '#ffffff12'; + +// Generate helix dots: two strands oscillating in opposition +function makeHelixDots(): { + dots: { x: number; y: number; color: string; size: number }[]; + rungs: { x1: number; x2: number; y: number }[]; +} { + const dots: { x: number; y: number; color: string; size: number }[] = []; + const rungs: { x1: number; x2: number; y: number }[] = []; + const centerX = 160; + const amplitude = 60; + const steps = 22; + + for (let i = 0; i < steps; i++) { + const y = 20 + (i / steps) * 600; + const phase = (i / steps) * Math.PI * 4; + const x1 = centerX + Math.sin(phase) * amplitude; + const x2 = centerX + Math.sin(phase + Math.PI) * amplitude; + + dots.push({ x: x1, y, color: i % 2 === 0 ? CYAN : MAGENTA, size: 8 }); + dots.push({ x: x2, y, color: i % 2 === 0 ? MAGENTA : CYAN, size: 8 }); + + // Rungs between the two strands + if (i % 2 === 0) { + rungs.push({ x1: Math.min(x1, x2), x2: Math.max(x1, x2), y }); + } + } + + return { dots, rungs }; +} + +const helix = makeHelixDots(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Helix rungs */} + {helix.rungs.map((r, i) => ( +
+ ))} + + {/* Helix dots */} + {helix.dots.map((d, i) => ( +
+ ))} + + {/* Strand connecting lines — strand 1 */} + {helix.dots + .filter((_, i) => i % 2 === 0) + .map((d, i, arr) => { + if (i === arr.length - 1) return null; + const next = arr[i + 1]; + const dx = next.x - d.x; + const dy = next.y - d.y; + const _len = Math.sqrt(dx * dx + dy * dy); + return ( +
+ ); + })} + + {/* Faint glow spots behind helix */} +
+
+ + {/* Content side */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
{meta.title}
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v147-star-chart.tsx b/packages/app/src/app/blog/[slug]/og-variants/v147-star-chart.tsx new file mode 100644 index 0000000..8d21a12 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v147-star-chart.tsx @@ -0,0 +1,262 @@ +/** + * V147: Star Chart — Very dark navy bg with scattered stars of varying sizes, + * constellation lines, a large planet/moon, and coordinate grid. Astronomy aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#050510'; +const STAR_DIM = '#ffffff30'; +const STAR_MID = '#ffffff60'; +const STAR_BRIGHT = '#ffffffcc'; +const GRID_LINE = '#ffffff08'; +const CONSTELLATION_LINE = '#4488ff25'; + +// Stars scattered across the sky +const stars: { x: number; y: number; size: number; brightness: string }[] = [ + { x: 80, y: 50, size: 2, brightness: STAR_DIM }, + { x: 200, y: 90, size: 3, brightness: STAR_MID }, + { x: 340, y: 30, size: 1, brightness: STAR_DIM }, + { x: 450, y: 120, size: 4, brightness: STAR_BRIGHT }, + { x: 560, y: 60, size: 2, brightness: STAR_MID }, + { x: 700, y: 40, size: 1, brightness: STAR_DIM }, + { x: 820, y: 100, size: 3, brightness: STAR_BRIGHT }, + { x: 950, y: 55, size: 2, brightness: STAR_MID }, + { x: 1050, y: 130, size: 1, brightness: STAR_DIM }, + { x: 130, y: 200, size: 2, brightness: STAR_MID }, + { x: 280, y: 250, size: 3, brightness: STAR_BRIGHT }, + { x: 420, y: 210, size: 1, brightness: STAR_DIM }, + { x: 600, y: 180, size: 2, brightness: STAR_MID }, + { x: 750, y: 230, size: 4, brightness: STAR_BRIGHT }, + { x: 880, y: 200, size: 1, brightness: STAR_DIM }, + { x: 1100, y: 220, size: 2, brightness: STAR_MID }, + { x: 150, y: 350, size: 1, brightness: STAR_DIM }, + { x: 300, y: 400, size: 2, brightness: STAR_MID }, + { x: 500, y: 370, size: 1, brightness: STAR_DIM }, + { x: 680, y: 420, size: 3, brightness: STAR_MID }, + { x: 850, y: 360, size: 1, brightness: STAR_DIM }, + { x: 1000, y: 390, size: 2, brightness: STAR_MID }, + { x: 90, y: 500, size: 2, brightness: STAR_DIM }, + { x: 400, y: 530, size: 1, brightness: STAR_DIM }, + { x: 620, y: 560, size: 2, brightness: STAR_MID }, + { x: 780, y: 510, size: 1, brightness: STAR_DIM }, + { x: 1080, y: 480, size: 3, brightness: STAR_MID }, +]; + +// Constellation — connected star positions +const constellation = [ + { x: 450, y: 120 }, + { x: 600, y: 180 }, + { x: 750, y: 230 }, + { x: 820, y: 100 }, + { x: 950, y: 55 }, +]; + +// Grid lines for coordinate system +const gridLinesH = [0, 105, 210, 315, 420, 525, 630]; +const gridLinesV = [0, 150, 300, 450, 600, 750, 900, 1050, 1200]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Grid lines — horizontal */} + {gridLinesH.map((y, i) => ( +
+ ))} + + {/* Grid lines — vertical */} + {gridLinesV.map((x, i) => ( +
+ ))} + + {/* Planet/Moon in top-right corner */} +
+ {/* Planet inner highlight */} +
+ + {/* Stars */} + {stars.map((s, i) => ( +
+ ))} + + {/* Constellation lines */} + {constellation.slice(0, -1).map((star, i) => { + const next = constellation[i + 1]; + const minX = Math.min(star.x, next.x); + const minY = Math.min(star.y, next.y); + const w = Math.abs(next.x - star.x) || 1; + const _h = Math.abs(next.y - star.y) || 1; + // Approximate line with a thin div (diagonal won't be perfect but suggests a connection) + return ( +
+ ); + })} + + {/* Coordinate labels */} +
+ RA 00h 00m +
+
+ DEC +90° 00' +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ CATALOG ENTRY +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v148-weather-map.tsx b/packages/app/src/app/blog/[slug]/og-variants/v148-weather-map.tsx new file mode 100644 index 0000000..1839ce1 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v148-weather-map.tsx @@ -0,0 +1,266 @@ +/** + * V148: Weather Map — Dark bg with concentric radar sweep circles in green, + * scattered blips, military/meteorological data readout aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a100a'; +const GREEN = '#00ff00'; +const GREEN_DIM = '#00ff0015'; +const GREEN_MID = '#00ff0025'; +const GREEN_TEXT = '#00cc00'; + +// Radar center +const CX = 200; +const CY = 315; + +// Concentric rings +const rings = [60, 120, 180, 240, 300]; + +// Blips on the radar +const blips: { angle: number; ring: number; size: number; opacity: number }[] = [ + { angle: 0.4, ring: 80, size: 6, opacity: 0.9 }, + { angle: 1.2, ring: 150, size: 4, opacity: 0.7 }, + { angle: 2.0, ring: 200, size: 5, opacity: 0.8 }, + { angle: 2.8, ring: 110, size: 3, opacity: 0.6 }, + { angle: 3.5, ring: 250, size: 7, opacity: 0.9 }, + { angle: 4.2, ring: 170, size: 4, opacity: 0.5 }, + { angle: 5.0, ring: 90, size: 5, opacity: 0.7 }, + { angle: 5.8, ring: 220, size: 6, opacity: 0.8 }, + { angle: 0.8, ring: 280, size: 4, opacity: 0.6 }, + { angle: 1.8, ring: 60, size: 3, opacity: 0.5 }, +]; + +// Cross-hair lines through center +const crosshairLen = 310; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top status bar */} +
+ STATION: INFERENCEX + LAT 37.7749 + LON -122.4194 + FREQ 5.625 GHz + MODE: ACTIVE SCAN +
+ + {/* Radar concentric rings */} + {rings.map((r, i) => ( +
+ ))} + + {/* Crosshair — horizontal */} +
+ {/* Crosshair — vertical */} +
+ + {/* Center dot */} +
+ + {/* Blips */} + {blips.map((b, i) => { + const bx = CX + Math.cos(b.angle) * b.ring; + const by = CY + Math.sin(b.angle) * b.ring; + return ( +
+ ); + })} + + {/* Range labels */} + {rings.map((r, i) => ( +
+ {r}nm +
+ ))} + + {/* Content area — right side */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ ADVISORY BULLETIN +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+ + {/* Bottom status */} +
+ TARGETS: {blips.length} DETECTED | SWEEP: 360° | SIGNAL: NOMINAL +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v149-periodic-element.tsx b/packages/app/src/app/blog/[slug]/og-variants/v149-periodic-element.tsx new file mode 100644 index 0000000..525ff08 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v149-periodic-element.tsx @@ -0,0 +1,260 @@ +/** + * V149: Periodic Element — Dark bg with a large centered periodic-table-style + * element cell. Reading time as atomic number, title initials as symbol. + * Surrounded by subtle grid lines suggesting the periodic table. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#08080e'; +const CELL_BG = '#10121c'; +const CELL_BORDER = '#2a3050'; +const ACCENT = '#5b8def'; +const GRID = '#ffffff06'; + +// Generate the "element symbol" from the title +function getSymbol(title: string): string { + const words = title.split(/\s+/).filter((w) => w.length > 0); + if (words.length >= 2) { + return (words[0][0] + words[1][0]).toUpperCase(); + } + return title.slice(0, 2).toUpperCase(); +} + +// Subtle grid lines around the main cell +const hLines = [0, 70, 140, 210, 280, 350, 420, 490, 560, 630]; +const vLines = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const symbol = getSymbol(meta.title); + + return new ImageResponse( +
+ {/* Background grid — horizontal */} + {hLines.map((y, i) => ( +
+ ))} + + {/* Background grid — vertical */} + {vLines.map((x, i) => ( +
+ ))} + + {/* Small ghost cells in background */} + {[ + { x: 50, y: 70 }, + { x: 150, y: 70 }, + { x: 1050, y: 70 }, + { x: 50, y: 140 }, + { x: 1050, y: 140 }, + { x: 950, y: 140 }, + { x: 50, y: 490 }, + { x: 150, y: 490 }, + { x: 250, y: 490 }, + { x: 950, y: 490 }, + { x: 1050, y: 490 }, + ].map((c, i) => ( +
+ ))} + + {/* Logo — top left */} +
+ +
+ + {/* Main element cell */} +
+ {/* Atomic number (reading time) */} +
+ {meta.readingTime} +
+ + {/* Element symbol */} +
+ {symbol} +
+ + {/* Element name (title, truncated) */} +
+ {meta.title.length > 30 ? meta.title.slice(0, 30) + '\u2026' : meta.title} +
+ + {/* Discovered by */} +
+ Discovered by {meta.author} +
+
+ + {/* Full title below cell */} +
+
+ {meta.title} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v150-microscope.tsx b/packages/app/src/app/blog/[slug]/og-variants/v150-microscope.tsx new file mode 100644 index 0000000..5acad55 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v150-microscope.tsx @@ -0,0 +1,271 @@ +/** + * V150: Microscope — Dark bg with a centered microscope slide and circular + * lens/petri dish view. Scale bar, magnification text, and lab labels. + * Scientific/clinical feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0c0c12'; +const SLIDE_BG = '#12141e'; +const LENS_BORDER = '#3a4060'; +const LABEL_COLOR = '#505878'; +const ACCENT = '#40c8a0'; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 36 : meta.title.length > 40 ? 42 : 48; + + return new ImageResponse( +
+ {/* Logo — top left */} +
+ +
+ + {/* Lab label — top right */} +
+ + SPECIMEN #{meta.readingTime.toString().padStart(4, '0')} + + | + PREPARED BY: {meta.author.toUpperCase()} +
+ + {/* Microscope slide — large rounded rectangle */} +
+ {/* Slide label — top left corner of slide */} +
+ SLIDE A-{meta.readingTime} +
+ + {/* Circular lens view */} +
+ {/* Inner subtle ring */} +
+ + {/* Specimen dots inside the lens (simulated microorganisms) */} + {[ + { x: 80, y: 90, s: 8 }, + { x: 180, y: 70, s: 5 }, + { x: 240, y: 140, s: 7 }, + { x: 100, y: 200, s: 6 }, + { x: 200, y: 230, s: 4 }, + { x: 140, y: 150, s: 9 }, + { x: 220, y: 190, s: 5 }, + ].map((dot, i) => ( +
+ ))} + + {/* Content inside the lens */} +
+
+ {meta.title.length > 50 ? meta.title.slice(0, 50) + '\u2026' : meta.title} +
+
+
+ + {/* Scale bar — bottom of slide */} +
+
+ 50 \u00b5m +
+ + {/* Magnification */} +
+ 100x +
+
+ + {/* Excerpt below slide */} +
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v151-seismograph.tsx b/packages/app/src/app/blog/[slug]/og-variants/v151-seismograph.tsx new file mode 100644 index 0000000..3a9a186 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v151-seismograph.tsx @@ -0,0 +1,263 @@ +/** + * V151: Seismograph — Dark bg with horizontal graph-paper lines, a jagged + * seismograph waveform across the middle, date/time stamps, and red alert + * sections at peak amplitudes. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0a0e'; +const GRAPH_LINE = '#ffffff08'; +const WAVE_COLOR = '#40ff80'; +const RED_ALERT = '#ff3030'; +const MUTED = '#506060'; + +// Seismograph waveform — a series of (x, y) positions relative to center line +// Center Y of the waveform is at y=315 (middle of canvas) +const CENTER_Y = 290; +const WAVE_POINTS: { x: number; y: number }[] = []; +const ALERT_ZONES: { x: number; width: number }[] = []; + +// Generate waveform data +(() => { + const segments = 80; + for (let i = 0; i <= segments; i++) { + const x = (i / segments) * 1200; + const t = i / segments; + // Create varying amplitude — calm, then burst, calm, burst pattern + let amplitude = 8; + if (t > 0.2 && t < 0.35) amplitude = 60 + Math.sin(t * 40) * 30; + if (t > 0.55 && t < 0.75) amplitude = 80 + Math.sin(t * 50) * 40; + if (t > 0.85 && t < 0.92) amplitude = 45 + Math.sin(t * 35) * 20; + + const noise = Math.sin(i * 2.7) * amplitude * 0.8 + Math.sin(i * 5.3) * amplitude * 0.3; + WAVE_POINTS.push({ x, y: CENTER_Y + noise }); + } + + // Alert zones where amplitude is high + ALERT_ZONES.push({ x: 240, width: 180 }); + ALERT_ZONES.push({ x: 660, width: 240 }); + ALERT_ZONES.push({ x: 1020, width: 84 }); +})(); + +// Horizontal graph paper lines +const hLines: number[] = []; +for (let y = 0; y <= 630; y += 30) hLines.push(y); + +// Vertical graph paper lines +const vLines: number[] = []; +for (let x = 0; x <= 1200; x += 60) vLines.push(x); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 44 : meta.title.length > 40 ? 52 : 58; + + return new ImageResponse( +
+ {/* Graph paper — horizontal lines */} + {hLines.map((y, i) => ( +
+ ))} + + {/* Graph paper — vertical lines */} + {vLines.map((x, i) => ( +
+ ))} + + {/* Alert zone backgrounds */} + {ALERT_ZONES.map((zone, i) => ( +
+ ))} + + {/* Center baseline */} +
+ + {/* Waveform segments — rendered as small positioned divs */} + {WAVE_POINTS.slice(0, -1).map((pt, i) => { + const next = WAVE_POINTS[i + 1]; + const minY = Math.min(pt.y, next.y); + const h = Math.abs(next.y - pt.y) || 1; + const isAlert = ALERT_ZONES.some((z) => pt.x >= z.x && pt.x <= z.x + z.width) && h > 15; + return ( +
+ ); + })} + + {/* Time stamps */} +
+ {meta.date} 00:00:00 UTC +
+
+ CHANNEL: A1 | GAIN: AUTO | RATE: 100 sps +
+ + {/* Magnitude labels */} +
+ +2.0 +
+
+ -2.0 +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v152-aurora.tsx b/packages/app/src/app/blog/[slug]/og-variants/v152-aurora.tsx new file mode 100644 index 0000000..e5d2879 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v152-aurora.tsx @@ -0,0 +1,225 @@ +/** + * V152: Aurora — Very dark bg with layered horizontal bands of green, purple, + * and blue at low opacity creating a northern lights curtain effect. Scattered + * star dots. Content at bottom. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#050812'; + +// Aurora curtain bands — positioned at the top, varying widths and colors +const auroraBands: { + left: number; + top: number; + width: number; + height: number; + color: string; +}[] = [ + // Green layer + { left: 0, top: 20, width: 400, height: 120, color: '#00ff8010' }, + { left: 200, top: 40, width: 350, height: 140, color: '#00ff8018' }, + { left: 500, top: 10, width: 300, height: 100, color: '#00ff800c' }, + { left: 750, top: 30, width: 450, height: 130, color: '#00ff8014' }, + // Purple layer + { left: 100, top: 60, width: 380, height: 110, color: '#8b00ff12' }, + { left: 400, top: 50, width: 300, height: 130, color: '#8b00ff0e' }, + { left: 700, top: 70, width: 350, height: 100, color: '#8b00ff10' }, + { left: 950, top: 40, width: 250, height: 120, color: '#8b00ff0c' }, + // Blue layer + { left: 50, top: 100, width: 320, height: 90, color: '#0066ff0e' }, + { left: 300, top: 90, width: 400, height: 110, color: '#0066ff12' }, + { left: 650, top: 110, width: 300, height: 80, color: '#0066ff0a' }, + { left: 900, top: 80, width: 300, height: 100, color: '#0066ff10' }, +]; + +// Stars scattered across the sky +const auroraStars: { x: number; y: number; size: number; opacity: number }[] = [ + { x: 60, y: 30, size: 2, opacity: 0.4 }, + { x: 180, y: 80, size: 1, opacity: 0.3 }, + { x: 300, y: 20, size: 3, opacity: 0.6 }, + { x: 420, y: 60, size: 1, opacity: 0.25 }, + { x: 540, y: 40, size: 2, opacity: 0.5 }, + { x: 650, y: 90, size: 1, opacity: 0.3 }, + { x: 770, y: 25, size: 2, opacity: 0.45 }, + { x: 880, y: 70, size: 3, opacity: 0.55 }, + { x: 1000, y: 35, size: 1, opacity: 0.3 }, + { x: 1100, y: 85, size: 2, opacity: 0.4 }, + { x: 120, y: 150, size: 1, opacity: 0.2 }, + { x: 350, y: 170, size: 2, opacity: 0.35 }, + { x: 580, y: 140, size: 1, opacity: 0.2 }, + { x: 810, y: 160, size: 2, opacity: 0.3 }, + { x: 1040, y: 145, size: 1, opacity: 0.25 }, + { x: 250, y: 210, size: 1, opacity: 0.15 }, + { x: 700, y: 220, size: 2, opacity: 0.2 }, + { x: 950, y: 200, size: 1, opacity: 0.15 }, + { x: 450, y: 250, size: 1, opacity: 0.1 }, + { x: 1100, y: 240, size: 1, opacity: 0.12 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Aurora bands */} + {auroraBands.map((band, i) => ( +
+ ))} + + {/* Stars */} + {auroraStars.map((s, i) => ( +
+ ))} + + {/* Horizon glow */} +
+ + {/* Silhouette line at bottom (treeline/mountains) */} + {[ + { left: 0, width: 160, height: 35 }, + { left: 130, width: 120, height: 50 }, + { left: 220, width: 180, height: 30 }, + { left: 370, width: 140, height: 45 }, + { left: 480, width: 200, height: 28 }, + { left: 650, width: 130, height: 42 }, + { left: 750, width: 180, height: 35 }, + { left: 900, width: 150, height: 48 }, + { left: 1020, width: 180, height: 32 }, + ].map((tree, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v153-coral-reef.tsx b/packages/app/src/app/blog/[slug]/og-variants/v153-coral-reef.tsx new file mode 100644 index 0000000..fce51f8 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v153-coral-reef.tsx @@ -0,0 +1,238 @@ +/** + * V153: Coral Reef — Deep ocean blue bg with organic coral shapes at the bottom + * (rounded rectangles and circles in coral colors) and bubbles floating upward. + * Underwater/marine biology aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#061425'; +const PINK = '#ff7eb3'; +const ORANGE = '#ff6b35'; +const YELLOW = '#ffd700'; +const TEAL = '#20b2aa'; + +// Coral formations at the bottom +const corals: { + left: number; + bottom: number; + width: number; + height: number; + color: string; + radius: number; +}[] = [ + // Base layer — wide flat corals + { left: -20, bottom: -10, width: 180, height: 80, color: `${PINK}30`, radius: 40 }, + { left: 140, bottom: -5, width: 120, height: 100, color: `${ORANGE}35`, radius: 50 }, + { left: 240, bottom: -8, width: 160, height: 70, color: `${TEAL}28`, radius: 35 }, + { left: 380, bottom: -10, width: 140, height: 90, color: `${PINK}25`, radius: 45 }, + { left: 500, bottom: -5, width: 100, height: 110, color: `${YELLOW}22`, radius: 50 }, + { left: 580, bottom: -8, width: 180, height: 75, color: `${ORANGE}28`, radius: 38 }, + { left: 740, bottom: -10, width: 130, height: 95, color: `${TEAL}30`, radius: 48 }, + { left: 850, bottom: -5, width: 160, height: 80, color: `${PINK}25`, radius: 40 }, + { left: 990, bottom: -8, width: 120, height: 100, color: `${YELLOW}28`, radius: 50 }, + { left: 1090, bottom: -10, width: 130, height: 85, color: `${ORANGE}30`, radius: 42 }, + // Second layer — taller coral pillars + { left: 60, bottom: 60, width: 60, height: 80, color: `${PINK}20`, radius: 30 }, + { left: 200, bottom: 55, width: 50, height: 100, color: `${TEAL}22`, radius: 25 }, + { left: 340, bottom: 50, width: 70, height: 70, color: `${ORANGE}18`, radius: 35 }, + { left: 520, bottom: 65, width: 55, height: 90, color: `${YELLOW}20`, radius: 28 }, + { left: 680, bottom: 55, width: 65, height: 75, color: `${PINK}22`, radius: 32 }, + { left: 820, bottom: 60, width: 50, height: 85, color: `${TEAL}18`, radius: 25 }, + { left: 960, bottom: 50, width: 60, height: 70, color: `${ORANGE}20`, radius: 30 }, + // Top spherical corals (brain coral look) + { left: 120, bottom: 120, width: 40, height: 40, color: `${PINK}15`, radius: 20 }, + { left: 450, bottom: 130, width: 35, height: 35, color: `${YELLOW}15`, radius: 18 }, + { left: 750, bottom: 115, width: 45, height: 45, color: `${TEAL}15`, radius: 22 }, + { left: 1020, bottom: 125, width: 38, height: 38, color: `${ORANGE}15`, radius: 19 }, +]; + +// Bubbles floating upward +const bubbles: { x: number; y: number; size: number; opacity: number }[] = [ + { x: 100, y: 380, size: 12, opacity: 0.15 }, + { x: 250, y: 300, size: 8, opacity: 0.12 }, + { x: 380, y: 350, size: 10, opacity: 0.1 }, + { x: 520, y: 280, size: 6, opacity: 0.08 }, + { x: 650, y: 320, size: 14, opacity: 0.12 }, + { x: 780, y: 260, size: 8, opacity: 0.1 }, + { x: 900, y: 340, size: 10, opacity: 0.14 }, + { x: 1050, y: 290, size: 7, opacity: 0.09 }, + { x: 180, y: 200, size: 5, opacity: 0.06 }, + { x: 450, y: 180, size: 8, opacity: 0.08 }, + { x: 700, y: 160, size: 6, opacity: 0.06 }, + { x: 950, y: 190, size: 9, opacity: 0.07 }, + { x: 320, y: 420, size: 11, opacity: 0.16 }, + { x: 850, y: 400, size: 9, opacity: 0.13 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Water depth gradient (layered bands) */} +
+ + {/* Coral formations */} + {corals.map((c, i) => ( +
+ ))} + + {/* Bubbles */} + {bubbles.map((b, i) => ( +
+ ))} + + {/* Caustic light streaks from surface */} + {[ + { left: 100, width: 2, opacity: 0.03 }, + { left: 350, width: 3, opacity: 0.04 }, + { left: 600, width: 2, opacity: 0.03 }, + { left: 850, width: 3, opacity: 0.04 }, + { left: 1050, width: 2, opacity: 0.03 }, + ].map((ray, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v154-crystal.tsx b/packages/app/src/app/blog/[slug]/og-variants/v154-crystal.tsx new file mode 100644 index 0000000..fdca8c3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v154-crystal.tsx @@ -0,0 +1,271 @@ +/** + * V154: Crystal Formation — Very dark bg with geometric crystal shard shapes + * (long thin rectangles at various angles suggesting crystal clusters) in + * amethyst purple and ice blue. Mineral specimen aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0a14'; +const AMETHYST = '#9966cc'; +const ICE_BLUE = '#b8d4e3'; +const CRYSTAL_WHITE = '#ffffff'; + +// Crystal shards — each is a positioned rectangle suggesting a crystal face +// Clustered from the bottom-left corner +const crystals: { + left: number; + top: number; + width: number; + height: number; + color: string; + opacity: number; + borderOnly: boolean; +}[] = [ + // Main large shards + { left: 30, top: 180, width: 14, height: 260, color: AMETHYST, opacity: 0.25, borderOnly: false }, + { left: 55, top: 140, width: 10, height: 320, color: ICE_BLUE, opacity: 0.15, borderOnly: true }, + { left: 80, top: 220, width: 18, height: 240, color: AMETHYST, opacity: 0.3, borderOnly: false }, + { + left: 110, + top: 160, + width: 8, + height: 300, + color: CRYSTAL_WHITE, + opacity: 0.08, + borderOnly: true, + }, + { left: 130, top: 250, width: 16, height: 210, color: AMETHYST, opacity: 0.2, borderOnly: false }, + { + left: 160, + top: 200, + width: 12, + height: 280, + color: ICE_BLUE, + opacity: 0.18, + borderOnly: false, + }, + // Angled shards (horizontal-ish, suggesting cross-growth) + { left: 20, top: 380, width: 140, height: 6, color: AMETHYST, opacity: 0.15, borderOnly: false }, + { left: 40, top: 340, width: 100, height: 4, color: ICE_BLUE, opacity: 0.1, borderOnly: true }, + { + left: 60, + top: 420, + width: 120, + height: 8, + color: CRYSTAL_WHITE, + opacity: 0.06, + borderOnly: true, + }, + // Secondary cluster + { + left: 190, + top: 300, + width: 10, + height: 180, + color: AMETHYST, + opacity: 0.18, + borderOnly: false, + }, + { left: 210, top: 280, width: 6, height: 200, color: ICE_BLUE, opacity: 0.12, borderOnly: true }, + { + left: 230, + top: 340, + width: 14, + height: 150, + color: AMETHYST, + opacity: 0.22, + borderOnly: false, + }, + // Small accent crystals + { left: 250, top: 380, width: 8, height: 120, color: ICE_BLUE, opacity: 0.1, borderOnly: false }, + { left: 270, top: 400, width: 6, height: 100, color: AMETHYST, opacity: 0.15, borderOnly: false }, + // Top-left smaller crystals (scattered) + { + left: 50, + top: 100, + width: 4, + height: 60, + color: CRYSTAL_WHITE, + opacity: 0.05, + borderOnly: true, + }, + { left: 100, top: 80, width: 6, height: 70, color: AMETHYST, opacity: 0.08, borderOnly: false }, + // A few crystals on the right edge for balance + { left: 1120, top: 500, width: 8, height: 80, color: AMETHYST, opacity: 0.08, borderOnly: false }, + { left: 1140, top: 520, width: 6, height: 70, color: ICE_BLUE, opacity: 0.06, borderOnly: true }, + { + left: 1160, + top: 480, + width: 4, + height: 100, + color: CRYSTAL_WHITE, + opacity: 0.04, + borderOnly: true, + }, +]; + +// Crystal "facet" highlights — small diamond-shaped dots at tips +const facets: { x: number; y: number; size: number; color: string }[] = [ + { x: 37, y: 180, size: 6, color: `${AMETHYST}40` }, + { x: 60, y: 140, size: 4, color: `${ICE_BLUE}30` }, + { x: 89, y: 220, size: 7, color: `${AMETHYST}50` }, + { x: 138, y: 250, size: 5, color: `${AMETHYST}35` }, + { x: 166, y: 200, size: 6, color: `${ICE_BLUE}40` }, + { x: 195, y: 300, size: 4, color: `${AMETHYST}30` }, + { x: 237, y: 340, size: 5, color: `${AMETHYST}45` }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Crystal shards */} + {crystals.map((c, i) => ( +
+ ))} + + {/* Crystal facet highlights */} + {facets.map((f, i) => ( +
+ ))} + + {/* Glow behind crystal cluster */} +
+ + {/* Content — right side */} +
+ {/* Logo */} +
+ +
+ + {/* Specimen label */} +
+
+ MINERAL SPECIMEN +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v155-telescope.tsx b/packages/app/src/app/blog/[slug]/og-variants/v155-telescope.tsx new file mode 100644 index 0000000..201034a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v155-telescope.tsx @@ -0,0 +1,344 @@ +/** + * V155: Telescope — Dark bg with a large centered circle (telescope eyepiece view). + * Content inside the circle, pure black outside, thin crosshair lines through center, + * and coordinate numbers at intersections. Observatory feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#000000'; +const EYEPIECE_BG = '#080810'; +const CROSSHAIR = '#ffffff18'; +const COORD_COLOR = '#ffffff20'; +const ACCENT = '#6699dd'; + +// Stars visible through the telescope +const telescopeStars: { x: number; y: number; size: number; opacity: number }[] = [ + { x: 460, y: 180, size: 2, opacity: 0.4 }, + { x: 580, y: 150, size: 1, opacity: 0.3 }, + { x: 700, y: 200, size: 3, opacity: 0.5 }, + { x: 520, y: 250, size: 1, opacity: 0.25 }, + { x: 650, y: 280, size: 2, opacity: 0.35 }, + { x: 750, y: 160, size: 1, opacity: 0.3 }, + { x: 490, y: 380, size: 2, opacity: 0.4 }, + { x: 620, y: 420, size: 1, opacity: 0.25 }, + { x: 730, y: 370, size: 2, opacity: 0.35 }, + { x: 550, y: 440, size: 1, opacity: 0.2 }, + { x: 680, y: 460, size: 2, opacity: 0.3 }, +]; + +// Eyepiece circle center and radius +const CX = 600; +const CY = 315; +const RADIUS = 260; + +// Corner vignette blocks to create the circular mask effect +// We use rectangles positioned around the circle to create the illusion +const _vignetteBlocks: { left: number; top: number; width: number; height: number }[] = [ + // Top strip + { left: 0, top: 0, width: 1200, height: CY - RADIUS }, + // Bottom strip + { left: 0, top: CY + RADIUS, width: 1200, height: 630 - (CY + RADIUS) }, + // Left strip + { left: 0, top: CY - RADIUS, width: CX - RADIUS, height: RADIUS * 2 }, + // Right strip + { left: CX + RADIUS, top: CY - RADIUS, width: 1200 - (CX + RADIUS), height: RADIUS * 2 }, + // Corner fill — approximate circle with 8 additional corner rectangles + // Top-left corner + { left: CX - RADIUS, top: CY - RADIUS, width: 76, height: 76 }, + { left: CX - RADIUS, top: CY - RADIUS + 76, width: 30, height: 50 }, + { left: CX - RADIUS + 76, top: CY - RADIUS, width: 50, height: 30 }, + // Top-right corner + { left: CX + RADIUS - 76, top: CY - RADIUS, width: 76, height: 76 }, + { left: CX + RADIUS - 30, top: CY - RADIUS + 76, width: 30, height: 50 }, + { left: CX + RADIUS - 126, top: CY - RADIUS, width: 50, height: 30 }, + // Bottom-left corner + { left: CX - RADIUS, top: CY + RADIUS - 76, width: 76, height: 76 }, + { left: CX - RADIUS, top: CY + RADIUS - 126, width: 30, height: 50 }, + { left: CX - RADIUS + 76, top: CY + RADIUS - 30, width: 50, height: 30 }, + // Bottom-right corner + { left: CX + RADIUS - 76, top: CY + RADIUS - 76, width: 76, height: 76 }, + { left: CX + RADIUS - 30, top: CY + RADIUS - 126, width: 30, height: 50 }, + { left: CX + RADIUS - 126, top: CY + RADIUS - 30, width: 50, height: 30 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 36 : meta.title.length > 40 ? 42 : 48; + + return new ImageResponse( +
+ {/* Eyepiece background circle */} +
+ + {/* Thin outer ring */} +
+ + {/* Stars through eyepiece */} + {telescopeStars.map((s, i) => ( +
+ ))} + + {/* Crosshair — horizontal line */} +
+ + {/* Crosshair — vertical line */} +
+ + {/* Crosshair center tick marks */} + {/* Left tick */} +
+ {/* Right tick */} +
+ {/* Top tick */} +
+ {/* Bottom tick */} +
+ + {/* Coordinate numbers at crosshair intersections */} +
+ 0,0 +
+
+ -260 +
+
+ +260 +
+ + {/* Content inside the eyepiece */} +
+ {/* Logo */} +
+ +
+ + {/* Title */} +
+ {meta.title.length > 60 ? meta.title.slice(0, 60) + '\u2026' : meta.title} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 80 ? meta.excerpt.slice(0, 80) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer — outside the circle, bottom */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read + {meta.tags && + meta.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v156-receipt.tsx b/packages/app/src/app/blog/[slug]/og-variants/v156-receipt.tsx new file mode 100644 index 0000000..ebc34fc --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v156-receipt.tsx @@ -0,0 +1,187 @@ +/** + * V156: Receipt — Thermal till slip with dashed separators, item-style title, and total line. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const dashes = '- - - - - - - - - - - - - - - - - - - - - - - - - - -'; + + return new ImageResponse( +
+ {/* Receipt slip */} +
+ {/* Store name */} +
+ +
+
+ + INFERENCEX BLOG + +
+
+ + www.inferencex.com + +
+ + {/* Dashes */} +
+ {dashes} +
+ + {/* Date & receipt number */} +
+ + DATE:{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + #00{meta.readingTime} +
+
+ + CASHIER: {meta.author.toUpperCase()} + +
+ + {/* Dashes */} +
+ {dashes} +
+ + {/* Item: Title */} +
+
+ 56 ? 18 : 20, fontWeight: 700, lineHeight: 1.3 }}> + {meta.title.toUpperCase()} + +
+
+ + {/* Qty line */} +
+ QTY: {meta.readingTime} MIN + $0.00 +
+ + {/* Tags */} + {meta.tags && ( +
+ + TAGS: {meta.tags.join(', ').toUpperCase()} + +
+ )} + + {/* Dashes */} +
+ {dashes} +
+ + {/* Total */} +
+ TOTAL + 1 ARTICLE +
+
+ TAX + $0.00 +
+ + {/* Dashes */} +
+ {dashes} +
+ + {/* Thank you */} +
+ + THANK YOU FOR READING + + PLEASE COME AGAIN +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v157-passport.tsx b/packages/app/src/app/blog/[slug]/og-variants/v157-passport.tsx new file mode 100644 index 0000000..8477bdc --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v157-passport.tsx @@ -0,0 +1,234 @@ +/** + * V157: Passport — Dark navy cover with inner off-white page, MRZ zone, and official document styling. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const surname = meta.author.split(' ').slice(-1)[0].toUpperCase(); + const givenNames = meta.author.split(' ').slice(0, -1).join(' ').toUpperCase() || 'AUTHOR'; + const mrzName = `${surname}<<${givenNames.replace(/ /g, '<')}`; + const mrzLine1 = `P + {/* Passport cover emblem text */} +
+ + BLOG PASSPORT + +
+ + {/* Inner page */} +
+ {/* Header */} +
+
+ + INFERENCEX + + + BLOG PASSPORT + +
+
+ +
+
+ + {/* Thin rule */} +
+ + {/* Body: photo placeholder + fields */} +
+ {/* Photo placeholder */} +
+ PHOTO +
+ + {/* Fields */} +
+ {/* Type / Country */} +
+
+ TYPE + P +
+
+ + COUNTRY CODE + + BLG +
+
+ + NATIONALITY + + {meta.author.toUpperCase()} +
+
+ + {/* Surname / Given Name = Title */} +
+ + TITLE / NAME + + 56 ? 17 : 20, + fontWeight: 700, + lineHeight: 1.25, + color: '#1a2744', + }} + > + {meta.title} + +
+ + {/* Date of issue / Expiry */} +
+
+ + DATE OF ISSUE + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ + READING TIME + + {meta.readingTime} MIN +
+
+ + {/* Tags */} + {meta.tags && ( +
+ + ENDORSEMENTS + + {meta.tags.join(' / ')} +
+ )} +
+
+ + {/* MRZ zone */} +
+ + {mrzLine1} + + + {mrzLine2} + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v158-ransom-note.tsx b/packages/app/src/app/blog/[slug]/og-variants/v158-ransom-note.tsx new file mode 100644 index 0000000..62f52d2 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v158-ransom-note.tsx @@ -0,0 +1,188 @@ +/** + * V158: Ransom Note — Chaotic cut-out collage with alternating sizes, colors, and offset word blocks. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const words = meta.title.split(/\s+/); + const colors = [ + '#ff3333', + '#ffffff', + '#ffdd00', + '#33ff66', + '#ff6633', + '#66ccff', + '#ff66cc', + '#ffffff', + ]; + const bgColors = [ + '#333333', + '#1a1a1a', + '#2a1a1a', + '#1a2a1a', + '#2a2a1a', + '#1a1a2a', + '#2a1a2a', + '#222222', + ]; + const sizes = [28, 36, 24, 40, 30, 34, 26, 38, 32, 22, 44, 20]; + const paddings = ['6px 10px', '8px 14px', '4px 8px', '10px 16px', '5px 12px', '7px 11px']; + const marginTops = [0, -4, 6, -2, 8, -6, 4, -3, 5, -5, 2, 7]; + const marginLefts = [0, 4, -2, 6, -4, 2, -6, 8, -3, 5, -1, 3]; + + return new ImageResponse( +
+ {/* Scattered cut-out header */} +
+
+ + INFERENCEX BLOG + +
+
+ + {/* Title words as cut-outs */} +
+ {words.map((word, i) => ( +
+ + {i % 2 === 0 ? word.toUpperCase() : word.toLowerCase()} + +
+ ))} +
+ + {/* Author and date cutouts */} +
+
+ {meta.author} +
+
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ + {meta.readingTime} min + +
+
+ + {/* Tags scattered */} + {meta.tags && ( +
+ {meta.tags.map((tag, i) => ( +
+ + {tag.toUpperCase()} + +
+ ))} +
+ )} + + {/* Logo */} +
+ +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v159-typewriter.tsx b/packages/app/src/app/blog/[slug]/og-variants/v159-typewriter.tsx new file mode 100644 index 0000000..fa36266 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v159-typewriter.tsx @@ -0,0 +1,174 @@ +/** + * V159: Typewriter — Off-white paper with monospace typed text, roller guide, and red ribbon accent. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Roller guide line at top */} +
+
+ + {/* Small carriage indicator at top right */} +
+ + {/* Page content area */} +
+ {/* Left margin line */} +
+ + {/* Date line */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Title — typed text */} +
+ + {meta.title} + +
+ + {/* Horizontal typed line */} +
+ + _______________________________________________ + +
+ + {/* Author — red ribbon accent */} +
+ by + {meta.author} +
+ + {/* Excerpt — lighter typed text */} +
+ + {meta.excerpt.length > 160 ? meta.excerpt.slice(0, 160) + '...' : meta.excerpt} + +
+ + {/* Reading time */} +
+ + Reading time: {meta.readingTime} minutes + +
+ + {/* Tags */} + {meta.tags && ( +
+ [{meta.tags.join('] [')}] +
+ )} +
+ + {/* Logo bottom-right */} +
+ + + INFERENCEX + +
+ + {/* Bottom roller line */} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v160-sticky-note.tsx b/packages/app/src/app/blog/[slug]/og-variants/v160-sticky-note.tsx new file mode 100644 index 0000000..96289c9 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v160-sticky-note.tsx @@ -0,0 +1,184 @@ +/** + * V160: Sticky Note — Yellow Post-it on dark background with pin indicator and informal handwritten feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Shadow behind sticky note (offset darker div) */} +
+ + {/* Sticky note */} +
+ {/* Pin circle at top center */} +
+ + {/* Small fold line top-left */} +
+ + {/* Title — main scrawl */} +
+ 56 ? 26 : titleSize > 48 ? 30 : 34, + fontWeight: 700, + color: '#1a1a1a', + lineHeight: 1.3, + fontFamily: 'serif', + }} + > + {meta.title} + +
+ + {/* Underline scribble */} +
+ + {/* Excerpt */} +
+ + {meta.excerpt.length > 100 ? meta.excerpt.slice(0, 100) + '...' : meta.excerpt} + +
+ + {/* Author and date */} +
+
+ + - {meta.author} + + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ ~{meta.readingTime} min +
+
+
+ + {/* Logo bottom-left on dark bg */} +
+ + + INFERENCEX + +
+ + {/* Tags as small notes at bottom-right */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag, i) => ( +
+ {tag} +
+ ))} +
+ )} +
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v161-safety-card.tsx b/packages/app/src/app/blog/[slug]/og-variants/v161-safety-card.tsx new file mode 100644 index 0000000..3c8c8b3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v161-safety-card.tsx @@ -0,0 +1,406 @@ +/** + * V161: Safety Card — Airline safety card with panel layout, numbered steps, and pictogram-style shapes. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top header bar */} +
+
+ + + INFERENCEX AIRLINES + +
+ + SAFETY INFORMATION CARD + +
+ + {/* "IN CASE OF READING" header */} +
+
+ + IN CASE OF READING + +
+
+ + {/* Title panel */} +
+ 56 ? 22 : 26, + fontWeight: 700, + color: '#1a1a1a', + textAlign: 'center', + lineHeight: 1.2, + }} + > + {meta.title} + +
+ + {/* Three numbered step panels */} +
+ {/* Step 1 */} +
+
+
+ 1 +
+ OPEN ARTICLE +
+ {/* Pictogram: person + rectangle (screen) */} +
+
+ {/* Head */} +
+ {/* Body */} +
+
+ {/* Screen */} +
+ BLOG +
+
+
+ + {/* Step 2 */} +
+
+
+ 2 +
+ + READ {meta.readingTime} MIN + +
+ {/* Pictogram: person reading */} +
+
+ {/* Head */} +
+ {/* Body */} +
+ {/* Arms holding document */} +
+
+
+
+
+
+
+ + approx. {meta.readingTime} minutes + +
+
+ + {/* Step 3 */} +
+
+
+ 3 +
+ SHARE +
+ {/* Pictogram: two people */} +
+
+
+
+
+ {/* Arrow */} +
+
+
+
+
+
+
+ with fellow passengers +
+
+
+ + {/* Bottom bar */} +
+ + By {meta.author} |{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ {meta.tags && + meta.tags.slice(0, 3).map((tag, i) => ( + 0 ? '8px' : '0' }} + > + {tag} + + ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v162-nutrition-label.tsx b/packages/app/src/app/blog/[slug]/og-variants/v162-nutrition-label.tsx new file mode 100644 index 0000000..e132cd6 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v162-nutrition-label.tsx @@ -0,0 +1,315 @@ +/** + * V162: Nutrition Label — Classic FDA nutrition facts layout with thick rules, serving info, and ingredients. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + return new ImageResponse( +
+ {/* Nutrition label container */} +
+ {/* Blog Facts header */} +
+ + Blog Facts + +
+ + {/* Thick rule */} +
+ + {/* Serving size */} +
+ + Serving Size 1 Article ({meta.readingTime} min) + +
+
+ Servings Per Blog: Many +
+ + {/* Medium thick rule */} +
+ + {/* Amount per serving */} +
+ + Amount Per Serving + +
+ + {/* Thin rule */} +
+ + {/* Calories */} +
+ + Calories from Insight + + 100% +
+ + {/* Medium rule */} +
+ + {/* % Daily Value header */} +
+ % Daily Value* +
+ + {/* Thin rule */} +
+ + {/* Knowledge */} +
+
+ Knowledge + {meta.readingTime}min +
+ 100% +
+ +
+ + {/* Author */} +
+
+ Author + {meta.author} +
+
+ +
+ + {/* Date */} +
+
+ Published + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ +
+ + {/* Reading Enjoyment */} +
+
+ + Reading Enjoyment{' '} + +
+ 99% +
+ + {/* Thick rule */} +
+ + {/* Ingredients */} +
+
+ + INGREDIENTS: + {meta.title}.{meta.tags ? ` Contains: ${meta.tags.join(', ')}.` : ''} + +
+
+ + {/* Thin rule */} +
+ + {/* Footnote */} +
+ + * Percent Daily Values are based on a 2,000 word reading diet. Your daily values may + vary. + +
+
+ + {/* Logo — outside label, bottom-right */} +
+ + + INFERENCEX + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v163-warning-sign.tsx b/packages/app/src/app/blog/[slug]/og-variants/v163-warning-sign.tsx new file mode 100644 index 0000000..227affb --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v163-warning-sign.tsx @@ -0,0 +1,218 @@ +/** + * V163: Warning Sign — Industrial caution sign with yellow triangle, hazard stripes, and bold warnings. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Warning triangle area */} +
+ {/* Triangle built from stacked rectangles (widening) */} +
+
+
+
+
+
+
+
+
+
+
+ + {/* Exclamation mark overlaid */} +
+
+
+
+
+ + {/* WARNING text */} +
+ + WARNING + +
+ + {/* Title */} +
+ 56 ? 24 : 28, + fontWeight: 700, + color: '#ffffff', + textAlign: 'center', + lineHeight: 1.3, + }} + > + {meta.title.toUpperCase()} + +
+ + {/* Caution subtitle */} +
+ + CAUTION: May contain {meta.readingTime} minutes of reading + +
+ + {/* Author and date */} +
+ + Issued by {meta.author} |{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 4).map((tag, i) => ( +
0 ? '6px' : '0', + }} + > + + {tag.toUpperCase()} + +
+ ))} +
+ )} + + {/* Hazard stripes at bottom */} +
+ {Array.from({ length: 24 }).map((_, i) => ( +
+ ))} +
+ + {/* Logo top-left */} +
+ + + INFERENCEX + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v164-test-pattern.tsx b/packages/app/src/app/blog/[slug]/og-variants/v164-test-pattern.tsx new file mode 100644 index 0000000..e960b33 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v164-test-pattern.tsx @@ -0,0 +1,207 @@ +/** + * V164: Test Pattern — Classic TV color bars with broadcast header, title, and "PLEASE STAND BY" footer. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const barColors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff']; + + return new ImageResponse( +
+ {/* Top broadcast header */} +
+
+ + + INFERENCEX BROADCAST + +
+
+ + {/* Channel / date info */} +
+ + CH.{meta.readingTime} |{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {meta.author.toUpperCase()} +
+ + {/* Color bars */} +
+ {barColors.map((color, i) => ( +
+ ))} +
+ + {/* Thin black separator */} +
+ + {/* Lower complement bars (darker versions) */} +
+ {barColors.map((_, i) => ( +
+ ))} +
+ + {/* Title area */} +
+ 56 ? 26 : 32, + fontWeight: 700, + color: '#ffffff', + textAlign: 'center', + lineHeight: 1.3, + maxWidth: '900px', + }} + > + {meta.title} + +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 4).map((tag, i) => ( + 0 ? '12px' : '0', + letterSpacing: '0.05em', + }} + > + {tag.toUpperCase()} + + ))} +
+ )} + + {/* PLEASE STAND BY footer */} +
+ + PLEASE STAND BY + +
+ + {/* Time code */} +
+ + TC 00:{String(meta.readingTime).padStart(2, '0')}:00:00 + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v165-boot-screen.tsx b/packages/app/src/app/blog/[slug]/og-variants/v165-boot-screen.tsx new file mode 100644 index 0000000..94f4e8a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v165-boot-screen.tsx @@ -0,0 +1,186 @@ +/** + * V165: Boot Screen — BIOS/boot sequence with green monospace text on pure black background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const green = '#00ff41'; + const dimGreen = '#00aa2a'; + + return new ImageResponse( +
+ {/* BIOS Header */} +
+
+ + + InferenceX BIOS v1.0 + +
+ (C) 2024 InferenceX Corp. +
+ + {/* Thin separator */} +
+ + {/* Boot lines */} +
+ InferenceX Blog Engine POST... +
+ +
+ + Memory Test: {meta.readingTime * 1024} KB OK + +
+ +
+ Detecting Blog Article............ Found +
+ +
+ Loading article... OK +
+ +
+ + ----------------------------------------------- + +
+ + {/* Article details */} +
+ Title: + + {meta.title.length > 70 ? meta.title.slice(0, 70) + '...' : meta.title} + +
+ +
+ Author: + {meta.author} +
+ +
+ Date: + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ +
+ Memory: + {meta.readingTime} MIN +
+ + {meta.tags && ( +
+ Tags: + [{meta.tags.join(', ')}] +
+ )} + +
+ + ----------------------------------------------- + +
+ +
+ + Excerpt: {meta.excerpt.length > 80 ? meta.excerpt.slice(0, 80) + '...' : meta.excerpt} + +
+ +
+ Initializing reader interface... OK +
+ +
+ All systems operational. +
+ + {/* Title displayed large */} +
+ 56 ? 28 : 34, + fontWeight: 900, + color: green, + textAlign: 'center', + lineHeight: 1.3, + maxWidth: '900px', + }} + > + {meta.title} + +
+ + {/* Press any key */} +
+ Press any key to continue... + {/* Blinking cursor (solid block) */} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v166-mondrian.tsx b/packages/app/src/app/blog/[slug]/og-variants/v166-mondrian.tsx new file mode 100644 index 0000000..2224b72 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v166-mondrian.tsx @@ -0,0 +1,265 @@ +/** + * V166: Mondrian — De Stijl composition with bold black grid lines and primary color cells on white. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Mondrian grid: define the black lines + const horizontalLines = [ + { top: 0, left: 0, width: 1200, height: 8 }, + { top: 180, left: 0, width: 1200, height: 7 }, + { top: 420, left: 0, width: 1200, height: 7 }, + { top: 622, left: 0, width: 1200, height: 8 }, + ]; + + const verticalLines = [ + { top: 0, left: 0, width: 8, height: 630 }, + { top: 0, left: 320, width: 7, height: 630 }, + { top: 0, left: 820, width: 7, height: 630 }, + { top: 0, left: 1192, width: 8, height: 630 }, + ]; + + // Colored cells (positioned within grid areas) + const colorCells = [ + // Top-left cell: RED + { top: 8, left: 8, width: 305, height: 165, color: '#e60000' }, + // Top-right cell: BLUE + { top: 8, left: 827, width: 365, height: 165, color: '#0000cc' }, + // Bottom-left cell: YELLOW + { top: 427, left: 8, width: 305, height: 195, color: '#ffd700' }, + // Bottom-right small cell: RED + { top: 427, left: 827, width: 365, height: 195, color: '#e60000' }, + // Middle-right cell: YELLOW + { top: 187, left: 827, width: 365, height: 226, color: '#ffd700' }, + ]; + + return new ImageResponse( +
+ {/* Colored cells */} + {colorCells.map((cell, i) => ( +
+ ))} + + {/* Horizontal black lines */} + {horizontalLines.map((line, i) => ( +
+ ))} + + {/* Vertical black lines */} + {verticalLines.map((line, i) => ( +
+ ))} + + {/* Main content area: large white center cell */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title */} +
+ {meta.title} +
+
+ + {/* Footer in bottom-center white cell */} +
+ {/* Excerpt */} +
+ {meta.excerpt.length > 100 ? meta.excerpt.slice(0, 100) + '\u2026' : meta.excerpt} +
+ + {/* Author + Date */} +
+ {meta.author} · {formattedDate} +
+ + {/* Reading time */} +
+ {meta.readingTime} min read +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+ + {/* Top center white cell label */} +
+
+ DE STIJL · {new Date(meta.date + 'T00:00:00Z').getFullYear()} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v167-rothko.tsx b/packages/app/src/app/blog/[slug]/og-variants/v167-rothko.tsx new file mode 100644 index 0000000..a140635 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v167-rothko.tsx @@ -0,0 +1,305 @@ +/** + * V167: Rothko — Color field painting with large stacked horizontal color blocks on dark background, contemplative and emotional. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + return new ImageResponse( +
+ {/* Outer subtle warm wash */} +
+ + {/* Top color field — dark maroon/crimson */} +
+ {/* Soft inner glow on top field */} +
+ + {/* Middle color field — deep navy/midnight blue */} +
+ {/* Soft inner glow on middle field */} +
+ + {/* Bottom color field — near-black */} +
+ {/* Subtle warmth in bottom field */} +
+ + {/* Narrow gap glow between top and middle fields */} +
+ + {/* Narrow gap glow between middle and bottom fields */} +
+ + {/* Logo — top left, muted */} +
+ + + INFERENCEX + +
+ + {/* Title — centered in the middle color field, low contrast */} +
+
+ {meta.title} +
+
+ + {/* Excerpt — floating in top color field */} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer — in bottom field */} +
+
+ {meta.author} · {formattedDate} · {meta.readingTime} min read +
+
+ + {/* Tags — top right, very subtle */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v168-kandinsky.tsx b/packages/app/src/app/blog/[slug]/og-variants/v168-kandinsky.tsx new file mode 100644 index 0000000..f4d78a7 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v168-kandinsky.tsx @@ -0,0 +1,298 @@ +/** + * V168: Kandinsky — Abstract geometric composition with scattered circles, triangles, and rectangles in bright colors on warm parchment. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Circles scattered around + const circles = [ + { top: 30, left: 50, size: 90, color: '#e60000', opacity: 0.7 }, + { top: 80, left: 900, size: 120, color: '#0044cc', opacity: 0.6 }, + { top: 400, left: 80, size: 60, color: '#ffd700', opacity: 0.8 }, + { top: 450, left: 1050, size: 100, color: '#e60000', opacity: 0.5 }, + { top: 200, left: 1050, size: 50, color: '#228b22', opacity: 0.6 }, + { top: 520, left: 500, size: 70, color: '#0044cc', opacity: 0.4 }, + { top: 15, left: 500, size: 40, color: '#000000', opacity: 0.5 }, + { top: 350, left: 950, size: 35, color: '#ffd700', opacity: 0.7 }, + { top: 100, left: 300, size: 25, color: '#228b22', opacity: 0.5 }, + { top: 550, left: 200, size: 45, color: '#000000', opacity: 0.3 }, + ]; + + // Rectangles + const rects = [ + { top: 150, left: 30, width: 80, height: 30, color: '#000000', opacity: 0.6 }, + { top: 500, left: 800, width: 120, height: 15, color: '#e60000', opacity: 0.5 }, + { top: 50, left: 700, width: 15, height: 100, color: '#000000', opacity: 0.4 }, + { top: 300, left: 1100, width: 60, height: 8, color: '#0044cc', opacity: 0.6 }, + { top: 470, left: 350, width: 8, height: 80, color: '#000000', opacity: 0.5 }, + { top: 20, left: 200, width: 50, height: 6, color: '#e60000', opacity: 0.4 }, + ]; + + // Lines (thin rectangles) + const lines = [ + { top: 180, left: 60, width: 200, height: 3, color: '#000000', opacity: 0.35 }, + { top: 400, left: 900, width: 180, height: 2, color: '#000000', opacity: 0.3 }, + { top: 100, left: 800, width: 3, height: 150, color: '#000000', opacity: 0.25 }, + { top: 350, left: 150, width: 3, height: 120, color: '#000000', opacity: 0.3 }, + { top: 550, left: 700, width: 250, height: 2, color: '#000000', opacity: 0.2 }, + { top: 280, left: 50, width: 150, height: 2, color: '#e60000', opacity: 0.2 }, + ]; + + // Triangles (CSS border trick): pointing up + const triangles = [ + { top: 250, left: 900, size: 60, color: '#ffd700', opacity: 0.7 }, + { top: 50, left: 400, size: 45, color: '#e60000', opacity: 0.5 }, + { top: 480, left: 650, size: 50, color: '#0044cc', opacity: 0.5 }, + { top: 150, left: 1100, size: 35, color: '#000000', opacity: 0.4 }, + { top: 500, left: 100, size: 40, color: '#228b22', opacity: 0.6 }, + ]; + + return new ImageResponse( +
+ {/* Circles */} + {circles.map((c, i) => ( +
+ ))} + + {/* Some circles as rings (border only) */} +
+
+ + {/* Rectangles */} + {rects.map((r, i) => ( +
+ ))} + + {/* Lines */} + {lines.map((l, i) => ( +
+ ))} + + {/* Triangles via CSS border trick */} + {triangles.map((t, i) => ( +
+ ))} + + {/* Central content zone */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} · {meta.readingTime} min read +
+
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v169-impossible.tsx b/packages/app/src/app/blog/[slug]/og-variants/v169-impossible.tsx new file mode 100644 index 0000000..24a2673 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v169-impossible.tsx @@ -0,0 +1,328 @@ +/** + * V169: Impossible Geometry — Escher-inspired impossible triangle illusion using positioned rectangles on dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Build an impossible triangle illusion using overlapping rectangles + // Three "beams" that appear to connect impossibly + // Each beam has a light face and a dark face to create 3D illusion + + // Beam 1: Bottom horizontal (left to right) + // Beam 2: Left side going up-right + // Beam 3: Right side going up-left + + // We approximate the Penrose triangle with carefully positioned rectangles + const cx = 200; // Center x offset for the figure + const cy = 80; // Center y offset + + return new ImageResponse( +
+ {/* Subtle grid background */} + {Array.from({ length: 20 }, (_, i) => ( +
+ ))} + {Array.from({ length: 30 }, (_, i) => ( +
+ ))} + + {/* Impossible triangle — built from rectangular "beams" */} + {/* Bottom beam: horizontal bar */} +
+ {/* Bottom beam top face */} +
+ + {/* Left beam: vertical bar going up */} +
+ {/* Left beam right face (lighter) */} +
+ + {/* Top connector: horizontal at top */} +
+ {/* Top connector bottom face */} +
+ + {/* Right beam: vertical bar going down */} +
+ {/* Right beam left face (darker) */} +
+ + {/* "Impossible" overlap: bottom-right corner where beams cross impossibly */} + {/* The right beam appears to go BEHIND the bottom beam, creating the paradox */} +
+ {/* But the top of the right beam passes in FRONT — paradox marker */} +
+ + {/* "Impossible" overlap: top-left corner */} +
+ + {/* Question mark accent near the impossible joint */} +
+ IMPOSSIBLE +
+ + {/* Content — right side */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} · {meta.readingTime} min read +
+ {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v170-op-art.tsx b/packages/app/src/app/blog/[slug]/og-variants/v170-op-art.tsx new file mode 100644 index 0000000..9459eca --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v170-op-art.tsx @@ -0,0 +1,260 @@ +/** + * V170: Op Art — Bridget Riley / Vasarely optical illusion with concentric rings and alternating black/white stripes creating a vibrating moiré effect. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Concentric rings from center — alternating black and white + const rings = Array.from({ length: 40 }, (_, i) => { + const outerRadius = 500 - i * 12; + if (outerRadius <= 0) return null; + return { + size: outerRadius * 2, + radius: outerRadius, + color: i % 2 === 0 ? '#000000' : '#ffffff', + }; + }).filter(Boolean) as { size: number; radius: number; color: string }[]; + + // Second set of concentric rings offset to create moiré + const rings2 = Array.from({ length: 35 }, (_, i) => { + const outerRadius = 420 - i * 11; + if (outerRadius <= 0) return null; + return { + size: outerRadius * 2, + radius: outerRadius, + color: i % 2 === 0 ? '#000000' : '#ffffff', + }; + }).filter(Boolean) as { size: number; radius: number; color: string }[]; + + // Vertical stripes on left side for additional optical interference + const stripes = Array.from({ length: 60 }, (_, i) => ({ + left: i * 4, + color: i % 2 === 0 ? '#000000' : '#ffffff', + })); + + return new ImageResponse( +
+ {/* Vertical stripes — left portion */} + {stripes.map((s, i) => ( +
+ ))} + + {/* Primary concentric rings — centered */} + {rings.map((ring, i) => ( +
+ ))} + + {/* Secondary concentric rings — offset for moiré */} + {rings2.map((ring, i) => ( +
+ ))} + + {/* Clear central zone for content */} +
+ {/* Inner border ring */} +
+ + {/* Content area */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 100 ? meta.excerpt.slice(0, 100) + '\u2026' : meta.excerpt} +
+ + {/* Footer */} +
+ {meta.author} · {formattedDate} · {meta.readingTime} min +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+ + {/* "OP ART" watermark in corner */} +
+ OP ART +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v171-data-mosh.tsx b/packages/app/src/app/blog/[slug]/og-variants/v171-data-mosh.tsx new file mode 100644 index 0000000..67c9605 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v171-data-mosh.tsx @@ -0,0 +1,277 @@ +/** + * V171: Glitch/Data Mosh — Digital corruption aesthetic with RGB channel offset title, horizontal tear lines, and displaced blocks. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Horizontal tear lines — rectangles that cut across the image + const tears = [ + { top: 95, left: 0, width: 1200, height: 3, color: '#ff0040', opacity: 0.6 }, + { top: 98, left: 200, width: 800, height: 1, color: '#00ff40', opacity: 0.4 }, + { top: 230, left: 0, width: 600, height: 2, color: '#0080ff', opacity: 0.5 }, + { top: 232, left: 400, width: 800, height: 1, color: '#ff0040', opacity: 0.3 }, + { top: 370, left: 100, width: 1100, height: 3, color: '#00ff40', opacity: 0.4 }, + { top: 374, left: 0, width: 500, height: 1, color: '#0080ff', opacity: 0.5 }, + { top: 490, left: 0, width: 1200, height: 2, color: '#ff0040', opacity: 0.5 }, + { top: 493, left: 300, width: 900, height: 1, color: '#00ff40', opacity: 0.3 }, + { top: 155, left: 0, width: 1200, height: 1, color: '#ffffff', opacity: 0.1 }, + { top: 310, left: 0, width: 1200, height: 1, color: '#ffffff', opacity: 0.08 }, + { top: 550, left: 0, width: 1200, height: 1, color: '#ffffff', opacity: 0.1 }, + ]; + + // Displaced blocks — chunks of the image that appear shifted + const displacedBlocks = [ + { top: 90, left: 50, width: 180, height: 12, color: '#0a0a12', shift: 15 }, + { top: 225, left: 600, width: 250, height: 10, color: '#0a0a12', shift: -20 }, + { top: 365, left: 100, width: 300, height: 14, color: '#0a0a12', shift: 25 }, + { top: 485, left: 400, width: 200, height: 11, color: '#0a0a12', shift: -10 }, + ]; + + // Static noise dots scattered across + const noiseDots = Array.from({ length: 50 }, (_, i) => ({ + top: (i * 97 + 31) % 610, + left: (i * 143 + 67) % 1180, + size: (i % 3) + 1, + color: i % 3 === 0 ? '#ff0040' : i % 3 === 1 ? '#00ff40' : '#0080ff', + opacity: 0.2 + (i % 5) * 0.05, + })); + + return new ImageResponse( +
+ {/* Noise dots */} + {noiseDots.map((dot, i) => ( +
+ ))} + + {/* RED channel title — offset left and up */} +
+ {meta.title} +
+ + {/* GREEN channel title — offset right */} +
+ {meta.title} +
+ + {/* BLUE channel title — offset down */} +
+ {meta.title} +
+ + {/* Tear lines */} + {tears.map((tear, i) => ( +
+ ))} + + {/* Displaced blocks */} + {displacedBlocks.map((block, i) => ( +
+ ))} + + {/* Main content layer */} +
+ {/* Header */} +
+
+ + + InferenceX + +
+
+ DATA_CORRUPT +
+
+ + {/* Title — primary white layer */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+
+ {meta.author} +
+
{formattedDate}
+
+ {meta.readingTime} min +
+
+ {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v172-risograph.tsx b/packages/app/src/app/blog/[slug]/og-variants/v172-risograph.tsx new file mode 100644 index 0000000..91d11b1 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v172-risograph.tsx @@ -0,0 +1,295 @@ +/** + * V172: Risograph — Two-color overprint effect with pink/red and blue shapes, misregistered overlap creating purple zones, scattered grain dots. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Grain dots — scattered small circles for risograph texture + const grainDots = Array.from({ length: 80 }, (_, i) => ({ + top: (i * 73 + 19) % 615, + left: (i * 127 + 43) % 1185, + size: (i % 4) + 1, + color: i % 2 === 0 ? '#ff4466' : '#2244cc', + opacity: 0.08 + (i % 6) * 0.02, + })); + + // Pink/red layer shapes + const pinkShapes = [ + { top: 40, left: 60, width: 280, height: 200, radius: 140 }, + { top: 350, left: 800, width: 320, height: 220, radius: 20 }, + { top: 100, left: 900, width: 180, height: 180, radius: 90 }, + { top: 420, left: 150, width: 160, height: 160, radius: 80 }, + ]; + + // Blue layer shapes — slightly offset from pink for misregistration + const blueShapes = [ + { top: 45, left: 68, width: 280, height: 200, radius: 140 }, + { top: 356, left: 808, width: 320, height: 220, radius: 20 }, + { top: 106, left: 906, width: 180, height: 180, radius: 90 }, + { top: 426, left: 156, width: 160, height: 160, radius: 80 }, + ]; + + // Purple overlap zones (where pink and blue intersect) + const purpleZones = [ + { top: 45, left: 68, width: 272, height: 195, radius: 130 }, + { top: 356, left: 808, width: 312, height: 214, radius: 18 }, + { top: 106, left: 906, width: 174, height: 174, radius: 87 }, + { top: 426, left: 156, width: 154, height: 154, radius: 77 }, + ]; + + // Decorative lines — riso-style ruled marks + const ruledLines = [ + { top: 265, left: 100, width: 400, height: 2 }, + { top: 270, left: 100, width: 400, height: 1 }, + { top: 395, left: 100, width: 400, height: 2 }, + { top: 400, left: 100, width: 400, height: 1 }, + ]; + + return new ImageResponse( +
+ {/* Grain texture dots */} + {grainDots.map((dot, i) => ( +
+ ))} + + {/* Pink/red layer */} + {pinkShapes.map((shape, i) => ( +
+ ))} + + {/* Blue layer — offset for misregistration */} + {blueShapes.map((shape, i) => ( +
+ ))} + + {/* Purple overlap zones */} + {purpleZones.map((zone, i) => ( +
+ ))} + + {/* Ruled lines */} + {ruledLines.map((line, i) => ( +
+ ))} + + {/* Content area */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title — rendered twice for riso overprint effect */} +
+ {/* Blue layer of title — offset */} +
+ {meta.title} +
+ {/* Pink layer of title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} · {formattedDate} · {meta.readingTime} min read +
+ {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
+ + {/* "RISO" watermark */} +
+ RISOGRAPH PRINT +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v173-linocut.tsx b/packages/app/src/app/blog/[slug]/og-variants/v173-linocut.tsx new file mode 100644 index 0000000..63aeb32 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v173-linocut.tsx @@ -0,0 +1,377 @@ +/** + * V173: Linocut/Woodcut — Bold white shapes on dark background with thick borders, decorative leaf patterns, high contrast printmaking aesthetic. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Leaf/branch patterns along edges — made from ellipses and small rectangles + // Left edge leaves + const leftLeaves = Array.from({ length: 8 }, (_, i) => ({ + top: 40 + i * 70, + left: 15, + width: 28, + height: 14, + rotation: i % 2 === 0 ? 0 : 1, + })); + + // Right edge leaves + const rightLeaves = Array.from({ length: 8 }, (_, i) => ({ + top: 40 + i * 70, + left: 1157, + width: 28, + height: 14, + rotation: i % 2 === 0 ? 1 : 0, + })); + + // Top edge leaves + const topLeaves = Array.from({ length: 12 }, (_, i) => ({ + top: 12, + left: 80 + i * 90, + width: 14, + height: 28, + })); + + // Bottom edge leaves + const bottomLeaves = Array.from({ length: 12 }, (_, i) => ({ + top: 590, + left: 80 + i * 90, + width: 14, + height: 28, + })); + + // Branch/stem lines along edges + const stems = [ + // Left vertical stem + { top: 30, left: 28, width: 3, height: 570 }, + // Right vertical stem + { top: 30, left: 1169, width: 3, height: 570 }, + // Top horizontal stem + { top: 25, left: 60, width: 1080, height: 3 }, + // Bottom horizontal stem + { top: 602, left: 60, width: 1080, height: 3 }, + ]; + + // Small dots at leaf-stem junctions + const junctionDots = [ + ...leftLeaves.map((l) => ({ top: l.top + 4, left: l.left + 15, size: 5 })), + ...rightLeaves.map((l) => ({ top: l.top + 4, left: l.left + 5, size: 5 })), + ]; + + return new ImageResponse( +
+ {/* Outer thick border — chunky frame */} +
+ {/* Inner border */} +
+ + {/* Branch stems */} + {stems.map((stem, i) => ( +
+ ))} + + {/* Left leaves */} + {leftLeaves.map((leaf, i) => ( +
+ ))} + + {/* Right leaves */} + {rightLeaves.map((leaf, i) => ( +
+ ))} + + {/* Top leaves */} + {topLeaves.map((leaf, i) => ( +
+ ))} + + {/* Bottom leaves */} + {bottomLeaves.map((leaf, i) => ( +
+ ))} + + {/* Junction dots */} + {junctionDots.map((dot, i) => ( +
+ ))} + + {/* Corner ornaments — chunky squares */} + {[ + { top: 8, left: 8 }, + { top: 8, left: 1172 }, + { top: 602, left: 8 }, + { top: 602, left: 1172 }, + ].map((corner, i) => ( +
+ ))} + + {/* Content area */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title block — bold stamped feel */} +
+ {/* Decorative line above title */} +
+ +
+ {meta.title.toUpperCase()} +
+ + {/* Decorative line below title */} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author.toUpperCase()} · {formattedDate.toUpperCase()} ·{' '} + {meta.readingTime} MIN +
+ {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v174-psychedelic.tsx b/packages/app/src/app/blog/[slug]/og-variants/v174-psychedelic.tsx new file mode 100644 index 0000000..3765403 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v174-psychedelic.tsx @@ -0,0 +1,356 @@ +/** + * V174: Psychedelic 60s — Groovy poster with nested rounded rectangles creating a tunnel/portal, bright saturated colors, peace signs. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Psychedelic color palette + const colors = [ + '#ff6600', // orange + '#ff00ff', // magenta + '#00ff00', // lime + '#9900ff', // purple + '#ffff00', // yellow + '#ff0066', // hot pink + '#00ffcc', // cyan-green + '#ff3300', // red-orange + ]; + + // Nested rounded rectangles creating a tunnel/portal effect + const tunnelLayers = Array.from({ length: 16 }, (_, i) => { + const inset = i * 22; + return { + top: 10 + inset, + left: 10 + inset, + right: 10 + inset, + bottom: 10 + inset, + color: colors[i % colors.length], + borderRadius: 30 + i * 4, + borderWidth: 6, + }; + }); + + // Flower shapes — circles arranged in a flower pattern + // Each "flower" is a center circle + 5-6 petal circles + const flowerCenters = [ + { top: 30, left: 30, size: 18 }, + { top: 560, left: 1130, size: 20 }, + { top: 540, left: 40, size: 16 }, + { top: 20, left: 1120, size: 18 }, + ]; + + // Peace sign — circle with lines: center circle + 3 lines (vertical + 2 diagonal approximated) + const peaceSignX = 60; + const peaceSignY = 280; + const peaceSignSize = 50; + + // Small scattered circles for "bubble" / groovy dots + const bubbles = Array.from({ length: 20 }, (_, i) => ({ + top: (i * 113 + 37) % 600, + left: (i * 157 + 91) % 1170, + size: 6 + (i % 4) * 3, + color: colors[i % colors.length], + opacity: 0.3 + (i % 4) * 0.1, + })); + + return new ImageResponse( +
+ {/* Nested tunnel rectangles */} + {tunnelLayers.map((layer, i) => ( +
+ ))} + + {/* Groovy bubbles */} + {bubbles.map((bubble, i) => ( +
+ ))} + + {/* Flower shapes — petal circles around a center */} + {flowerCenters.map((flower, fi) => { + const petals = Array.from({ length: 6 }, (_, pi) => { + const angle = (pi / 6) * Math.PI * 2; + const petalDist = flower.size * 0.9; + return { + top: flower.top + Math.sin(angle) * petalDist - flower.size * 0.35, + left: flower.left + Math.cos(angle) * petalDist - flower.size * 0.35, + size: flower.size * 0.7, + }; + }); + return [ + // Petals + ...petals.map((petal, pi) => ( +
+ )), + // Center +
, + ]; + })} + + {/* Peace sign — circle + vertical line + two angled lines (approximated) */} + {/* Circle */} +
+ {/* Vertical line */} +
+ {/* Left "leg" — approximated with a rectangle */} +
+ {/* Right "leg" */} +
+ + {/* Content zone — dark center */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + INFERENCEX + +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Excerpt */} +
+ {meta.excerpt.length > 110 ? meta.excerpt.slice(0, 110) + '\u2026' : meta.excerpt} +
+ + {/* Footer */} +
+ {meta.author} · {formattedDate} · {meta.readingTime} min +
+ + {/* Tags */} + {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag, i) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+ + {/* "GROOVY" watermark */} +
+ FAR OUT +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v175-hologram.tsx b/packages/app/src/app/blog/[slug]/og-variants/v175-hologram.tsx new file mode 100644 index 0000000..f9d0741 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v175-hologram.tsx @@ -0,0 +1,388 @@ +/** + * V175: Cyberpunk Hologram — Holographic projection on very dark background with scan lines, cyan/magenta glitch offset, AR registration corners. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const formattedDate = new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); + + // Horizontal scan lines across the entire image + const scanLines = Array.from({ length: 105 }, (_, i) => ({ + top: i * 6, + opacity: i % 3 === 0 ? 0.12 : 0.06, + })); + + // Corner registration marks (AR/hologram style L-brackets) + // Each corner has a horizontal and vertical arm + const cornerMarks = [ + // Top-left + { hTop: 30, hLeft: 30, hWidth: 40, vTop: 30, vLeft: 30, vHeight: 40 }, + // Top-right + { hTop: 30, hLeft: 1130, hWidth: 40, vTop: 30, vLeft: 1166, vHeight: 40 }, + // Bottom-left + { hTop: 580, hLeft: 30, hWidth: 40, vTop: 560, vLeft: 30, vHeight: 40 }, + // Bottom-right + { hTop: 580, hLeft: 1130, hWidth: 40, vTop: 560, vLeft: 1166, vHeight: 40 }, + ]; + + // Small data readout elements + const dataReadouts = [ + { top: 35, left: 85, text: 'SYS.ONLINE' }, + { top: 35, left: 1000, text: 'FREQ: 847.3 THz' }, + { top: 585, left: 85, text: 'PROJ.STABLE' }, + { top: 585, left: 980, text: 'RES: 1200x630' }, + ]; + + // Vertical interference bars + const interferenceBars = [ + { left: 180, width: 1, height: 630, opacity: 0.08 }, + { left: 400, width: 2, height: 630, opacity: 0.05 }, + { left: 750, width: 1, height: 630, opacity: 0.07 }, + { left: 1020, width: 2, height: 630, opacity: 0.04 }, + ]; + + return new ImageResponse( +
+ {/* Scan lines */} + {scanLines.map((line, i) => ( +
+ ))} + + {/* Vertical interference bars */} + {interferenceBars.map((bar, i) => ( +
+ ))} + + {/* Corner registration marks */} + {cornerMarks.map((corner, i) => ( +
+ {/* Horizontal arm */} +
+ {/* Vertical arm */} +
+ {/* Corner dot */} +
+
+ ))} + + {/* Data readouts */} + {dataReadouts.map((readout, i) => ( +
+ {readout.text} +
+ ))} + + {/* HOLOGRAPHIC PROJECTION watermark — horizontal across center */} +
+ HOLOGRAPHIC PROJECTION +
+ + {/* Magenta glitch offset of title */} +
+ {meta.title} +
+ + {/* Cyan secondary offset */} +
+ {meta.title} +
+ + {/* Holographic projection border */} +
+ + {/* Main content */} +
+ {/* Header */} +
+
+ + + InferenceX + +
+
+ {/* Pulsing indicator */} +
+
+ HOLO.ACTIVE +
+
+
+ + {/* Title — primary cyan layer */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+
+ {meta.author} +
+
+ {formattedDate} +
+
+ {meta.readingTime} min read +
+
+ {meta.tags && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag.toUpperCase()} +
+ ))} +
+ )} +
+
+ + {/* Bottom status bar */} +
+ BLADE RUNNER HOLO-SYS v4.9 // REPLICANT CERTIFIED +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v26-halftone-dots.tsx b/packages/app/src/app/blog/[slug]/og-variants/v26-halftone-dots.tsx new file mode 100644 index 0000000..c46783c --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v26-halftone-dots.tsx @@ -0,0 +1,139 @@ +/** + * V26: Halftone Dots — dots decrease in size from one corner to the other. + * Dark background with teal and gold dots scattered in a gradient halftone pattern. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +function generateHalftoneDots() { + const dots: { x: number; y: number; size: number; color: string; opacity: number }[] = []; + const cols = 18; + const rows = 10; + const spacingX = 1200 / cols; + const spacingY = 630 / rows; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const distFromTopLeft = Math.sqrt(c * c + r * r) / Math.sqrt(cols * cols + rows * rows); + const dotSize = Math.max(3, 28 * (1 - distFromTopLeft)); + const isTeal = (r + c) % 3 !== 0; + dots.push({ + x: c * spacingX + spacingX / 2, + y: r * spacingY + spacingY / 2, + size: dotSize, + color: isTeal ? '#2dd4bf' : '#f59e0b', + opacity: 0.15 + 0.55 * (1 - distFromTopLeft), + }); + } + } + return dots; +} + +const DOTS = generateHalftoneDots(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Halftone dots */} + {DOTS.map((dot, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v27-dot-grid.tsx b/packages/app/src/app/blog/[slug]/og-variants/v27-dot-grid.tsx new file mode 100644 index 0000000..813ebbf --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v27-dot-grid.tsx @@ -0,0 +1,149 @@ +/** + * V27: Dot Grid — uniform dot grid background like graph paper but with dots. + * Subtle evenly spaced small dots on a warm dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +function generateGridDots() { + const dots: { x: number; y: number }[] = []; + const spacing = 32; + for (let y = spacing; y < 630; y += spacing) { + for (let x = spacing; x < 1200; x += spacing) { + dots.push({ x, y }); + } + } + return dots; +} + +const GRID_DOTS = generateGridDots(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Grid dots */} + {GRID_DOTS.map((dot, i) => ( +
+ ))} + + {/* Highlight region — slightly brighter dots near center */} + {GRID_DOTS.filter((dot) => { + const dx = dot.x - 600; + const dy = dot.y - 315; + return Math.sqrt(dx * dx + dy * dy) < 250; + }).map((dot, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v28-constellation.tsx b/packages/app/src/app/blog/[slug]/og-variants/v28-constellation.tsx new file mode 100644 index 0000000..f1b6b7b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v28-constellation.tsx @@ -0,0 +1,218 @@ +/** + * V28: Constellation — dots connected by thin lines creating a star-map effect. + * Dark navy background with white and ice-blue dots and connecting lines. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Star { + x: number; + y: number; + size: number; + bright: boolean; +} + +interface Line { + x1: number; + y1: number; + x2: number; + y2: number; + length: number; + angle: number; +} + +function generateConstellation() { + const stars: Star[] = [ + { x: 80, y: 60, size: 5, bright: true }, + { x: 220, y: 120, size: 3, bright: false }, + { x: 150, y: 230, size: 4, bright: true }, + { x: 350, y: 80, size: 3, bright: false }, + { x: 450, y: 180, size: 6, bright: true }, + { x: 600, y: 60, size: 4, bright: false }, + { x: 700, y: 140, size: 5, bright: true }, + { x: 850, y: 50, size: 3, bright: false }, + { x: 950, y: 120, size: 4, bright: true }, + { x: 1100, y: 80, size: 5, bright: false }, + { x: 1050, y: 200, size: 3, bright: true }, + { x: 900, y: 250, size: 4, bright: false }, + { x: 130, y: 450, size: 3, bright: false }, + { x: 280, y: 520, size: 5, bright: true }, + { x: 420, y: 480, size: 4, bright: false }, + { x: 560, y: 550, size: 3, bright: true }, + { x: 750, y: 500, size: 5, bright: false }, + { x: 900, y: 560, size: 4, bright: true }, + { x: 1050, y: 480, size: 3, bright: false }, + { x: 1150, y: 550, size: 4, bright: true }, + { x: 50, y: 350, size: 3, bright: false }, + { x: 1140, y: 320, size: 4, bright: true }, + { x: 500, y: 40, size: 3, bright: true }, + { x: 780, y: 580, size: 3, bright: false }, + ]; + + const connections: [number, number][] = [ + [0, 1], + [1, 2], + [1, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10], + [10, 11], + [6, 11], + [12, 13], + [13, 14], + [14, 15], + [15, 16], + [16, 17], + [17, 18], + [18, 19], + [2, 12], + [4, 14], + [11, 21], + [0, 20], + [5, 22], + [17, 23], + ]; + + const lines: Line[] = connections.map(([a, b]) => { + const dx = stars[b].x - stars[a].x; + const dy = stars[b].y - stars[a].y; + const length = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx) * (180 / Math.PI); + return { x1: stars[a].x, y1: stars[a].y, x2: stars[b].x, y2: stars[b].y, length, angle }; + }); + + return { stars, lines }; +} + +const { stars: STARS, lines: LINES } = generateConstellation(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Connecting lines */} + {LINES.map((line, i) => ( +
+ ))} + + {/* Stars */} + {STARS.map((star, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v29-particle-burst.tsx b/packages/app/src/app/blog/[slug]/og-variants/v29-particle-burst.tsx new file mode 100644 index 0000000..f0926ec --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v29-particle-burst.tsx @@ -0,0 +1,210 @@ +/** + * V29: Particle Burst — dots and short lines radiating outward from bottom-left corner. + * Creates an energy/explosion feel with positioned elements fanning out. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Particle { + x: number; + y: number; + size: number; + color: string; + opacity: number; +} + +interface Ray { + x: number; + y: number; + length: number; + angle: number; + color: string; + opacity: number; +} + +function generateBurst() { + const particles: Particle[] = []; + const rays: Ray[] = []; + const originX = -20; + const originY = 660; + const colors = ['#f97316', '#fb923c', '#fbbf24', '#f59e0b', '#ef4444', '#fcd34d']; + + for (let i = 0; i < 45; i++) { + const angle = -Math.PI / 2 + (Math.random() * Math.PI) / 1.8 + 0.15; + const dist = 80 + Math.random() * 700; + const x = originX + Math.cos(angle) * dist; + const y = originY + Math.sin(angle) * dist; + const size = 4 + Math.random() * 14; + const distRatio = dist / 780; + particles.push({ + x, + y, + size, + color: colors[i % colors.length], + opacity: 0.2 + 0.6 * (1 - distRatio), + }); + } + + for (let i = 0; i < 20; i++) { + const angle = (-Math.PI / 2 + (Math.random() * Math.PI) / 1.8 + 0.15) * (180 / Math.PI); + const dist = 50 + Math.random() * 500; + const radAngle = (angle * Math.PI) / 180; + const x = originX + Math.cos(radAngle) * dist; + const y = originY + Math.sin(radAngle) * dist; + const length = 20 + Math.random() * 60; + rays.push({ + x, + y, + length, + angle, + color: colors[i % colors.length], + opacity: 0.15 + Math.random() * 0.25, + }); + } + + return { particles, rays }; +} + +const { particles: PARTICLES, rays: RAYS } = generateBurst(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Glow at origin */} +
+ + {/* Rays */} + {RAYS.map((ray, i) => ( +
+ ))} + + {/* Particles */} + {PARTICLES.map((p, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v30-dot-border.tsx b/packages/app/src/app/blog/[slug]/og-variants/v30-dot-border.tsx new file mode 100644 index 0000000..f205484 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v30-dot-border.tsx @@ -0,0 +1,187 @@ +/** + * V30: Dot Border — dots arranged along all four edges creating a dotted frame. + * Gold dots with varied sizes on a dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface BorderDot { + x: number; + y: number; + size: number; + opacity: number; +} + +function generateBorderDots() { + const dots: BorderDot[] = []; + const margin = 24; + const spacing = 28; + const sizes = [5, 6, 7, 8, 6, 5, 7, 8, 6, 7]; + + // Top edge + for (let x = margin; x < 1200 - margin; x += spacing) { + const idx = Math.floor(x / spacing) % sizes.length; + dots.push({ x, y: margin, size: sizes[idx], opacity: 0.5 + (idx % 3) * 0.15 }); + } + // Bottom edge + for (let x = margin; x < 1200 - margin; x += spacing) { + const idx = Math.floor(x / spacing) % sizes.length; + dots.push({ + x, + y: 630 - margin, + size: sizes[(idx + 3) % sizes.length], + opacity: 0.5 + (idx % 3) * 0.15, + }); + } + // Left edge (skip corners) + for (let y = margin + spacing; y < 630 - margin; y += spacing) { + const idx = Math.floor(y / spacing) % sizes.length; + dots.push({ + x: margin, + y, + size: sizes[(idx + 1) % sizes.length], + opacity: 0.5 + (idx % 3) * 0.15, + }); + } + // Right edge (skip corners) + for (let y = margin + spacing; y < 630 - margin; y += spacing) { + const idx = Math.floor(y / spacing) % sizes.length; + dots.push({ + x: 1200 - margin, + y, + size: sizes[(idx + 5) % sizes.length], + opacity: 0.5 + (idx % 3) * 0.15, + }); + } + + // Inner border (second ring, slightly smaller dots) + const innerMargin = 50; + const innerSpacing = 36; + // Top inner + for (let x = innerMargin; x < 1200 - innerMargin; x += innerSpacing) { + dots.push({ x, y: innerMargin, size: 4, opacity: 0.3 }); + } + // Bottom inner + for (let x = innerMargin; x < 1200 - innerMargin; x += innerSpacing) { + dots.push({ x, y: 630 - innerMargin, size: 4, opacity: 0.3 }); + } + // Left inner + for (let y = innerMargin + innerSpacing; y < 630 - innerMargin; y += innerSpacing) { + dots.push({ x: innerMargin, y, size: 4, opacity: 0.3 }); + } + // Right inner + for (let y = innerMargin + innerSpacing; y < 630 - innerMargin; y += innerSpacing) { + dots.push({ x: 1200 - innerMargin, y, size: 4, opacity: 0.3 }); + } + + return dots; +} + +const BORDER_DOTS = generateBorderDots(); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Border dots */} + {BORDER_DOTS.map((dot, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u2014 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u2014 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v31-concentric-rings.tsx b/packages/app/src/app/blog/[slug]/og-variants/v31-concentric-rings.tsx new file mode 100644 index 0000000..bb120ec --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v31-concentric-rings.tsx @@ -0,0 +1,142 @@ +/** + * V31: Concentric Rings — large circles centered on the right side, partially off-screen. + * Creates depth with 7 rings of decreasing opacity using border-only circles. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const RINGS = [ + { radius: 120, border: 2, opacity: 0.5, color: '#06b6d4' }, + { radius: 200, border: 2, opacity: 0.42, color: '#22d3ee' }, + { radius: 290, border: 1.5, opacity: 0.34, color: '#67e8f9' }, + { radius: 390, border: 1.5, opacity: 0.26, color: '#06b6d4' }, + { radius: 500, border: 1, opacity: 0.18, color: '#22d3ee' }, + { radius: 620, border: 1, opacity: 0.12, color: '#67e8f9' }, + { radius: 760, border: 1, opacity: 0.07, color: '#06b6d4' }, +]; + +const CENTER_X = 1050; +const CENTER_Y = 315; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Concentric rings */} + {RINGS.map((ring, i) => ( +
+ ))} + + {/* Center glow */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v32-venn-overlap.tsx b/packages/app/src/app/blog/[slug]/og-variants/v32-venn-overlap.tsx new file mode 100644 index 0000000..21dbc51 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v32-venn-overlap.tsx @@ -0,0 +1,166 @@ +/** + * V32: Venn Overlap — 3 large translucent circles overlapping Venn-diagram style. + * Each circle uses a different color (teal, gold, blue) with content over the intersection. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const CIRCLES = [ + { cx: 480, cy: 240, r: 220, color: '#0d9488', opacity: 0.2 }, + { cx: 720, cy: 240, r: 220, color: '#2563eb', opacity: 0.2 }, + { cx: 600, cy: 440, r: 220, color: '#d97706', opacity: 0.2 }, +]; + +const CIRCLE_BORDERS = [ + { cx: 480, cy: 240, r: 220, color: '#14b8a6', opacity: 0.4 }, + { cx: 720, cy: 240, r: 220, color: '#3b82f6', opacity: 0.4 }, + { cx: 600, cy: 440, r: 220, color: '#f59e0b', opacity: 0.4 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Venn circles — fills */} + {CIRCLES.map((c, i) => ( +
+ ))} + + {/* Venn circles — borders */} + {CIRCLE_BORDERS.map((c, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Top row: logo + tags */} +
+
+ + + InferenceX + +
+ {meta.tags && meta.tags.length > 0 && ( +
+ {meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+ )} +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v33-floating-bubbles.tsx b/packages/app/src/app/blog/[slug]/og-variants/v33-floating-bubbles.tsx new file mode 100644 index 0000000..9ca793a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v33-floating-bubbles.tsx @@ -0,0 +1,151 @@ +/** + * V33: Floating Bubbles — various sized circles scattered across the background. + * Different opacities and colors for a playful, airy feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Bubble { + x: number; + y: number; + size: number; + color: string; + opacity: number; + hasBorder: boolean; +} + +const BUBBLES: Bubble[] = [ + { x: 60, y: 40, size: 50, color: '#a78bfa', opacity: 0.2, hasBorder: false }, + { x: 180, y: 130, size: 18, color: '#34d399', opacity: 0.35, hasBorder: false }, + { x: 90, y: 280, size: 36, color: '#f472b6', opacity: 0.15, hasBorder: true }, + { x: 300, y: 60, size: 12, color: '#fbbf24', opacity: 0.5, hasBorder: false }, + { x: 420, y: 30, size: 28, color: '#60a5fa', opacity: 0.2, hasBorder: true }, + { x: 550, y: 80, size: 60, color: '#a78bfa', opacity: 0.1, hasBorder: false }, + { x: 700, y: 25, size: 20, color: '#34d399', opacity: 0.4, hasBorder: false }, + { x: 850, y: 60, size: 42, color: '#f472b6', opacity: 0.12, hasBorder: true }, + { x: 1000, y: 40, size: 14, color: '#fbbf24', opacity: 0.45, hasBorder: false }, + { x: 1100, y: 100, size: 80, color: '#60a5fa', opacity: 0.08, hasBorder: false }, + { x: 1140, y: 250, size: 30, color: '#a78bfa', opacity: 0.25, hasBorder: false }, + { x: 50, y: 480, size: 44, color: '#34d399', opacity: 0.15, hasBorder: true }, + { x: 200, y: 550, size: 22, color: '#f472b6', opacity: 0.3, hasBorder: false }, + { x: 380, y: 510, size: 56, color: '#fbbf24', opacity: 0.1, hasBorder: false }, + { x: 520, y: 570, size: 16, color: '#60a5fa', opacity: 0.4, hasBorder: false }, + { x: 680, y: 530, size: 38, color: '#a78bfa', opacity: 0.18, hasBorder: true }, + { x: 820, y: 490, size: 10, color: '#34d399', opacity: 0.5, hasBorder: false }, + { x: 940, y: 560, size: 70, color: '#f472b6', opacity: 0.07, hasBorder: false }, + { x: 1080, y: 500, size: 26, color: '#fbbf24', opacity: 0.22, hasBorder: false }, + { x: 1150, y: 420, size: 48, color: '#60a5fa', opacity: 0.1, hasBorder: true }, + { x: 30, y: 380, size: 8, color: '#a78bfa', opacity: 0.55, hasBorder: false }, + { x: 1060, y: 340, size: 15, color: '#34d399', opacity: 0.35, hasBorder: false }, + { x: 750, y: 470, size: 24, color: '#fbbf24', opacity: 0.28, hasBorder: false }, + { x: 350, y: 420, size: 32, color: '#60a5fa', opacity: 0.14, hasBorder: true }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Bubbles */} + {BUBBLES.map((b, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v34-ripple-waves.tsx b/packages/app/src/app/blog/[slug]/og-variants/v34-ripple-waves.tsx new file mode 100644 index 0000000..84c61d8 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v34-ripple-waves.tsx @@ -0,0 +1,182 @@ +/** + * V34: Ripple Waves — concentric quarter-circles emanating from the bottom-left corner. + * Thin borders with decreasing opacity creating a radar/sonar feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const RIPPLES = [ + { radius: 100, opacity: 0.55, width: 2 }, + { radius: 190, opacity: 0.45, width: 2 }, + { radius: 290, opacity: 0.38, width: 1.5 }, + { radius: 400, opacity: 0.3, width: 1.5 }, + { radius: 520, opacity: 0.22, width: 1 }, + { radius: 650, opacity: 0.16, width: 1 }, + { radius: 790, opacity: 0.1, width: 1 }, + { radius: 940, opacity: 0.06, width: 1 }, + { radius: 1100, opacity: 0.03, width: 1 }, +]; + +const ORIGIN_X = 0; +const ORIGIN_Y = 630; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Ripple quarter-circles from bottom-left */} + {RIPPLES.map((ripple, i) => ( +
+ ))} + + {/* Origin glow */} +
+ + {/* Pulse dot at origin */} +
+ + {/* Scan line accent — a thicker arc */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt — positioned toward upper right to avoid ripple overlap */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v35-corner-arcs.tsx b/packages/app/src/app/blog/[slug]/og-variants/v35-corner-arcs.tsx new file mode 100644 index 0000000..8dcdcaf --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v35-corner-arcs.tsx @@ -0,0 +1,224 @@ +/** + * V35: Corner Arcs — quarter-circle arcs in each corner of the image. + * Different sizes and colors per corner creating a framing effect with content centered. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface CornerArc { + corner: 'tl' | 'tr' | 'bl' | 'br'; + radius: number; + color: string; + opacity: number; + borderWidth: number; +} + +const ARCS: CornerArc[] = [ + // Top-left corner — rose/pink arcs + { corner: 'tl', radius: 180, color: '#f43f5e', opacity: 0.35, borderWidth: 2.5 }, + { corner: 'tl', radius: 130, color: '#fb7185', opacity: 0.25, borderWidth: 2 }, + { corner: 'tl', radius: 80, color: '#fda4af', opacity: 0.18, borderWidth: 1.5 }, + + // Top-right corner — cyan arcs + { corner: 'tr', radius: 220, color: '#06b6d4', opacity: 0.3, borderWidth: 2.5 }, + { corner: 'tr', radius: 160, color: '#22d3ee', opacity: 0.22, borderWidth: 2 }, + { corner: 'tr', radius: 100, color: '#67e8f9', opacity: 0.15, borderWidth: 1.5 }, + + // Bottom-left corner — amber arcs + { corner: 'bl', radius: 200, color: '#f59e0b', opacity: 0.3, borderWidth: 2.5 }, + { corner: 'bl', radius: 140, color: '#fbbf24', opacity: 0.2, borderWidth: 2 }, + + // Bottom-right corner — violet arcs + { corner: 'br', radius: 160, color: '#8b5cf6', opacity: 0.35, borderWidth: 2.5 }, + { corner: 'br', radius: 110, color: '#a78bfa', opacity: 0.25, borderWidth: 2 }, + { corner: 'br', radius: 60, color: '#c4b5fd', opacity: 0.18, borderWidth: 1.5 }, +]; + +function getArcPosition(arc: CornerArc) { + const d = arc.radius * 2; + switch (arc.corner) { + case 'tl': + return { left: -arc.radius, top: -arc.radius, width: d, height: d }; + case 'tr': + return { left: 1200 - arc.radius, top: -arc.radius, width: d, height: d }; + case 'bl': + return { left: -arc.radius, top: 630 - arc.radius, width: d, height: d }; + case 'br': + return { left: 1200 - arc.radius, top: 630 - arc.radius, width: d, height: d }; + } +} + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Corner arcs */} + {ARCS.map((arc, i) => { + const pos = getArcPosition(arc); + return ( +
+ ); + })} + + {/* Corner accent dots */} +
+
+
+
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + \u00b7 + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + \u00b7 + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v36-scan-lines.tsx b/packages/app/src/app/blog/[slug]/og-variants/v36-scan-lines.tsx new file mode 100644 index 0000000..423bc73 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v36-scan-lines.tsx @@ -0,0 +1,113 @@ +/** + * V36: Scan Lines — CRT/retro monitor effect with many thin horizontal lines + * spanning full width, spaced evenly apart. Green-tinted lines on dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0f0a'; +const LINE_COLOR = '#00ff4118'; +const LINE_BRIGHT = '#00ff4130'; +const ACCENT = '#00ff41'; + +// Generate scan lines every 10px, with occasional brighter ones +const scanLines = Array.from({ length: 63 }, (_, i) => ({ + top: i * 10, + color: i % 5 === 0 ? LINE_BRIGHT : LINE_COLOR, + height: i % 5 === 0 ? 2 : 1, +})); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Scan lines */} + {scanLines.map((line, i) => ( +
+ ))} + + {/* Phosphor glow bar at top */} +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + // + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v37-vertical-blinds.tsx b/packages/app/src/app/blog/[slug]/og-variants/v37-vertical-blinds.tsx new file mode 100644 index 0000000..21db99b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v37-vertical-blinds.tsx @@ -0,0 +1,130 @@ +/** + * V37: Vertical Blinds — Alternating vertical bars of slightly different shades, + * like window blinds. Subtle background texture with varying stripe widths. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#1a1a2e'; + +// Vertical blinds with varying widths and alternating shades +const blinds = [ + { left: 0, width: 55, color: '#1e1e34' }, + { left: 55, width: 70, color: '#22223a' }, + { left: 125, width: 45, color: '#1c1c30' }, + { left: 170, width: 80, color: '#24243e' }, + { left: 250, width: 50, color: '#1e1e34' }, + { left: 300, width: 65, color: '#20203a' }, + { left: 365, width: 75, color: '#1c1c32' }, + { left: 440, width: 40, color: '#262642' }, + { left: 480, width: 60, color: '#1e1e36' }, + { left: 540, width: 70, color: '#22223c' }, + { left: 610, width: 55, color: '#1a1a30' }, + { left: 665, width: 80, color: '#24243e' }, + { left: 745, width: 45, color: '#202038' }, + { left: 790, width: 65, color: '#1c1c34' }, + { left: 855, width: 50, color: '#262640' }, + { left: 905, width: 75, color: '#1e1e36' }, + { left: 980, width: 60, color: '#22223a' }, + { left: 1040, width: 45, color: '#1c1c32' }, + { left: 1085, width: 70, color: '#24243e' }, + { left: 1155, width: 45, color: '#202038' }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Vertical blinds */} + {blinds.map((b, i) => ( +
+ ))} + + {/* Thin separator lines between blinds */} + {blinds.map((b, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v38-crosshatch.tsx b/packages/app/src/app/blog/[slug]/og-variants/v38-crosshatch.tsx new file mode 100644 index 0000000..3cc2368 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v38-crosshatch.tsx @@ -0,0 +1,153 @@ +/** + * V38: Crosshatch — Diagonal line segments crossing in both directions, + * creating a woven/textile feel. Simulated with short positioned line segments. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#121216'; +const HATCH_COLOR_A = '#ffffff08'; +const HATCH_COLOR_B = '#ffffff06'; + +// Forward-slash diagonal segments (top-left to bottom-right) — short horizontal lines offset +const forwardLines: { left: number; top: number; width: number }[] = []; +for (let col = -2; col < 30; col++) { + for (let row = 0; row < 16; row++) { + forwardLines.push({ + left: col * 45 + row * 3, + top: row * 40, + width: 35, + }); + } +} + +// Backslash diagonal segments (top-right to bottom-left) — perpendicular set +const backLines: { left: number; top: number; height: number }[] = []; +for (let col = 0; col < 28; col++) { + for (let row = -2; row < 18; row++) { + backLines.push({ + left: col * 45 + row * 3, + top: row * 40, + height: 35, + }); + } +} + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + // Limit the number of elements for perf — take a subset that covers the canvas + const fwdSubset = forwardLines.filter((_, i) => i % 3 === 0).slice(0, 80); + const bkSubset = backLines.filter((_, i) => i % 3 === 0).slice(0, 80); + + return new ImageResponse( +
+ {/* Forward hatch lines (horizontal segments, staggered) */} + {fwdSubset.map((l, i) => ( +
+ ))} + + {/* Back hatch lines (vertical segments, staggered) */} + {bkSubset.map((l, i) => ( +
+ ))} + + {/* Accent crosshatch nodes at intersections */} + {Array.from({ length: 12 }, (_, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v39-sound-wave.tsx b/packages/app/src/app/blog/[slug]/og-variants/v39-sound-wave.tsx new file mode 100644 index 0000000..f07ee5b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v39-sound-wave.tsx @@ -0,0 +1,134 @@ +/** + * V39: Sound Wave — Audio waveform/equalizer visualization with vertical bars + * of varying heights arranged at the bottom. Teal/cyan bars on dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0e14'; +const BAR_COLOR = '#06b6d4'; + +// 48 bars creating a waveform shape — heights follow a bell-curve-ish pattern +const barHeights = [ + 20, 35, 25, 50, 40, 65, 30, 80, 55, 95, 45, 110, 70, 130, 85, 150, 100, 170, 120, 185, 140, 200, + 160, 210, 180, 215, 190, 210, 175, 195, 155, 180, 135, 160, 110, 140, 90, 120, 70, 95, 50, 75, 35, + 55, 25, 40, 20, 30, +]; + +const barWidth = 16; +const barGap = 9; +const totalBarsWidth = barHeights.length * (barWidth + barGap) - barGap; +const startX = Math.floor((1200 - totalBarsWidth) / 2); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Waveform bars at the bottom */} + {barHeights.map((h, i) => ( +
+ ))} + + {/* Bar caps — bright tops */} + {barHeights.map((h, i) => ( +
+ ))} + + {/* Horizontal baseline */} +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v40-radial-rays.tsx b/packages/app/src/app/blog/[slug]/og-variants/v40-radial-rays.tsx new file mode 100644 index 0000000..2383d98 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v40-radial-rays.tsx @@ -0,0 +1,154 @@ +/** + * V40: Radial Rays — Thin lines radiating outward from the bottom center + * like a sunrise. Gold/amber rays fanning upward on dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0c0a08'; +const RAY_COLOR_A = '#f59e0b12'; +const RAY_COLOR_B = '#f59e0b08'; +const GOLD = '#f59e0b'; + +// Generate rays from bottom center (600, 630) fanning out +// Each ray is a tall thin div positioned at bottom-center, rotated by offset +// Since transforms aren't reliable in Satori, we simulate with lines from origin to top +const originX = 600; +const originY = 630; +const rayCount = 30; + +const rays: { endX: number; endY: number; color: string }[] = []; +for (let i = 0; i < rayCount; i++) { + const angle = -Math.PI / 2 + ((i - rayCount / 2) / rayCount) * Math.PI * 0.9; + const length = 800; + rays.push({ + endX: originX + Math.cos(angle) * length, + endY: originY + Math.sin(angle) * length, + color: i % 3 === 0 ? RAY_COLOR_A : RAY_COLOR_B, + }); +} + +// Approximate each ray as a very thin tall div — we use the horizontal/vertical decomposition +// For each ray, place a 1px-wide vertical div and a 1px-tall horizontal div +function raySegments(endX: number, endY: number, segments: number) { + const segs: { left: number; top: number; width: number; height: number }[] = []; + for (let s = 0; s < segments; s++) { + const t = s / segments; + const x = originX + (endX - originX) * t; + const y = originY + (endY - originY) * t; + const nextT = (s + 1) / segments; + const nx = originX + (endX - originX) * nextT; + const ny = originY + (endY - originY) * nextT; + segs.push({ + left: Math.min(x, nx), + top: Math.min(y, ny), + width: Math.max(1, Math.abs(nx - x)), + height: Math.max(1, Math.abs(ny - y)), + }); + } + return segs; +} + +// For perf, use fewer segments per ray +const allSegments = rays.flatMap((ray, ri) => + raySegments(ray.endX, ray.endY, 20).map((seg) => ({ ...seg, color: ray.color, ri })), +); + +// Limit total segments +const limitedSegments = allSegments.filter((_, i) => i % 2 === 0).slice(0, 200); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Ray segments */} + {limitedSegments.map((seg, i) => ( +
+ ))} + + {/* Glow at origin point */} +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v41-isometric-grid.tsx b/packages/app/src/app/blog/[slug]/og-variants/v41-isometric-grid.tsx new file mode 100644 index 0000000..b7bdb72 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v41-isometric-grid.tsx @@ -0,0 +1,175 @@ +/** + * V41: Isometric Grid — Diamond/rhombus shapes arranged in an isometric grid pattern. + * Subtle, architectural feel using diamond-shaped borders. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#101018'; +const DIAMOND_BORDER = '#ffffff0a'; +const DIAMOND_ACCENT = '#6366f115'; + +// Create diamond shapes using 4 border edges per diamond +// Each diamond is at grid position (col, row), offset every other row +const cellW = 80; +const cellH = 50; + +interface DiamondEdge { + left: number; + top: number; + width: number; + height: number; + isHorizontal: boolean; +} + +const diamonds: DiamondEdge[] = []; + +for (let row = -1; row < 14; row++) { + for (let col = -1; col < 17; col++) { + const offsetX = row % 2 === 0 ? 0 : cellW / 2; + const cx = col * cellW + offsetX; + const cy = row * cellH; + + // Top-right edge (horizontal segment) + diamonds.push({ left: cx, top: cy, width: cellW / 2, height: 1, isHorizontal: true }); + // Top-left edge + diamonds.push({ + left: cx - cellW / 2, + top: cy, + width: cellW / 2, + height: 1, + isHorizontal: true, + }); + // Left vertical half + diamonds.push({ + left: cx - cellW / 2, + top: cy, + width: 1, + height: cellH / 2, + isHorizontal: false, + }); + // Right vertical half + diamonds.push({ + left: cx + cellW / 2, + top: cy, + width: 1, + height: cellH / 2, + isHorizontal: false, + }); + } +} + +// Limit to a reasonable number +const visibleDiamonds = diamonds + .filter((d) => d.left >= -10 && d.left <= 1210 && d.top >= -10 && d.top <= 640) + .slice(0, 250); + +// Accent diamonds — a few filled +const accentPositions = [ + { left: 160, top: 50, size: 30 }, + { left: 880, top: 100, size: 24 }, + { left: 400, top: 500, size: 20 }, + { left: 1040, top: 450, size: 28 }, + { left: 720, top: 30, size: 22 }, + { left: 200, top: 400, size: 26 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Isometric grid lines */} + {visibleDiamonds.map((d, i) => ( +
+ ))} + + {/* Accent filled diamonds (squares approximation) */} + {accentPositions.map((a, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v42-blueprint.tsx b/packages/app/src/app/blog/[slug]/og-variants/v42-blueprint.tsx new file mode 100644 index 0000000..8ab55ac --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v42-blueprint.tsx @@ -0,0 +1,243 @@ +/** + * V42: Blueprint — Blue grid lines on dark navy background. Major grid lines + * every ~100px, minor every ~25px. White text for content. Engineering feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a1628'; +const MAJOR_LINE = '#1e40af20'; +const MINOR_LINE = '#1e40af0c'; +const ACCENT = '#3b82f6'; + +// Major grid lines (every 100px) +const majorHorizontal = Array.from({ length: 7 }, (_, i) => i * 100); +const majorVertical = Array.from({ length: 13 }, (_, i) => i * 100); + +// Minor grid lines (every 25px) +const minorHorizontal = Array.from({ length: 26 }, (_, i) => i * 25).filter((v) => v % 100 !== 0); +const minorVertical = Array.from({ length: 49 }, (_, i) => i * 25).filter((v) => v % 100 !== 0); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Minor horizontal lines */} + {minorHorizontal.map((y, i) => ( +
+ ))} + + {/* Minor vertical lines */} + {minorVertical.map((x, i) => ( +
+ ))} + + {/* Major horizontal lines */} + {majorHorizontal.map((y, i) => ( +
+ ))} + + {/* Major vertical lines */} + {majorVertical.map((x, i) => ( +
+ ))} + + {/* Blueprint corner marks */} + {/* Top-left */} +
+
+ {/* Top-right */} +
+
+ {/* Bottom-left */} +
+
+ {/* Bottom-right */} +
+
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v43-glitch-grid.tsx b/packages/app/src/app/blog/[slug]/og-variants/v43-glitch-grid.tsx new file mode 100644 index 0000000..656dbe3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v43-glitch-grid.tsx @@ -0,0 +1,182 @@ +/** + * V43: Glitch Grid — Grid lines that are interrupted, offset, or missing segments. + * Digital glitch aesthetic with some lines in different colors (red, cyan). Dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0a0c'; + +// Horizontal grid segments — intentionally broken/offset +const hSegments = [ + // Row 1 — broken + { left: 0, top: 80, width: 350, color: '#ffffff0a' }, + { left: 400, top: 82, width: 280, color: '#ef444420' }, + { left: 720, top: 80, width: 480, color: '#ffffff08' }, + // Row 2 — shifted + { left: 0, top: 160, width: 600, color: '#ffffff0a' }, + { left: 650, top: 163, width: 550, color: '#06b6d418' }, + // Row 3 — partial + { left: 100, top: 240, width: 200, color: '#ffffff0a' }, + { left: 500, top: 240, width: 400, color: '#ffffff08' }, + { left: 950, top: 238, width: 250, color: '#ef444415' }, + // Row 4 — full but faint + { left: 0, top: 320, width: 1200, color: '#ffffff06' }, + // Row 5 — glitched + { left: 0, top: 400, width: 180, color: '#06b6d418' }, + { left: 220, top: 402, width: 350, color: '#ffffff0a' }, + { left: 620, top: 398, width: 200, color: '#ef444418' }, + { left: 860, top: 400, width: 340, color: '#ffffff08' }, + // Row 6 + { left: 50, top: 480, width: 500, color: '#ffffff0a' }, + { left: 600, top: 482, width: 300, color: '#06b6d415' }, + { left: 940, top: 480, width: 260, color: '#ffffff08' }, + // Row 7 — near bottom + { left: 0, top: 560, width: 400, color: '#ffffff06' }, + { left: 500, top: 558, width: 700, color: '#ef444410' }, +]; + +// Vertical grid segments — also broken +const vSegments = [ + { left: 100, top: 0, height: 280, color: '#ffffff08' }, + { left: 100, top: 320, height: 310, color: '#ffffff06' }, + { left: 200, top: 50, height: 580, color: '#ffffff06' }, + { left: 300, top: 0, height: 200, color: '#06b6d412' }, + { left: 302, top: 240, height: 390, color: '#ffffff08' }, + { left: 400, top: 0, height: 630, color: '#ffffff06' }, + { left: 500, top: 100, height: 250, color: '#ffffff08' }, + { left: 500, top: 400, height: 230, color: '#ef444412' }, + { left: 600, top: 0, height: 400, color: '#ffffff06' }, + { left: 602, top: 440, height: 190, color: '#06b6d415' }, + { left: 700, top: 0, height: 630, color: '#ffffff06' }, + { left: 800, top: 0, height: 160, color: '#ef444410' }, + { left: 798, top: 200, height: 430, color: '#ffffff08' }, + { left: 900, top: 50, height: 580, color: '#ffffff06' }, + { left: 1000, top: 0, height: 320, color: '#ffffff08' }, + { left: 1000, top: 360, height: 270, color: '#06b6d412' }, + { left: 1100, top: 0, height: 630, color: '#ffffff06' }, +]; + +// Glitch artifacts — bright offset rectangles +const glitchArtifacts = [ + { left: 380, top: 78, width: 20, height: 6, color: '#ef444430' }, + { left: 640, top: 160, width: 12, height: 8, color: '#06b6d435' }, + { left: 940, top: 236, width: 16, height: 5, color: '#ef444425' }, + { left: 210, top: 400, width: 14, height: 7, color: '#06b6d430' }, + { left: 810, top: 396, width: 22, height: 6, color: '#ef444428' }, + { left: 590, top: 480, width: 12, height: 5, color: '#06b6d425' }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Horizontal broken grid segments */} + {hSegments.map((s, i) => ( +
+ ))} + + {/* Vertical broken grid segments */} + {vSegments.map((s, i) => ( +
+ ))} + + {/* Glitch artifacts */} + {glitchArtifacts.map((g, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v44-perspective-lines.tsx b/packages/app/src/app/blog/[slug]/og-variants/v44-perspective-lines.tsx new file mode 100644 index 0000000..17efb09 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v44-perspective-lines.tsx @@ -0,0 +1,180 @@ +/** + * V44: Perspective Lines — Lines converging toward a vanishing point, creating + * depth illusion. Lines spread from center-bottom outward. Subtle dark grey on darker bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#08080c'; +const LINE_COLOR = '#ffffff08'; +const LINE_BRIGHT = '#ffffff10'; + +// Vanishing point at center-bottom +const vpX = 600; +const vpY = 580; + +// Generate perspective lines from vanishing point outward to top edge +// Each line goes from VP to a point along the top/sides +const perspectiveTargets: { x: number; y: number }[] = []; + +// Fan across the top edge +for (let i = 0; i <= 24; i++) { + perspectiveTargets.push({ x: i * 50, y: 0 }); +} + +// A few to the sides at mid-height +perspectiveTargets.push({ x: 0, y: 150 }); +perspectiveTargets.push({ x: 0, y: 300 }); +perspectiveTargets.push({ x: 0, y: 450 }); +perspectiveTargets.push({ x: 1200, y: 150 }); +perspectiveTargets.push({ x: 1200, y: 300 }); +perspectiveTargets.push({ x: 1200, y: 450 }); + +// For each line, create segments from VP to target +function lineSegments(tx: number, ty: number, count: number) { + const segs: { left: number; top: number; width: number; height: number }[] = []; + for (let s = 0; s < count; s++) { + const t0 = s / count; + const t1 = (s + 1) / count; + const x0 = vpX + (tx - vpX) * t0; + const y0 = vpY + (ty - vpY) * t0; + const x1 = vpX + (tx - vpX) * t1; + const y1 = vpY + (ty - vpY) * t1; + segs.push({ + left: Math.round(Math.min(x0, x1)), + top: Math.round(Math.min(y0, y1)), + width: Math.max(1, Math.round(Math.abs(x1 - x0))), + height: Math.max(1, Math.round(Math.abs(y1 - y0))), + }); + } + return segs; +} + +const allLineSegs = perspectiveTargets.flatMap((t, ti) => + lineSegments(t.x, t.y, 15).map((seg) => ({ + ...seg, + color: ti % 4 === 0 ? LINE_BRIGHT : LINE_COLOR, + })), +); + +// Horizontal perspective lines — getting closer together toward VP +const horizLines = [ + { top: 100, color: LINE_COLOR }, + { top: 200, color: LINE_BRIGHT }, + { top: 300, color: LINE_COLOR }, + { top: 400, color: LINE_COLOR }, + { top: 480, color: LINE_BRIGHT }, + { top: 540, color: LINE_COLOR }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Converging line segments */} + {allLineSegs.map((seg, i) => ( +
+ ))} + + {/* Horizontal perspective lines */} + {horizLines.map((h, i) => ( +
+ ))} + + {/* Vanishing point marker */} +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v45-topographic.tsx b/packages/app/src/app/blog/[slug]/og-variants/v45-topographic.tsx new file mode 100644 index 0000000..53df01a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v45-topographic.tsx @@ -0,0 +1,161 @@ +/** + * V45: Topographic — Organic, wavy contour lines creating an elevation map effect. + * Lines slightly waver using positioned divs with borderTop. Earthy tones. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#12110e'; +const CONTOUR_A = '#a0845020'; +const CONTOUR_B = '#a0845015'; +const CONTOUR_C = '#a084500c'; +const EARTH = '#c4956a'; + +// Contour lines — each is a series of short horizontal segments at slightly varying Y positions +// to create the wavy contour effect +function makeContourLine( + baseY: number, + color: string, + amplitude: number, + frequency: number, +): { left: number; top: number; width: number; color: string }[] { + const segments: { left: number; top: number; width: number; color: string }[] = []; + const segWidth = 30; + for (let x = 0; x < 1200; x += segWidth) { + // Simple wave: sin-based offset + const wave = Math.sin((x / 1200) * Math.PI * frequency) * amplitude; + const wave2 = Math.sin((x / 1200) * Math.PI * frequency * 2.3 + 1.5) * (amplitude * 0.4); + segments.push({ + left: x, + top: Math.round(baseY + wave + wave2), + width: segWidth + 1, // +1 to avoid gaps + color, + }); + } + return segments; +} + +const contourLines = [ + ...makeContourLine(60, CONTOUR_C, 8, 3), + ...makeContourLine(100, CONTOUR_B, 12, 2.5), + ...makeContourLine(140, CONTOUR_A, 10, 4), + ...makeContourLine(190, CONTOUR_B, 15, 2), + ...makeContourLine(240, CONTOUR_C, 8, 3.5), + ...makeContourLine(280, CONTOUR_A, 12, 2.8), + ...makeContourLine(330, CONTOUR_B, 10, 3.2), + ...makeContourLine(380, CONTOUR_C, 14, 2.2), + ...makeContourLine(420, CONTOUR_A, 8, 4.2), + ...makeContourLine(470, CONTOUR_B, 12, 2.6), + ...makeContourLine(510, CONTOUR_C, 10, 3.8), + ...makeContourLine(560, CONTOUR_A, 14, 2.4), + ...makeContourLine(600, CONTOUR_B, 8, 3.6), +]; + +// Elevation markers — small circles at key intersections +const markers = [ + { left: 180, top: 135 }, + { left: 520, top: 195 }, + { left: 850, top: 280 }, + { left: 350, top: 420 }, + { left: 700, top: 470 }, + { left: 1020, top: 560 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Contour line segments */} + {contourLines.map((seg, i) => ( +
+ ))} + + {/* Elevation markers */} + {markers.map((m, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v46-ocean-depths.tsx b/packages/app/src/app/blog/[slug]/og-variants/v46-ocean-depths.tsx new file mode 100644 index 0000000..c48e0c8 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v46-ocean-depths.tsx @@ -0,0 +1,189 @@ +/** + * V46: Ocean Depths — deep navy bg with cyan and deep teal accents. + * Layered horizontal wave-like bars at bottom. Fluid, underwater feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a1628'; +const CYAN = '#00d4ff'; +const TEAL = '#006994'; + +// Wave bars at the bottom — layered horizontal bands with varying widths and offsets +const waveBars = [ + { bottom: 0, left: 0, width: 1200, height: 18, color: TEAL, opacity: 0.9 }, + { bottom: 18, left: 60, width: 1080, height: 10, color: CYAN, opacity: 0.15 }, + { bottom: 30, left: 0, width: 900, height: 14, color: TEAL, opacity: 0.6 }, + { bottom: 46, left: 200, width: 1000, height: 8, color: CYAN, opacity: 0.25 }, + { bottom: 56, left: 0, width: 700, height: 12, color: TEAL, opacity: 0.4 }, + { bottom: 70, left: 300, width: 900, height: 6, color: CYAN, opacity: 0.18 }, + { bottom: 80, left: 50, width: 500, height: 10, color: TEAL, opacity: 0.3 }, + { bottom: 94, left: 400, width: 800, height: 5, color: CYAN, opacity: 0.12 }, + { bottom: 102, left: 0, width: 350, height: 8, color: TEAL, opacity: 0.2 }, + { bottom: 114, left: 600, width: 600, height: 4, color: CYAN, opacity: 0.1 }, + { bottom: 122, left: 100, width: 250, height: 6, color: TEAL, opacity: 0.15 }, + { bottom: 132, left: 700, width: 500, height: 3, color: CYAN, opacity: 0.08 }, +]; + +// Subtle floating particles for underwater atmosphere +const particles = [ + { x: 980, y: 80, size: 6, opacity: 0.12 }, + { x: 1100, y: 200, size: 4, opacity: 0.08 }, + { x: 850, y: 140, size: 5, opacity: 0.1 }, + { x: 1050, y: 320, size: 3, opacity: 0.06 }, + { x: 750, y: 60, size: 4, opacity: 0.09 }, + { x: 1140, y: 400, size: 5, opacity: 0.07 }, + { x: 900, y: 260, size: 3, opacity: 0.05 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Subtle deep-water tint band at top */} +
+ + {/* Vertical teal accent stripe on left */} +
+ + {/* Floating particles */} + {particles.map((p, i) => ( +
+ ))} + + {/* Wave bars at bottom */} + {waveBars.map((bar, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v47-sunset-fire.tsx b/packages/app/src/app/blog/[slug]/og-variants/v47-sunset-fire.tsx new file mode 100644 index 0000000..dbcf049 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v47-sunset-fire.tsx @@ -0,0 +1,178 @@ +/** + * V47: Sunset Fire — dark bg with overlapping warm-colored rectangles + * creating a sunset horizon effect at the top. Warm glow feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#1a0a0a'; +const ORANGE = '#ff6b35'; +const RED = '#c41e3a'; +const PURPLE = '#6b2fa0'; + +// Overlapping sunset rectangles at the top +const sunsetBlocks = [ + // Background layers — wide, spanning top + { left: 0, top: 0, width: 1200, height: 60, color: PURPLE, opacity: 0.5 }, + { left: 0, top: 8, width: 1200, height: 40, color: RED, opacity: 0.35 }, + { left: 0, top: 20, width: 1200, height: 28, color: ORANGE, opacity: 0.25 }, + // Overlapping blocks creating depth + { left: 0, top: 0, width: 400, height: 80, color: PURPLE, opacity: 0.6 }, + { left: 250, top: 10, width: 500, height: 65, color: RED, opacity: 0.5 }, + { left: 600, top: 5, width: 600, height: 70, color: ORANGE, opacity: 0.4 }, + { left: 100, top: 30, width: 350, height: 50, color: ORANGE, opacity: 0.35 }, + { left: 500, top: 20, width: 300, height: 55, color: PURPLE, opacity: 0.4 }, + { left: 800, top: 35, width: 400, height: 40, color: RED, opacity: 0.45 }, + // Smaller accent blocks + { left: 0, top: 70, width: 200, height: 20, color: ORANGE, opacity: 0.2 }, + { left: 350, top: 75, width: 150, height: 15, color: RED, opacity: 0.15 }, + { left: 700, top: 68, width: 250, height: 22, color: PURPLE, opacity: 0.18 }, + { left: 1000, top: 72, width: 200, height: 18, color: ORANGE, opacity: 0.12 }, +]; + +// Warm accent dots scattered — embers +const embers = [ + { x: 120, y: 130, size: 4, color: ORANGE, opacity: 0.2 }, + { x: 980, y: 150, size: 5, color: RED, opacity: 0.15 }, + { x: 550, y: 120, size: 3, color: ORANGE, opacity: 0.18 }, + { x: 1100, y: 180, size: 4, color: PURPLE, opacity: 0.12 }, + { x: 300, y: 160, size: 3, color: RED, opacity: 0.1 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Sunset horizon blocks */} + {sunsetBlocks.map((block, i) => ( +
+ ))} + + {/* Ember dots */} + {embers.map((e, i) => ( +
+ ))} + + {/* Bottom warm accent line */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v48-forest-canopy.tsx b/packages/app/src/app/blog/[slug]/og-variants/v48-forest-canopy.tsx new file mode 100644 index 0000000..46359ab --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v48-forest-canopy.tsx @@ -0,0 +1,201 @@ +/** + * V48: Forest Canopy — very dark green bg with scattered vertical rectangles + * of varying greens simulating tree trunks and leaves. Organic, natural. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a1a0a'; +const DARK_GREEN = '#1a5c1a'; +const MID_GREEN = '#2d8a2d'; +const LIGHT_GREEN = '#4caf50'; + +// Tree trunk-like vertical rectangles +const trunks = [ + { left: 30, top: 0, width: 12, height: 630, color: DARK_GREEN, opacity: 0.5 }, + { left: 80, top: 80, width: 8, height: 550, color: MID_GREEN, opacity: 0.3 }, + { left: 160, top: 0, width: 14, height: 400, color: DARK_GREEN, opacity: 0.35 }, + { left: 250, top: 200, width: 10, height: 430, color: MID_GREEN, opacity: 0.25 }, + { left: 950, top: 0, width: 16, height: 630, color: DARK_GREEN, opacity: 0.45 }, + { left: 1020, top: 50, width: 10, height: 580, color: MID_GREEN, opacity: 0.3 }, + { left: 1100, top: 0, width: 12, height: 500, color: DARK_GREEN, opacity: 0.4 }, + { left: 1160, top: 120, width: 8, height: 510, color: MID_GREEN, opacity: 0.2 }, +]; + +// Leaf-like scattered rectangles (small, various positions) +const leaves = [ + { left: 20, top: 40, width: 35, height: 8, color: LIGHT_GREEN, opacity: 0.15 }, + { left: 60, top: 70, width: 28, height: 6, color: MID_GREEN, opacity: 0.2 }, + { left: 140, top: 20, width: 40, height: 7, color: LIGHT_GREEN, opacity: 0.12 }, + { left: 100, top: 110, width: 22, height: 5, color: MID_GREEN, opacity: 0.18 }, + { left: 200, top: 60, width: 30, height: 8, color: DARK_GREEN, opacity: 0.25 }, + { left: 920, top: 30, width: 45, height: 8, color: LIGHT_GREEN, opacity: 0.14 }, + { left: 980, top: 90, width: 30, height: 6, color: MID_GREEN, opacity: 0.2 }, + { left: 1050, top: 45, width: 38, height: 7, color: LIGHT_GREEN, opacity: 0.1 }, + { left: 1080, top: 100, width: 25, height: 5, color: MID_GREEN, opacity: 0.16 }, + { left: 1130, top: 70, width: 32, height: 6, color: DARK_GREEN, opacity: 0.22 }, + // Bottom scattered foliage + { left: 40, top: 560, width: 50, height: 10, color: LIGHT_GREEN, opacity: 0.12 }, + { left: 150, top: 580, width: 35, height: 8, color: MID_GREEN, opacity: 0.1 }, + { left: 960, top: 570, width: 40, height: 9, color: LIGHT_GREEN, opacity: 0.1 }, + { left: 1070, top: 550, width: 30, height: 7, color: MID_GREEN, opacity: 0.08 }, +]; + +// Small dot-like elements representing forest floor +const dots = [ + { x: 50, y: 600, size: 4, opacity: 0.15 }, + { x: 120, y: 610, size: 3, opacity: 0.1 }, + { x: 200, y: 595, size: 5, opacity: 0.08 }, + { x: 1000, y: 605, size: 4, opacity: 0.12 }, + { x: 1120, y: 590, size: 3, opacity: 0.1 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Tree trunks */} + {trunks.map((t, i) => ( +
+ ))} + + {/* Leaf rectangles */} + {leaves.map((l, i) => ( +
+ ))} + + {/* Forest floor dots */} + {dots.map((d, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v49-arctic-frost.tsx b/packages/app/src/app/blog/[slug]/og-variants/v49-arctic-frost.tsx new file mode 100644 index 0000000..3463abb --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v49-arctic-frost.tsx @@ -0,0 +1,197 @@ +/** + * V49: Arctic Frost — very dark blue-grey bg with light icy accents. + * Scattered small crystalline shapes (thin bordered diamonds/squares). Clean, cold, premium. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0d1117'; +const ICE_LIGHT = '#e0f2ff'; +const ICE_MID = '#b3d9ff'; +const ICE_DIM = '#6ba3cc'; + +// Crystalline shapes — thin bordered squares scattered across the image +const crystals: { + x: number; + y: number; + size: number; + borderColor: string; + opacity: number; + borderWidth: number; +}[] = [ + { x: 80, y: 50, size: 24, borderColor: ICE_LIGHT, opacity: 0.15, borderWidth: 1 }, + { x: 150, y: 180, size: 16, borderColor: ICE_MID, opacity: 0.12, borderWidth: 1 }, + { x: 60, y: 320, size: 20, borderColor: ICE_LIGHT, opacity: 0.1, borderWidth: 1 }, + { x: 200, y: 450, size: 14, borderColor: ICE_MID, opacity: 0.08, borderWidth: 1 }, + { x: 1020, y: 40, size: 28, borderColor: ICE_LIGHT, opacity: 0.14, borderWidth: 1 }, + { x: 1100, y: 160, size: 18, borderColor: ICE_MID, opacity: 0.1, borderWidth: 1 }, + { x: 950, y: 280, size: 22, borderColor: ICE_LIGHT, opacity: 0.12, borderWidth: 1 }, + { x: 1060, y: 400, size: 15, borderColor: ICE_MID, opacity: 0.08, borderWidth: 1 }, + { x: 1150, y: 320, size: 12, borderColor: ICE_LIGHT, opacity: 0.06, borderWidth: 1 }, + { x: 500, y: 30, size: 10, borderColor: ICE_MID, opacity: 0.07, borderWidth: 1 }, + { x: 700, y: 580, size: 12, borderColor: ICE_LIGHT, opacity: 0.06, borderWidth: 1 }, + { x: 350, y: 570, size: 16, borderColor: ICE_MID, opacity: 0.08, borderWidth: 1 }, +]; + +// Small ice particle dots +const iceParticles = [ + { x: 120, y: 100, size: 3, opacity: 0.18 }, + { x: 300, y: 60, size: 2, opacity: 0.12 }, + { x: 1080, y: 90, size: 3, opacity: 0.15 }, + { x: 900, y: 200, size: 2, opacity: 0.1 }, + { x: 180, y: 400, size: 2, opacity: 0.08 }, + { x: 1000, y: 500, size: 3, opacity: 0.1 }, + { x: 600, y: 20, size: 2, opacity: 0.06 }, + { x: 850, y: 590, size: 2, opacity: 0.05 }, +]; + +// Thin horizontal frost lines +const frostLines = [ + { top: 0, left: 0, width: 1200, height: 1, opacity: 0.15 }, + { top: 629, left: 0, width: 1200, height: 1, opacity: 0.15 }, + { top: 160, left: 0, width: 80, height: 1, opacity: 0.08 }, + { top: 160, left: 1120, width: 80, height: 1, opacity: 0.08 }, + { top: 470, left: 0, width: 60, height: 1, opacity: 0.06 }, + { top: 470, left: 1140, width: 60, height: 1, opacity: 0.06 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Frost lines */} + {frostLines.map((line, i) => ( +
+ ))} + + {/* Crystalline bordered squares */} + {crystals.map((c, i) => ( +
+ ))} + + {/* Ice particles */} + {iceParticles.map((p, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v50-volcanic-ember.tsx b/packages/app/src/app/blog/[slug]/og-variants/v50-volcanic-ember.tsx new file mode 100644 index 0000000..819cbdc --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v50-volcanic-ember.tsx @@ -0,0 +1,173 @@ +/** + * V50: Volcanic Ember — near-black bg with red and orange accent elements. + * Glowing ember dots and short lines scattered. Dramatic, intense. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0a0504'; +const RED = '#dc2626'; +const ORANGE = '#f97316'; + +// Glowing ember dots — scattered across the image +const embers: { x: number; y: number; size: number; color: string; opacity: number }[] = [ + { x: 60, y: 80, size: 6, color: RED, opacity: 0.5 }, + { x: 180, y: 45, size: 4, color: ORANGE, opacity: 0.35 }, + { x: 320, y: 120, size: 5, color: RED, opacity: 0.3 }, + { x: 90, y: 200, size: 3, color: ORANGE, opacity: 0.25 }, + { x: 250, y: 300, size: 7, color: RED, opacity: 0.2 }, + { x: 140, y: 450, size: 4, color: ORANGE, opacity: 0.3 }, + { x: 50, y: 540, size: 5, color: RED, opacity: 0.35 }, + { x: 300, y: 500, size: 3, color: ORANGE, opacity: 0.2 }, + { x: 900, y: 50, size: 5, color: RED, opacity: 0.4 }, + { x: 1050, y: 100, size: 4, color: ORANGE, opacity: 0.3 }, + { x: 1130, y: 220, size: 6, color: RED, opacity: 0.25 }, + { x: 980, y: 350, size: 3, color: ORANGE, opacity: 0.2 }, + { x: 1100, y: 430, size: 5, color: RED, opacity: 0.35 }, + { x: 1020, y: 550, size: 4, color: ORANGE, opacity: 0.25 }, + { x: 880, y: 480, size: 3, color: RED, opacity: 0.15 }, + { x: 1160, y: 580, size: 6, color: ORANGE, opacity: 0.3 }, + // Central sparse embers + { x: 500, y: 30, size: 3, color: RED, opacity: 0.12 }, + { x: 700, y: 580, size: 3, color: ORANGE, opacity: 0.1 }, + { x: 600, y: 600, size: 4, color: RED, opacity: 0.15 }, +]; + +// Short glowing lines — like cracks in cooling lava +const cracks = [ + { left: 30, top: 150, width: 40, height: 2, color: RED, opacity: 0.3 }, + { left: 200, top: 250, width: 25, height: 2, color: ORANGE, opacity: 0.2 }, + { left: 100, top: 380, width: 35, height: 2, color: RED, opacity: 0.25 }, + { left: 60, top: 500, width: 30, height: 2, color: ORANGE, opacity: 0.3 }, + { left: 1000, top: 170, width: 45, height: 2, color: RED, opacity: 0.25 }, + { left: 1080, top: 310, width: 30, height: 2, color: ORANGE, opacity: 0.2 }, + { left: 950, top: 450, width: 35, height: 2, color: RED, opacity: 0.15 }, + { left: 1120, top: 520, width: 25, height: 2, color: ORANGE, opacity: 0.2 }, + // Bottom lava cracks + { left: 0, top: 625, width: 1200, height: 3, color: RED, opacity: 0.4 }, + { left: 0, top: 627, width: 800, height: 2, color: ORANGE, opacity: 0.2 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Ember dots */} + {embers.map((e, i) => ( +
+ ))} + + {/* Lava cracks */} + {cracks.map((c, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v51-royal-purple.tsx b/packages/app/src/app/blog/[slug]/og-variants/v51-royal-purple.tsx new file mode 100644 index 0000000..a49785d --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v51-royal-purple.tsx @@ -0,0 +1,316 @@ +/** + * V51: Royal Purple — deep purple bg with gold ornamental accents. + * Thin horizontal rules, corner elements. Rich, luxurious feel. Cream text. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#1a0a2e'; +const GOLD = '#F7B041'; +const CREAM = '#faf5e4'; +const GOLD_DIM = '#b8862d'; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top gold border */} +
+ + {/* Bottom gold border */} +
+ + {/* Top-left corner ornament — horizontal */} +
+ {/* Top-left corner ornament — vertical */} +
+ + {/* Top-right corner ornament — horizontal */} +
+ {/* Top-right corner ornament — vertical */} +
+ + {/* Bottom-left corner ornament — horizontal */} +
+ {/* Bottom-left corner ornament — vertical */} +
+ + {/* Bottom-right corner ornament — horizontal */} +
+ {/* Bottom-right corner ornament — vertical */} +
+ + {/* Thin horizontal gold rule — upper */} +
+ + {/* Thin horizontal gold rule — lower */} +
+ + {/* Small gold diamond accents along the rules */} +
+
+ + {/* Subtle purple depth panels on sides */} +
+
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v52-copper-patina.tsx b/packages/app/src/app/blog/[slug]/og-variants/v52-copper-patina.tsx new file mode 100644 index 0000000..4d43d9f --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v52-copper-patina.tsx @@ -0,0 +1,216 @@ +/** + * V52: Copper Patina — dark teal-grey bg with copper and verdigris accents. + * Aged/oxidized metal feel. Mix of border elements and solid accent bars. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0f1a1a'; +const COPPER = '#b87333'; +const VERDIGRIS = '#4a8c7f'; +const COPPER_DARK = '#8a5522'; +const VERDIGRIS_LIGHT = '#6aac9f'; + +// Solid accent bars — representing metal plate edges +const bars = [ + // Left edge copper plates + { left: 0, top: 0, width: 5, height: 180, color: COPPER, opacity: 0.6 }, + { left: 0, top: 200, width: 5, height: 140, color: VERDIGRIS, opacity: 0.4 }, + { left: 0, top: 360, width: 5, height: 270, color: COPPER, opacity: 0.5 }, + // Right edge patina + { left: 1195, top: 0, width: 5, height: 250, color: VERDIGRIS, opacity: 0.4 }, + { left: 1195, top: 270, width: 5, height: 160, color: COPPER, opacity: 0.5 }, + { left: 1195, top: 450, width: 5, height: 180, color: VERDIGRIS, opacity: 0.35 }, + // Top copper bar + { left: 0, top: 0, width: 1200, height: 3, color: COPPER, opacity: 0.5 }, + // Bottom verdigris bar + { left: 0, top: 627, width: 1200, height: 3, color: VERDIGRIS, opacity: 0.5 }, +]; + +// Bordered panel elements — oxidized metal plate outlines +const panels = [ + { left: 30, top: 30, width: 120, height: 80, borderColor: COPPER, opacity: 0.15 }, + { left: 1050, top: 30, width: 120, height: 80, borderColor: VERDIGRIS, opacity: 0.12 }, + { left: 30, top: 520, width: 100, height: 70, borderColor: VERDIGRIS, opacity: 0.1 }, + { left: 1070, top: 530, width: 100, height: 60, borderColor: COPPER, opacity: 0.12 }, +]; + +// Patina texture — scattered small horizontal marks +const marks = [ + { left: 40, top: 150, width: 30, height: 2, color: VERDIGRIS, opacity: 0.2 }, + { left: 100, top: 180, width: 20, height: 1, color: COPPER_DARK, opacity: 0.15 }, + { left: 60, top: 420, width: 25, height: 2, color: VERDIGRIS, opacity: 0.12 }, + { left: 1100, top: 180, width: 35, height: 2, color: COPPER, opacity: 0.18 }, + { left: 1050, top: 400, width: 28, height: 1, color: VERDIGRIS, opacity: 0.15 }, + { left: 1120, top: 350, width: 20, height: 2, color: COPPER_DARK, opacity: 0.1 }, +]; + +// Rivets — small dots like metal fasteners +const rivets = [ + { x: 35, y: 35, size: 5, color: COPPER }, + { x: 145, y: 35, size: 5, color: COPPER }, + { x: 35, y: 105, size: 5, color: COPPER }, + { x: 145, y: 105, size: 5, color: COPPER }, + { x: 1055, y: 35, size: 5, color: VERDIGRIS }, + { x: 1165, y: 35, size: 5, color: VERDIGRIS }, + { x: 1055, y: 105, size: 5, color: VERDIGRIS }, + { x: 1165, y: 105, size: 5, color: VERDIGRIS }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Accent bars */} + {bars.map((bar, i) => ( +
+ ))} + + {/* Bordered panel outlines */} + {panels.map((p, i) => ( +
+ ))} + + {/* Patina marks */} + {marks.map((m, i) => ( +
+ ))} + + {/* Rivets */} + {rivets.map((r, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v53-neon-night.tsx b/packages/app/src/app/blog/[slug]/og-variants/v53-neon-night.tsx new file mode 100644 index 0000000..8e736f2 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v53-neon-night.tsx @@ -0,0 +1,365 @@ +/** + * V53: Neon Night — black bg with hot pink and electric blue accent lines + * and borders. High contrast cyberpunk/nightclub aesthetic. Glowing line effects. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#050505'; +const PINK = '#ff1493'; +const BLUE = '#00e5ff'; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top neon line — pink */} +
+ {/* Top glow — pink (wider, dimmer) */} +
+ + {/* Bottom neon line — blue */} +
+ {/* Bottom glow — blue */} +
+ + {/* Left vertical neon — pink */} +
+
+ + {/* Right vertical neon — blue */} +
+
+ + {/* Diagonal accent lines — pink */} +
+
+ + {/* Diagonal accent lines — blue */} +
+
+ + {/* Neon bordered box — top right corner accent */} +
+ + {/* Neon bordered box — bottom left corner accent */} +
+ + {/* Scattered neon dots */} +
+
+
+
+ + {/* Horizontal mid accent lines */} +
+
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v54-sandstone.tsx b/packages/app/src/app/blog/[slug]/og-variants/v54-sandstone.tsx new file mode 100644 index 0000000..49d70d2 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v54-sandstone.tsx @@ -0,0 +1,209 @@ +/** + * V54: Sandstone — dark warm grey bg with terracotta, sand, and brown accents. + * Layered horizontal bands at edges suggesting geological strata. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#1a1714'; +const TERRACOTTA = '#c45a3c'; +const SAND = '#d4a853'; +const BROWN = '#8b6914'; + +// Top geological strata — horizontal bands +const topStrata = [ + { top: 0, left: 0, width: 1200, height: 8, color: TERRACOTTA, opacity: 0.5 }, + { top: 8, left: 0, width: 1200, height: 5, color: BROWN, opacity: 0.35 }, + { top: 13, left: 0, width: 1200, height: 10, color: SAND, opacity: 0.2 }, + { top: 23, left: 0, width: 1200, height: 4, color: TERRACOTTA, opacity: 0.25 }, + { top: 27, left: 0, width: 800, height: 6, color: BROWN, opacity: 0.15 }, + { top: 33, left: 200, width: 1000, height: 3, color: SAND, opacity: 0.1 }, + { top: 36, left: 0, width: 500, height: 4, color: TERRACOTTA, opacity: 0.1 }, +]; + +// Bottom geological strata — thicker, more prominent +const bottomStrata = [ + { bottom: 0, left: 0, width: 1200, height: 12, color: BROWN, opacity: 0.6 }, + { bottom: 12, left: 0, width: 1200, height: 8, color: TERRACOTTA, opacity: 0.45 }, + { bottom: 20, left: 0, width: 1200, height: 6, color: SAND, opacity: 0.3 }, + { bottom: 26, left: 0, width: 1200, height: 10, color: BROWN, opacity: 0.25 }, + { bottom: 36, left: 100, width: 1100, height: 5, color: TERRACOTTA, opacity: 0.2 }, + { bottom: 41, left: 0, width: 900, height: 4, color: SAND, opacity: 0.15 }, + { bottom: 45, left: 300, width: 900, height: 6, color: BROWN, opacity: 0.12 }, + { bottom: 51, left: 0, width: 600, height: 3, color: TERRACOTTA, opacity: 0.08 }, + { bottom: 54, left: 500, width: 700, height: 4, color: SAND, opacity: 0.06 }, +]; + +// Side accent — thin vertical sediment lines +const sideLines = [ + { left: 0, top: 50, width: 3, height: 530, color: TERRACOTTA, opacity: 0.3 }, + { left: 6, top: 80, width: 2, height: 460, color: BROWN, opacity: 0.2 }, + { left: 1197, top: 50, width: 3, height: 530, color: SAND, opacity: 0.25 }, + { left: 1192, top: 100, width: 2, height: 400, color: TERRACOTTA, opacity: 0.15 }, +]; + +// Scattered sediment fragments +const fragments = [ + { x: 40, y: 100, width: 20, height: 3, color: SAND, opacity: 0.12 }, + { x: 80, y: 160, width: 15, height: 2, color: TERRACOTTA, opacity: 0.1 }, + { x: 1100, y: 120, width: 25, height: 3, color: BROWN, opacity: 0.1 }, + { x: 1060, y: 180, width: 18, height: 2, color: SAND, opacity: 0.08 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top strata */} + {topStrata.map((s, i) => ( +
+ ))} + + {/* Bottom strata */} + {bottomStrata.map((s, i) => ( +
+ ))} + + {/* Side lines */} + {sideLines.map((l, i) => ( +
+ ))} + + {/* Sediment fragments */} + {fragments.map((f, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v55-monochrome-steel.tsx b/packages/app/src/app/blog/[slug]/og-variants/v55-monochrome-steel.tsx new file mode 100644 index 0000000..497c7dd --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v55-monochrome-steel.tsx @@ -0,0 +1,229 @@ +/** + * V55: Monochrome Steel — pure grey palette only. Multiple grey rectangles + * creating an industrial steel plate effect. No color at all. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BG = '#0e0e0e'; + +// Steel plate panels — overlapping grey rectangles creating industrial depth +const plates = [ + // Large background plates + { left: 0, top: 0, width: 400, height: 630, color: '#1a1a1a', opacity: 0.8 }, + { left: 800, top: 0, width: 400, height: 630, color: '#1a1a1a', opacity: 0.6 }, + // Mid-tone plates + { left: 380, top: 0, width: 40, height: 630, color: '#2a2a2a', opacity: 0.5 }, + { left: 780, top: 0, width: 40, height: 630, color: '#2a2a2a', opacity: 0.5 }, + // Horizontal plate joints + { left: 0, top: 0, width: 1200, height: 4, color: '#4a4a4a', opacity: 0.6 }, + { left: 0, top: 626, width: 1200, height: 4, color: '#4a4a4a', opacity: 0.6 }, + { left: 0, top: 200, width: 1200, height: 2, color: '#2a2a2a', opacity: 0.3 }, + { left: 0, top: 430, width: 1200, height: 2, color: '#2a2a2a', opacity: 0.3 }, + // Vertical plate seams + { left: 0, top: 0, width: 3, height: 630, color: '#4a4a4a', opacity: 0.5 }, + { left: 1197, top: 0, width: 3, height: 630, color: '#4a4a4a', opacity: 0.5 }, +]; + +// Bolt/rivet positions at plate intersections +const bolts = [ + // Top row + { x: 15, y: 15, size: 8 }, + { x: 395, y: 15, size: 8 }, + { x: 415, y: 15, size: 8 }, + { x: 795, y: 15, size: 8 }, + { x: 815, y: 15, size: 8 }, + { x: 1177, y: 15, size: 8 }, + // Bottom row + { x: 15, y: 607, size: 8 }, + { x: 395, y: 607, size: 8 }, + { x: 415, y: 607, size: 8 }, + { x: 795, y: 607, size: 8 }, + { x: 815, y: 607, size: 8 }, + { x: 1177, y: 607, size: 8 }, + // Mid row + { x: 15, y: 197, size: 6 }, + { x: 1177, y: 197, size: 6 }, + { x: 15, y: 427, size: 6 }, + { x: 1177, y: 427, size: 6 }, +]; + +// Surface texture — short horizontal scratches +const scratches = [ + { left: 50, top: 80, width: 40, height: 1, opacity: 0.08 }, + { left: 200, top: 150, width: 30, height: 1, opacity: 0.06 }, + { left: 500, top: 100, width: 50, height: 1, opacity: 0.05 }, + { left: 900, top: 130, width: 35, height: 1, opacity: 0.07 }, + { left: 1050, top: 60, width: 45, height: 1, opacity: 0.06 }, + { left: 100, top: 500, width: 30, height: 1, opacity: 0.05 }, + { left: 700, top: 520, width: 40, height: 1, opacity: 0.04 }, + { left: 1000, top: 480, width: 35, height: 1, opacity: 0.06 }, +]; + +// Highlight edges — subtle bright lines on plate edges for depth +const highlights = [ + { left: 400, top: 4, width: 1, height: 194, color: '#4a4a4a', opacity: 0.4 }, + { left: 800, top: 4, width: 1, height: 194, color: '#4a4a4a', opacity: 0.4 }, + { left: 400, top: 204, width: 1, height: 224, color: '#4a4a4a', opacity: 0.3 }, + { left: 800, top: 204, width: 1, height: 224, color: '#4a4a4a', opacity: 0.3 }, + { left: 400, top: 434, width: 1, height: 192, color: '#4a4a4a', opacity: 0.35 }, + { left: 800, top: 434, width: 1, height: 192, color: '#4a4a4a', opacity: 0.35 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Steel plates */} + {plates.map((p, i) => ( +
+ ))} + + {/* Highlight edges */} + {highlights.map((h, i) => ( +
+ ))} + + {/* Bolts */} + {bolts.map((b, i) => ( +
+ ))} + + {/* Surface scratches */} + {scratches.map((s, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + / + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + / + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v56-vertical-split.tsx b/packages/app/src/app/blog/[slug]/og-variants/v56-vertical-split.tsx new file mode 100644 index 0000000..59d2455 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v56-vertical-split.tsx @@ -0,0 +1,132 @@ +/** + * V56: Vertical Split — 40/60 vertical split with left teal panel and right dark area. + * Logo and tags in the left panel, title and excerpt on the right. Thin gold divider line. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Left 40% teal panel */} +
+ {/* Logo */} +
+ +
+ + {/* Tags */} +
+ {meta.tags && + meta.tags.slice(0, 4).map((tag) => ( +
+ {tag} +
+ ))} +
+ + {/* Author + date */} +
+
+ {meta.author} +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + {' \u00b7 '} + {meta.readingTime} min read +
+
+
+ + {/* Gold divider line */} +
+ + {/* Right 60% dark content area */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '\u2026' : meta.excerpt} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v57-top-banner.tsx b/packages/app/src/app/blog/[slug]/og-variants/v57-top-banner.tsx new file mode 100644 index 0000000..83fca5f --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v57-top-banner.tsx @@ -0,0 +1,141 @@ +/** + * V57: Top Banner — Wide colored horizontal band across the top 30% with logo and label. + * Main content below in dark area with title and excerpt. Corporate and clean. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top 30% banner */} +
+
+ +
+ InferenceX Blog +
+
+ +
+ {meta.tags && + meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+
+ + {/* Main content area */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v58-right-sidebar.tsx b/packages/app/src/app/blog/[slug]/og-variants/v58-right-sidebar.tsx new file mode 100644 index 0000000..ec2e3ac --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v58-right-sidebar.tsx @@ -0,0 +1,184 @@ +/** + * V58: Right Sidebar — Main content on left 70%, right 30% has a lighter info panel + * with author, date, reading time, and tags stacked vertically. Large bold title on left. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Left 70% main content */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Spacer */} +
+
+ + {/* Right 30% sidebar */} +
+ {/* Author */} +
+
+ Author +
+
+ {meta.author} +
+
+ + {/* Date */} +
+
+ Published +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+ + {/* Reading time */} +
+
+ Read Time +
+
+ {meta.readingTime} min +
+
+ + {/* Tags */} +
+
+ Tags +
+
+ {meta.tags && + meta.tags.slice(0, 4).map((tag) => ( +
+ {tag} +
+ ))} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v59-left-accent-bar.tsx b/packages/app/src/app/blog/[slug]/og-variants/v59-left-accent-bar.tsx new file mode 100644 index 0000000..8735666 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v59-left-accent-bar.tsx @@ -0,0 +1,102 @@ +/** + * V59: Left Accent Bar — A 6px wide gold vertical bar on the far left edge. + * Content indented from it, creating a pull-quote / editorial margin note feel. Very clean. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Gold accent bar */} +
+ + {/* Content indented from the bar */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v60-bottom-dock.tsx b/packages/app/src/app/blog/[slug]/og-variants/v60-bottom-dock.tsx new file mode 100644 index 0000000..5e45d97 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v60-bottom-dock.tsx @@ -0,0 +1,141 @@ +/** + * V60: Bottom Dock — Content (logo, title, excerpt) in the upper area. A distinct bottom + * panel with a different bg shade acts as a dock with author, date, reading time, tags in a row. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Upper content area */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+
+ + {/* Bottom dock panel */} +
+ {/* Left side: author, date, reading time */} +
+ {meta.author} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ {meta.readingTime} min read +
+ + {/* Right side: tags */} +
+ {meta.tags && + meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v61-z-layout.tsx b/packages/app/src/app/blog/[slug]/og-variants/v61-z-layout.tsx new file mode 100644 index 0000000..54bdb06 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v61-z-layout.tsx @@ -0,0 +1,208 @@ +/** + * V61: Z-Layout — Logo top-left, horizontal line across top, title center-right, + * diagonal connecting element, date/author bottom-left. Eye follows a Z-path across the card. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top horizontal line (Z top stroke) */} +
+ + {/* Diagonal connector elements (Z middle stroke) */} + {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} + + {/* Small diamond accents along the diagonal */} +
+
+ + {/* Bottom horizontal line (Z bottom stroke) */} +
+ + {/* Z top-left: Logo */} +
+ +
+ + {/* Z center-right: Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '\u2026' : meta.excerpt} +
+
+ + {/* Z bottom-left: Author + date */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+ + {/* Tags bottom-right */} +
+ {meta.tags && + meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v62-card-in-card.tsx b/packages/app/src/app/blog/[slug]/og-variants/v62-card-in-card.tsx new file mode 100644 index 0000000..1d874b3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v62-card-in-card.tsx @@ -0,0 +1,148 @@ +/** + * V62: Card-in-Card — Outer frame has a slightly lighter bg, inner card with visible + * border and borderRadius contains all content. Floating card effect with 24px gap. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Inner floating card */} +
+ {/* Header: logo + tags */} +
+
+ +
+
+ {meta.tags && + meta.tags.slice(0, 3).map((tag) => ( +
+ {tag} +
+ ))} +
+
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+ + {/* Gold accent dot */} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v63-three-column.tsx b/packages/app/src/app/blog/[slug]/og-variants/v63-three-column.tsx new file mode 100644 index 0000000..b809b68 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v63-three-column.tsx @@ -0,0 +1,189 @@ +/** + * V63: Three Column — Thin left column (60px) with decorative vertical line and dots, + * wide center column with title/excerpt, thin right column (60px) with decorative elements. + * Creates a columnar page feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Left decorative column */} +
+ {/* Vertical line */} +
+ {/* Decorative dots */} + {[80, 180, 280, 380, 480].map((top) => ( +
+ ))} +
+ + {/* Center content column */} +
+ {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+ + {/* Right decorative column */} +
+ {/* Vertical line */} +
+ {/* Decorative small squares */} + {[120, 250, 400, 530].map((top) => ( +
+ ))} + {/* Gold accent line segment */} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v64-ruled-sections.tsx b/packages/app/src/app/blog/[slug]/og-variants/v64-ruled-sections.tsx new file mode 100644 index 0000000..a0a9df6 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v64-ruled-sections.tsx @@ -0,0 +1,161 @@ +/** + * V64: Ruled Sections — Header/body/footer with divider lines. Three clear sections + * separated by thin horizontal lines. Header: logo + label. Body: title + excerpt. + * Footer: author + date + tags. Like a structured document. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Header section */} +
+
+ +
+ InferenceX Blog +
+
+
+ {meta.readingTime} min read +
+
+ + {/* Top divider */} +
+ + {/* Body section (takes most space) */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 150 ? meta.excerpt.slice(0, 150) + '\u2026' : meta.excerpt} +
+
+ + {/* Bottom divider */} +
+ + {/* Footer section */} +
+
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ {meta.tags && + meta.tags.slice(0, 4).map((tag) => ( +
+ {tag} +
+ ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v65-staircase-blocks.tsx b/packages/app/src/app/blog/[slug]/og-variants/v65-staircase-blocks.tsx new file mode 100644 index 0000000..d377217 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v65-staircase-blocks.tsx @@ -0,0 +1,145 @@ +/** + * V65: Staircase Blocks — Asymmetric overlapping rectangles stepping down from top-right + * to bottom-left, each a different shade. Content flows over them with zIndex. + * Creates dynamic layered depth. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const steps = [ + { left: 700, top: -30, width: 560, height: 220, color: '#14181e' }, + { left: 540, top: 120, width: 480, height: 200, color: '#161c22' }, + { left: 360, top: 240, width: 520, height: 190, color: '#181e24' }, + { left: 160, top: 350, width: 500, height: 180, color: '#1a2028' }, + { left: 0, top: 460, width: 560, height: 200, color: '#1c222c' }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Staircase blocks */} + {steps.map((step, i) => ( +
+ ))} + + {/* Gold accent on middle step edge */} +
+ + {/* Teal accent on second step */} +
+ + {/* Logo */} +
+ +
+ + {/* Title + excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v66-drop-cap.tsx b/packages/app/src/app/blog/[slug]/og-variants/v66-drop-cap.tsx new file mode 100644 index 0000000..11b8c32 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v66-drop-cap.tsx @@ -0,0 +1,104 @@ +/** + * V66: Drop Cap — Giant drop cap: the first letter of the title is rendered as a massive + * (200px) character in gold, positioned behind/beside the rest of the title. + * Creates a manuscript/book feel. Dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const firstLetter = meta.title.charAt(0).toUpperCase(); + const restOfTitle = meta.title.slice(1); + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Title area with drop cap */} +
+ {/* Drop cap letter */} +
+ {firstLetter} +
+ + {/* Rest of title */} +
+ {restOfTitle} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v67-all-caps-impact.tsx b/packages/app/src/app/blog/[slug]/og-variants/v67-all-caps-impact.tsx new file mode 100644 index 0000000..6e09c9d --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v67-all-caps-impact.tsx @@ -0,0 +1,88 @@ +/** + * V67: All Caps Impact — Title rendered in ALL CAPS with generous letterSpacing (6-8px). + * Heavy weight (900). No excerpt shown. Maximum readability. Title is THE design. + * Minimal footer. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Title — ALL CAPS, heavy weight, wide letter spacing */} +
+ {meta.title.toUpperCase()} +
+ + {/* Minimal footer */} +
+
{meta.author.toUpperCase()}
+
+
+ {new Date(meta.date + 'T00:00:00Z') + .toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }) + .toUpperCase()} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v68-serif-elegant.tsx b/packages/app/src/app/blog/[slug]/og-variants/v68-serif-elegant.tsx new file mode 100644 index 0000000..f325b8c --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v68-serif-elegant.tsx @@ -0,0 +1,120 @@ +/** + * V68: Serif Elegant — Simulate a serif/editorial feel with thin decorative horizontal + * rules above and below the title. Lighter font weight (400), generous lineHeight (1.5). + * Cream text (#faf5e4) on charcoal (#1a1a1a). Literary/refined. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Title section with decorative rules */} +
+ {/* Top decorative rule */} +
+ + {/* Title */} +
+ {meta.title} +
+ + {/* Bottom decorative rule */} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+ | +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v69-stacked-words.tsx b/packages/app/src/app/blog/[slug]/og-variants/v69-stacked-words.tsx new file mode 100644 index 0000000..969ca5e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v69-stacked-words.tsx @@ -0,0 +1,111 @@ +/** + * V69: Stacked Words — Each word of the title on its own line. Uses large fontSize + * and tight lineHeight. Creates a vertical waterfall of text. Left-aligned. Bold weight. + * Trims to first 5-6 words if title is long. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + + const words = meta.title.split(/\s+/); + const displayWords = words.length > 6 ? words.slice(0, 6) : words; + const wordCount = displayWords.length; + const wordSize = wordCount > 5 ? 52 : wordCount > 4 ? 58 : 64; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Stacked words */} +
+ {displayWords.map((word, i) => ( +
+ {word} +
+ ))} + {words.length > 6 && ( +
+ ... +
+ )} +
+ + {/* Footer */} +
+
{meta.author}
+
+ / +
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v70-underline-accent.tsx b/packages/app/src/app/blog/[slug]/og-variants/v70-underline-accent.tsx new file mode 100644 index 0000000..8929b7e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v70-underline-accent.tsx @@ -0,0 +1,111 @@ +/** + * V70: Underline Accent — Title with thick underline: a bold colored bar (6px tall, gold) + * directly under the title text. Simple but effective. The underline extends slightly + * beyond the text width. Clean, modern. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Title with underline accent */} +
+ {/* Title text */} +
+ {meta.title} +
+ + {/* Thick gold underline bar */} +
+ + {/* Excerpt */} +
+ {meta.excerpt && meta.excerpt.length > 100 + ? meta.excerpt.slice(0, 100) + '...' + : meta.excerpt} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v71-highlight-marker.tsx b/packages/app/src/app/blog/[slug]/og-variants/v71-highlight-marker.tsx new file mode 100644 index 0000000..f42ed41 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v71-highlight-marker.tsx @@ -0,0 +1,109 @@ +/** + * V71: Highlight Marker — Highlighted text effect: title has a semi-transparent colored + * background (like a highlighter marker). Uses backgroundColor on the title div. + * Yellow-green highlight (#e5f54020) on dark bg. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Title with highlighter effect */} +
+
+ {meta.title} +
+ + {/* Excerpt below */} +
+ {meta.excerpt && meta.excerpt.length > 110 + ? meta.excerpt.slice(0, 110) + '...' + : meta.excerpt} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+
{meta.readingTime}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v72-title-badge.tsx b/packages/app/src/app/blog/[slug]/og-variants/v72-title-badge.tsx new file mode 100644 index 0000000..e7d398e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v72-title-badge.tsx @@ -0,0 +1,101 @@ +/** + * V72: Title Badge — Title wrapped in a large bordered rectangle with rounded corners + * (borderRadius 16). Badge has a subtle border and slightly different bg. Makes the + * title feel like a label/tag. Content below. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 42 : meta.title.length > 40 ? 48 : 56; + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Badge with title */} +
+
+ {meta.title} +
+ + {/* Excerpt below badge */} +
+ {meta.excerpt && meta.excerpt.length > 100 + ? meta.excerpt.slice(0, 100) + '...' + : meta.excerpt} +
+
+ + {/* Footer */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v73-ultra-tall.tsx b/packages/app/src/app/blog/[slug]/og-variants/v73-ultra-tall.tsx new file mode 100644 index 0000000..855d123 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v73-ultra-tall.tsx @@ -0,0 +1,76 @@ +/** + * V73: Ultra Tall — Very large fontSize (80-96px) but narrow letterSpacing (-2px). + * Title dominates vertically. Minimal other content. Logo tiny in corner. Date small + * at bottom. All about the title presence. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + + const displayTitle = meta.title.length > 50 ? meta.title.slice(0, 50) + '...' : meta.title; + const titleSize = displayTitle.length > 40 ? 72 : 88; + + return new ImageResponse( +
+ {/* Tiny logo in top-left corner */} +
+ +
+ + {/* Massive title — dominates the frame */} +
+ {displayTitle} +
+ + {/* Minimal footer — small date and author */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v74-two-size.tsx b/packages/app/src/app/blog/[slug]/og-variants/v74-two-size.tsx new file mode 100644 index 0000000..71ff5de --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v74-two-size.tsx @@ -0,0 +1,104 @@ +/** + * V74: Two-Size Title — First word is HUGE (96px, gold), remaining words are normal + * size (48px, white) on the next line. Creates visual hierarchy within the title itself. + * Dynamic, editorial. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + + const words = meta.title.split(/\s+/); + const firstWord = words[0] || ''; + const remainingWords = words.slice(1).join(' '); + + return new ImageResponse( +
+ {/* Logo */} +
+ +
+ + {/* Two-size title */} +
+ {/* First word — massive and gold */} +
+ {firstWord} +
+ + {/* Remaining words — smaller and white */} + {remainingWords && ( +
+ {remainingWords} +
+ )} +
+ + {/* Footer */} +
+
{meta.author}
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v75-centered-zen.tsx b/packages/app/src/app/blog/[slug]/og-variants/v75-centered-zen.tsx new file mode 100644 index 0000000..78c3db7 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v75-centered-zen.tsx @@ -0,0 +1,96 @@ +/** + * V75: Centered Zen — Everything perfectly centered vertically and horizontally. + * Logo centered top, title centered middle, author+date centered bottom. Generous + * whitespace. Peaceful, balanced. Minimal decorative elements. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 44 : meta.title.length > 40 ? 52 : 60; + + return new ImageResponse( +
+ {/* Logo — centered top */} +
+ +
+ + {/* Title — centered middle */} +
+
+ {meta.title} +
+
+ + {/* Author + date — centered bottom */} +
+
{meta.author}
+
+
+ {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v76-terminal.tsx b/packages/app/src/app/blog/[slug]/og-variants/v76-terminal.tsx new file mode 100644 index 0000000..36eff10 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v76-terminal.tsx @@ -0,0 +1,170 @@ +/** + * V76: Terminal — Console/terminal window with green monospace text on black. + * Fake title bar with traffic-light dots, prompt prefix, command-style output. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Terminal border */} +
+ + {/* Title bar */} +
+ {/* Traffic light dots */} +
+
+
+
+ inferencex@blog ~ terminal +
+
+ + {/* Terminal body */} +
+ {/* Logo */} +
+ +
+ + {/* Title with prompt */} +
+
+ $ cat blog/post.md +
+
+ > {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '...' : meta.excerpt} +
+
+ + {/* Footer as command output */} +
+
+ author: + {meta.author} + | + date: + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + eta: + {meta.readingTime} min +
+
$ _
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v77-code-editor.tsx b/packages/app/src/app/blog/[slug]/og-variants/v77-code-editor.tsx new file mode 100644 index 0000000..5d473f0 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v77-code-editor.tsx @@ -0,0 +1,181 @@ +/** + * V77: Code Editor — Dark editor background with line numbers, syntax-highlighted title, + * and a file tab element at the top. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const lineNumbers = Array.from({ length: 22 }, (_, i) => i + 1); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Tab bar */} +
+ {/* Active tab */} +
+ TS + blog-post.tsx +
+ {/* Inactive tab */} +
+ MD + README.md +
+
+ + {/* Editor body */} +
+ {/* Line numbers gutter */} +
+ {lineNumbers.map((n) => ( +
+ {n} +
+ ))} +
+ + {/* Code content */} +
+ {/* Logo */} +
+ +
+ + {/* Code-style title */} +
+
{'// Blog post'}
+
+ export const + title + = +
+
+ "{meta.title}" +
+
+ /* {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '...' : meta.excerpt} */ +
+
+ + {/* Footer as code */} +
+ const + author + = + "{meta.author}" + , + date + = + + " + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + " + +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v78-matrix-rain.tsx b/packages/app/src/app/blog/[slug]/og-variants/v78-matrix-rain.tsx new file mode 100644 index 0000000..18bbe25 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v78-matrix-rain.tsx @@ -0,0 +1,160 @@ +/** + * V78: Matrix Rain — Dark green-black background with columns of characters/numbers + * at varying opacities, evoking the classic digital rain effect. White content overlaid. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Each column is a vertical string of characters at a specific x position +const columns = [ + { x: 30, chars: '7F2A9B1D4E', opacity: 0.08 }, + { x: 80, chars: '01A3C8F5B2', opacity: 0.12 }, + { x: 140, chars: '9D7E3A0C6F', opacity: 0.06 }, + { x: 200, chars: 'B4F1C8A02E', opacity: 0.15 }, + { x: 260, chars: '3A7D9F1B5C', opacity: 0.07 }, + { x: 330, chars: '8E2C6A0F4D', opacity: 0.1 }, + { x: 390, chars: 'F5B3D7A1C9', opacity: 0.05 }, + { x: 450, chars: '2E8A4F0C6B', opacity: 0.13 }, + { x: 520, chars: 'D1C7F3A9B5', opacity: 0.06 }, + { x: 580, chars: '6A0E4C8F2D', opacity: 0.09 }, + { x: 650, chars: 'B9D3F7A1C5', opacity: 0.14 }, + { x: 710, chars: '4E8A0C6F2B', opacity: 0.07 }, + { x: 780, chars: 'A3D9F5B1C7', opacity: 0.11 }, + { x: 840, chars: '0E6A4C8F2D', opacity: 0.05 }, + { x: 910, chars: 'F7B3D1A9C5', opacity: 0.12 }, + { x: 970, chars: '2E8A4F0C6B', opacity: 0.08 }, + { x: 1040, chars: '9D3F7A1C5B', opacity: 0.1 }, + { x: 1100, chars: '4E8A0C6F2D', opacity: 0.06 }, + { x: 1160, chars: 'B1C7F3A9D5', opacity: 0.09 }, +]; + +// Bright "lead" characters at various positions +const brightChars = [ + { x: 80, y: 120, char: 'A', opacity: 0.6 }, + { x: 200, y: 280, char: '7', opacity: 0.5 }, + { x: 450, y: 90, char: 'F', opacity: 0.7 }, + { x: 650, y: 400, char: '3', opacity: 0.55 }, + { x: 910, y: 200, char: 'C', opacity: 0.65 }, + { x: 1040, y: 480, char: '9', opacity: 0.5 }, + { x: 330, y: 520, char: 'E', opacity: 0.45 }, + { x: 780, y: 60, char: '1', opacity: 0.6 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Matrix rain columns */} + {columns.map((col, i) => ( +
+ {col.chars.split('').map((c, j) => ( + + {c} + + ))} +
+ ))} + + {/* Bright lead characters */} + {brightChars.map((bc, i) => ( +
+ {bc.char} +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + | + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v79-binary-accent.tsx b/packages/app/src/app/blog/[slug]/og-variants/v79-binary-accent.tsx new file mode 100644 index 0000000..d7d0531 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v79-binary-accent.tsx @@ -0,0 +1,166 @@ +/** + * V79: Binary Accent — Strings of "01" scattered across the background in low opacity, + * some horizontal, some vertical. Tech/crypto aesthetic with white and gold content. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Horizontal binary strings +const hLines = [ + { x: 20, y: 30, text: '01001101 10110010 01110001', opacity: 0.06 }, + { x: 300, y: 80, text: '11010011 01001110 10100101', opacity: 0.04 }, + { x: 50, y: 160, text: '10110100 01101001 11001010', opacity: 0.07 }, + { x: 600, y: 45, text: '01011010 10010111', opacity: 0.05 }, + { x: 800, y: 130, text: '11100110 01010011 10001101', opacity: 0.04 }, + { x: 100, y: 380, text: '01101110 10011001', opacity: 0.06 }, + { x: 700, y: 420, text: '10100011 01110100', opacity: 0.05 }, + { x: 20, y: 520, text: '01010110 11001011 10110001', opacity: 0.04 }, + { x: 500, y: 560, text: '11011001 01000110 10101110', opacity: 0.07 }, + { x: 900, y: 590, text: '01101010 10010011', opacity: 0.05 }, + { x: 400, y: 250, text: '10001110 01010101', opacity: 0.03 }, +]; + +// Vertical binary columns +const vCols = [ + { x: 60, topY: 200, chars: '10110100', opacity: 0.05 }, + { x: 250, topY: 50, chars: '01101011', opacity: 0.04 }, + { x: 480, topY: 300, chars: '110010', opacity: 0.06 }, + { x: 750, topY: 100, chars: '01010110', opacity: 0.03 }, + { x: 1000, topY: 250, chars: '1001101', opacity: 0.05 }, + { x: 1130, topY: 40, chars: '01100101', opacity: 0.04 }, + { x: 150, topY: 400, chars: '10011', opacity: 0.06 }, + { x: 900, topY: 350, chars: '011010', opacity: 0.04 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Horizontal binary strings */} + {hLines.map((line, i) => ( +
+ {line.text} +
+ ))} + + {/* Vertical binary columns */} + {vCols.map((col, i) => ( +
+ {col.chars.split('').map((c, j) => ( + + {c} + + ))} +
+ ))} + + {/* Subtle gold accent line */} +
+ + {/* Logo */} +
+ +
+ + {/* Title */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + | + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v80-network-nodes.tsx b/packages/app/src/app/blog/[slug]/og-variants/v80-network-nodes.tsx new file mode 100644 index 0000000..dfc7e09 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v80-network-nodes.tsx @@ -0,0 +1,186 @@ +/** + * V80: Network Nodes — Network topology with positioned circle nodes connected + * by thin lines. Cyan nodes on dark background. Content overlaid. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Network nodes +const nodes = [ + { x: 60, y: 50, s: 14, opacity: 0.6 }, + { x: 200, y: 100, s: 18, opacity: 0.8 }, + { x: 150, y: 250, s: 12, opacity: 0.5 }, + { x: 350, y: 60, s: 16, opacity: 0.7 }, + { x: 500, y: 30, s: 12, opacity: 0.4 }, + { x: 700, y: 70, s: 20, opacity: 0.9 }, + { x: 900, y: 50, s: 14, opacity: 0.6 }, + { x: 1050, y: 100, s: 16, opacity: 0.7 }, + { x: 1140, y: 60, s: 12, opacity: 0.5 }, + { x: 80, y: 450, s: 16, opacity: 0.6 }, + { x: 250, y: 530, s: 14, opacity: 0.5 }, + { x: 450, y: 500, s: 18, opacity: 0.7 }, + { x: 600, y: 560, s: 12, opacity: 0.4 }, + { x: 800, y: 520, s: 16, opacity: 0.8 }, + { x: 950, y: 480, s: 14, opacity: 0.6 }, + { x: 1100, y: 540, s: 20, opacity: 0.7 }, + { x: 30, y: 300, s: 12, opacity: 0.3 }, + { x: 1160, y: 320, s: 14, opacity: 0.4 }, +]; + +// Connection lines (horizontal and vertical segments) +const hLines = [ + { x: 67, y: 57, w: 133, opacity: 0.15 }, + { x: 207, y: 109, w: 143, opacity: 0.12 }, + { x: 366, y: 68, w: 134, opacity: 0.1 }, + { x: 512, y: 36, w: 188, opacity: 0.08 }, + { x: 720, y: 78, w: 180, opacity: 0.14 }, + { x: 914, y: 57, w: 136, opacity: 0.1 }, + { x: 1066, y: 108, w: 74, opacity: 0.12 }, + { x: 96, y: 458, w: 154, opacity: 0.13 }, + { x: 264, y: 537, w: 186, opacity: 0.1 }, + { x: 468, y: 508, w: 132, opacity: 0.11 }, + { x: 612, y: 566, w: 188, opacity: 0.09 }, + { x: 816, y: 528, w: 134, opacity: 0.14 }, + { x: 964, y: 488, w: 136, opacity: 0.12 }, +]; + +const vLines = [ + { x: 67, y: 64, h: 186, opacity: 0.1 }, + { x: 207, y: 118, h: 132, opacity: 0.08 }, + { x: 157, y: 262, h: 188, opacity: 0.07 }, + { x: 707, y: 90, h: 170, opacity: 0.1 }, + { x: 1057, y: 116, h: 204, opacity: 0.09 }, + { x: 87, y: 306, h: 144, opacity: 0.08 }, + { x: 807, y: 360, h: 160, opacity: 0.1 }, + { x: 1107, y: 380, h: 160, opacity: 0.09 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Connection lines (horizontal) */} + {hLines.map((l, i) => ( +
+ ))} + + {/* Connection lines (vertical) */} + {vLines.map((l, i) => ( +
+ ))} + + {/* Nodes */} + {nodes.map((n, i) => ( +
+ ))} + + {/* Logo */} +
+ +
+ + {/* Title */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + | + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v81-dashboard.tsx b/packages/app/src/app/blog/[slug]/og-variants/v81-dashboard.tsx new file mode 100644 index 0000000..235633e --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v81-dashboard.tsx @@ -0,0 +1,201 @@ +/** + * V81: Dashboard — Data dashboard panel with a top bar, KPI-style title display, + * data labels for author/date, and subtle grid lines in the background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Grid lines +const hGridLines = Array.from({ length: 12 }, (_, i) => ({ y: 60 + i * 50 })); +const vGridLines = Array.from({ length: 10 }, (_, i) => ({ x: 100 + i * 120 })); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Grid lines horizontal */} + {hGridLines.map((line, i) => ( +
+ ))} + + {/* Grid lines vertical */} + {vGridLines.map((line, i) => ( +
+ ))} + + {/* Top bar */} +
+
+
+ + INFERENCEX ANALYTICS + +
+
+ OVERVIEW + BLOG + METRICS +
+
+ + {/* Dashboard body */} +
+ {/* Logo + status indicators */} +
+ +
+
+ READ TIME + + {meta.readingTime}m + +
+
+ TAGS + + {meta.tags ? meta.tags.length : 0} + +
+
+
+ + {/* KPI-style title */} +
+
LATEST POST
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt} +
+
+ + {/* Data labels footer */} +
+
+ AUTHOR + {meta.author} +
+
+ PUBLISHED + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ STATUS +
+
+ Published +
+
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v82-chip-layout.tsx b/packages/app/src/app/blog/[slug]/og-variants/v82-chip-layout.tsx new file mode 100644 index 0000000..ede6abd --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v82-chip-layout.tsx @@ -0,0 +1,250 @@ +/** + * V82: Chip Layout — Processor chip aesthetic with a central bordered rectangle + * containing the title, thin "pins" extending from each side, and PCB-style background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Chip dimensions +const CHIP_LEFT = 160; +const CHIP_TOP = 130; +const CHIP_WIDTH = 880; +const CHIP_HEIGHT = 370; + +// Pins extending from each side +const topPins = Array.from({ length: 18 }, (_, i) => ({ + x: CHIP_LEFT + 40 + i * 48, + y: CHIP_TOP - 40, + w: 2, + h: 40, +})); + +const bottomPins = Array.from({ length: 18 }, (_, i) => ({ + x: CHIP_LEFT + 40 + i * 48, + y: CHIP_TOP + CHIP_HEIGHT, + w: 2, + h: 40, +})); + +const leftPins = Array.from({ length: 8 }, (_, i) => ({ + x: CHIP_LEFT - 40, + y: CHIP_TOP + 30 + i * 42, + w: 40, + h: 2, +})); + +const rightPins = Array.from({ length: 8 }, (_, i) => ({ + x: CHIP_LEFT + CHIP_WIDTH, + y: CHIP_TOP + 30 + i * 42, + w: 40, + h: 2, +})); + +// PCB traces extending from pins to edges +const traces = [ + { x: 0, y: 165, w: 120, h: 1 }, + { x: 0, y: 250, w: 120, h: 1 }, + { x: 0, y: 375, w: 120, h: 1 }, + { x: 1080, y: 190, w: 120, h: 1 }, + { x: 1080, y: 300, w: 120, h: 1 }, + { x: 1080, y: 420, w: 120, h: 1 }, + { x: 300, y: 0, w: 1, h: 90 }, + { x: 540, y: 0, w: 1, h: 90 }, + { x: 780, y: 0, w: 1, h: 90 }, + { x: 350, y: 540, w: 1, h: 90 }, + { x: 600, y: 540, w: 1, h: 90 }, + { x: 850, y: 540, w: 1, h: 90 }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 44 : meta.title.length > 40 ? 52 : 58; + + return new ImageResponse( +
+ {/* PCB traces */} + {traces.map((t, i) => ( +
+ ))} + + {/* Top pins */} + {topPins.map((p, i) => ( +
+ ))} + + {/* Bottom pins */} + {bottomPins.map((p, i) => ( +
+ ))} + + {/* Left pins */} + {leftPins.map((p, i) => ( +
+ ))} + + {/* Right pins */} + {rightPins.map((p, i) => ( +
+ ))} + + {/* Chip body */} +
+ {/* Corner notch indicator */} +
+ + {/* Logo + chip label */} +
+ + IX-BLOG-PROC +
+ + {/* Title */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 120 ? meta.excerpt.slice(0, 120) + '...' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + | + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + | + {meta.readingTime} min +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v83-data-table.tsx b/packages/app/src/app/blog/[slug]/og-variants/v83-data-table.tsx new file mode 100644 index 0000000..3f4b026 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v83-data-table.tsx @@ -0,0 +1,321 @@ +/** + * V83: Data Table — Spreadsheet/table layout with header row, main content row, + * and footer cells divided by grid lines throughout. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 44 : meta.title.length > 40 ? 52 : 58; + + return new ImageResponse( +
+ {/* Header row */} +
+ {/* Row number column */} +
+ # +
+ {/* Column A: Title */} +
+ TITLE +
+ {/* Column B: Status */} +
+ STATUS +
+ {/* Column C: Reading Time */} +
+ READ TIME +
+
+ + {/* Logo row */} +
+
+ 0 +
+
+ +
+
+ Blog +
+
+ - +
+
+ + {/* Main content row — title + excerpt */} +
+
+ 1 +
+
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '...' : meta.excerpt} +
+
+
+
+
+ Published +
+
+
+ {meta.readingTime} min +
+
+ + {/* Footer row — author & date in cells */} +
+
+ 2 +
+
+
+ Author: + {meta.author} +
+
+
+
+ Date: + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v84-progress-bar.tsx b/packages/app/src/app/blog/[slug]/og-variants/v84-progress-bar.tsx new file mode 100644 index 0000000..4353b5c --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v84-progress-bar.tsx @@ -0,0 +1,208 @@ +/** + * V84: Progress Bar — Futuristic HUD with a loading/progress bar, tech-style + * title display, reading time as percentage, and status indicators. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// HUD corner brackets +const corners = [ + // Top-left + { x: 30, y: 30, w: 40, h: 2, display: 'flex' as const }, + { x: 30, y: 30, w: 2, h: 40, display: 'flex' as const }, + // Top-right + { x: 1130, y: 30, w: 40, h: 2, display: 'flex' as const }, + { x: 1168, y: 30, w: 2, h: 40, display: 'flex' as const }, + // Bottom-left + { x: 30, y: 598, w: 40, h: 2, display: 'flex' as const }, + { x: 30, y: 560, w: 2, h: 40, display: 'flex' as const }, + // Bottom-right + { x: 1130, y: 598, w: 40, h: 2, display: 'flex' as const }, + { x: 1168, y: 560, w: 2, h: 40, display: 'flex' as const }, +]; + +// Decorative tick marks along the progress bar +const ticks = Array.from({ length: 20 }, (_, i) => ({ + x: 80 + i * 52, + y: 470, + h: i % 5 === 0 ? 12 : 6, +})); + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + const progressPercent = Math.min(100, Math.max(10, 100 - meta.readingTime * 5)); + const barWidth = Math.round(1040 * (progressPercent / 100)); + + return new ImageResponse( +
+ {/* HUD corner brackets */} + {corners.map((c, i) => ( +
+ ))} + + {/* Top scan line */} +
+ + {/* Logo + status */} +
+ +
+
+ SYSTEM ACTIVE +
+
+ + {/* Loading title */} +
+
+ LOADING CONTENT... +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 130 ? meta.excerpt.slice(0, 130) + '...' : meta.excerpt} +
+
+ + {/* Progress section */} +
+ {/* Tick marks */} +
+ {ticks.map((t, i) => ( +
+ ))} +
+ + {/* Progress bar track */} +
+ {/* Filled portion */} +
+
+ + {/* Labels under progress bar */} +
+
+ {meta.author} + | + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ {progressPercent}% + {meta.readingTime} min read +
+
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v85-api-docs.tsx b/packages/app/src/app/blog/[slug]/og-variants/v85-api-docs.tsx new file mode 100644 index 0000000..5875cf3 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v85-api-docs.tsx @@ -0,0 +1,203 @@ +/** + * V85: API Docs — Styled like REST API documentation with endpoint path, + * description, and response headers. Clean documentation-style layout. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 44 : meta.title.length > 40 ? 52 : 58; + + return new ImageResponse( +
+ {/* Top bar */} +
+
+ + API REFERENCE +
+
+ v1.0 +
+
+
+ + {/* Main content area */} +
+ {/* Endpoint */} +
+
+ GET +
+ /api/v1/blog/ + {'{'} + slug + {'}'} +
+ + {/* Separator */} +
+ + {/* Description (title) */} +
+ DESCRIPTION +
+ {meta.title} +
+
+ + {/* Excerpt as summary */} +
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt} +
+ + {/* Separator */} +
+ + {/* Response headers */} +
+ RESPONSE HEADERS +
+
+ X-Author: + {meta.author} +
+
+ X-Published: + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+
+ X-Read-Time: + {meta.readingTime} min +
+
+ Content-Type: + application/blog+markdown +
+
+
+
+ + {/* Bottom status bar */} +
+ Status: 200 OK +
+ {meta.tags && + meta.tags.slice(0, 3).map((tag, i) => ( + + #{tag} + + ))} +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v86-hexagon-cells.tsx b/packages/app/src/app/blog/[slug]/og-variants/v86-hexagon-cells.tsx new file mode 100644 index 0000000..b176e63 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v86-hexagon-cells.tsx @@ -0,0 +1,206 @@ +/** + * V86: Hexagon Cells — honeycomb pattern of hexagonal cell shapes in amber/gold tones. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface HexCell { + x: number; + y: number; + w: number; + h: number; + opacity: number; + border: boolean; +} + +const HEX_CELLS: HexCell[] = [ + // Row 1 + { x: 40, y: 20, w: 72, h: 80, opacity: 0.08, border: false }, + { x: 150, y: 20, w: 72, h: 80, opacity: 0.12, border: true }, + { x: 260, y: 20, w: 72, h: 80, opacity: 0.06, border: false }, + { x: 370, y: 20, w: 72, h: 80, opacity: 0.1, border: true }, + { x: 480, y: 20, w: 72, h: 80, opacity: 0.07, border: false }, + { x: 590, y: 20, w: 72, h: 80, opacity: 0.14, border: true }, + { x: 700, y: 20, w: 72, h: 80, opacity: 0.05, border: false }, + { x: 810, y: 20, w: 72, h: 80, opacity: 0.11, border: true }, + { x: 920, y: 20, w: 72, h: 80, opacity: 0.08, border: false }, + { x: 1030, y: 20, w: 72, h: 80, opacity: 0.13, border: true }, + { x: 1140, y: 20, w: 72, h: 80, opacity: 0.06, border: false }, + // Row 2 offset + { x: 95, y: 90, w: 72, h: 80, opacity: 0.1, border: true }, + { x: 205, y: 90, w: 72, h: 80, opacity: 0.06, border: false }, + { x: 315, y: 90, w: 72, h: 80, opacity: 0.15, border: true }, + { x: 425, y: 90, w: 72, h: 80, opacity: 0.08, border: false }, + { x: 535, y: 90, w: 72, h: 80, opacity: 0.12, border: true }, + { x: 645, y: 90, w: 72, h: 80, opacity: 0.07, border: false }, + { x: 755, y: 90, w: 72, h: 80, opacity: 0.1, border: true }, + { x: 865, y: 90, w: 72, h: 80, opacity: 0.14, border: false }, + { x: 975, y: 90, w: 72, h: 80, opacity: 0.06, border: true }, + { x: 1085, y: 90, w: 72, h: 80, opacity: 0.09, border: false }, + // Row 3 + { x: 40, y: 160, w: 72, h: 80, opacity: 0.12, border: true }, + { x: 150, y: 160, w: 72, h: 80, opacity: 0.07, border: false }, + { x: 260, y: 160, w: 72, h: 80, opacity: 0.1, border: true }, + { x: 810, y: 160, w: 72, h: 80, opacity: 0.09, border: false }, + { x: 920, y: 160, w: 72, h: 80, opacity: 0.13, border: true }, + { x: 1030, y: 160, w: 72, h: 80, opacity: 0.06, border: false }, + { x: 1140, y: 160, w: 72, h: 80, opacity: 0.11, border: true }, + // Row 5 + { x: 40, y: 440, w: 72, h: 80, opacity: 0.06, border: false }, + { x: 150, y: 440, w: 72, h: 80, opacity: 0.1, border: true }, + { x: 260, y: 440, w: 72, h: 80, opacity: 0.08, border: false }, + { x: 810, y: 440, w: 72, h: 80, opacity: 0.12, border: true }, + { x: 920, y: 440, w: 72, h: 80, opacity: 0.05, border: false }, + { x: 1030, y: 440, w: 72, h: 80, opacity: 0.14, border: true }, + { x: 1140, y: 440, w: 72, h: 80, opacity: 0.07, border: false }, + // Row 6 offset + { x: 95, y: 510, w: 72, h: 80, opacity: 0.09, border: true }, + { x: 205, y: 510, w: 72, h: 80, opacity: 0.12, border: false }, + { x: 315, y: 510, w: 72, h: 80, opacity: 0.06, border: true }, + { x: 425, y: 510, w: 72, h: 80, opacity: 0.1, border: false }, + { x: 535, y: 510, w: 72, h: 80, opacity: 0.08, border: true }, + { x: 645, y: 510, w: 72, h: 80, opacity: 0.13, border: false }, + { x: 755, y: 510, w: 72, h: 80, opacity: 0.07, border: true }, + { x: 865, y: 510, w: 72, h: 80, opacity: 0.11, border: false }, + { x: 975, y: 510, w: 72, h: 80, opacity: 0.06, border: true }, + { x: 1085, y: 510, w: 72, h: 80, opacity: 0.14, border: false }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Hexagonal cells — approximated with tall rounded rectangles */} + {HEX_CELLS.map((cell, i) => ( +
+ ))} + + {/* Warm ambient glow — top right */} +
+ + {/* Warm ambient glow — bottom left */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v87-triangle-mosaic.tsx b/packages/app/src/app/blog/[slug]/og-variants/v87-triangle-mosaic.tsx new file mode 100644 index 0000000..2a45330 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v87-triangle-mosaic.tsx @@ -0,0 +1,171 @@ +/** + * V87: Triangle Mosaic — scattered triangular shapes at low opacity creating a kaleidoscope mosaic feel. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Triangle { + x: number; + y: number; + borderWidth: number; + color: string; + opacity: number; + direction: 'up' | 'down' | 'left' | 'right'; +} + +const TRIANGLES: Triangle[] = [ + // Scattered across background — CSS triangle trick using borders + { x: 30, y: 25, borderWidth: 22, color: '#ef4444', opacity: 0.1, direction: 'up' }, + { x: 140, y: 80, borderWidth: 18, color: '#3b82f6', opacity: 0.08, direction: 'down' }, + { x: 260, y: 40, borderWidth: 26, color: '#10b981', opacity: 0.12, direction: 'up' }, + { x: 380, y: 100, borderWidth: 15, color: '#f59e0b', opacity: 0.09, direction: 'right' }, + { x: 500, y: 30, borderWidth: 20, color: '#8b5cf6', opacity: 0.11, direction: 'down' }, + { x: 620, y: 70, borderWidth: 24, color: '#ec4899', opacity: 0.07, direction: 'up' }, + { x: 740, y: 45, borderWidth: 17, color: '#06b6d4', opacity: 0.13, direction: 'left' }, + { x: 860, y: 90, borderWidth: 21, color: '#f97316', opacity: 0.08, direction: 'up' }, + { x: 980, y: 35, borderWidth: 19, color: '#14b8a6', opacity: 0.1, direction: 'down' }, + { x: 1100, y: 75, borderWidth: 23, color: '#a855f7', opacity: 0.12, direction: 'up' }, + { x: 70, y: 180, borderWidth: 16, color: '#6366f1', opacity: 0.09, direction: 'right' }, + { x: 200, y: 150, borderWidth: 25, color: '#f43f5e', opacity: 0.06, direction: 'down' }, + { x: 1050, y: 200, borderWidth: 20, color: '#22c55e', opacity: 0.1, direction: 'up' }, + { x: 1150, y: 160, borderWidth: 14, color: '#eab308', opacity: 0.11, direction: 'left' }, + { x: 50, y: 420, borderWidth: 22, color: '#3b82f6', opacity: 0.08, direction: 'up' }, + { x: 170, y: 480, borderWidth: 19, color: '#ef4444', opacity: 0.12, direction: 'down' }, + { x: 300, y: 530, borderWidth: 24, color: '#8b5cf6', opacity: 0.07, direction: 'right' }, + { x: 440, y: 490, borderWidth: 16, color: '#10b981', opacity: 0.1, direction: 'up' }, + { x: 560, y: 550, borderWidth: 21, color: '#f59e0b', opacity: 0.09, direction: 'down' }, + { x: 680, y: 500, borderWidth: 18, color: '#ec4899', opacity: 0.13, direction: 'left' }, + { x: 800, y: 540, borderWidth: 23, color: '#06b6d4', opacity: 0.06, direction: 'up' }, + { x: 920, y: 480, borderWidth: 15, color: '#f97316', opacity: 0.11, direction: 'down' }, + { x: 1040, y: 530, borderWidth: 20, color: '#14b8a6', opacity: 0.08, direction: 'right' }, + { x: 1140, y: 470, borderWidth: 17, color: '#a855f7', opacity: 0.1, direction: 'up' }, + { x: 100, y: 340, borderWidth: 13, color: '#22c55e', opacity: 0.07, direction: 'down' }, + { x: 1100, y: 350, borderWidth: 19, color: '#6366f1', opacity: 0.09, direction: 'up' }, +]; + +function getTriangleStyle(t: Triangle): Record { + const base: Record = { + display: 'flex', + position: 'absolute', + left: t.x, + top: t.y, + width: 0, + height: 0, + opacity: t.opacity, + }; + const b = t.borderWidth; + const transparent = 'transparent'; + if (t.direction === 'up') { + base.borderLeft = `${b}px solid ${transparent}`; + base.borderRight = `${b}px solid ${transparent}`; + base.borderBottom = `${b * 1.7}px solid ${t.color}`; + } else if (t.direction === 'down') { + base.borderLeft = `${b}px solid ${transparent}`; + base.borderRight = `${b}px solid ${transparent}`; + base.borderTop = `${b * 1.7}px solid ${t.color}`; + } else if (t.direction === 'left') { + base.borderTop = `${b}px solid ${transparent}`; + base.borderBottom = `${b}px solid ${transparent}`; + base.borderRight = `${b * 1.7}px solid ${t.color}`; + } else { + base.borderTop = `${b}px solid ${transparent}`; + base.borderBottom = `${b}px solid ${transparent}`; + base.borderLeft = `${b * 1.7}px solid ${t.color}`; + } + return base; +} + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Triangle mosaic elements */} + {TRIANGLES.map((tri, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v88-diamond-lattice.tsx b/packages/app/src/app/blog/[slug]/og-variants/v88-diamond-lattice.tsx new file mode 100644 index 0000000..fbec544 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v88-diamond-lattice.tsx @@ -0,0 +1,162 @@ +/** + * V88: Diamond Lattice — pattern of diamond shapes (border-only rotated squares) forming a lattice across the background. Silver/grey on dark. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Diamond { + x: number; + y: number; + size: number; + opacity: number; + filled: boolean; +} + +const DIAMONDS: Diamond[] = []; + +// Generate a lattice grid of diamonds +const COLS = 12; +const ROWS = 7; +const SPACING_X = 105; +const SPACING_Y = 95; +const OFFSET_X = 30; +const OFFSET_Y = 10; + +for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const x = OFFSET_X + col * SPACING_X + (row % 2 === 1 ? SPACING_X / 2 : 0); + const y = OFFSET_Y + row * SPACING_Y; + // Skip diamonds in the center content area + if (y > 140 && y < 430 && x > 80 && x < 960) continue; + const diamondSize = 28 + ((col + row) % 3) * 6; + const opacity = 0.06 + ((col * row) % 5) * 0.02; + const filled = (col + row) % 7 === 0; + DIAMONDS.push({ x, y, size: diamondSize, opacity, filled }); + } +} + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Diamond lattice elements — rotated squares */} + {DIAMONDS.map((d, i) => ( +
+ ))} + + {/* Subtle connecting lines — horizontal */} + {[95, 190, 475, 570].map((y, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v89-chevron-pattern.tsx b/packages/app/src/app/blog/[slug]/og-variants/v89-chevron-pattern.tsx new file mode 100644 index 0000000..9c4310b --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v89-chevron-pattern.tsx @@ -0,0 +1,193 @@ +/** + * V89: Chevron Pattern — V-shaped chevron elements stacked vertically along the right side, suggesting forward motion. Teal/cyan color. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +interface Chevron { + x: number; + y: number; + armWidth: number; + armHeight: number; + opacity: number; + color: string; +} + +// Right-pointing chevrons stacked along the right side +const CHEVRONS: Chevron[] = [ + // Primary column — right side + { x: 1020, y: 30, armWidth: 40, armHeight: 3, opacity: 0.25, color: '#06b6d4' }, + { x: 1020, y: 80, armWidth: 40, armHeight: 3, opacity: 0.2, color: '#22d3ee' }, + { x: 1020, y: 130, armWidth: 40, armHeight: 3, opacity: 0.3, color: '#06b6d4' }, + { x: 1020, y: 180, armWidth: 40, armHeight: 3, opacity: 0.15, color: '#67e8f9' }, + { x: 1020, y: 230, armWidth: 40, armHeight: 3, opacity: 0.22, color: '#06b6d4' }, + { x: 1020, y: 280, armWidth: 40, armHeight: 3, opacity: 0.18, color: '#22d3ee' }, + { x: 1020, y: 330, armWidth: 40, armHeight: 3, opacity: 0.28, color: '#06b6d4' }, + { x: 1020, y: 380, armWidth: 40, armHeight: 3, opacity: 0.14, color: '#67e8f9' }, + { x: 1020, y: 430, armWidth: 40, armHeight: 3, opacity: 0.24, color: '#22d3ee' }, + { x: 1020, y: 480, armWidth: 40, armHeight: 3, opacity: 0.16, color: '#06b6d4' }, + { x: 1020, y: 530, armWidth: 40, armHeight: 3, opacity: 0.2, color: '#22d3ee' }, + { x: 1020, y: 580, armWidth: 40, armHeight: 3, opacity: 0.12, color: '#67e8f9' }, + // Secondary column — further right, offset + { x: 1090, y: 55, armWidth: 32, armHeight: 2, opacity: 0.12, color: '#06b6d4' }, + { x: 1090, y: 105, armWidth: 32, armHeight: 2, opacity: 0.1, color: '#22d3ee' }, + { x: 1090, y: 155, armWidth: 32, armHeight: 2, opacity: 0.14, color: '#06b6d4' }, + { x: 1090, y: 205, armWidth: 32, armHeight: 2, opacity: 0.08, color: '#67e8f9' }, + { x: 1090, y: 255, armWidth: 32, armHeight: 2, opacity: 0.12, color: '#06b6d4' }, + { x: 1090, y: 305, armWidth: 32, armHeight: 2, opacity: 0.1, color: '#22d3ee' }, + { x: 1090, y: 355, armWidth: 32, armHeight: 2, opacity: 0.14, color: '#06b6d4' }, + { x: 1090, y: 405, armWidth: 32, armHeight: 2, opacity: 0.08, color: '#67e8f9' }, + { x: 1090, y: 455, armWidth: 32, armHeight: 2, opacity: 0.12, color: '#22d3ee' }, + { x: 1090, y: 505, armWidth: 32, armHeight: 2, opacity: 0.1, color: '#06b6d4' }, + { x: 1090, y: 555, armWidth: 32, armHeight: 2, opacity: 0.14, color: '#22d3ee' }, + // Subtle left-side accents + { x: 30, y: 100, armWidth: 24, armHeight: 2, opacity: 0.06, color: '#06b6d4' }, + { x: 30, y: 200, armWidth: 24, armHeight: 2, opacity: 0.08, color: '#22d3ee' }, + { x: 30, y: 300, armWidth: 24, armHeight: 2, opacity: 0.05, color: '#06b6d4' }, + { x: 30, y: 400, armWidth: 24, armHeight: 2, opacity: 0.07, color: '#22d3ee' }, + { x: 30, y: 500, armWidth: 24, armHeight: 2, opacity: 0.06, color: '#06b6d4' }, +]; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Chevron elements — each is two angled arms forming a > shape */} + {CHEVRONS.map((ch, i) => ( +
+ {/* Upper arm — angled down-right */} +
+ {/* Lower arm — angled up-right */} +
+
+ ))} + + {/* Teal glow — right side */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v90-zigzag-border.tsx b/packages/app/src/app/blog/[slug]/og-variants/v90-zigzag-border.tsx new file mode 100644 index 0000000..dd52700 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v90-zigzag-border.tsx @@ -0,0 +1,233 @@ +/** + * V90: Zigzag Border — top and bottom edges with zigzag/sawtooth pattern made of triangular elements. Gold zigzag on dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +// Generate zigzag teeth for top and bottom edges +const TOOTH_WIDTH = 40; +const TOOTH_HEIGHT = 28; +const NUM_TEETH = Math.ceil(1200 / TOOTH_WIDTH) + 1; + +interface Tooth { + x: number; + edge: 'top' | 'bottom'; +} + +const TEETH: Tooth[] = []; +for (let i = 0; i < NUM_TEETH; i++) { + TEETH.push({ x: i * TOOTH_WIDTH, edge: 'top' }); + TEETH.push({ x: i * TOOTH_WIDTH, edge: 'bottom' }); +} + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top zigzag strip background */} +
+ + {/* Top zigzag — downward-pointing triangles along the bottom of the gold strip */} + {Array.from({ length: NUM_TEETH }).map((_, i) => ( +
+ ))} + + {/* Top zigzag line accent */} +
+ + {/* Bottom zigzag strip background */} +
+ + {/* Bottom zigzag — upward-pointing triangles along the top of the gold strip */} + {Array.from({ length: NUM_TEETH }).map((_, i) => ( +
+ ))} + + {/* Bottom zigzag line accent */} +
+ + {/* Subtle gold dust accents */} + {[ + { x: 100, y: 80, s: 4 }, + { x: 300, y: 120, s: 3 }, + { x: 600, y: 70, s: 5 }, + { x: 900, y: 100, s: 3 }, + { x: 1100, y: 85, s: 4 }, + { x: 150, y: 540, s: 3 }, + { x: 450, y: 560, s: 4 }, + { x: 750, y: 530, s: 3 }, + { x: 1050, y: 555, s: 5 }, + ].map((dot, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v91-corner-ornaments.tsx b/packages/app/src/app/blog/[slug]/og-variants/v91-corner-ornaments.tsx new file mode 100644 index 0000000..8e95924 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v91-corner-ornaments.tsx @@ -0,0 +1,534 @@ +/** + * V91: Corner Ornaments — decorative ornamental elements in each corner made of small lines and dots, like certificate/diploma corners. Gold/brass on dark slate. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const ORNAMENT_COLOR = '#c9a84c'; +const ORNAMENT_OPACITY = 0.45; +const DOT_SIZE = 6; +const LINE_THICKNESS = 2; +const CORNER_INSET = 28; +const ARM_LENGTH = 70; +const INNER_ARM = 50; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* ===== TOP-LEFT CORNER ===== */} + {/* Outer horizontal line */} +
+ {/* Outer vertical line */} +
+ {/* Inner horizontal line */} +
+ {/* Inner vertical line */} +
+ {/* Corner dot */} +
+ {/* End dot horizontal */} +
+ {/* End dot vertical */} +
+ {/* Mid-dot on horizontal */} +
+ + {/* ===== TOP-RIGHT CORNER ===== */} +
+
+
+
+
+
+
+
+ + {/* ===== BOTTOM-LEFT CORNER ===== */} +
+
+
+
+
+
+
+
+ + {/* ===== BOTTOM-RIGHT CORNER ===== */} +
+
+
+
+
+
+
+
+ + {/* Thin border frame inside the ornaments */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v92-ribbon-banner.tsx b/packages/app/src/app/blog/[slug]/og-variants/v92-ribbon-banner.tsx new file mode 100644 index 0000000..cbfae93 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v92-ribbon-banner.tsx @@ -0,0 +1,212 @@ +/** + * V92: Ribbon Banner — diagonal ribbon across the top-right corner with reading time. Overlapping divs create a ribbon fold effect. Gold ribbon on dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Ribbon — main diagonal band */} +
+ + {meta.readingTime} MIN READ + +
+ + {/* Ribbon fold shadow — left side */} +
+ + {/* Ribbon fold shadow — right side */} +
+ + {/* Secondary ribbon — thinner accent band */} +
+ + {/* Tertiary ribbon — thinner accent band below */} +
+ + {/* Subtle warm accent circle */} +
+ + {/* Decorative thin line along left edge */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v93-badge-seal.tsx b/packages/app/src/app/blog/[slug]/og-variants/v93-badge-seal.tsx new file mode 100644 index 0000000..4c3240a --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v93-badge-seal.tsx @@ -0,0 +1,252 @@ +/** + * V93: Badge/Seal — circular badge element in the bottom-right corner with reading time, like a wax seal. Decorative concentric rings. Gold on dark background. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const sealX = 1040; + const sealY = 460; + + return new ImageResponse( +
+ {/* Seal — outermost ring */} +
+ + {/* Seal — second ring */} +
+ + {/* Seal — third ring (dotted effect via dashed) */} +
+ + {/* Seal — filled center */} +
+ + {/* Seal — inner bright circle */} +
+ + {/* Seal text container */} +
+ + {meta.readingTime} + + + MIN READ + +
+ + {/* Decorative dots around the seal */} + {[0, 45, 90, 135, 180, 225, 270, 315].map((angle, i) => { + const rad = (angle * Math.PI) / 180; + const r = 82; + const dx = Math.cos(rad) * r; + const dy = Math.sin(rad) * r; + return ( +
+ ); + })} + + {/* Subtle warm ambience */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v94-bracket-frame.tsx b/packages/app/src/app/blog/[slug]/og-variants/v94-bracket-frame.tsx new file mode 100644 index 0000000..4973d18 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v94-bracket-frame.tsx @@ -0,0 +1,304 @@ +/** + * V94: Bracket Frame — large typographic brackets [ ] framing the title on left and right. Tall thin elements with horizontal caps creating a code/syntax feel. Teal brackets. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const BRACKET_COLOR = '#14b8a6'; +const BRACKET_THICKNESS = 4; +const CAP_LENGTH = 28; +const BRACKET_HEIGHT = 320; +const BRACKET_OPACITY = 0.5; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + const bracketTopY = 155; + + return new ImageResponse( +
+ {/* ===== LEFT BRACKET [ ===== */} + {/* Vertical bar */} +
+ {/* Top cap */} +
+ {/* Bottom cap */} +
+ + {/* Inner left bracket — thinner, slightly offset */} +
+
+
+ + {/* ===== RIGHT BRACKET ] ===== */} + {/* Vertical bar */} +
+ {/* Top cap */} +
+ {/* Bottom cap */} +
+ + {/* Inner right bracket */} +
+
+
+ + {/* Subtle horizontal rule accents */} +
+ + {/* Subtle dot accents at bracket midpoints */} + {[ + { x: 36 + BRACKET_THICKNESS / 2, y: bracketTopY + BRACKET_HEIGHT / 2 }, + { x: 1200 - 36 - BRACKET_THICKNESS / 2, y: bracketTopY + BRACKET_HEIGHT / 2 }, + ].map((dot, i) => ( +
+ ))} + + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt — centered between brackets */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v95-arrow-accent.tsx b/packages/app/src/app/blog/[slug]/og-variants/v95-arrow-accent.tsx new file mode 100644 index 0000000..6982498 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v95-arrow-accent.tsx @@ -0,0 +1,286 @@ +/** + * V95: Arrow Accent — large directional arrow element pointing right, positioned behind the title. Suggests forward momentum. Subtle, low opacity. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +const ARROW_COLOR = '#6366f1'; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* ===== LARGE ARROW — pointing right, behind content ===== */} + {/* Arrow shaft */} +
+ + {/* Arrow head — upper diagonal */} +
+ + {/* Arrow head — lower diagonal */} +
+ + {/* ===== SECONDARY SMALLER ARROW — right side, higher ===== */} + {/* Shaft */} +
+ + {/* Head upper */} +
+ + {/* Head lower */} +
+ + {/* ===== TERTIARY ARROW — bottom right ===== */} +
+
+
+ + {/* Accent line — thin horizontal stripe */} +
+ + {/* Accent dots — trajectory path */} + {[ + { x: 140, y: 316 }, + { x: 200, y: 316 }, + { x: 260, y: 316 }, + { x: 320, y: 316 }, + { x: 380, y: 316 }, + ].map((dot, i) => ( +
+ ))} + + {/* Subtle glow */} +
+ + {/* Content */} +
+ {/* Logo */} +
+ + + InferenceX + +
+ + {/* Title & excerpt */} +
+
+ {meta.title} +
+
+ {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} +
+
+ + {/* Footer */} +
+ {meta.author} + {'\u00b7'} + + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {'\u00b7'} + {meta.readingTime} min read +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v96-magazine-cover.tsx b/packages/app/src/app/blog/[slug]/og-variants/v96-magazine-cover.tsx new file mode 100644 index 0000000..b249ea2 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v96-magazine-cover.tsx @@ -0,0 +1,151 @@ +/** + * V96: Magazine Cover — Bold masthead with high-fashion magazine feel. White on black. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Masthead */} +
+ + InferenceX + +
+ + {/* Issue date line */} +
+ + {new Date(meta.date + 'T00:00:00Z') + .toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }) + .toUpperCase()}{' '} + ISSUE + +
+ + {/* Main title — huge and centered */} +
+ + {meta.title} + +
+ + {/* Cover lines (excerpt) */} +
+ + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Author line */} +
+ + By {meta.author} + +
+ + {/* Logo watermark bottom-right */} +
+ +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v97-newspaper.tsx b/packages/app/src/app/blog/[slug]/og-variants/v97-newspaper.tsx new file mode 100644 index 0000000..05b63a7 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v97-newspaper.tsx @@ -0,0 +1,177 @@ +/** + * V97: Newspaper — Classic newspaper headline layout with masthead, dateline, and column hint. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Top rule */} +
+ + {/* Masthead */} +
+ + THE INFERENCEX TIMES + +
+ + {/* Bottom rule under masthead */} +
+ + {/* Date and edition line */} +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + + {meta.readingTime} min read +
+ + {/* Headline */} +
+ + {meta.title} + +
+ + {/* Two-column area with vertical divider */} +
+ {/* Left column — dateline + excerpt */} +
+ + SAN FRANCISCO,{' '} + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })}{' '} + —{' '} + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+ + {/* Vertical divider */} +
+ + {/* Right column — author and tags */} +
+ + By {meta.author} + + + {meta.tags ? meta.tags.join(' | ') : ''} + +
+
+ + {/* Logo bottom-left */} +
+ +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v98-book-cover.tsx b/packages/app/src/app/blog/[slug]/og-variants/v98-book-cover.tsx new file mode 100644 index 0000000..f7c9b08 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v98-book-cover.tsx @@ -0,0 +1,122 @@ +/** + * V98: Book Cover — Timeless centered design with title, author, and publisher mark. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Subtle outer border */} +
+ {/* Top spacer */} +
+ + {/* Title */} +
+ + {meta.title} + +
+ + {/* Decorative line */} +
+ + {/* Author */} +
+ + {meta.author} + +
+ + {/* Bottom spacer */} +
+ + {/* Publisher mark — logo at bottom center */} +
+ + + INFERENCEX + +
+
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/[slug]/og-variants/v99-academic-paper.tsx b/packages/app/src/app/blog/[slug]/og-variants/v99-academic-paper.tsx new file mode 100644 index 0000000..4172c42 --- /dev/null +++ b/packages/app/src/app/blog/[slug]/og-variants/v99-academic-paper.tsx @@ -0,0 +1,151 @@ +/** + * V99: Academic Paper — Formal journal article layout with proceedings header and abstract. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ImageResponse } from 'next/og'; + +import type { BlogPostMeta } from '@/lib/blog'; + +export const size = { width: 1200, height: 630 }; + +export async function renderOgImage(meta: BlogPostMeta) { + const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`; + const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64; + + return new ImageResponse( +
+ {/* Proceedings header */} +
+
+ + + INFERENCEX PROCEEDINGS + +
+ + {new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ + {/* Paper title */} +
+ + {meta.title} + +
+ + {/* Authors */} +
+ {meta.author} +
+ + {/* Affiliation / tags */} +
+ + {meta.tags ? meta.tags.join(', ') : ''} + +
+ + {/* Abstract */} +
+
+ + Abstract—{' '} + {meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '\u2026' : meta.excerpt} + +
+
+ + {/* Footer line */} +
+ InferenceX Proceedings + {meta.readingTime} min read +
+
, + size, + ); +} diff --git a/packages/app/src/app/blog/og-preview/page.tsx b/packages/app/src/app/blog/og-preview/page.tsx index bbeedda..28a9d42 100644 --- a/packages/app/src/app/blog/og-preview/page.tsx +++ b/packages/app/src/app/blog/og-preview/page.tsx @@ -48,6 +48,156 @@ const VARIANTS = [ name: 'V24: Gold Split Bold — Gold left panel, massive title right, no fine details', }, { id: 'v25', name: 'V25: Gold Accent Stripe — Thick gold left stripe, massive title, minimal' }, + { id: 'v26', name: 'V26: Halftone Dots — Dots decrease in size from corner to corner' }, + { id: 'v27', name: 'V27: Dot Grid — Uniform dot grid background' }, + { id: 'v28', name: 'V28: Constellation — Connected dots like a star map' }, + { id: 'v29', name: 'V29: Particle Burst — Dots radiating from bottom-left' }, + { id: 'v30', name: 'V30: Dot Border — Dots forming a dotted frame' }, + { id: 'v31', name: 'V31: Concentric Rings — Large circles centered right, partially off-screen' }, + { id: 'v32', name: 'V32: Venn Overlap — 3 translucent circles overlapping' }, + { id: 'v33', name: 'V33: Floating Bubbles — Various sized circles scattered' }, + { id: 'v34', name: 'V34: Ripple Waves — Quarter-circles from bottom-left' }, + { id: 'v35', name: 'V35: Corner Arcs — Quarter-circle arcs in each corner' }, + { id: 'v36', name: 'V36: Scan Lines — CRT/retro horizontal scan lines' }, + { id: 'v37', name: 'V37: Vertical Blinds — Alternating vertical bars' }, + { id: 'v38', name: 'V38: Crosshatch — Diagonal crossing line segments' }, + { id: 'v39', name: 'V39: Sound Wave — Audio waveform/equalizer bars' }, + { id: 'v40', name: 'V40: Radial Rays — Lines radiating from bottom center' }, + { id: 'v41', name: 'V41: Isometric Grid — Diamond shapes in isometric pattern' }, + { id: 'v42', name: 'V42: Blueprint — Blue grid on dark navy, engineering feel' }, + { id: 'v43', name: 'V43: Glitch Grid — Interrupted/offset grid lines, digital glitch' }, + { id: 'v44', name: 'V44: Perspective Lines — Lines converging to vanishing point' }, + { id: 'v45', name: 'V45: Topographic — Wavy contour lines, elevation map' }, + { id: 'v46', name: 'V46: Ocean Depths — Deep navy with cyan/teal accents' }, + { id: 'v47', name: 'V47: Sunset Fire — Warm orange/red/purple rectangles' }, + { id: 'v48', name: 'V48: Forest Canopy — Dark green with tree-like verticals' }, + { id: 'v49', name: 'V49: Arctic Frost — Ice blue/white crystalline accents' }, + { id: 'v50', name: 'V50: Volcanic Ember — Black with red/orange ember dots' }, + { id: 'v51', name: 'V51: Royal Purple — Deep purple with gold ornaments' }, + { id: 'v52', name: 'V52: Copper Patina — Teal-grey with copper/verdigris' }, + { id: 'v53', name: 'V53: Neon Night — Black with hot pink/electric blue' }, + { id: 'v54', name: 'V54: Sandstone — Warm grey with terracotta/sand strata' }, + { id: 'v55', name: 'V55: Monochrome Steel — Pure grey palette, industrial' }, + { id: 'v56', name: 'V56: Vertical Split — 40/60 teal left, dark right' }, + { id: 'v57', name: 'V57: Top Banner — Colored 30% top band with logo' }, + { id: 'v58', name: 'V58: Right Sidebar — Left content, right info panel' }, + { id: 'v59', name: 'V59: Left Accent Bar — Thin gold vertical bar, editorial' }, + { id: 'v60', name: 'V60: Bottom Dock — Content above, metadata dock below' }, + { id: 'v61', name: 'V61: Z-Layout — Eye follows Z-path across card' }, + { id: 'v62', name: 'V62: Card-in-Card — Floating inner card with border' }, + { id: 'v63', name: 'V63: Three Column — Thin decorative side columns' }, + { id: 'v64', name: 'V64: Ruled Sections — Header/body/footer with dividers' }, + { id: 'v65', name: 'V65: Staircase Blocks — Overlapping rectangles stepping down' }, + { id: 'v66', name: 'V66: Drop Cap — Giant gold first letter of title' }, + { id: 'v67', name: 'V67: All Caps Impact — ALL CAPS, heavy weight, max readability' }, + { id: 'v68', name: 'V68: Serif Elegant — Light weight, decorative rules, literary' }, + { id: 'v69', name: 'V69: Stacked Words — Each word on its own line' }, + { id: 'v70', name: 'V70: Underline Accent — Bold gold bar under title' }, + { id: 'v71', name: 'V71: Highlight Marker — Highlighted text background effect' }, + { id: 'v72', name: 'V72: Title Badge — Title in bordered rounded rectangle' }, + { id: 'v73', name: 'V73: Ultra Tall — Very large font, narrow letter spacing' }, + { id: 'v74', name: 'V74: Two-Size Title — First word huge gold, rest normal' }, + { id: 'v75', name: 'V75: Centered Zen — Everything centered, generous whitespace' }, + { id: 'v76', name: 'V76: Terminal — Green text on black, console prompt' }, + { id: 'v77', name: 'V77: Code Editor — Line numbers, syntax-highlight colors' }, + { id: 'v78', name: 'V78: Matrix Rain — Character columns, green on black' }, + { id: 'v79', name: 'V79: Binary Accent — "01" strings scattered in background' }, + { id: 'v80', name: 'V80: Network Nodes — Connected circle nodes, topology' }, + { id: 'v81', name: 'V81: Dashboard — Data dashboard panel with KPI style' }, + { id: 'v82', name: 'V82: Chip Layout — Processor chip with pins, PCB feel' }, + { id: 'v83', name: 'V83: Data Table — Spreadsheet layout with grid lines' }, + { id: 'v84', name: 'V84: Progress Bar — Futuristic HUD with loading bar' }, + { id: 'v85', name: 'V85: API Docs — REST endpoint documentation style' }, + { id: 'v86', name: 'V86: Hexagon Cells — Honeycomb pattern, amber/gold' }, + { id: 'v87', name: 'V87: Triangle Mosaic — Scattered triangles, kaleidoscope' }, + { id: 'v88', name: 'V88: Diamond Lattice — Diamond shapes forming lattice' }, + { id: 'v89', name: 'V89: Chevron Pattern — V-shapes suggesting forward motion' }, + { id: 'v90', name: 'V90: Zigzag Border — Sawtooth edges top and bottom' }, + { id: 'v91', name: 'V91: Corner Ornaments — Decorative certificate-style corners' }, + { id: 'v92', name: 'V92: Ribbon Banner — Diagonal ribbon with reading time' }, + { id: 'v93', name: 'V93: Badge Seal — Circular wax seal with reading time' }, + { id: 'v94', name: 'V94: Bracket Frame — Large [ ] brackets framing title' }, + { id: 'v95', name: 'V95: Arrow Accent — Large arrow behind title, momentum' }, + { id: 'v96', name: 'V96: Magazine Cover — Bold masthead, high-fashion feel' }, + { id: 'v97', name: 'V97: Newspaper — Classic headline with masthead/dateline' }, + { id: 'v98', name: 'V98: Book Cover — Centered elegant, timeless design' }, + { id: 'v99', name: 'V99: Academic Paper — Journal article with abstract' }, + { id: 'v100', name: 'V100: Postcard — Stamp area, address-line formatting' }, + { id: 'v101', name: 'V101: Playbill — Theater poster, dramatic presentation' }, + { id: 'v102', name: 'V102: Trading Card — Stats section, holographic border' }, + { id: 'v103', name: 'V103: Ticket — Event ticket with tear-off stub' }, + { id: 'v104', name: 'V104: Album Cover — Vinyl record cover aesthetic' }, + { id: 'v105', name: 'V105: Movie Poster — Cinematic with glow and genres' }, + { id: 'v106', name: 'V106: Scattered Rects — Random rectangles, abstract art' }, + { id: 'v107', name: 'V107: Stacked Cards — Layered card depth illusion' }, + { id: 'v108', name: 'V108: Waveform Edge — Audio visualizer bars at bottom' }, + { id: 'v109', name: 'V109: Mountain Silhouette — Layered mountain ranges' }, + { id: 'v110', name: 'V110: Pixel Blocks — Pixel-art dissolve effect' }, + { id: 'v111', name: 'V111: Layered Panels — Overlapping translucent rectangles' }, + { id: 'v112', name: 'V112: Spiral Dots — Dots in spiral pattern, galaxy feel' }, + { id: 'v113', name: 'V113: Slash Marks — "/" characters scattered as texture' }, + { id: 'v114', name: 'V114: Noise Dots — Tiny dots like film grain' }, + { id: 'v115', name: 'V115: Organic Blobs — Large soft circles, color regions' }, + { id: 'v116', name: 'V116: Monogram Watermark — Huge "IX" watermark behind content' }, + { id: 'v117', name: 'V117: Timeline — Horizontal line with date marker dot' }, + { id: 'v118', name: 'V118: Quote Marks — Giant quotation marks framing title' }, + { id: 'v119', name: 'V119: Classified — Declassified dossier with stamp' }, + { id: 'v120', name: 'V120: Retro VHS — VHS distortion, offset colored layers' }, + { id: 'v121', name: 'V121: Barcode — Barcode lines at bottom, commercial feel' }, + { id: 'v122', name: 'V122: Postmark — Circular postmark stamp, vintage ink' }, + { id: 'v123', name: 'V123: Breaking News — TV news chyron, red banner' }, + { id: 'v124', name: 'V124: Japanese Minimal — Extreme negative space, zen' }, + { id: 'v125', name: 'V125: Brutalist — Thick borders, high contrast, anti-design' }, + { id: 'v126', name: 'V126: Ancient Scroll — Parchment with rolled scroll edges' }, + { id: 'v127', name: 'V127: Cave Painting — Ochre hand-prints, primitive marks' }, + { id: 'v128', name: 'V128: Art Deco — Gold sunburst rays, Gatsby-era elegance' }, + { id: 'v129', name: 'V129: Constructivist — Soviet propaganda, red/black diagonal' }, + { id: 'v130', name: 'V130: Ukiyo-e — Japanese woodblock, wave patterns, cartouche' }, + { id: 'v131', name: 'V131: Stained Glass — Jewel-toned panes with lead borders' }, + { id: 'v132', name: 'V132: Hieroglyphs — Egyptian cartouche, gold glyph bands' }, + { id: 'v133', name: 'V133: Illuminated — Medieval manuscript, ornate initial letter' }, + { id: 'v134', name: 'V134: Pop Art — Warhol 2x2 quadrants, bold colors' }, + { id: 'v135', name: 'V135: Bauhaus — White bg, primary color shapes, Dessau school' }, + { id: 'v136', name: 'V136: Chalkboard — Green board, chalk text, eraser' }, + { id: 'v137', name: 'V137: Neon Sign — Glowing tubes on black, double-line effect' }, + { id: 'v138', name: 'V138: Leather Book — Embossed dark brown, gold double frame' }, + { id: 'v139', name: 'V139: Shipping Label — Kraft brown, FRAGILE stamp, barcode' }, + { id: 'v140', name: 'V140: Polaroid — White-bordered photo with handwritten caption' }, + { id: 'v141', name: 'V141: Cassette — Tape reels, SIDE A, retro label' }, + { id: 'v142', name: 'V142: License Plate — Embossed text, state label, stickers' }, + { id: 'v143', name: 'V143: Credit Card — Chip, card number, holographic stripe' }, + { id: 'v144', name: 'V144: Prescription — Rx label, dosage instructions' }, + { id: 'v145', name: 'V145: Billboard — Highway sign, green bg, EXIT number' }, + { id: 'v146', name: 'V146: DNA Helix — Double strand dots with connecting rungs' }, + { id: 'v147', name: 'V147: Star Chart — Stars, constellations, coordinate grid' }, + { id: 'v148', name: 'V148: Weather Map — Radar rings, blips, station readout' }, + { id: 'v149', name: 'V149: Periodic Element — Table cell with atomic number/symbol' }, + { id: 'v150', name: 'V150: Microscope — Slide with circular lens view, scale bar' }, + { id: 'v151', name: 'V151: Seismograph — Waveform on graph paper, red alerts' }, + { id: 'v152', name: 'V152: Aurora — Northern lights curtain, stars, treeline' }, + { id: 'v153', name: 'V153: Coral Reef — Underwater with coral shapes, bubbles' }, + { id: 'v154', name: 'V154: Crystal — Amethyst/ice crystal shards from corner' }, + { id: 'v155', name: 'V155: Telescope — Eyepiece circle with crosshairs, stars' }, + { id: 'v156', name: 'V156: Receipt — Thermal paper, dashed lines, TOTAL: 1 ARTICLE' }, + { id: 'v157', name: 'V157: Passport — Document page with MRZ zone, photo placeholder' }, + { id: 'v158', name: 'V158: Ransom Note — Cut-out words, mixed sizes/colors, chaotic' }, + { id: 'v159', name: 'V159: Typewriter — Off-white paper, typed text, carriage' }, + { id: 'v160', name: 'V160: Sticky Note — Yellow Post-it on dark bg, pin at top' }, + { id: 'v161', name: 'V161: Safety Card — Airline style, numbered steps, pictograms' }, + { id: 'v162', name: 'V162: Nutrition Label — FDA format, Blog Facts, serving size' }, + { id: 'v163', name: 'V163: Warning Sign — Yellow triangle, hazard stripes' }, + { id: 'v164', name: 'V164: Test Pattern — TV color bars, PLEASE STAND BY' }, + { id: 'v165', name: 'V165: Boot Screen — BIOS text, green on black, system boot' }, + { id: 'v166', name: 'V166: Mondrian — Primary color grid, De Stijl composition' }, + { id: 'v167', name: 'V167: Rothko — Stacked color field blocks, contemplative' }, + { id: 'v168', name: 'V168: Kandinsky — Scattered geometric shapes, abstract energy' }, + { id: 'v169', name: 'V169: Impossible — Escher impossible triangle illusion' }, + { id: 'v170', name: 'V170: Op Art — Concentric circles, moiré vibrating effect' }, + { id: 'v171', name: 'V171: Data Mosh — RGB channel offset, glitch corruption' }, + { id: 'v172', name: 'V172: Risograph — Two-color overprint, offset registration' }, + { id: 'v173', name: 'V173: Linocut — Bold white on dark, woodcut leaf patterns' }, + { id: 'v174', name: 'V174: Psychedelic — Nested rounded rects, groovy 60s colors' }, + { id: 'v175', name: 'V175: Hologram — Cyan scan lines, magenta offset, Blade Runner' }, ]; function OgImg({ v, w, label }: { v: string; w: number; label: string }) {