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",
},
};

View File

@@ -0,0 +1,39 @@
import { LeadParagraph } from '../components/ArticleParagraph';
import { H2, H3 } from '../components/ArticleHeading';
import { Paragraph } from '../components/ArticleParagraph';
import { ArticleBlockquote } from '../components/ArticleBlockquote';
import { Marker } from '../components/Marker';
import { ComparisonRow } from '../components/Landing/ComparisonRow';
import { StatsDisplay } from '../components/StatsDisplay';
import { Mermaid } from '../components/Mermaid';
import { DiagramState } from '../components/DiagramState';
import { DiagramTimeline } from '../components/DiagramTimeline';
import { DiagramGantt } from '../components/DiagramGantt';
import { DiagramPie } from '../components/DiagramPie';
import { DiagramSequence } from '../components/DiagramSequence';
import { IconList, IconListItem } from '../components/IconList';
import { Section } from '../components/Section';
import { Reveal } from '../components/Reveal';
export const mdxComponents = {
LeadParagraph,
H2,
H3,
Paragraph,
ArticleBlockquote,
Marker,
ComparisonRow,
StatsDisplay,
Mermaid,
DiagramState,
DiagramTimeline,
DiagramGantt,
DiagramPie,
DiagramSequence,
IconList,
IconListItem,
Section,
Reveal
};

View File

@@ -0,0 +1,55 @@
import { ComponentDefinition } from '@mintel/content-engine';
export const componentDefinitions: ComponentDefinition[] = [
{
name: 'LeadParagraph',
description: 'Large, introductory text for the beginning of the article.',
usageExample: '<LeadParagraph>First meaningful sentence.</LeadParagraph>'
},
{
name: 'H2',
description: 'Section heading.',
usageExample: '<H2>Section Title</H2>'
},
{
name: 'H3',
description: 'Subsection heading.',
usageExample: '<H3>Subtitle</H3>'
},
{
name: 'Paragraph',
description: 'Standard body text paragraph.',
usageExample: '<Paragraph>Some text...</Paragraph>'
},
{
name: 'ArticleBlockquote',
description: 'A prominent quote block for key insights.',
usageExample: '<ArticleBlockquote>Important quote</ArticleBlockquote>'
},
{
name: 'Marker',
description: 'Yellow highlighter effect for very important phrases.',
usageExample: '<Marker>Highlighted Text</Marker>'
},
{
name: 'ComparisonRow',
description: 'A component comparing a negative vs positive scenario.',
usageExample: '<ComparisonRow description="Cost Comparison" negativeLabel="Lock-In" negativeText="High costs" positiveLabel="Open" positiveText="Control" />'
},
{
name: 'StatsDisplay',
description: 'A bold visual component to highlight a key statistic or number.',
usageExample: '<StatsDisplay value="42%" label="Cost Reduction" subtext="Average savings by switching to open standards." />'
},
{
name: 'Mermaid',
description: 'Renders a Mermaid diagram.',
usageExample: '<Mermaid graph="graph TD..." id="my-diagram" />'
},
{
name: 'DiagramState',
description: 'A state transition diagram.',
usageExample: '<DiagramState states={["A", "B"]} ... />'
}
];

View File

@@ -0,0 +1,3 @@
export * from './definitions';
export { mdxComponents } from './components';

View File

@@ -1,180 +0,0 @@
export interface BlogPost {
title: string;
description: string;
date: string;
slug: string;
tags: string[];
}
export const blogPosts: BlogPost[] = [
// Gruppe 1: Schmerzpunkte & Fehlerbehebung
{
title: "Warum Ihre Website bei Google PageSpeed scheitert",
description:
"Technische Optimierung ist heute kein Luxus mehr, sondern überlebenswichtig für Ihre Sichtbarkeit.",
date: "2026-02-15",
slug: "why-pagespeed-fails",
tags: ["performance", "engineering"],
},
{
title: "Langsame Ladezeiten: Diese technischen Altlasten kosten Sie Kunden",
description:
"Wie Sie versteckte Performance-Killer identifizieren und eliminieren, bevor sie Ihren Umsatz gefährden.",
date: "2026-02-14",
slug: "slow-loading-costs-customers",
tags: ["performance", "business"],
},
{
title: "Warum Ihre Agentur für kleine Änderungen Wochen braucht",
description:
"Starre Prozesse vs. flexible Architektur: So brechen Sie den Flaschenhals in Ihrer Entwicklung auf.",
date: "2026-02-13",
slug: "why-agencies-are-slow",
tags: ["architecture", "engineering"],
},
{
title: "Die versteckten Kosten von WordPress-Plugins",
description:
"Warum die 'einfache Lösung' oft zur teuren Wartungsfalle wird und wie Sie echte Unabhängigkeit gewinnen.",
date: "2026-02-12",
slug: "hidden-costs-of-wordpress-plugins",
tags: ["architecture", "business"],
},
{
title: "Warum Ihre Website nach jedem Update kaputtgeht",
description:
"Systematische Stabilität vs. Flickschusterei: Warum Test-Automatisierung Ihr wichtigstes Investment ist.",
date: "2026-02-11",
slug: "why-websites-break-after-updates",
tags: ["engineering", "architecture"],
},
// Gruppe 2: Souveränität & Recht
{
title:
"Website ohne Cookie-Banner: So funktioniert datenschutzkonformes Design",
description:
"Nutzererfahrung ohne nervige Popups: Wie Sie Vertrauen gewinnen und DSGVO-konform bleiben.",
date: "2026-02-10",
slug: "website-without-cookie-banners",
tags: ["privacy", "ux-design"],
},
{
title: "Warum ich keine US-Cloud-Plattformen für Ihre Daten nutze",
description:
"Souveränität durch lokale Infrastruktur: Warum Ihre Daten in Europa am sichersten sind.",
date: "2026-02-09",
slug: "no-us-cloud-platforms",
tags: ["privacy", "architecture"],
},
{
title: "DSGVO-Konformität ohne Abmahnrisiko: Der System-Ansatz",
description:
"Rechtssicherheit ist kein Zufall, sondern das Ergebnis eines klaren technischen Konzepts.",
date: "2026-02-08",
slug: "gdpr-conformity-system-approach",
tags: ["privacy", "architecture"],
},
{
title: "Warum Baukasten-Systeme Ihre digitale Unabhängigkeit gefährden",
description:
"Vendor Lock-in vermeiden: Warum Sie die volle Kontrolle über Ihren Code und Ihre Daten behalten müssen.",
date: "2026-02-07",
slug: "builder-systems-threaten-independence",
tags: ["architecture", "business"],
},
{
title: "Analytics ohne Tracking: Erfolg messen, ohne Kunden zu nerven",
description:
"Privacy-first Metriken: Wie Sie wertvolle Insights gewinnen, ohne die Privatsphäre Ihrer Nutzer zu verletzen.",
date: "2026-02-06",
slug: "analytics-without-tracking",
tags: ["privacy", "ux-design"],
},
// Gruppe 3: Effizienz & Investment
{
title: "Warum eine schnelle Website Ihren CO₂-Fußabdruck halbiert",
description:
"Digitale Nachhaltigkeit: Wie effizienter Code nicht nur Kunden, sondern auch das Klima schont.",
date: "2026-02-05",
slug: "fast-website-carbon-footprint",
tags: ["performance", "engineering"],
},
{
title:
"Fixpreis statt Stundensatz: Warum ich keine Kostenvoranschläge schätze",
description:
"Transparenz und Ergebnisfokus: Warum klassische Schätzungen oft am Ziel vorbeischießen.",
date: "2026-02-04",
slug: "fixed-price-vs-hourly-rate",
tags: ["business", "engineering"],
},
{
title: "Warum ich erst baue und wir dann darüber reden",
description:
"Prototyping-first: Warum echte Interaktion wertvoller ist als hundert Mockups.",
date: "2026-02-03",
slug: "build-first-talk-later",
tags: ["engineering", "ux-design"],
},
{
title: "Website-Pflege: Warum Sie kein CMS brauchen, um Inhalte zu ändern",
description:
"Modernes Content Management: Effizienz durch Entkopplung von Design und Redaktion.",
date: "2026-02-02",
slug: "maintenance-without-cms",
tags: ["architecture", "business"],
},
{
title: "Warum meine Websites auch nach fünf Jahren nicht veralten",
description:
"Invesitionssicherheit durch zukunftssichere Technologie-Stacks und zeitloses Design.",
date: "2026-02-01",
slug: "timeless-websites",
tags: ["business", "architecture"],
},
// Gruppe 4: Technik & Handwerk
{
title:
"Clean Code: Warum die Struktur hinter der Oberfläche über Ihren Erfolg entscheidet",
description:
"Wartbarkeit als Wettbewerbsvorteil: Warum Qualität im Verborgenen beginnt.",
date: "2026-01-31",
slug: "clean-code-success",
tags: ["engineering", "architecture"],
},
{
title: "Responsives Design: Warum Skalieren allein nicht ausreicht",
description:
"Echte Adaptivität vs. einfache Größenanpassung: UX über alle Viewports hinweg.",
date: "2026-01-30",
slug: "responsive-design-scaling",
tags: ["ux-design", "engineering"],
},
{
title: "Hosting und Betrieb: Was hinter einem stabilen System steckt",
description:
"Managed Infrastructure: Warum die Wahl der Umgebung entscheidend für die Performance ist.",
date: "2026-01-29",
slug: "hosting-and-operation",
tags: ["architecture", "performance"],
},
{
title: "Warum ich keine fertigen Templates verwende",
description:
"Individualität als Standard: Warum 'von der Stange' oft teure Anpassungen nach sich zieht.",
date: "2026-01-28",
slug: "no-ready-made-templates",
tags: ["ux-design", "performance"],
},
{
title: "Schnittstellen ohne Stress: So gelingt der Daten-Sync zu Ihrem CRM",
description:
"Automatisierung durch Integration: Wie Sie manuelle Arbeit durch saubere APIs eliminieren.",
date: "2026-01-27",
slug: "seamless-crm-sync",
tags: ["architecture", "engineering"],
},
];

View File

@@ -1,139 +0,0 @@
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<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
"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",
},
};

View File

@@ -1,28 +0,0 @@
import type { BlogPost } from './blogPosts';
export const embedDemoPost: BlogPost = {
title: "Rich Content Embedding Demo",
description: "Testing our new free embed components for YouTube, Twitter, and other platforms",
date: "2024-02-15",
slug: "embed-demo",
tags: ["embeds", "components", "tutorial"]
};
// This would be used in your blog post template to demonstrate the components
export const embedDemoContent = {
youtube: {
videoId: "dQw4w9WgXcQ", // Replace with actual video ID
title: "Demo Video",
style: "minimal"
},
twitter: {
tweetId: "1234567890123456789", // Replace with actual tweet ID
theme: "dark",
align: "center"
},
generic: {
url: "https://vimeo.com/123456789", // Replace with actual URL
type: "video",
maxWidth: "800px"
}
};

View File

@@ -1,628 +0,0 @@
/**
* File Examples Data Structure
*
* This module manages file examples for blog posts.
* Each example includes the file content, metadata, and can be easily copied or downloaded.
*/
export interface FileExample {
id: string;
filename: string;
content: string;
language: string;
description?: string;
tags?: string[];
postSlug?: string;
createdAt: string;
updatedAt: string;
}
export interface FileExampleGroup {
groupId: string;
title: string;
description?: string;
files: FileExample[];
}
// In-memory storage (for development)
// In production, this could be backed by a database or file system
const fileExamplesStore = new Map<string, FileExample>();
// Sample file examples for demonstration
export const sampleFileExamples: FileExampleGroup[] = [
{
groupId: "python-data-processing",
title: "Python Data Processing Example",
description: "A complete example of processing data with error handling",
files: [
{
id: "python-data-processor",
filename: "data_processor.py",
content: `import json
import logging
from typing import List, Dict, Any
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class DataProcessor:
def __init__(self, input_path: str, output_path: str):
self.input_path = Path(input_path)
self.output_path = Path(output_path)
def load_data(self) -> List[Dict[str, Any]]:
"""Load JSON data from input file."""
if not self.input_path.exists():
raise FileNotFoundError(f"Input file not found: {self.input_path}")
with open(self.input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
logger.info(f"Loaded {len(data)} records")
return data
def process_records(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Process records and add computed fields."""
processed = []
for record in data:
# Add timestamp
import time
record['processed_at'] = time.time()
# Normalize keys
record['id'] = record.get('id', '').lower()
processed.append(record)
logger.info(f"Processed {len(processed)} records")
return processed
def save_data(self, data: List[Dict[str, Any]]) -> None:
"""Save processed data to output file."""
self.output_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
logger.info(f"Saved {len(data)} records to {self.output_path}")
def run(self) -> None:
"""Execute the complete processing pipeline."""
try:
data = self.load_data()
processed = self.process_records(data)
self.save_data(processed)
logger.info("Processing completed successfully")
except Exception as e:
logger.error(f"Processing failed: {e}")
raise
if __name__ == "__main__":
# Example usage
processor = DataProcessor(
input_path="data/input.json",
output_path="data/processed.json"
)
processor.run()`,
language: "python",
description: "A robust data processor with logging and error handling",
tags: ["python", "data-processing", "logging"],
postSlug: "debugging-tips",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "python-config-example",
filename: "config.py",
content: `"""
Configuration management for the data processor
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class Config:
"""Configuration for data processing."""
input_path: str
output_path: str
batch_size: int = 1000
max_workers: int = 4
enable_caching: bool = True
log_level: str = "INFO"
@classmethod
def from_dict(cls, data: dict) -> 'Config':
"""Create config from dictionary."""
return cls(**data)
def to_dict(self) -> dict:
"""Convert config to dictionary."""
return {
'input_path': self.input_path,
'output_path': self.output_path,
'batch_size': self.batch_size,
'max_workers': self.max_workers,
'enable_caching': self.enable_caching,
'log_level': self.log_level
}
# Default configuration
DEFAULT_CONFIG = Config(
input_path="data/input.json",
output_path="data/output.json",
batch_size=500,
max_workers=2
)`,
language: "python",
description: "Configuration management using dataclasses",
tags: ["python", "configuration", "dataclasses"],
postSlug: "debugging-tips",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
},
{
groupId: "typescript-architecture",
title: "TypeScript Architecture Patterns",
description: "Modern TypeScript patterns for scalable applications",
files: [
{
id: "ts-interface-example",
filename: "interfaces.ts",
content: `/**
* Core interfaces for a scalable TypeScript application
*/
// Repository pattern
export interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: string, entity: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
// Service layer interface
export interface Service<T> {
get(id: string): Promise<T>;
list(): Promise<T[]>;
create(data: any): Promise<T>;
update(id: string, data: any): Promise<T>;
remove(id: string): Promise<void>;
}
// Event system
export interface DomainEvent {
type: string;
payload: any;
timestamp: Date;
source: string;
}
export interface EventHandler {
handle(event: DomainEvent): Promise<void>;
}
export interface EventPublisher {
publish(event: DomainEvent): Promise<void>;
subscribe(handler: EventHandler): void;
}
// Result type for error handling
export type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
export namespace Result {
export function ok<T>(value: T): Result<T> {
return { success: true, value };
}
export function fail<E extends Error>(error: E): Result<never, E> {
return { success: false, error };
}
export function isOk<T, E>(result: Result<T, E>): result is { success: true; value: T } {
return result.success;
}
export function isFail<T, E>(result: Result<T, E>): result is { success: false; error: E } {
return !result.success;
}
}`,
language: "typescript",
description: "TypeScript interfaces for clean architecture",
tags: ["typescript", "architecture", "interfaces"],
postSlug: "architecture-patterns",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "ts-service-example",
filename: "userService.ts",
content: `import { Repository, Service, Result, DomainEvent, EventPublisher } from './interfaces';
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
interface CreateUserDTO {
email: string;
name: string;
}
class UserService implements Service<User> {
constructor(
private readonly userRepository: Repository<User>,
private readonly eventPublisher: EventPublisher
) {}
async get(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error(\`User with id \${id} not found\`);
}
return user;
}
async list(): Promise<User[]> {
return this.userRepository.findAll();
}
async create(data: CreateUserDTO): Promise<User> {
// Validate email
if (!this.isValidEmail(data.email)) {
throw new Error('Invalid email format');
}
// Create user
const user = await this.userRepository.create({
...data,
createdAt: new Date(),
updatedAt: new Date()
});
// Publish event
const event: DomainEvent = {
type: 'USER_CREATED',
payload: { userId: user.id, email: user.email },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const existing = await this.get(id);
const updated = await this.userRepository.update(id, {
...data,
updatedAt: new Date()
});
const event: DomainEvent = {
type: 'USER_UPDATED',
payload: { userId: id, changes: data },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
return updated;
}
async remove(id: string): Promise<void> {
const success = await this.userRepository.delete(id);
if (!success) {
throw new Error(\`Failed to delete user \${id}\`);
}
const event: DomainEvent = {
type: 'USER_DELETED',
payload: { userId: id },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
return emailRegex.test(email);
}
// Additional business logic
async getUserByEmail(email: string): Promise<User | null> {
const users = await this.userRepository.findAll();
return users.find(u => u.email === email) || null;
}
}
export { UserService, type User, type CreateUserDTO };`,
language: "typescript",
description: "Service implementation with domain events",
tags: ["typescript", "service-layer", "domain-events"],
postSlug: "architecture-patterns",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
},
{
groupId: "docker-deployment",
title: "Docker Deployment Configuration",
description: "Production-ready Docker setup",
files: [
{
id: "dockerfile",
filename: "Dockerfile",
content: `# Multi-stage build for optimized production image
FROM node:20-alpine AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts
# Build stage
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S astro -u 1001
# Copy built assets
COPY --from=builder --chown=astro:nodejs /app/dist ./dist
COPY --from=deps --chown=astro:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=astro:nodejs /app/package*.json ./
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD node -e "require('http').get('http://localhost:4321/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run as non-root
USER astro
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]`,
language: "dockerfile",
description: "Multi-stage Docker build for production",
tags: ["docker", "production", "multi-stage"],
postSlug: "docker-deployment",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "docker-compose",
filename: "docker-compose.yml",
content: `version: '3.8'
services:
web:
build:
context: .
target: production
ports:
- "8080:4321"
environment:
- NODE_ENV=production
- PORT=4321
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4321/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
# Optional: Add Redis for caching
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
deploy:
resources:
limits:
memory: 256M
# Optional: Add Caddy for reverse proxy
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
redis_data:
caddy_data:
caddy_config:`,
language: "yaml",
description: "Multi-service Docker Compose setup",
tags: ["docker", "compose", "orchestration"],
postSlug: "docker-deployment",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
}
];
// Helper functions for managing file examples
export class FileExampleManager {
static async getFileExample(id: string): Promise<FileExample | undefined> {
// First check in-memory store
const stored = fileExamplesStore.get(id);
if (stored) return stored;
// Search in sample data
for (const group of sampleFileExamples) {
const file = group.files.find(f => f.id === id);
if (file) return file;
}
return undefined;
}
static async getFilesByTag(tag: string): Promise<FileExample[]> {
const results: FileExample[] = [];
for (const group of sampleFileExamples) {
for (const file of group.files) {
if (file.tags?.includes(tag)) {
results.push(file);
}
}
}
return results;
}
static async searchFiles(query: string): Promise<FileExample[]> {
const lowerQuery = query.toLowerCase();
const results: FileExample[] = [];
for (const group of sampleFileExamples) {
for (const file of group.files) {
const searchable = [
file.filename,
file.description,
file.language,
...(file.tags || [])
].join(' ').toLowerCase();
if (searchable.includes(lowerQuery)) {
results.push(file);
}
}
}
return results;
}
static async getAvailableTags(): Promise<string[]> {
const tags = new Set<string>();
for (const group of sampleFileExamples) {
for (const file of group.files) {
file.tags?.forEach(tag => tags.add(tag));
}
}
return Array.from(tags).sort();
}
static async createFileExample(example: Omit<FileExample, 'id' | 'createdAt' | 'updatedAt'>): Promise<FileExample> {
const id = `${example.filename.replace(/[^a-zA-Z0-9]/g, '-')}-${Date.now()}`;
const newExample: FileExample = {
...example,
id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
fileExamplesStore.set(id, newExample);
return newExample;
}
static async updateFileExample(id: string, updates: Partial<FileExample>): Promise<FileExample | undefined> {
const existing = await this.getFileExample(id);
if (!existing) return undefined;
const updated: FileExample = {
...existing,
...updates,
updatedAt: new Date().toISOString()
};
fileExamplesStore.set(id, updated);
return updated;
}
static async deleteFileExample(id: string): Promise<boolean> {
return fileExamplesStore.delete(id);
}
static async getAllGroups(): Promise<FileExampleGroup[]> {
return sampleFileExamples;
}
static async getGroup(groupId: string): Promise<FileExampleGroup | undefined> {
return sampleFileExamples.find(g => g.groupId === groupId);
}
static async downloadFile(id: string): Promise<{ filename: string; content: string; mimeType: string } | null> {
const file = await this.getFileExample(id);
if (!file) return null;
const mimeType = this.getMimeType(file.language);
return {
filename: file.filename,
content: file.content,
mimeType
};
}
static async downloadMultiple(ids: string[]): Promise<Array<{ filename: string; content: string }>> {
const files = await Promise.all(ids.map(id => this.getFileExample(id)));
return files
.filter((f): f is FileExample => f !== undefined)
.map(f => ({ filename: f.filename, content: f.content }));
}
private static getMimeType(language: string): string {
const mimeTypes: Record<string, string> = {
'python': 'text/x-python',
'typescript': 'text/x-typescript',
'javascript': 'text/javascript',
'dockerfile': 'text/x-dockerfile',
'yaml': 'text/yaml',
'json': 'application/json',
'html': 'text/html',
'css': 'text/css',
'sql': 'text/x-sql',
'bash': 'text/x-shellscript',
'text': 'text/plain'
};
return mimeTypes[language] || 'text/plain';
}
}

5
apps/web/src/mdx.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.mdx' {
let MDXComponent: (props: any) => JSX.Element;
export default MDXComponent;
}

View File

@@ -1,311 +0,0 @@
/**
* Test the integration between blog posts and file examples
* This simulates what happens when a blog post is rendered
*/
import { blogPosts } from "../data/blogPosts";
import { FileExampleManager } from "../data/fileExamples";
/* eslint-disable no-unused-vars */
export async function testBlogPostIntegration() {
console.log("🧪 Testing Blog Post + File Examples Integration...\n");
let passed = 0;
let failed = 0;
const test = (name: string, fn: () => void | Promise<void>) => {
try {
const result = fn();
if (result instanceof Promise) {
return result
.then(() => {
console.log(`${name}`);
passed++;
})
.catch((err) => {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
});
} else {
console.log(`${name}`);
passed++;
}
} catch (err: any) {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
}
};
// Test 1: Blog posts exist
test("Blog posts are loaded", () => {
if (!blogPosts || blogPosts.length === 0) {
throw new Error("No blog posts found");
}
console.log(` Found ${blogPosts.length} posts`);
});
// Test 2: Each post has required fields
test("All posts have required fields", () => {
for (const post of blogPosts) {
if (!post.slug || !post.title || !post.tags) {
throw new Error(`Post ${post.slug} missing required fields`);
}
}
});
// Test 3: Debugging-tips post should have file examples
test("debugging-tips post has file examples", async () => {
const post = blogPosts.find((p) => p.slug === "debugging-tips");
if (!post) {
throw new Error("debugging-tips post not found");
}
// Check if it would trigger file examples
const _showFileExamples = post.tags?.some((tag) =>
[
"architecture",
"design-patterns",
"system-design",
"docker",
"deployment",
].includes(tag),
);
// debugging-tips has tags ['debugging', 'tools'] so showFileExamples would be false
// But it has hardcoded FileExamplesList in the template
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups
.flatMap((g) => g.files)
.filter((f) => f.postSlug === "debugging-tips");
if (filesForPost.length === 0) {
throw new Error("No files found for debugging-tips");
}
console.log(` Found ${filesForPost.length} files for debugging-tips`);
});
// Test 4: Architecture-patterns post should have file examples
test("architecture-patterns post has file examples", async () => {
const post = blogPosts.find((p) => p.slug === "architecture-patterns");
if (!post) {
throw new Error("architecture-patterns post not found");
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some((tag) =>
[
"architecture",
"design-patterns",
"system-design",
"docker",
"deployment",
].includes(tag),
);
if (!showFileExamples) {
throw new Error("architecture-patterns should show file examples");
}
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups
.flatMap((g) => g.files)
.filter((f) => f.postSlug === "architecture-patterns");
if (filesForPost.length === 0) {
throw new Error("No files found for architecture-patterns");
}
console.log(
` Found ${filesForPost.length} files for architecture-patterns`,
);
});
// Test 5: Docker-deployment post should have file examples
test("docker-deployment post has file examples", async () => {
const post = blogPosts.find((p) => p.slug === "docker-deployment");
if (!post) {
throw new Error("docker-deployment post not found");
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some((tag) =>
[
"architecture",
"design-patterns",
"system-design",
"docker",
"deployment",
].includes(tag),
);
if (!showFileExamples) {
throw new Error("docker-deployment should show file examples");
}
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups
.flatMap((g) => g.files)
.filter((f) => f.postSlug === "docker-deployment");
if (filesForPost.length === 0) {
throw new Error("No files found for docker-deployment");
}
console.log(` Found ${filesForPost.length} files for docker-deployment`);
});
// Test 6: First-note post should NOT have file examples
test("first-note post has no file examples", async () => {
const post = blogPosts.find((p) => p.slug === "first-note");
if (!post) {
throw new Error("first-note post not found");
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some((tag) =>
[
"architecture",
"design-patterns",
"system-design",
"docker",
"deployment",
].includes(tag),
);
if (showFileExamples) {
throw new Error("first-note should NOT show file examples");
}
// Verify no files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups
.flatMap((g) => g.files)
.filter((f) => f.postSlug === "first-note");
if (filesForPost.length > 0) {
throw new Error("Files found for first-note, but none should exist");
}
console.log(` Correctly has no files`);
});
// Test 7: Simulate FileExamplesList filtering for debugging-tips
test("FileExamplesList filtering works for debugging-tips", async () => {
const postSlug = "debugging-tips";
const groupId = "python-data-processing";
const allGroups = await FileExampleManager.getAllGroups();
const loadedGroups = allGroups
.filter((g) => g.groupId === groupId)
.map((g) => ({
...g,
files: g.files.filter((f) => f.postSlug === postSlug),
}))
.filter((g) => g.files.length > 0);
if (loadedGroups.length === 0) {
throw new Error(
"No groups loaded for debugging-tips with python-data-processing",
);
}
if (loadedGroups[0].files.length === 0) {
throw new Error("No files in the group");
}
console.log(` Would show ${loadedGroups[0].files.length} files`);
});
// Test 8: Simulate FileExamplesList filtering for architecture-patterns
test("FileExamplesList filtering works for architecture-patterns", async () => {
const postSlug = "architecture-patterns";
const tags = ["architecture", "design-patterns", "system-design"];
const allGroups = await FileExampleManager.getAllGroups();
const loadedGroups = allGroups
.map((g) => ({
...g,
files: g.files.filter((f) => {
if (f.postSlug !== postSlug) return false;
if (tags && tags.length > 0) {
return f.tags?.some((tag) => tags.includes(tag));
}
return true;
}),
}))
.filter((g) => g.files.length > 0);
if (loadedGroups.length === 0) {
throw new Error("No groups loaded for architecture-patterns");
}
const totalFiles = loadedGroups.reduce((sum, g) => sum + g.files.length, 0);
if (totalFiles === 0) {
throw new Error("No files found");
}
console.log(
` Would show ${totalFiles} files across ${loadedGroups.length} groups`,
);
});
// Test 9: Verify all file examples have postSlug
test("All file examples have postSlug property", async () => {
const groups = await FileExampleManager.getAllGroups();
const filesWithoutPostSlug = groups
.flatMap((g) => g.files)
.filter((f) => !f.postSlug);
if (filesWithoutPostSlug.length > 0) {
throw new Error(`${filesWithoutPostSlug.length} files missing postSlug`);
}
console.log(
` All ${groups.flatMap((g) => g.files).length} files have postSlug`,
);
});
// Test 10: Verify postSlugs match blog post slugs
test("File example postSlugs match blog post slugs", async () => {
const groups = await FileExampleManager.getAllGroups();
const filePostSlugs = new Set(
groups.flatMap((g) => g.files).map((f) => f.postSlug),
);
const blogPostSlugs = new Set(blogPosts.map((p) => p.slug));
for (const slug of filePostSlugs) {
if (slug && !blogPostSlugs.has(slug)) {
throw new Error(`File postSlug "${slug}" doesn't match any blog post`);
}
}
console.log(` All file postSlugs match blog posts`);
});
// Wait for async tests
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(
`\n📊 Integration Test Results: ${passed} passed, ${failed} failed`,
);
if (failed === 0) {
console.log("🎉 All integration tests passed!");
console.log(
"\n✅ The file examples system is correctly integrated with blog posts!",
);
} else {
console.log("❌ Some integration tests failed");
process.exit(1);
}
}
// Export for use in other test files

View File

@@ -1,283 +0,0 @@
/**
* Comprehensive tests for the file examples system
*/
import {
FileExampleManager,
sampleFileExamples,
type FileExample as _FileExample,
} from "../data/fileExamples";
// Test helper to run all tests
export async function runFileExamplesTests() {
console.log("🧪 Running File Examples System Tests...\n");
let passed = 0;
let failed = 0;
const test = (name: string, fn: () => void | Promise<void>) => {
try {
const result = fn();
if (result instanceof Promise) {
return result
.then(() => {
console.log(`${name}`);
passed++;
})
.catch((err) => {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
});
} else {
console.log(`${name}`);
passed++;
}
} catch (err: any) {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
}
};
// Test 1: Data structure exists
test("File examples data is loaded", () => {
if (!sampleFileExamples || sampleFileExamples.length === 0) {
throw new Error("No file examples found");
}
console.log(` Found ${sampleFileExamples.length} groups`);
});
// Test 2: FileExampleManager exists
test("FileExampleManager class is available", () => {
if (!FileExampleManager) {
throw new Error("FileExampleManager not found");
}
});
// Test 3: Sample data has correct structure
test("Sample data has correct structure", () => {
const group = sampleFileExamples[0];
if (!group.groupId || !group.title || !Array.isArray(group.files)) {
throw new Error("Invalid group structure");
}
const file = group.files[0];
if (!file.id || !file.filename || !file.content || !file.language) {
throw new Error("Invalid file structure");
}
// Check for postSlug
if (!file.postSlug) {
throw new Error("Files missing postSlug property");
}
});
// Test 4: Get all groups
test("FileExampleManager.getAllGroups() works", async () => {
const groups = await FileExampleManager.getAllGroups();
if (!Array.isArray(groups) || groups.length === 0) {
throw new Error("getAllGroups returned invalid result");
}
});
// Test 5: Get specific group
test("FileExampleManager.getGroup() works", async () => {
const group = await FileExampleManager.getGroup("python-data-processing");
if (!group) {
throw new Error("Group not found");
}
if (group.groupId !== "python-data-processing") {
throw new Error("Wrong group returned");
}
});
// Test 6: Search files
test("FileExampleManager.searchFiles() works", async () => {
const results = await FileExampleManager.searchFiles("python");
if (!Array.isArray(results)) {
throw new Error("searchFiles returned invalid result");
}
if (results.length === 0) {
throw new Error('No results found for "python"');
}
});
// Test 7: Get file by ID
test("FileExampleManager.getFileExample() works", async () => {
const file = await FileExampleManager.getFileExample(
"python-data-processor",
);
if (!file) {
throw new Error("File not found");
}
if (file.id !== "python-data-processor") {
throw new Error("Wrong file returned");
}
});
// Test 8: Filter by postSlug
test("Filter files by postSlug", async () => {
const groups = await FileExampleManager.getAllGroups();
const debuggingFiles = groups
.flatMap((g) => g.files)
.filter((f) => f.postSlug === "debugging-tips");
if (debuggingFiles.length === 0) {
throw new Error("No files found for debugging-tips");
}
console.log(` Found ${debuggingFiles.length} files for debugging-tips`);
});
// Test 9: Filter by postSlug and groupId
test("Filter files by postSlug and groupId", async () => {
const groups = await FileExampleManager.getAllGroups();
const filtered = groups
.filter((g) => g.groupId === "python-data-processing")
.map((g) => ({
...g,
files: g.files.filter((f) => f.postSlug === "debugging-tips"),
}))
.filter((g) => g.files.length > 0);
if (filtered.length === 0) {
throw new Error(
"No files found for debugging-tips in python-data-processing",
);
}
console.log(` Found ${filtered[0].files.length} files`);
});
// Test 10: Filter by postSlug and tags
test("Filter files by postSlug and tags", async () => {
const groups = await FileExampleManager.getAllGroups();
const tags = ["architecture", "design-patterns"];
const filtered = groups
.map((g) => ({
...g,
files: g.files.filter(
(f) =>
f.postSlug === "architecture-patterns" &&
f.tags?.some((tag) => tags.includes(tag)),
),
}))
.filter((g) => g.files.length > 0);
if (filtered.length === 0) {
throw new Error("No files found for architecture-patterns with tags");
}
console.log(` Found ${filtered[0].files.length} files`);
});
// Test 11: Download single file
test("Download single file", async () => {
const result = await FileExampleManager.downloadFile(
"python-data-processor",
);
if (!result) {
throw new Error("Download failed");
}
if (!result.filename || !result.content || !result.mimeType) {
throw new Error("Invalid download result");
}
});
// Test 12: Download multiple files
test("Download multiple files", async () => {
const files = await FileExampleManager.downloadMultiple([
"python-data-processor",
"python-config-example",
]);
if (!Array.isArray(files) || files.length !== 2) {
throw new Error("Invalid multiple download result");
}
});
// Test 13: Get available tags
test("Get available tags", async () => {
const tags = await FileExampleManager.getAvailableTags();
if (!Array.isArray(tags) || tags.length === 0) {
throw new Error("No tags found");
}
if (!tags.includes("python") || !tags.includes("architecture")) {
throw new Error("Expected tags not found");
}
});
// Test 14: Create new file example
test("Create new file example", async () => {
const newExample = await FileExampleManager.createFileExample({
filename: "test.py",
content: 'print("test")',
language: "python",
description: "Test file",
tags: ["test"],
postSlug: "test-post",
});
if (!newExample.id) {
throw new Error("New example has no ID");
}
// Verify it was added
const retrieved = await FileExampleManager.getFileExample(newExample.id);
if (!retrieved || retrieved.filename !== "test.py") {
throw new Error("New example not found");
}
});
// Test 15: Update file example
test("Update file example", async () => {
const updated = await FileExampleManager.updateFileExample(
"python-data-processor",
{
description: "Updated description",
},
);
if (!updated || updated.description !== "Updated description") {
throw new Error("Update failed");
}
});
// Test 16: Delete file example
test("Delete file example", async () => {
// First create one
const created = await FileExampleManager.createFileExample({
filename: "delete-test.py",
content: "test",
language: "python",
postSlug: "test",
});
// Then delete it
const deleted = await FileExampleManager.deleteFileExample(created.id);
if (!deleted) {
throw new Error("Delete failed");
}
// Verify it's gone
const retrieved = await FileExampleManager.getFileExample(created.id);
if (retrieved) {
throw new Error("File still exists after deletion");
}
});
// Wait for all async tests to complete
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`);
if (failed === 0) {
console.log("🎉 All tests passed!");
} else {
console.log("❌ Some tests failed");
process.exit(1);
}
}
// Export for use in other test files