feat: complete MDX migration for blog, fix diagram fidelity and refactor styling architecture

This commit is contained in:
2026-02-17 21:36:59 +01:00
parent bff58e7cfa
commit cce6aa0935
75 changed files with 12282 additions and 12227 deletions

View File

@@ -19,19 +19,19 @@ interface BlockquoteProps {
className?: string;
}
export const Blockquote: React.FC<BlockquoteProps> = ({ children, className = '' }) => (
<blockquote className={`border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-xl md:text-2xl font-serif ${className}`}>
export const ArticleBlockquote: React.FC<BlockquoteProps> = ({ children, className = '' }) => (
<blockquote className={`not-prose border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-xl md:text-2xl font-serif ${className}`}>
{children}
</blockquote>
);
interface CodeBlockProps {
code?: string;
children?: React.ReactNode;
language?: string;
showLineNumbers?: boolean;
className?: string;
}
code?: string;
children?: React.ReactNode;
language?: string;
showLineNumbers?: boolean;
className?: string;
}
// Language mapping for Prism.js
@@ -70,54 +70,54 @@ const highlightCode = (code: string, language: string): { html: string; prismLan
console.warn('Prism highlighting failed:', error);
return { html: code.trim(), prismLanguage: 'text' };
}
};
};
export const CodeBlock: React.FC<CodeBlockProps> = ({
code,
children,
language = 'text',
showLineNumbers = false,
className = ''
}) => {
const codeContent = code || (typeof children === 'string' ? children : String(children || '')).trim();
const { html: highlightedCode, prismLanguage } = language !== 'text' ? highlightCode(codeContent, language) : { html: codeContent, prismLanguage: 'text' };
const lines = codeContent.split('\n');
code,
children,
language = 'text',
showLineNumbers = false,
className = ''
}) => {
const codeContent = code || (typeof children === 'string' ? children : String(children || '')).trim();
const { html: highlightedCode, prismLanguage } = language !== 'text' ? highlightCode(codeContent, language) : { html: codeContent, prismLanguage: 'text' };
const lines = codeContent.split('\n');
return (
<>
<style dangerouslySetInnerHTML={{ __html: syntaxHighlightingStyles }} />
<div className="relative my-6">
{language !== 'text' && (
<div className="absolute top-3 right-3 text-[10px] font-bold uppercase tracking-widest bg-white text-slate-500 px-2 py-1 rounded-md z-10 border border-slate-100 shadow-sm">
{language}
</div>
)}
<pre
className={`m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border border-slate-200 bg-white rounded-2xl ${className} ${showLineNumbers ? 'pl-12' : ''}`}
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
>
{showLineNumbers ? (
<div className="relative">
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
{lines.map((_, i) => (
<div key={i} className="leading-relaxed">{i + 1}</div>
))}
</div>
<div className="pl-10">
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
</div>
</div>
) : (
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
)}
</pre>
</div>
</>
);
};
return (
<>
<style dangerouslySetInnerHTML={{ __html: syntaxHighlightingStyles }} />
<div className="relative my-6">
{language !== 'text' && (
<div className="absolute top-3 right-3 text-[10px] font-bold uppercase tracking-widest bg-white text-slate-500 px-2 py-1 rounded-md z-10 border border-slate-100 shadow-sm">
{language}
</div>
)}
<pre
className={`not-prose m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border border-slate-200 bg-white rounded-2xl ${className} ${showLineNumbers ? 'pl-12' : ''}`}
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
>
{showLineNumbers ? (
<div className="relative">
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
{lines.map((_, i) => (
<div key={i} className="leading-relaxed">{i + 1}</div>
))}
</div>
<div className="pl-10">
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
</div>
</div>
) : (
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
)}
</pre>
</div>
</>
);
};
export const InlineCode: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => (
<code className={`bg-white text-slate-800 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] border border-slate-200 ${className}`}>
<code className={`not-prose bg-white text-slate-800 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] border border-slate-200 ${className}`}>
{children}
</code>
);

View File

@@ -7,7 +7,7 @@ interface HeadingProps {
export const H1: React.FC<HeadingProps> = ({ children, className = "" }) => (
<h1
className={`text-4xl md:text-5xl font-bold text-slate-900 mb-6 mt-8 leading-[1.1] tracking-tight ${className}`}
className={`not-prose text-4xl md:text-5xl font-bold text-slate-900 mb-6 mt-8 leading-[1.1] tracking-tight ${className}`}
>
{children}
</h1>
@@ -15,7 +15,7 @@ export const H1: React.FC<HeadingProps> = ({ children, className = "" }) => (
export const H2: React.FC<HeadingProps> = ({ children, className = "" }) => (
<h2
className={`text-3xl md:text-4xl font-bold text-slate-900 mb-4 mt-10 leading-[1.2] tracking-tight ${className}`}
className={`not-prose text-3xl md:text-4xl font-bold text-slate-900 mb-4 mt-10 leading-[1.2] tracking-tight ${className}`}
>
{children}
</h2>
@@ -23,7 +23,7 @@ export const H2: React.FC<HeadingProps> = ({ children, className = "" }) => (
export const H3: React.FC<HeadingProps> = ({ children, className = "" }) => (
<h3
className={`text-2xl md:text-3xl font-bold text-slate-900 mb-3 mt-8 leading-[1.3] tracking-tight ${className}`}
className={`not-prose text-2xl md:text-3xl font-bold text-slate-900 mb-3 mt-8 leading-[1.3] tracking-tight ${className}`}
>
{children}
</h3>

View File

@@ -9,20 +9,20 @@ export const Paragraph: React.FC<ParagraphProps> = ({
children,
className = "",
}) => (
<p
className={`text-slate-700 font-serif text-lg md:text-xl leading-[1.6] mb-4 ${className}`}
<div
className={`not-prose text-slate-700 font-serif text-lg md:text-xl leading-[1.6] mb-4 [&_p]:mb-4 last:[&_p]:mb-0 ${className}`}
>
{children}
</p>
</div>
);
export const LeadParagraph: React.FC<ParagraphProps> = ({
children,
className = "",
}) => (
<p
className={`text-xl md:text-2xl text-slate-700 font-serif italic leading-snug mb-6 ${className}`}
<div
className={`not-prose text-xl md:text-2xl text-slate-700 font-serif italic leading-snug mb-6 [&_p]:mb-6 last:[&_p]:mb-0 ${className}`}
>
{children}
</p>
</div>
);

View File

@@ -17,6 +17,7 @@ interface DiagramGanttProps {
caption?: string;
id?: string;
showShare?: boolean;
fontSize?: string;
}
export const DiagramGantt: React.FC<DiagramGanttProps> = ({
@@ -25,25 +26,32 @@ export const DiagramGantt: React.FC<DiagramGanttProps> = ({
caption,
id,
showShare = true,
fontSize = "16px",
}) => {
const ganttGraph = `gantt
dateFormat YYYY-MM-DD
${tasks
.map((task) => {
const deps = task.dependencies?.length
? `, after ${task.dependencies.join(" ")}`
: "";
return ` ${task.name} :${task.id}, ${task.start}, ${task.duration}${deps}`;
})
.join("\n")}`;
${(tasks || [])
.map((task) => {
const deps = task.dependencies?.length
? `, after ${task.dependencies.join(" ")}`
: "";
return ` ${task.name} :${task.id}, ${task.start}, ${task.duration}${deps}`;
})
.join("\n")}`;
return (
<div className="my-12">
<Mermaid graph={ganttGraph} id={id} title={title} showShare={showShare} />
<Mermaid
graph={ganttGraph}
id={id}
title={title}
showShare={showShare}
fontSize={fontSize}
/>
{caption && (
<p className="text-center text-xs text-slate-400 mt-4 italic">
<div className="text-center text-xs text-slate-400 mt-4 italic">
{caption}
</p>
</div>
)}
</div>
);

View File

@@ -14,6 +14,7 @@ interface DiagramPieProps {
caption?: string;
id?: string;
showShare?: boolean;
fontSize?: string;
}
export const DiagramPie: React.FC<DiagramPieProps> = ({
@@ -22,17 +23,24 @@ export const DiagramPie: React.FC<DiagramPieProps> = ({
caption,
id,
showShare = true,
fontSize = "16px",
}) => {
const pieGraph = `pie
${data.map((slice) => ` "${slice.label}" : ${slice.value}`).join("\n")}`;
${(data || []).map((slice) => ` "${slice.label}" : ${slice.value}`).join("\n")}`;
return (
<div className="my-12">
<Mermaid graph={pieGraph} id={id} title={title} showShare={showShare} />
<Mermaid
graph={pieGraph}
id={id}
title={title}
showShare={showShare}
fontSize={fontSize}
/>
{caption && (
<p className="text-center text-xs text-slate-400 mt-4 italic">
<div className="text-center text-xs text-slate-400 mt-4 italic">
{caption}
</p>
</div>
)}
</div>
);

View File

@@ -17,6 +17,7 @@ interface DiagramSequenceProps {
caption?: string;
id?: string;
showShare?: boolean;
fontSize?: string;
}
export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
@@ -26,6 +27,7 @@ export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
caption,
id,
showShare = true,
fontSize = "16px",
}) => {
const getArrow = (type?: string) => {
switch (type) {
@@ -39,8 +41,8 @@ export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
};
const sequenceGraph = `sequenceDiagram
${participants.map((p) => ` participant ${p}`).join("\n")}
${messages.map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join("\n")}`;
${(participants || []).map((p) => ` participant ${p}`).join("\n")}
${(messages || []).map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join("\n")}`;
return (
<div className="my-12">
@@ -49,11 +51,12 @@ ${messages.map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).j
id={id}
title={title}
showShare={showShare}
fontSize={fontSize}
/>
{caption && (
<p className="text-center text-xs text-slate-400 mt-4 italic">
<div className="text-center text-xs text-slate-400 mt-4 italic">
{caption}
</p>
</div>
)}
</div>
);

View File

@@ -18,6 +18,7 @@ interface DiagramStateProps {
caption?: string;
id?: string;
showShare?: boolean;
fontSize?: string;
}
export const DiagramState: React.FC<DiagramStateProps> = ({
@@ -29,24 +30,31 @@ export const DiagramState: React.FC<DiagramStateProps> = ({
caption,
id,
showShare = true,
fontSize = "16px",
}) => {
const stateGraph = `stateDiagram-v2
${initialState ? ` [*] --> ${initialState}` : ""}
${transitions
.map((t) => {
const label = t.label ? ` : ${t.label}` : "";
return ` ${t.from} --> ${t.to}${label}`;
})
.join("\n")}
${finalStates.map((s) => ` ${s} --> [*]`).join("\n")}`;
${(transitions || [])
.map((t) => {
const label = t.label ? ` : ${t.label}` : "";
return ` ${t.from} --> ${t.to}${label}`;
})
.join("\n")}
${(finalStates || []).map((s) => ` ${s} --> [*]`).join("\n")}`;
return (
<div className="my-12">
<Mermaid graph={stateGraph} id={id} title={title} showShare={showShare} />
<Mermaid
graph={stateGraph}
id={id}
title={title}
showShare={showShare}
fontSize={fontSize}
/>
{caption && (
<p className="text-center text-xs text-slate-400 mt-4 italic">
<div className="text-center text-xs text-slate-400 mt-4 italic">
{caption}
</p>
</div>
)}
</div>
);

View File

@@ -14,6 +14,7 @@ interface DiagramTimelineProps {
caption?: string;
id?: string;
showShare?: boolean;
fontSize?: string;
}
export const DiagramTimeline: React.FC<DiagramTimelineProps> = ({
@@ -22,10 +23,11 @@ export const DiagramTimeline: React.FC<DiagramTimelineProps> = ({
caption,
id,
showShare = true,
fontSize = "16px",
}) => {
const timelineGraph = `timeline
title ${title || "Timeline"}
${events.map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
${(events || []).map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
return (
<div className="my-12">
@@ -34,11 +36,12 @@ ${events.map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
id={id}
title={title}
showShare={showShare}
fontSize={fontSize}
/>
{caption && (
<p className="text-center text-xs text-slate-400 mt-4 italic">
<div className="text-center text-xs text-slate-400 mt-4 italic">
{caption}
</p>
</div>
)}
</div>
);

View File

@@ -1,86 +0,0 @@
'use client';
import React, { useState } from 'react';
import { FileExample } from './FileExample';
import type { FileExampleGroup } from '../data/fileExamples';
interface FileExamplesListProps {
groups: FileExampleGroup[];
}
export const FileExamplesList: React.FC<FileExamplesListProps> = ({ groups }) => {
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
const toggleAllInGroup = (groupId: string, files: any[]) => {
const isAnyExpanded = files.some(f => expandedGroups[f.id]);
const newExpanded = { ...expandedGroups };
files.forEach(f => {
newExpanded[f.id] = !isAnyExpanded;
});
setExpandedGroups(newExpanded);
};
if (groups.length === 0) {
return (
<div className="bg-slate-50 border border-slate-200 rounded-lg px-4 py-6 text-center">
<svg className="w-6 h-6 mx-auto text-slate-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-slate-500 text-sm">No files found</p>
</div>
);
}
return (
<div className="space-y-3">
{groups.map((group) => (
<section
key={group.groupId}
className="not-prose bg-white border border-slate-200/80 rounded-lg w-full overflow-hidden"
data-file-examples-group
>
<header className="px-3 py-2 grid grid-cols-[1fr_auto] items-center gap-3 border-b border-slate-200/80 bg-white">
<h3 className="m-0 text-xs font-semibold text-slate-900 truncate tracking-tight leading-none">{group.title}</h3>
<div className="flex items-center gap-2">
<span className="text-[11px] text-slate-600 bg-slate-100/80 border border-slate-200/60 rounded-full px-2 py-0.5 tabular-nums">
{group.files.length} files
</span>
<button
type="button"
className="toggle-all-btn h-8 w-8 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100"
title="Toggle all"
onClick={() => toggleAllInGroup(group.groupId, group.files)}
>
{group.files.some(f => expandedGroups[f.id]) ? (
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 15l7-7 7 7" />
</svg>
) : (
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
)}
</button>
</div>
</header>
<div className="px-2 py-2 space-y-2">
{group.files.map((file) => (
<FileExample
key={file.id}
filename={file.filename}
content={file.content}
language={file.language}
description={file.description}
tags={file.tags}
id={file.id}
/>
))}
</div>
</section>
))}
</div>
);
};

View File

@@ -19,7 +19,7 @@ interface IconListItemProps {
export const IconList: React.FC<IconListProps> = ({
children,
className = "",
}) => <ul className={`space-y-4 ${className}`}>{children}</ul>;
}) => <ul className={`not-prose space-y-4 ${className}`}>{children}</ul>;
export const IconListItem: React.FC<IconListItemProps> = ({
children,

View File

@@ -26,7 +26,7 @@ export const ComparisonRow: React.FC<ComparisonRowProps> = ({
}) => {
return (
<Reveal delay={delay}>
<div className="space-y-4">
<div className="not-prose space-y-4">
{description && (
<Label className="text-slate-400 text-[10px] tracking-[0.2em] uppercase">
{description}

View File

@@ -5,22 +5,67 @@ import mermaid from "mermaid";
import { DiagramShareButton } from "./DiagramShareButton";
interface MermaidProps {
graph: string;
graph?: string;
children?: React.ReactNode;
id?: string;
title?: string;
showShare?: boolean;
fontSize?: string;
nodeFontSize?: string;
labelFontSize?: string;
actorFontSize?: string;
messageFontSize?: string;
noteFontSize?: string;
titleFontSize?: string;
sectionFontSize?: string;
legendFontSize?: string;
}
export const Mermaid: React.FC<MermaidProps> = ({
graph,
children,
id: providedId,
title,
showShare = false,
fontSize = "16px",
nodeFontSize,
labelFontSize,
actorFontSize,
messageFontSize,
noteFontSize,
titleFontSize,
sectionFontSize,
legendFontSize,
}) => {
const [id, setId] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [svgContent, setSvgContent] = useState<string>("");
// Extract text from React children nodes (MDX parses multi-line content as React nodes)
const extractTextFromChildren = (node: React.ReactNode): string => {
if (typeof node === 'string') return node;
if (typeof node === 'number') return String(node);
if (Array.isArray(node)) return node.map(extractTextFromChildren).join('\n');
if (React.isValidElement(node)) {
const props = node.props as { children?: React.ReactNode };
if (props.children) {
return extractTextFromChildren(props.children);
}
}
return '';
};
const rawGraph = graph || extractTextFromChildren(children) || "";
// MDXRemote double-escapes \n in plain string props (e.g., "graph TD\\nA-->B" becomes "graph TD\\\\nA-->B")
// We need to unescape these back to real newlines for Mermaid to parse
const sanitizedGraph = rawGraph
.trim()
.replace(/^`+|`+$/g, '')
.replace(/\\n/g, '\n')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
useEffect(() => {
setId(
providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`,
@@ -63,7 +108,15 @@ export const Mermaid: React.FC<MermaidProps> = ({
// Font
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "14px",
fontSize: fontSize,
nodeFontSize: nodeFontSize || fontSize,
labelFontSize: labelFontSize || fontSize,
actorFontSize: actorFontSize || fontSize,
messageFontSize: messageFontSize || fontSize,
noteFontSize: noteFontSize || fontSize,
titleFontSize: titleFontSize || "20px",
sectionFontSize: sectionFontSize || fontSize,
legendFontSize: legendFontSize || fontSize,
// Pie Chart Colors - High Contrast Industrial Palette
pie1: "#0f172a", // Deep Navy
@@ -84,14 +137,21 @@ export const Mermaid: React.FC<MermaidProps> = ({
const render = async () => {
if (containerRef.current && id) {
if (!sanitizedGraph || sanitizedGraph.trim() === "") {
console.warn("Mermaid: Empty or invalid graph provided, skipping render.");
return;
}
try {
const { svg } = await mermaid.render(`${id}-svg`, graph);
const { svg } = await mermaid.render(`${id}-svg`, sanitizedGraph);
containerRef.current.innerHTML = svg;
setSvgContent(svg);
setIsRendered(true);
} catch (err) {
console.error("Mermaid rendering failed:", err);
setError("Failed to render diagram. Please check the syntax.");
console.error("Graph that failed:", sanitizedGraph);
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to render diagram: ${errorMessage}`);
setIsRendered(true);
}
}
@@ -100,7 +160,7 @@ export const Mermaid: React.FC<MermaidProps> = ({
if (id) {
render();
}
}, [graph, id]);
}, [sanitizedGraph, id]);
if (!id) return null;
@@ -127,7 +187,7 @@ export const Mermaid: React.FC<MermaidProps> = ({
{error}
</div>
) : (
graph
sanitizedGraph
)}
</div>
</div>

View File

@@ -0,0 +1,27 @@
import React from 'react';
interface StatsDisplayProps {
value: string | number;
label: string;
subtext?: string;
className?: string;
}
export const StatsDisplay: React.FC<StatsDisplayProps> = ({ value, label, subtext, className = '' }) => {
return (
<div className={`not-prose flex flex-col items-center justify-center p-8 my-10 bg-gradient-to-br from-slate-50 to-slate-100 border border-slate-200 rounded-2xl shadow-sm text-center ${className}`}>
<span className="text-7xl font-black text-slate-900 tracking-tighter tabular-nums leading-none">
{value}
</span>
<span className="text-xl font-bold text-slate-700 mt-3 uppercase tracking-wide">
{label}
</span>
{subtext && (
<span className="text-sm font-medium text-slate-500 mt-2 max-w-xs leading-relaxed">
{subtext}
</span>
)}
</div>
);
};

View File

@@ -12,7 +12,7 @@ export function TextSelectionShare() {
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { trackEvent } = useAnalytics();
const selectionTimeoutRef = useRef<NodeJS.Timeout>();
const selectionTimeoutRef = useRef<NodeJS.Timeout>(undefined);
useEffect(() => {
const handleSelection = () => {

View File

@@ -41,22 +41,22 @@ export const LeadText: React.FC<TypographyProps> = ({
children,
className = "",
}) => (
<p
<div
className={`text-sm md:text-xl font-serif italic text-slate-500 leading-relaxed ${className}`}
>
{children}
</p>
</div>
);
export const BodyText: React.FC<TypographyProps> = ({
children,
className = "",
}) => (
<p
<div
className={`text-sm md:text-base text-slate-500 leading-relaxed ${className}`}
>
{children}
</p>
</div>
);
export const Label: React.FC<TypographyProps> = ({

View File

@@ -1,9 +1,9 @@
import React from "react";
/* eslint-disable react/prop-types */
import type {
ThumbnailIcon,
BlogThumbnailConfig,
} from "../../data/blogThumbnails";
import { blogThumbnails } from "../../data/blogThumbnails";
} from "./blogThumbnails";
import { blogThumbnails } from "./blogThumbnails";
interface BlogThumbnailSVGProps {
slug: string;

View File

@@ -0,0 +1,140 @@
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.
* Updated to match the new MDX slugs.
*/
export const blogThumbnails: Record<string, BlogThumbnailConfig> = {
// 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
"green-it-sustainable-web": {
icon: "leaf",
accent: "#22c55e",
keyword: "GREEN",
},
"fixed-price-digital-projects": {
icon: "price",
accent: "#0ea5e9",
keyword: "PRICING",
},
"build-first-digital-architecture": {
icon: "prototype",
accent: "#a855f7",
keyword: "PROTOTYPE",
},
"maintenance-for-headless-systems": {
icon: "gear",
accent: "#64748b",
keyword: "MAINTAIN",
},
"digital-longevity-architecture": {
icon: "hourglass",
accent: "#0d9488",
keyword: "LONGEVITY",
},
// Group 4: Tech & Craft
"clean-code-for-business-value": {
icon: "code",
accent: "#2563eb",
keyword: "QUALITY",
},
"responsive-design-high-fidelity": {
icon: "responsive",
accent: "#7c3aed",
keyword: "ADAPTIVE",
},
"professional-hosting-operations": {
icon: "server",
accent: "#475569",
keyword: "INFRA",
},
"why-no-templates-matter": {
icon: "template",
accent: "#e11d48",
keyword: "CUSTOM",
},
"crm-synchronization-headless": {
icon: "sync",
accent: "#0891b2",
keyword: "SYNC",
},
};