- {/* Background Technographic Accent */}
+ {/* Background Grid Pattern */}
@@ -51,15 +109,16 @@ export function OGImageTemplate({
flexDirection: "column",
position: "relative",
zIndex: 10,
+ maxWidth: "850px",
}}
>
{/* Label / Category */}
{label && (
40 ? "64px" : "82px",
+ fontSize: title.length > 40 ? "56px" : "72px",
fontWeight: 700,
color: slateDark,
lineHeight: "1.1",
- maxWidth: "950px",
- marginBottom: "32px",
+ marginBottom: "28px",
display: "flex",
- letterSpacing: "-0.025em",
+ letterSpacing: "-0.03em",
}}
>
{title}
@@ -90,16 +148,15 @@ export function OGImageTemplate({
{description && (
- {description.length > 160
- ? description.substring(0, 157) + "..."
+ {description.length > 120
+ ? description.substring(0, 117) + "..."
: description}
)}
@@ -109,7 +166,7 @@ export function OGImageTemplate({
- {/* Blue Brand Strip */}
+ {/* Keyword badge (bottom-right) */}
+ {keyword && (
+
+ )}
+
+ {/* Accent Strip */}
diff --git a/apps/web/src/components/blog/BlogThumbnailSVG.tsx b/apps/web/src/components/blog/BlogThumbnailSVG.tsx
new file mode 100644
index 0000000..5f563a9
--- /dev/null
+++ b/apps/web/src/components/blog/BlogThumbnailSVG.tsx
@@ -0,0 +1,1111 @@
+import React from "react";
+import type {
+ ThumbnailIcon,
+ BlogThumbnailConfig,
+} from "../../data/blogThumbnails";
+import { blogThumbnails } from "../../data/blogThumbnails";
+
+interface BlogThumbnailSVGProps {
+ slug: string;
+ variant?: "square" | "banner";
+ className?: string;
+}
+
+// Grid pattern used in the background
+const GridPattern: React.FC<{ size: number }> = ({ size }) => (
+
+
+
+
+
+);
+
+// ─── Icon Renderers ───────────────────────────────────────────────
+
+function renderGauge(cx: number, cy: number, accent: string) {
+ const r = 52;
+ // Arc from ~210° to ~330° (lower half open)
+ const startAngle = (210 * Math.PI) / 180;
+ const endAngle = (330 * Math.PI) / 180;
+ const x1 = cx + r * Math.cos(startAngle);
+ const y1 = cy + r * Math.sin(startAngle);
+ const x2 = cx + r * Math.cos(endAngle);
+ const y2 = cy + r * Math.sin(endAngle);
+ // Needle at ~280° (pointing upper-right = fast/danger zone)
+ const needleAngle = (280 * Math.PI) / 180;
+ const nx = cx + (r - 14) * Math.cos(needleAngle);
+ const ny = cy + (r - 14) * Math.sin(needleAngle);
+
+ return (
+
+
+
+
+
+ {/* Tick marks */}
+ {[210, 240, 270, 300, 330].map((deg) => {
+ const rad = (deg * Math.PI) / 180;
+ const tx1 = cx + (r + 6) * Math.cos(rad);
+ const ty1 = cy + (r + 6) * Math.sin(rad);
+ const tx2 = cx + (r + 12) * Math.cos(rad);
+ const ty2 = cy + (r + 12) * Math.sin(rad);
+ return (
+
+ );
+ })}
+
+ );
+}
+
+function renderBottleneck(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Wide top */}
+
+ {/* Narrow middle (bottleneck) */}
+
+
+ {/* Flow lines */}
+ {[-20, 0, 20].map((offset) => (
+
+ ))}
+
+ {/* Wide bottom */}
+
+ {/* Arrow down through throttle */}
+
+
+ );
+}
+
+function renderPlugin(cx: number, cy: number, accent: string) {
+ const s = 28;
+ return (
+
+ {/* Connected piece 1 */}
+
+ {/* Connected piece 2 */}
+
+ {/* Connected piece 3 */}
+
+ {/* Disconnected piece (offset) */}
+
+ {/* Connector dots */}
+
+
+ {/* Warning */}
+
+ !
+
+
+ );
+}
+
+function renderShield(cx: number, cy: number, accent: string) {
+ return (
+
+
+
+ {/* Checkmark */}
+
+
+ );
+}
+
+function renderCookie(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Cookie circle */}
+
+ {/* Chips */}
+
+
+
+
+ {/* Strikethrough */}
+
+
+ );
+}
+
+function renderCloud(cx: number, cy: number, accent: string) {
+ return (
+
+
+ {/* Lock icon inside cloud */}
+
+
+
+
+ );
+}
+
+function renderLock(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Lock body */}
+
+ {/* Lock shackle */}
+
+ {/* Keyhole */}
+
+
+ {/* Chain links */}
+ {[-42, 42].map((offset) => (
+
+
+ 0 ? 14 : -14)}
+ cy={cy + 12}
+ rx="10"
+ ry="6"
+ fill="none"
+ stroke="#cbd5e1"
+ strokeWidth="1.5"
+ />
+
+ ))}
+
+ );
+}
+
+function renderChart(cx: number, cy: number, accent: string) {
+ const barWidth = 14;
+ const heights = [30, 50, 38, 55, 42];
+ const baseY = cy + 35;
+ return (
+
+ {/* Bars */}
+ {heights.map((h, i) => (
+
+ ))}
+ {/* Base line */}
+
+ {/* Eye with strikethrough */}
+
+
+
+
+ );
+}
+
+function renderLeaf(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Leaf shape */}
+
+
+ {/* Stem/vein */}
+
+ {/* Circuit nodes on leaf veins */}
+ {[-20, 0, 20].map((offset, i) => (
+
+
+
+
+ ))}
+
+ );
+}
+
+function renderPrice(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Tag shape */}
+
+ {/* Hole */}
+
+ {/* Equals sign */}
+
+
+
+ );
+}
+
+function renderPrototype(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Browser frame */}
+
+ {/* Title bar */}
+
+ {/* Dots */}
+
+
+
+ {/* Wireframe lines */}
+
+
+
+ {/* Accent wireframe block */}
+
+
+
+ );
+}
+
+function renderGear(cx: number, cy: number, accent: string) {
+ const teeth = 8;
+ const innerR = 24;
+ const outerR = 36;
+ const toothWidth = 0.2;
+
+ let d = "";
+ for (let i = 0; i < teeth; i++) {
+ const angle = (i * 2 * Math.PI) / teeth;
+ const a1 = angle - toothWidth;
+ const a2 = angle + toothWidth;
+ const midAngle = (a1 + a2) / 2;
+
+ if (i === 0) {
+ d += `M ${cx + outerR * Math.cos(a1)} ${cy + outerR * Math.sin(a1)} `;
+ }
+ d += `L ${cx + outerR * Math.cos(a2)} ${cy + outerR * Math.sin(a2)} `;
+
+ const nextAngle = ((i + 1) * 2 * Math.PI) / teeth;
+ const na1 = nextAngle - toothWidth;
+ d += `L ${cx + innerR * Math.cos(a2)} ${cy + innerR * Math.sin(a2)} `;
+ d += `L ${cx + innerR * Math.cos(na1)} ${cy + innerR * Math.sin(na1)} `;
+ d += `L ${cx + outerR * Math.cos(na1)} ${cy + outerR * Math.sin(na1)} `;
+ }
+ d += "Z";
+
+ return (
+
+
+
+ {/* Infinity loop */}
+
+
+ );
+}
+
+function renderHourglass(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Top triangle */}
+
+
+ {/* Bottom triangle */}
+
+ {/* "Sand" fill in bottom */}
+
+ {/* Top and bottom lines */}
+
+
+ {/* Circuit nodes as "digital sand" */}
+ {[10, 22, 34].map((y, i) => (
+
+ ))}
+
+ );
+}
+
+function renderCode(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* < */}
+
+ {/* / */}
+
+ {/* > */}
+
+
+ );
+}
+
+function renderResponsive(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Outer desktop */}
+
+ {/* Tablet */}
+
+ {/* Phone */}
+
+ {/* Phone screen */}
+
+ {/* Phone home button */}
+
+
+ );
+}
+
+function renderServer(cx: number, cy: number, accent: string) {
+ const unitH = 22;
+ return (
+
+ {/* Server units */}
+ {[0, 1, 2].map((i) => (
+
+
+ {/* Status LEDs */}
+
+
+ {/* Drive bays */}
+ {[0, 1, 2, 3].map((j) => (
+
+ ))}
+
+ ))}
+ {/* Signal waves */}
+ {[10, 18, 26].map((r, i) => (
+
+ ))}
+
+ );
+}
+
+function renderTemplate(cx: number, cy: number, accent: string) {
+ return (
+
+ {/* Grid layout */}
+ {[0, 1, 2, 3].map((i) => {
+ const row = Math.floor(i / 2);
+ const col = i % 2;
+ return (
+
+ );
+ })}
+ {/* Strikethrough diagonal */}
+
+
+
+ );
+}
+
+function renderSync(cx: number, cy: number, accent: string) {
+ const r = 36;
+ return (
+
+ {/* Circular arrows */}
+
+
+ {/* Arrowheads */}
+
+
+ {/* Data dots flowing */}
+
+
+ {/* Center node */}
+
+
+
+ );
+}
+
+// ─── Icon dispatcher ──────────────────────────────────────────────
+
+const iconRenderers: Record<
+ ThumbnailIcon,
+ (cx: number, cy: number, accent: string) => React.ReactNode
+> = {
+ gauge: renderGauge,
+ bottleneck: renderBottleneck,
+ plugin: renderPlugin,
+ shield: renderShield,
+ cookie: renderCookie,
+ cloud: renderCloud,
+ lock: renderLock,
+ chart: renderChart,
+ leaf: renderLeaf,
+ price: renderPrice,
+ prototype: renderPrototype,
+ gear: renderGear,
+ hourglass: renderHourglass,
+ code: renderCode,
+ responsive: renderResponsive,
+ server: renderServer,
+ template: renderTemplate,
+ sync: renderSync,
+};
+
+// ─── Main Component ──────────────────────────────────────────────
+
+export const BlogThumbnailSVG: React.FC
= ({
+ slug,
+ variant = "square",
+ className,
+}) => {
+ const config = blogThumbnails[slug];
+ if (!config) return null;
+
+ const isBanner = variant === "banner";
+ const vbWidth = isBanner ? 480 : 240;
+ const vbHeight = isBanner ? 160 : 240;
+
+ // For banner, we shift the icon more to the right
+ const iconCx = isBanner ? 340 : vbWidth / 2;
+ const iconCy = vbHeight / 2;
+
+ return (
+
+ );
+};
diff --git a/apps/web/src/data/blogThumbnails.ts b/apps/web/src/data/blogThumbnails.ts
new file mode 100644
index 0000000..ed72f5c
--- /dev/null
+++ b/apps/web/src/data/blogThumbnails.ts
@@ -0,0 +1,139 @@
+export type ThumbnailIcon =
+ | "gauge"
+ | "bottleneck"
+ | "plugin"
+ | "shield"
+ | "cookie"
+ | "cloud"
+ | "lock"
+ | "chart"
+ | "leaf"
+ | "price"
+ | "prototype"
+ | "gear"
+ | "hourglass"
+ | "code"
+ | "responsive"
+ | "server"
+ | "template"
+ | "sync";
+
+export interface BlogThumbnailConfig {
+ icon: ThumbnailIcon;
+ accent: string;
+ keyword: string;
+}
+
+/**
+ * Mapping of blog post slugs to their unique thumbnail configuration.
+ * Each entry defines the abstract SVG illustration style for a given post.
+ */
+export const blogThumbnails: Record = {
+ // Group 1: Pain Points & Troubleshooting
+ "why-pagespeed-fails": {
+ icon: "gauge",
+ accent: "#ef4444",
+ keyword: "SPEED",
+ },
+ "slow-loading-costs-customers": {
+ icon: "gauge",
+ accent: "#f97316",
+ keyword: "LATENCY",
+ },
+ "why-agencies-are-slow": {
+ icon: "bottleneck",
+ accent: "#8b5cf6",
+ keyword: "PROCESS",
+ },
+ "hidden-costs-of-wordpress-plugins": {
+ icon: "plugin",
+ accent: "#ec4899",
+ keyword: "PLUGINS",
+ },
+ "why-websites-break-after-updates": {
+ icon: "shield",
+ accent: "#f59e0b",
+ keyword: "STABILITY",
+ },
+
+ // Group 2: Sovereignty & Law
+ "website-without-cookie-banners": {
+ icon: "cookie",
+ accent: "#10b981",
+ keyword: "PRIVACY",
+ },
+ "no-us-cloud-platforms": {
+ icon: "cloud",
+ accent: "#3b82f6",
+ keyword: "SOVEREIGN",
+ },
+ "gdpr-conformity-system-approach": {
+ icon: "shield",
+ accent: "#06b6d4",
+ keyword: "DSGVO",
+ },
+ "builder-systems-threaten-independence": {
+ icon: "lock",
+ accent: "#f43f5e",
+ keyword: "LOCK-IN",
+ },
+ "analytics-without-tracking": {
+ icon: "chart",
+ accent: "#8b5cf6",
+ keyword: "ANALYTICS",
+ },
+
+ // Group 3: Efficiency & Investment
+ "fast-website-carbon-footprint": {
+ icon: "leaf",
+ accent: "#22c55e",
+ keyword: "GREEN",
+ },
+ "fixed-price-vs-hourly-rate": {
+ icon: "price",
+ accent: "#0ea5e9",
+ keyword: "PRICING",
+ },
+ "build-first-talk-later": {
+ icon: "prototype",
+ accent: "#a855f7",
+ keyword: "PROTOTYPE",
+ },
+ "maintenance-without-cms": {
+ icon: "gear",
+ accent: "#64748b",
+ keyword: "MAINTAIN",
+ },
+ "timeless-websites": {
+ icon: "hourglass",
+ accent: "#0d9488",
+ keyword: "LONGEVITY",
+ },
+
+ // Group 4: Tech & Craft
+ "clean-code-success": {
+ icon: "code",
+ accent: "#2563eb",
+ keyword: "QUALITY",
+ },
+ "responsive-design-scaling": {
+ icon: "responsive",
+ accent: "#7c3aed",
+ keyword: "ADAPTIVE",
+ },
+ "hosting-and-operation": {
+ icon: "server",
+ accent: "#475569",
+ keyword: "INFRA",
+ },
+ "no-ready-made-templates": {
+ icon: "template",
+ accent: "#e11d48",
+ keyword: "CUSTOM",
+ },
+ "seamless-crm-sync": {
+ icon: "sync",
+ accent: "#0891b2",
+ keyword: "SYNC",
+ },
+};