feat: complete MDX migration for blog, fix diagram fidelity and refactor styling architecture
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
27
apps/web/src/components/StatsDisplay.tsx
Normal file
27
apps/web/src/components/StatsDisplay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
140
apps/web/src/components/blog/blogThumbnails.ts
Normal file
140
apps/web/src/components/blog/blogThumbnails.ts
Normal 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",
|
||||
},
|
||||
};
|
||||
39
apps/web/src/content-engine/components.ts
Normal file
39
apps/web/src/content-engine/components.ts
Normal 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
|
||||
};
|
||||
55
apps/web/src/content-engine/definitions.ts
Normal file
55
apps/web/src/content-engine/definitions.ts
Normal 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"]} ... />'
|
||||
}
|
||||
];
|
||||
3
apps/web/src/content-engine/registry.tsx
Normal file
3
apps/web/src/content-engine/registry.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export * from './definitions';
|
||||
export { mdxComponents } from './components';
|
||||
@@ -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"],
|
||||
},
|
||||
];
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
@@ -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
5
apps/web/src/mdx.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
declare module '*.mdx' {
|
||||
let MDXComponent: (props: any) => JSX.Element;
|
||||
export default MDXComponent;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user