feat: complete MDX migration for blog, fix diagram fidelity and refactor styling architecture
This commit is contained in:
@@ -1,268 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { FileExampleManager } from '../../../src/data/fileExamples';
|
||||
|
||||
// Simple ZIP creation without external dependencies
|
||||
class SimpleZipCreator {
|
||||
private files: Array<{ filename: string; content: string }> = [];
|
||||
|
||||
addFile(filename: string, content: string) {
|
||||
this.files.push({ filename, content });
|
||||
}
|
||||
|
||||
// Create a basic ZIP file structure
|
||||
create(): number[] {
|
||||
const encoder = new TextEncoder();
|
||||
const chunks: number[][] = [];
|
||||
|
||||
let offset = 0;
|
||||
const centralDirectory: Array<{
|
||||
name: string;
|
||||
offset: number;
|
||||
size: number;
|
||||
compressedSize: number;
|
||||
}> = [];
|
||||
|
||||
// Process each file
|
||||
for (const file of this.files) {
|
||||
const contentBytes = Array.from(encoder.encode(file.content));
|
||||
const filenameBytes = Array.from(encoder.encode(file.filename));
|
||||
|
||||
// Local file header
|
||||
const localHeader: number[] = [];
|
||||
|
||||
// Local file header signature (little endian)
|
||||
localHeader.push(0x50, 0x4b, 0x03, 0x04);
|
||||
// Version needed to extract
|
||||
localHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
localHeader.push(0, 0);
|
||||
// Compression method (0 = store)
|
||||
localHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// CRC32 (0 for simplicity)
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Uncompressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Filename length
|
||||
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
localHeader.push(0, 0);
|
||||
|
||||
// Add filename
|
||||
localHeader.push(...filenameBytes);
|
||||
|
||||
chunks.push(localHeader);
|
||||
chunks.push(contentBytes);
|
||||
|
||||
// Store info for central directory
|
||||
centralDirectory.push({
|
||||
name: file.filename,
|
||||
offset: offset,
|
||||
size: contentBytes.length,
|
||||
compressedSize: contentBytes.length
|
||||
});
|
||||
|
||||
offset += localHeader.length + contentBytes.length;
|
||||
}
|
||||
|
||||
// Central directory
|
||||
const centralDirectoryChunks: number[][] = [];
|
||||
let centralDirectoryOffset = offset;
|
||||
|
||||
for (const entry of centralDirectory) {
|
||||
const filenameBytes = Array.from(encoder.encode(entry.name));
|
||||
const centralHeader: number[] = [];
|
||||
|
||||
// Central directory header signature
|
||||
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
|
||||
// Version made by
|
||||
centralHeader.push(20, 0);
|
||||
// Version needed to extract
|
||||
centralHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
centralHeader.push(0, 0);
|
||||
// Compression method
|
||||
centralHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// CRC32
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
|
||||
// Uncompressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.size, 4));
|
||||
// Filename length
|
||||
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
centralHeader.push(0, 0);
|
||||
// File comment length
|
||||
centralHeader.push(0, 0);
|
||||
// Disk number start
|
||||
centralHeader.push(0, 0);
|
||||
// Internal file attributes
|
||||
centralHeader.push(0, 0);
|
||||
// External file attributes
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Relative offset of local header
|
||||
centralHeader.push(...intToLittleEndian(entry.offset, 4));
|
||||
|
||||
// Add filename
|
||||
centralHeader.push(...filenameBytes);
|
||||
|
||||
centralDirectoryChunks.push(centralHeader);
|
||||
}
|
||||
|
||||
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
|
||||
// End of central directory
|
||||
const endOfCentralDirectory: number[] = [];
|
||||
|
||||
// End of central directory signature
|
||||
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
|
||||
// Number of this disk
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Number of the disk with the start of the central directory
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Total number of entries on this disk
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Total number of entries
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Size of the central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
|
||||
// Offset of start of central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
|
||||
// ZIP file comment length
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
|
||||
// Combine all chunks
|
||||
const result: number[] = [];
|
||||
chunks.forEach(chunk => result.push(...chunk));
|
||||
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
|
||||
result.push(...endOfCentralDirectory);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert integer to little endian bytes
|
||||
function intToLittleEndian(value: number, bytes: number): number[] {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
result.push((value >> (i * 8)) & 0xff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { fileIds } = body;
|
||||
|
||||
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'fileIds array is required and must not be empty' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get file contents
|
||||
const files = await Promise.all(
|
||||
fileIds.map(async (id) => {
|
||||
const file = await FileExampleManager.getFileExample(id);
|
||||
if (!file) {
|
||||
throw new Error(`File with id ${id} not found`);
|
||||
}
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
||||
// Create ZIP
|
||||
const zipCreator = new SimpleZipCreator();
|
||||
files.forEach(file => {
|
||||
zipCreator.addFile(file.filename, file.content);
|
||||
});
|
||||
|
||||
const zipData = zipCreator.create();
|
||||
const buffer = Buffer.from(new Uint8Array(zipData));
|
||||
|
||||
// Return ZIP file
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('ZIP download error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create zip file', details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileId = searchParams.get('id');
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'id parameter is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const file = await FileExampleManager.getFileExample(fileId);
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const content = encoder.encode(file.content);
|
||||
const buffer = Buffer.from(content);
|
||||
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': getMimeType(file.language),
|
||||
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('File download error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to download file' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get MIME type
|
||||
function 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';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { blogPosts } from "../../../src/data/blogPosts";
|
||||
import { blogThumbnails } from "../../../src/data/blogThumbnails";
|
||||
import { allPosts } from "contentlayer/generated";
|
||||
import { blogThumbnails } from "../../../src/components/blog/blogThumbnails";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function Image({
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
const thumbnail = blogThumbnails[slug];
|
||||
|
||||
const title = post?.title || "Marc Mintel";
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { blogPosts } from "../../../src/data/blogPosts";
|
||||
import { allPosts } from "contentlayer/generated";
|
||||
import { BlogPostHeader } from "../../../src/components/blog/BlogPostHeader";
|
||||
import { Section } from "../../../src/components/Section";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
import { BlogPostClient } from "../../../src/components/BlogPostClient";
|
||||
import { PostComponents } from "../../../src/components/blog/posts";
|
||||
import { TextSelectionShare } from "../../../src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "../../../src/components/blog/BlogPostStickyBar";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import { MDXComponents } from "../../../mdx-components";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return blogPosts.map((post) => ({
|
||||
return allPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export async function generateMetadata({
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) return {};
|
||||
|
||||
@@ -48,7 +49,7 @@ export default async function BlogPostPage({
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
@@ -60,11 +61,9 @@ export default async function BlogPostPage({
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300; // Average post length
|
||||
const wordCount = post.description.split(/\s+/).length + 300;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
const PostContent = PostComponents[slug];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
@@ -78,7 +77,6 @@ export default async function BlogPostPage({
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
{/* Sticky Progress Bar */}
|
||||
<BlogPostStickyBar
|
||||
title={post.title}
|
||||
url={`https://mintel.me/blog/${slug}`}
|
||||
@@ -89,7 +87,7 @@ export default async function BlogPostPage({
|
||||
<Reveal delay={0.4} width="100%">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags.map((tag, index) => (
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
@@ -100,13 +98,12 @@ export default async function BlogPostPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{PostContent ? (
|
||||
<PostContent />
|
||||
) : (
|
||||
<div className="p-8 bg-slate-50 border border-slate-200 rounded-lg italic text-slate-500">
|
||||
Inhalt wird bald veröffentlicht...
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-none">
|
||||
<MDXRemote
|
||||
source={post.body.raw.replace(/^---[\s\S]*?\n---\s*/, '')}
|
||||
components={MDXComponents}
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { MediumCard } from "../../src/components/MediumCard";
|
||||
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
|
||||
import { blogPosts } from "../../src/data/blogPosts";
|
||||
import { allPosts as contentPosts } from "contentlayer/generated";
|
||||
import { SectionHeader } from "../../src/components/SectionHeader";
|
||||
import { Reveal } from "../../src/components/Reveal";
|
||||
import { Section } from "../../src/components/Section";
|
||||
@@ -19,7 +19,7 @@ export default function BlogPage() {
|
||||
// Memoize allPosts
|
||||
const allPosts = React.useMemo(
|
||||
() =>
|
||||
[...blogPosts].sort(
|
||||
[...contentPosts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
),
|
||||
[],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { blogPosts } from '../src/data/blogPosts';
|
||||
import { allPosts } from 'contentlayer/generated';
|
||||
import { technologies } from './technologies/[slug]/data';
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
}));
|
||||
|
||||
// 2. Dynamic Blog Posts
|
||||
const blogRoutes = blogPosts.map((post) => ({
|
||||
const blogRoutes = allPosts.map((post) => ({
|
||||
url: `${baseUrl}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: 'monthly' as const,
|
||||
@@ -44,7 +44,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
}));
|
||||
|
||||
// 4. Tag Pages
|
||||
const allTags = [...new Set(blogPosts.flatMap((post) => post.tags))];
|
||||
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
||||
const tagRoutes = allTags.map((tag) => ({
|
||||
url: `${baseUrl}/tags/${tag}`,
|
||||
lastModified: new Date(),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { blogPosts } from "../../../src/data/blogPosts";
|
||||
import { allPosts } from "contentlayer/generated";
|
||||
import { MediumCard } from "../../../src/components/MediumCard";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allTags = Array.from(
|
||||
new Set(blogPosts.flatMap((post) => post.tags || [])),
|
||||
new Set(allPosts.flatMap((post) => post.tags || [])),
|
||||
);
|
||||
return allTags.map((tag) => ({
|
||||
tag,
|
||||
@@ -19,7 +18,7 @@ export default async function TagPage({
|
||||
params: Promise<{ tag: string }>;
|
||||
}) {
|
||||
const { tag } = await params;
|
||||
const posts = blogPosts.filter((post) => post.tags?.includes(tag));
|
||||
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||
|
||||
11
apps/web/content-engine.config.ts
Normal file
11
apps/web/content-engine.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ComponentDefinition } from '@mintel/content-engine';
|
||||
import path from 'path';
|
||||
import { componentDefinitions } from './src/content-engine/definitions';
|
||||
|
||||
export const config = {
|
||||
// Path to documentation files used as context for the AI
|
||||
contextDir: path.join(process.cwd(), 'docs'),
|
||||
|
||||
// Custom UI components available for injection
|
||||
components: componentDefinitions
|
||||
};
|
||||
150
apps/web/content/blog/analytics-without-tracking.mdx
Normal file
150
apps/web/content/blog/analytics-without-tracking.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: "Analytics ohne Tracking: Erkenntnis ohne Überwachung"
|
||||
description: "Wie Sie Besucherströme präzise messen, ohne die Privatsphäre Ihrer Kunden zu verletzen."
|
||||
date: "2026-02-09"
|
||||
tags: ["privacy", "analytics"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." – Das
|
||||
ist eine weit verbreitete Fehlannahme.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner täglichen Arbeit als Digital Architect beweise ich das
|
||||
Gegenteil:{" "}
|
||||
<Marker>Maximale Erkenntnis erfordert keine maximale Überwachung.</Marker>
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir Erfolg präzise messen, ohne die Privatsphäre
|
||||
Ihrer Kunden an US-Konzerne zu verkaufen.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Analytics ohne den Beigeschmack der Überwachung</H2>
|
||||
<Paragraph>
|
||||
Klassische Analytics-Tools funktionieren wie ein Trojaner.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu
|
||||
bilden, die weit über Ihre Website hinausgehen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dies zwingt Sie rechtlich in die Knie – Sie brauchen Banner, Consent-Tools
|
||||
und riskieren Abmahnungen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Paradoxon:{" "}
|
||||
<Marker>Die meisten dieser Daten brauchen Sie gar nicht.</Marker>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie wollen wissen, welche Inhalte funktionieren, nicht wie der Nutzer in
|
||||
seiner Freizeit heißt.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="privacy-analytics-flow" title="Privacy Analytics Workflow" showShare={true}>
|
||||
graph LR
|
||||
Traffic["Besucherstrom"] --> Filter["Privacy-Proxy"]
|
||||
Filter --> Metrics["Aggregate Metriken (Trends)"]
|
||||
Filter --> Zero["Zero PII (No Personal Info)"]
|
||||
Metrics --> Insights["Optimierung Ihres Business"]
|
||||
Zero --> Compliance["100% DSGVO & Banner-Frei"]
|
||||
style Insights fill:#4ade80,stroke:#333
|
||||
style Compliance fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Ethisches Tracking: Wir gewinnen wertvolle Business-Insights, während
|
||||
die Identität der Nutzer absolut geschützt bleibt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Qualität der Daten vor Quantität der Profile</H3>
|
||||
<Paragraph>
|
||||
Mein Ansatz basiert auf aggregierten Trends statt auf individueller
|
||||
Verfolgung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir messen Seitenaufrufe, Verweildauer und Conversions – aber wir tun es{" "}
|
||||
<Marker>anonym und am Edge</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist statistisch genauso wertvoll für Ihr Marketing, aber
|
||||
moralisch und rechtlich weit überlegen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von
|
||||
GA4-Komplexität abhängig zu machen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Architektur für ethische Insights</H2>
|
||||
<Paragraph>Ich integriere Analytics direkt in Ihre Plattform.</Paragraph>
|
||||
<Paragraph>
|
||||
Keine externen Scripte bedeutet auch: Mehr Performance für Ihre Nutzer.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Cookieless Tracking:</strong> Wir erkennen wiederkehrende Nutzer
|
||||
über kurzlebige, anonyme Hashes. Keine Speicherung am Endgerät nötig.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>First-Party Data:</strong> Die Daten bleiben auf Ihrem Server.
|
||||
Kein Abfluss an Drittanbieter-Netzwerke zur Werbeoptimierung.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Lightweight Implementation:</strong> Statt 100KB
|
||||
Analytics-Ballast nutzen wir Lösungen, die weniger als 1KB wiegen.{" "}
|
||||
<Marker>Geschwindigkeit trifft auf Erkenntnis.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der strategische Vergleich im Lead-Tracking"
|
||||
negativeLabel="Google Analytics / GA4"
|
||||
negativeText="Abhängigkeit von Consent-Raten, Daten-Leakage, komplexe & träge Interfaces"
|
||||
positiveLabel="Mintel Privacy Analytics"
|
||||
positiveText="Volle Lead-Transparenz, Banner-Freiheit, blitzschnelle Auswertung"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Der unternehmerische Hebel: Banner-freie Leads</H2>
|
||||
<Paragraph>
|
||||
Wenn Sie kein Banner brauchen, messen Sie 100 % Ihres Traffics.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Bei Google Analytics verlieren Sie oft 40-60 % der Daten, weil Nutzer den
|
||||
Consent ablehnen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein System liefert Ihnen <Marker>die echten Zahlen</Marker>, weil die
|
||||
Hürde der Zustimmung technisch entfällt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ehrlichkeit zahlt sich hier direkt in der Genauigkeit Ihrer
|
||||
Marketing-Planung aus.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Gute Entscheidungen brauchen ein solides Fundament – keine lückenhaften
|
||||
Statistiken.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann sollten Sie umstellen?</H2>
|
||||
<Paragraph>Haben Sie genug von rechtlichen Grauzonen?</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue für Entscheider, die{" "}
|
||||
<Marker>Datenschutz als Teil ihres Markenversprechens</Marker> begreifen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir Analytics von einer Last zu einer Stärke machen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Wissen ist Macht, Respekt ist Zukunft</H2>
|
||||
<Paragraph>Messen Sie, was zählt – und schützen Sie, wer zählt.</Paragraph>
|
||||
<Paragraph>
|
||||
Ich begleite Sie bei der Installation einer Lösung, die Professionalität
|
||||
und Ethik brillant vereint.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Insights ohne Reue.</Marker> Das ist modernes digitales
|
||||
Management.
|
||||
</Paragraph>
|
||||
189
apps/web/content/blog/build-first-digital-architecture.mdx
Normal file
189
apps/web/content/blog/build-first-digital-architecture.mdx
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: "Build-First: Warum Bauen besser ist als Kaufen"
|
||||
description: "Software-Miete vs. digitales Eigentum: Warum maßgeschneiderte Systeme am Ende die wirtschaftlichere Wahl sind."
|
||||
date: "2026-02-05"
|
||||
tags: ["architecture", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Gekauft ist schneller als gebaut." – In der digitalen Welt ist das oft
|
||||
der teuerste Irrtum, den ein Unternehmen begehen kann.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect sehe ich täglich, wie
|
||||
Standard-SaaS-Lösungen Innovationen im Keim ersticken.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Sie bezahlen für Features, die Sie nicht brauchen, während Ihnen die
|
||||
entscheidenden 5 % fehlen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Bauen die neue Form der Effizienz</Marker>{" "}
|
||||
ist und wie Sie sich echte Marktvorteile sichern.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die Sackgasse der Generic-Software</H2>
|
||||
<Paragraph>
|
||||
Standard-Software ist darauf ausgelegt, dem kleinsten gemeinsamen Nenner
|
||||
zu gefallen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Man bekommt ein schnelles Resultat, läuft aber sofort gegen eine Wand,
|
||||
wenn man Prozesse wirklich optimieren will.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ihre Wettbewerber nutzen wahrscheinlich exakt die gleiche Software wie
|
||||
Sie.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wo bleibt da der <Marker>technologische Vorsprung</Marker>?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer nur mietet, wird niemals Marktführer. Wahre Überlegenheit entsteht
|
||||
durch maßgeschneiderte Systeme.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="build-vs-buy-decision" title="Build vs. Buy Entscheidung" showShare={true}>
|
||||
graph TD
|
||||
Need["Individuelles Business-Bedürfnis"] --> Path["Strategische Entscheidung"]
|
||||
Path --> Buy["Software-Abo (SaaS)"]
|
||||
Path --> Build["Bespoke Architecture (Mintel)"]
|
||||
Buy --> Compromise["Kompromisse & Monatliche Fixkosten"]
|
||||
Build --> Competitive["Wettbewerbsvorteil & Unendliche Freiheit"]
|
||||
Compromise --> Stagnation["Digitaler Stillstand"]
|
||||
Competitive --> Growth["Skalierung ohne Grenzen"]
|
||||
style Build fill:#4ade80,stroke:#333
|
||||
style Growth fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Build vs. Buy: Investieren Sie in Ihr eigenes geistiges Eigentum statt
|
||||
in die monatliche Miete von Fremdprodukten.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Bauen bedeutet heute: Strategisches Kombinieren</H3>
|
||||
<Paragraph>
|
||||
"Bauen" heißt heute nicht mehr, jedes Rad neu zu erfinden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nutze moderne Frameworks und spezialisierte Microservices, um Ihr
|
||||
individuelles System zu komponieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist so flexibel wie eine Eigenentwicklung, aber so schnell
|
||||
einsatzbereit wie ein Standardprodukt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dabei besitzen Sie den Code und kontrollieren die Roadmap.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>digitale Handwerkskunst am Puls der Zeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der wirtschaftliche Case von 'Build-First'</H2>
|
||||
<Paragraph>
|
||||
Die initialen Kosten für Individualsoftware wirken oft höher als eine
|
||||
monatliche Lizenzgebühr.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Doch bei SaaS-Modellen steigen die Kosten linear mit Ihrem Wachstum.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Maßgeschneiderte Software amortisiert sich oft nach 12-18 Monaten – durch
|
||||
wegfallende Lizenzen und massiv gesteigerte Effizienz.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie investieren in ein <Marker>Firmen-Asset</Marker>, das den Wert Ihres
|
||||
Unternehmens steigert.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Software-Miete ist ein Kostenblock, Software-Bau ist eine Investition.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="build-first-timeline" title="Typischer Build-First Projektverlauf" showShare={true}>
|
||||
timeline
|
||||
title Typischer Build-First Projektverlauf
|
||||
Monat 1-2 : Strategische Planung & Blueprint
|
||||
Monat 3-4 : Core-Architektur & MVP
|
||||
Monat 5-6 : Feature-Ausbau & Testing
|
||||
Monat 7-8 : Launch & Optimierung
|
||||
Monat 9+ : Kontinuierliche Evolution
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Von der Vision zum skalierbaren System: Ein strukturierter Weg zur digitalen Souveränität.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Exakter Prozess-Match:</strong> Das System passt sich Ihren
|
||||
Abläufen an, nicht umgekehrt. Keine unnötigen Klicks mehr.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Skalierung nach Ihren Regeln:</strong> Keine künstlichen Limits
|
||||
durch Nutzerzahlen oder Datenvolumen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Sicherheits-Vorsprung:</strong> Ihr System ist kein Ziel für
|
||||
Massen-Exploits, die Standard-Systeme täglich bedrohen.{" "}
|
||||
<Marker>Individualität ist Schutz.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact auf Ihr unternehmerisches Wachstum"
|
||||
negativeLabel="Software as a Service (SaaS)"
|
||||
negativeText="Abhängigkeit von Anbieter-Preisen, starre Features, kein Kapitalwertaufbau"
|
||||
positiveLabel="Bespoke Asset (Mintel)"
|
||||
positiveText="0 € Lizenzkosten, unbegrenzte Features, Steigerung des Firmenwerts"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Digitales Eigentum als strategischer Hebel</H2>
|
||||
<Paragraph>
|
||||
Wer den Code besitzt, besitzt die Zukunft seines Unternehmens.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie jemals an einen Exit oder eine Fusion denken, ist technische
|
||||
Unabhängigkeit ein entscheidender Faktor.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe Ihnen eine Architektur, die{" "}
|
||||
<Marker>frei von technologischen Altlasten</Marker> ist.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Bauen wir Systeme, die so einzigartig sind wie Ihre Vision.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Unikate' erschaffe</H2>
|
||||
<Paragraph>
|
||||
Ich bin der Architekt für Gründer, die keine Lust mehr auf "geht technisch
|
||||
leider nicht" haben.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist Ihr Business-Modell am Markt einzigartig? Dann sollte es Ihre Software
|
||||
auch sein.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich steige dort ein, wo{" "}
|
||||
<Marker>technische Brillanz zur strategischen Waffe</Marker> wird.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Hören Sie auf zu mieten, fangen Sie an zu bauen</H2>
|
||||
<Paragraph>
|
||||
Wahrer Reichtum im Digitalen entsteht durch Eigentum und Souveränität.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein System erschaffen, das genau so hart arbeitet wie
|
||||
Sie.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>
|
||||
Qualität ist kein Zufallsprodukt, sondern eine bewusste Entscheidung für
|
||||
den Bau.
|
||||
</Marker>{" "}
|
||||
Ihr Erfolg verdient ein Original.
|
||||
</Paragraph>
|
||||
169
apps/web/content/blog/builder-systems-threaten-independence.mdx
Normal file
169
apps/web/content/blog/builder-systems-threaten-independence.mdx
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: "Baukasten-Systeme bedrohen Ihre Unabhängigkeit"
|
||||
description: "Vendor Lock-In verstehen und vermeiden: Warum eine maßgeschneiderte Architektur Ihr wertvollstes Asset ist."
|
||||
date: "2026-02-08"
|
||||
tags: ["architecture", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Wir können nicht wechseln, das wäre zu teuer."
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect ist das der Anfang vom Ende jeder
|
||||
technologischen Innovation.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Vendor Lock-In ist die <Marker>digitale Version einer Geiselnahme</Marker>
|
||||
.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir Systeme bauen, die Ihnen jederzeit die volle
|
||||
Freiheit lassen – technologisch und wirtschaftlich.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die unsichtbaren Ketten proprietärer Systeme</H2>
|
||||
<Paragraph>
|
||||
Viele Unternehmen lassen sich von der Bequemlichkeit großer
|
||||
SaaS-Plattformen oder Baukästen blenden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Man bekommt ein schnelles Feature, gibt aber dafür die Kontrolle über
|
||||
seine Daten und seine Codebasis ab.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Nach zwei Jahren sind Sie so tief im Ökosystem eines Anbieters verstrickt,
|
||||
dass ein Auszug unmöglich scheint.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Der Anbieter weiß das – und diktiert fortan die Preise und das Tempo Ihrer
|
||||
Entwicklung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>technologische Erpressbarkeit</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre Unabhängigkeit beginnt bei der strategischen Wahl der Architektur.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="vendor-lockin-fork" title="Vendor Lock-In vs. Offene Architektur" showShare={true}>
|
||||
graph TD
|
||||
Prop["Proprietäre Blackbox"] --> Lock["Steigende Kosten & Starre Features"]
|
||||
Open["Open-Source-Kern & Offene Standards"] --> Flex["Volle Kontrolle & Anbieter-Freiheit"]
|
||||
Lock --> Crisis["Digitale Sackgasse"]
|
||||
Flex --> Evolution["Permanente Innovation"]
|
||||
style Open fill:#4ade80,stroke:#333
|
||||
style Crisis fill:#fca5a5,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Die Gabelung der digitalen Strategie: Wählen Sie Freiheit durch
|
||||
Architektur, statt Komfort durch Abhängigkeit.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Technologische Souveränität als Asset</H3>
|
||||
<Paragraph>Software sollte für Sie arbeiten, nicht umgekehrt.</Paragraph>
|
||||
<Paragraph>
|
||||
Indem wir auf offene Standards und portable Architekturen setzen,
|
||||
verwandeln wir Code in ein echtes Firmen-Asset.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie können den Cloud-Anbieter wechseln, die Agentur tauschen oder das Team
|
||||
skalieren – <Marker>ohne jemals bei Null anfangen zu müssen</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>Das ist das Privileg der technologischen Elite.</Paragraph>
|
||||
<Paragraph>
|
||||
Portabilität ist kein technisches Gimmick, sondern eine unternehmerische
|
||||
Notwendigkeit.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Architektur der Ungebundenheit</H2>
|
||||
<Paragraph>Ich baue keine "Käfige" aus fertigen Plugins.</Paragraph>
|
||||
<Paragraph>Mein Framework basiert auf Modularität und Klarheit.</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Standard-basiertes Engineering:</strong> Wir nutzen
|
||||
Technologien, die weltweit verstanden werden. Keine geheimen
|
||||
"Spezial-Module" eines einzelnen Anbieters.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Daten-Portabilität:</strong> Ihre Daten gehören Ihnen. Zu jeder
|
||||
Zeit. Wir bauen Schnittstellen, die den Export so einfach machen wie den
|
||||
Import.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Cloud-agnostisches Hosting:</strong> Wir nutzen
|
||||
Container-Technologie. Ob AWS, Azure oder lokale Anbieter –{" "}
|
||||
<Marker>Ihr Code läuft überall gleich perfekt.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="vendor-dependency-state" title="Vendor Lock-In Progression" showShare={true}>
|
||||
stateDiagram-v2
|
||||
["*"] --> Independent
|
||||
Independent --> Integrated : Adopt Platform
|
||||
Integrated --> Dependent : Deep Integration
|
||||
Dependent --> Locked : Critical Mass
|
||||
Locked --> Migration : Exit Decision
|
||||
Migration --> Independent : Successful Exit
|
||||
Integrated --> Independent : Early Exit
|
||||
Locked --> ["*"]
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der Weg in die Abhängigkeit: Je tiefer die Integration, desto schwieriger der Ausstieg.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der ökonomische Vergleich Ihrer Unabhängigkeit"
|
||||
negativeLabel="Closed Ecosystem (Walled Garden)"
|
||||
negativeText="Diktierte Preise, starre Roadmap des Anbieters, Datenverlust bei Wechsel"
|
||||
positiveLabel="Bespoke Open-Core System"
|
||||
positiveText="Totale Preis-Kontrolle, unbegrenzte Features, volle Exit-Option ab Tag 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Der strategische Hebel für langfristige Rendite</H2>
|
||||
<Paragraph>Systeme ohne Lock-In altern besser.</Paragraph>
|
||||
<Paragraph>
|
||||
Sie lassen sich schrittweise modernisieren, statt alle fünf Jahre komplett
|
||||
neu gebaut werden zu müssen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das spart Millionen an Opportunitätskosten und Fehl-Investitionen.
|
||||
</Paragraph>
|
||||
<Paragraph>Seien Sie der Herr über Ihr digitales Schicksal.</Paragraph>
|
||||
<Paragraph>
|
||||
Investieren Sie in <Marker>intelligente Unabhängigkeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Freiheits-Systeme' erstelle</H2>
|
||||
<Paragraph>
|
||||
Ich arbeite für Gründer, die ihr Unternehmen langfristig wertvoll
|
||||
aufstellen wollen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist digitale Exzellenz Teil Ihrer Exit-Strategie oder Ihres Erbes? Dann
|
||||
brauchen Sie meine Architektur.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue keine Provisorien, sondern <Marker>nachhaltige Werte</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Freiheit ist eine Wahl</H2>
|
||||
<Paragraph>
|
||||
Technologie sollte Ihnen Flügel verleihen, keine Fesseln anlegen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein System schaffen, das so flexibel ist wie Ihr
|
||||
Business.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>
|
||||
Werden Sie unersetzbar durch Qualität, nicht durch Abhängigkeit.
|
||||
</Marker>
|
||||
Ihr Erfolg verdient absolute Freiheit.
|
||||
</Paragraph>
|
||||
154
apps/web/content/blog/clean-code-for-business-value.mdx
Normal file
154
apps/web/content/blog/clean-code-for-business-value.mdx
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: "Clean Code: Warum technische Qualität Ihr Business skaliert"
|
||||
description: "Software-Engineering als Wertanlage: Wie saubere Code-Strukturen Ihre Wartungskosten senken und Innovationen beschleunigen."
|
||||
date: "2026-01-30"
|
||||
tags: ["development", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Code ist nicht nur eine Anweisung für Maschinen. Es ist das Fundament
|
||||
Ihres digitalen Unternehmenswertes.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect sehe ich oft "historisch
|
||||
gewachsenen" Code, der eher einem verfilzten Knäuel gleicht als einer
|
||||
Architektur.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Clean Code kein Luxus</Marker> ist, sondern
|
||||
die harte Währung für Ihre Zukunftsfähigkeit.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die versteckten Kosten von 'Quick-and-Dirty'</H2>
|
||||
<Paragraph>
|
||||
Softwareentwicklung unter Zeitdruck führt oft zu unsauberen Abkürzungen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Kurzfristig spart das Stunden, langfristig erstickt es jede Innovation.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Unsauberer Code wird mit jedem Monat schwerer zu warten und zu erweitern.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>technologische Verstopfung</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ihre Entwickler verbringen dann 80 % ihrer Zeit mit Bugfixing, statt neue
|
||||
Features zu bauen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Clean Code hingegen ist eine Investition in die Geschwindigkeit von
|
||||
morgen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="clean-code-architecture" title="Clean Code Architektur" showShare={true}>
|
||||
graph TD
|
||||
Clean["Clean Code Architektur"] --> Easy["Einfache Wartbarkeit"]
|
||||
Clean --> Scalable["Schnelle Erweiterbarkeit"]
|
||||
Easy --> LowCost["Geringe langfristige Kosten"]
|
||||
Scalable --> Market["Schnellerer Markteintritt (Time-to-Market)"]
|
||||
LowCost --> Profit["Höherer ROI für Ihr Business"]
|
||||
Market --> Profit
|
||||
style Profit fill:#4ade80,stroke:#333
|
||||
style Clean fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Die Logik der Qualität: Sauberer Code zahlt sich durch sinkende
|
||||
Betriebskosten und steigendes Innovationstempo aus.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Code als Kommunikationsmittel</H3>
|
||||
<Paragraph>Code wird viel öfter gelesen als geschrieben.</Paragraph>
|
||||
<Paragraph>
|
||||
Deshalb ist Klarheit mein oberstes Gebot. Ein gut strukturiertes System
|
||||
"erklärt" sich selbst.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das macht Sie unabhängig von einzelnen Personen. Jedes neue Teammitglied
|
||||
findet sich sofort zurecht.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>Souveränität durch Transparenz</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schreibe Code für Menschen, nicht nur für den Compiler.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Prinzipien für eine glasklare Architektur</H2>
|
||||
<Paragraph>
|
||||
Wie unterscheidet sich meine Arbeit von Standard-Agentur-Code?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Durch die kompromisslose Anwendung von Engineering-Prinzipien:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Single Responsibility:</strong> Jede Komponente tut genau eine
|
||||
Sache – und die perfekt. Das macht Fehlerbehebungen zum Kinderspiel.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Selbstkontrolle:</strong> Bevor eine Änderung
|
||||
live geht, wird sie von hunderten automatischen Tests geprüft.{" "}
|
||||
<Marker>Qualität ist bei mir systemimmanent.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Dokumentation im Design:</strong> Ich baue Systeme, deren
|
||||
Struktur so logisch ist, dass Handbücher überflüssig werden.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der wirtschaftliche Vergleich der Codequalität"
|
||||
negativeLabel="Agentur-Standard (Spaghetti)"
|
||||
negativeText="Chaos-Abhängigkeit, expontentiell steigende Wartungskosten, Angst vor jedem Update"
|
||||
positiveLabel="Mintel Boutique Standard"
|
||||
positiveText="Lineares Wachstum, sinkende technische Schulden, volle Kontrolle ab Tag 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Wahrer Profit durch technische Brillanz</H2>
|
||||
<Paragraph>Clean Code senkt Ihre Betriebskosten massiv.</Paragraph>
|
||||
<Paragraph>
|
||||
Es ist die Basis für Skalierbarkeit. Nur ein sauberes Fundament trägt ein
|
||||
Hochhaus.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie planen, Ihr digitales Business über Jahre zu führen, ist{" "}
|
||||
<Marker>Codequalität Ihre wichtigste Versicherung</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Software sollte ein Vermögenswert sein, keine Verbindlichkeit.
|
||||
</Paragraph>
|
||||
<Paragraph>Gießen wir ein Fundament, das Stand hält.</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Pures Gold' schreibe</H2>
|
||||
<Paragraph>
|
||||
Ich bin der Architekt für Entscheider, die den Wert ihrer digitalen Assets
|
||||
langfristig maximieren wollen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Haben Sie genug von Systemen, die bei jeder Änderung zusammenbrechen? Dann
|
||||
passen wir zusammen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe <Marker>Ruhe im Maschinenraum</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Qualität gewinnt immer</H2>
|
||||
<Paragraph>Es gibt keine Abkürzung zu exzellenter Software.</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam den Ballast von unsauberem Code hinter uns.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen eine Architektur, die nicht nur heute funktioniert, sondern
|
||||
auch in vielen Jahren noch durch ihre{" "}
|
||||
<Marker>Eleganz und Klarheit</Marker> besticht.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Purer Code. Purere Ergebnisse.</Marker> Ihr Erfolg verdient dieses
|
||||
Niveau.
|
||||
</Paragraph>
|
||||
159
apps/web/content/blog/crm-synchronization-headless.mdx
Normal file
159
apps/web/content/blog/crm-synchronization-headless.mdx
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: "CRM-Synchronisation: Daten-Silos architektonisch auflösen"
|
||||
description: "Vom Kontaktformular direkt in den Sales-Funnel: Wie automatisierte Datenflüsse menschliche Fehler eliminieren und Leads beschleunigen."
|
||||
date: "2026-01-31"
|
||||
tags: ["crm", "architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Die wertvollsten Daten Ihres Unternehmens liegen oft in Silos versteckt.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ihre Website sammelt Leads, aber Ihr CRM "weiß" nichts davon – oder erst
|
||||
nach manueller Übertragung.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>Ich beende das Zeitalter der Daten-Inseln.</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie <Marker>nahtlose CRM-Integration</Marker> Ihre
|
||||
Marketing-Effizienz verdoppelt und menschliche Fehler eliminiert.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Das Problem der manuellen Daten-Brücke</H2>
|
||||
<Paragraph>
|
||||
Viele Unternehmen nutzen Kontaktformulare, die lediglich E-Mails
|
||||
versenden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein Mitarbeiter muss diese E-Mails lesen und die Daten händisch in
|
||||
Salesforce, HubSpot oder Pipedrive übertragen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist nicht nur zeitfressend, sondern auch eine Riskante Fehlerquelle.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Leads gehen verloren, die Reaktionszeit sinkt und die Datenqualität
|
||||
leidet.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In einer digitalen Welt ist{" "}
|
||||
<Marker>manuelle Datenpflege ein Anachronismus</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre Professionalität bedeutet: Die Software erledigt die Arbeit im
|
||||
Hintergrund.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<div className="my-12">
|
||||
<Mermaid id="crm-sync-sequence" title="Automatisierter CRM-Sync Ablauf" showShare={true}>
|
||||
sequenceDiagram
|
||||
participant Besucher
|
||||
participant Website
|
||||
participant ValidationLayer
|
||||
participant CRM
|
||||
participant SalesTeam
|
||||
Besucher->>Website: Formular absenden
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der automatisierte Lead-Fluss: Von der ersten Interaktion bis zum CRM-Eintrag in Millisekunden – ohne menschliches Eingreifen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Echtzeit-Synchronität als Wettbewerbsvorteil</H3>
|
||||
<Paragraph>Kunden erwarten heute sofortige Reaktionen.</Paragraph>
|
||||
<Paragraph>
|
||||
Ein Lead, der erst nach 24 Stunden kontaktiert wird, ist oft schon beim
|
||||
Wettbewerber gelandet.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Meine Architektur sorgt dafür, dass Ihre Sales-Teams{" "}
|
||||
<Marker>Sekunden nach dem Klick</Marker> arbeitsfähig sind.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir automatisieren die Qualifizierung und Zuweisung, damit keine Zeit
|
||||
verloren geht.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Geschwindigkeit ist im Digitalvertrieb der entscheidende Faktor.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Hebel for Ihre Daten-Souveränität</H2>
|
||||
<Paragraph>
|
||||
Integration bedeutet für mich mehr als nur das Verbinden von APIs.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es geht um die Schaffung eines <Marker>Single Source of Truth</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Resiliente API-Anbindung:</strong> Wir bauen Puffer-Systeme.
|
||||
Sollte Ihr CRM kurzzeitig offline sein, gehen keine Leads verloren.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Smart Data Enrichment:</strong> Wir bereiten die Daten so auf,
|
||||
dass Ihr Sales-Team sofort alle Kontext-Infos hat.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>DSGVO-konforme Übertragung:</strong> Alle Daten fließen
|
||||
Ende-zu-Ende verschlüsselt und nach strengsten europäischen Standards.{" "}
|
||||
<Marker>Sicherheit trifft auf Komfort.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact auf Ihre Sales-Performance"
|
||||
negativeLabel="Manuelle E-Mail-Übertragung"
|
||||
negativeText="Hohe Fehlerquote, langsame Response-Time, lückenhafte Daten-Historie"
|
||||
positiveLabel="Automatisierte CRM-Sync"
|
||||
positiveText="0 % Datenverlust, Echtzeit-Benachrichtigung, perfekte Lead-Transparenz"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Warum die 'Billig-Lösung' hier teuer wird</H2>
|
||||
<Paragraph>
|
||||
Einfache Plugins für CRM-Anbindungen sind oft instabil und unflexibel.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie brechen bei Updates oder können individuelle Felder nicht korrekt
|
||||
abbilden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen eine <Marker>robuste Brücke</Marker>, die mit Ihren
|
||||
Anforderungen mitwächst.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Verlässlichkeit ist in der Lead-Generierung die wichtigste Eigenschaft.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Vermeiden Sie das Risiko von Daten-Silos durch professionelles
|
||||
Engineering.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Daten-Autobahnen' baue</H2>
|
||||
<Paragraph>
|
||||
Mein Fokus liegt auf Unternehmen, deren Erfolg von der Qualität ihrer
|
||||
Leads abhängt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Begreifen Sie Ihr <Marker>CRM als Herzstück Ihrer Wertschöpfung</Marker>?
|
||||
Dann bin ich Ihr Architekt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schlage die Brücke zwischen Ihrer Website und Ihrem Erfolg.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Lassen Sie Ihre Daten fließen</H2>
|
||||
<Paragraph>
|
||||
Technologie sollte Reibung eliminieren, nicht neue Hürden schaffen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam Ihre Website zu einem integralen Bestandteil Ihres
|
||||
Sales-Motors machen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Präzision in der Schnittstelle, Klarheit im Ergebnis.</Marker> Ihr
|
||||
Business verdient einen reibungslosen Datenfluss.
|
||||
</Paragraph>
|
||||
152
apps/web/content/blog/digital-longevity-architecture.mdx
Normal file
152
apps/web/content/blog/digital-longevity-architecture.mdx
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
title: "Digital Longevity: Architektur für das nächste Jahrzehnt"
|
||||
description: "Software ohne Verfallsdatum: So bauen wir Systeme, die technologische Trends überdauern und langfristige Werte schaffen."
|
||||
date: "2026-02-02"
|
||||
tags: ["architecture", "longevity"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
In der schnelllebigen Tech-Welt gilt Software oft schon nach zwei Jahren
|
||||
als veraltet.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich halte das für eine massive Verschwendung von Kapital und Energie.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Wahre Qualität zeigt sich darin, wie ein System altert.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir <Marker>digitale Werte für Jahrzehnte</Marker>{" "}
|
||||
schaffen – durch vorausschauende Architektur und zeitlose Standards.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Gegen die Wegwerf-Mentalität im Code</H2>
|
||||
<Paragraph>
|
||||
Viele Agenturen bauen "Schönwetter-Lösungen", die nur bis zur nächsten
|
||||
Trend-Welle halten.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Man setzt auf kurzlebige Frameworks oder proprietäre Blackboxes, die nach
|
||||
kurzer Zeit nicht mehr unterstützt werden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis: Alle drei Jahre ist ein teurer Relaunch fällig.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>geplante Obsoleszenz der Software</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein Boutique-Ansatz ist das Gegenteil davon: Ich baue Systeme, die durch
|
||||
ihre innere Ordnung und Robustheit bestechen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Guter Code ist wie eine solide Immobilie – er braucht Pflege, aber keine
|
||||
Abrissbirne.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="technology-longevity" title="Technologie Langlebigkeit" showShare={true}>
|
||||
graph TD
|
||||
Logic["Zukunftsfähige Kern-Logik"] --> Standards["Offene Web-Standards"]
|
||||
Logic --> Modular["Modulare Komponenten"]
|
||||
Standards --> Decade["Lebensdauer > 10 Jahre"]
|
||||
Modular --> Update["Einfache Teil-Modernisierung"]
|
||||
Decade --> ROI["Maximaler Return on Investment"]
|
||||
Update --> ROI
|
||||
style ROI fill:#4ade80,stroke:#333
|
||||
style Decade fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Architektur der Langlebigkeit: Durch die Trennung von Logik und Trends
|
||||
sichern wir den Wert Ihrer digitalen Investition über Generationen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die Ästhetik der Zeitlosigkeit</H3>
|
||||
<Paragraph>Langlebigkeit hat auch eine visuelle Komponente.</Paragraph>
|
||||
<Paragraph>
|
||||
Ich vermeide "modische" Spielereien, die morgen schon peinlich wirken
|
||||
könnten.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre technische Eleganz ist schlicht, funktional und hochpräzise.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein <Marker>industrieller, klarer Look</Marker> altert langsamer als jede
|
||||
verspielte Grafik.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe Designs, die heute beeindrucken und in fünf Jahren noch immer
|
||||
souverän wirken.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Prinzipien für ewige Systeme</H2>
|
||||
<Paragraph>Wie baut man Software, die nicht veraltet?</Paragraph>
|
||||
<Paragraph>Durch die kompromisslose Auswahl der Fundamente:</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Bohrmaschinen statt Spielzeug:</strong> Ich nutze nur
|
||||
Technologien mit breitem industriellem Rückhalt. Keine "Hype"-Tools ohne
|
||||
Langzeit-Sicherheit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Strict Separation of Concerns:</strong> Wir trennen Design,
|
||||
Daten und Logik so sauber, dass man Einzelteile austauschen kann, ohne
|
||||
das Ganze zu gefährden.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Evolution:</strong> Mein System prüft sich
|
||||
selbst. Wir erkennen frühzeitig, wenn externe Standards sich ändern und
|
||||
passen uns proaktiv an.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der wirtschaftliche Impact der Halbwertszeit"
|
||||
negativeLabel="Trend-fokussierte Agentur"
|
||||
negativeText="Relaunch alle 3 Jahre nötig, technologische Schulden, hohe Lizenz-Abhängigkeit"
|
||||
positiveLabel="Mintel Longevity Standard"
|
||||
positiveText="Laufzeit von 10+ Jahren möglich, Code als bleibendes Asset, volle Souveränität"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Rendite durch technologische Beständigkeit</H2>
|
||||
<Paragraph>Wahrer ROI entsteht erst über die Zeit.</Paragraph>
|
||||
<Paragraph>
|
||||
Wer nicht ständig neu bauen muss, hat mehr Kapital für echtes Wachstum zur
|
||||
Verfügung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Langlebige Software ist zudem das{" "}
|
||||
<Marker>nachhaltigste digitale Werkzeug</Marker>, das Sie besitzen können.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Investieren Sie in Substanz, nicht in flüchtige Effekte.
|
||||
</Paragraph>
|
||||
<Paragraph>Vorsprung hat der, dessen Fundament felsenfest steht.</Paragraph>
|
||||
|
||||
<H2>Wann ist 'Ewigkeit' Ihr Ziel?</H2>
|
||||
<Paragraph>
|
||||
Suchen Sie einen Partner für den Aufbau einer digitalen Marke, die Bestand
|
||||
hat?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich arbeite für Gründer, die <Marker>Generationen-Projekte</Marker>{" "}
|
||||
führen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie technologische Stabilität als Teil Ihres Vermächtnisses
|
||||
begreifen, bin ich Ihr Architekt.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Werte schaffen, die bleiben</H2>
|
||||
<Paragraph>Digitale Exzellenz misst sich am Erfolg von morgen.</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein System gießen, das die Zeit überdauert.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Qualität ist Beständigkeit.</Marker> Ihr Erfolg verdient eine
|
||||
Architektur ohne Verfallsdatum.
|
||||
</Paragraph>
|
||||
205
apps/web/content/blog/fixed-price-digital-projects.mdx
Normal file
205
apps/web/content/blog/fixed-price-digital-projects.mdx
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Der strategische Festpreis: Warum Budgetsicherheit Qualität fördert"
|
||||
description: "Keine Angst vor unendlichen Stunden: Erfahren Sie, warum ein Festpreis-Modell der fairste Weg zu exzellenten digitalen Produkten ist."
|
||||
date: "2026-02-04"
|
||||
tags: ["management", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Sicherheitsdenken ist tief in der menschlichen Natur verwurzelt.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Am Ende eines Projekts wollen wir wissen, worauf wir uns eingelassen haben
|
||||
– finanziell und zeitlich.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect baue ich{" "}
|
||||
<Marker>Sicherheit durch Transparenz</Marker>.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum ein intelligenter Festpreis der fairste Weg zu
|
||||
exzellenten Ergebnissen ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die Falle der unendlichen Stunden</H2>
|
||||
<Paragraph>
|
||||
In der klassischen Softwareentwicklung ist Abrechnung nach Stunden (Time &
|
||||
Material) der Standard.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Doch das setzt einen völlig falschen Anreiz: Je länger ein Entwickler
|
||||
braucht, desto mehr verdient er.
|
||||
</Paragraph>
|
||||
<Paragraph>Effizienz wird ökonomisch bestraft.</Paragraph>
|
||||
<Paragraph>
|
||||
Ich hingegen habe mein gesamtes Business auf Geschwindigkeit und Klarheit
|
||||
optimiert.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mit einem Festpreis drehen wir den Spieß um:{" "}
|
||||
<Marker>Mein Anreiz ist Ihre schnellstmögliche Zufriedenheit.</Marker>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
So entsteht echte Partnerschaft statt eines Interessenkonflikts.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="fixed-price-model" title="Festpreis vs. Stundensatz Modell" showShare={true}>
|
||||
graph LR
|
||||
Goal["Ihr Projektziel"] --> Model["Wirtschaftliches Modell"]
|
||||
Model --> TnM["Abrechnung nach Stunden"]
|
||||
Model --> Fixed["Strategischer Festpreis (Mintel)"]
|
||||
TnM --> Risk["Uferlose Kosten & Zeitdruck"]
|
||||
Fixed --> Safety["Kalkulations-Sicherheit & Fokus"]
|
||||
Safety --> Quality["Maximale Qualität durch Effizienz"]
|
||||
style Fixed fill:#4ade80,stroke:#333
|
||||
style Quality fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Das Modell des Vertrauens: Fixe Budgets schaffen den Raum für
|
||||
kompromisslose inhaltliche Qualität.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Planung ist das halbe Fundament</H3>
|
||||
<Paragraph>
|
||||
Ein Festpreis funktioniert nur, wenn die Vision klar ist.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Deshalb investiere ich zu Beginn massiv Zeit in die Analyse und das
|
||||
"Blueprint-Design".
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn wir das Fundament präzise geplant haben, gibt es im Bauprozess keine
|
||||
bösen Überraschungen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>digitales Engineering mit norddeutscher Klarheit</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie bezahlen nicht für mein Ausprobieren, sondern für die punktgenaue
|
||||
Umsetzung meiner Erfahrung.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Warum meine Kalkulation hält</H2>
|
||||
<Paragraph>Ich arbeite nicht mit Schätzungen, sondern mit Daten.</Paragraph>
|
||||
<Paragraph>
|
||||
Durch meine automatisierte Toolchain weiß ich exakt, wie lange bestimmte
|
||||
Architekturschritte dauern.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Integrierte Risiko-Abdeckung:</strong> Unvorhergesehenes ist in
|
||||
meinem Preis bereits einkalkuliert. Sie tragen kein finanzielles Risiko.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Klare Deliverables:</strong> Wir definieren präzise
|
||||
Meilensteine. Sie wissen zu jedem Zeitpunkt, was Sie für Ihr Investment
|
||||
erhalten.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>0 % Budget-Overrun:</strong> Mein Wort gilt.{" "}
|
||||
<Marker>
|
||||
Zusatzkosten entstehen nur, wenn Sie den Scope aktiv erweitern.
|
||||
</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<DiagramGantt
|
||||
tasks={[
|
||||
{
|
||||
id: "blueprint",
|
||||
name: "Blueprint & Planung",
|
||||
start: "2024-01-01",
|
||||
duration: "2w",
|
||||
},
|
||||
{
|
||||
id: "design",
|
||||
name: "Design System",
|
||||
start: "2024-01-15",
|
||||
duration: "1w",
|
||||
dependencies: ["blueprint"],
|
||||
},
|
||||
{
|
||||
id: "core",
|
||||
name: "Core Development",
|
||||
start: "2024-01-22",
|
||||
duration: "3w",
|
||||
dependencies: ["design"],
|
||||
},
|
||||
{
|
||||
id: "testing",
|
||||
name: "Testing & QA",
|
||||
start: "2024-02-12",
|
||||
duration: "1w",
|
||||
dependencies: ["core"],
|
||||
},
|
||||
{
|
||||
id: "launch",
|
||||
name: "Launch & Deployment",
|
||||
start: "2024-02-19",
|
||||
duration: "1w",
|
||||
dependencies: ["testing"],
|
||||
},
|
||||
]}
|
||||
title="Festpreis-Projekt: Klare Meilensteine"
|
||||
caption="Transparente Zeitplanung mit festen Deliverables – Sie wissen immer, wo Sie stehen."
|
||||
id="fixed-price-gantt"
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der ökonomische Vergleich Ihrer Projektsicherheit"
|
||||
negativeLabel="Schätzpreis nach Stunden"
|
||||
negativeText="Kosten-Explosion möglich, fehlender Anreiz zur schnellen Fertigstellung"
|
||||
positiveLabel="Strategischer Festpreis"
|
||||
positiveText="Absolute Budgetsicherheit, Fokus auf Pure Output, maximale Effizienz"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Ihr Investment in Sicherheit</H2>
|
||||
<Paragraph>Ein Festpreis befreit den Kopf.</Paragraph>
|
||||
<Paragraph>
|
||||
Statt bei jedem Meeting die Uhr hämmern zu hören, konzentrieren wir uns
|
||||
auf das Wesentliche: Ihren Markterfolg.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das schafft eine Atmosphäre von{" "}
|
||||
<Marker>Kreativität und technologischem Mut</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer keine Angst vor dem Budget hat, baut die besseren Lösungen.
|
||||
</Paragraph>
|
||||
<Paragraph>Ich schaffe Ihnen den Rahmen für diese Exzellenz.</Paragraph>
|
||||
|
||||
<H2>Wann ist ein Festpreis der richtige Weg?</H2>
|
||||
<Paragraph>
|
||||
Mein Modell ist ideal für Entscheider, die unternehmerische Verantwortung
|
||||
für Budgets tragen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist Ihnen ein{" "}
|
||||
<Marker>planbares Ergebnis wichtiger als vage Hoffnungen</Marker>? Dann
|
||||
passen wir zusammen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich arbeite für die Profis, die Qualität zum festen Preis wertschätzen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Klarheit ist kein Luxus</H2>
|
||||
<Paragraph>
|
||||
Lassen wir das Rätselraten bei der Preisgestaltung hinter uns.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich biete Ihnen eine Zusammenarbeit auf Augenhöhe, bei der das Ergebnis im
|
||||
Mittelpunkt steht.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns gemeinsam Ihr Projekt auf ein{" "}
|
||||
<Marker>solides finanzielles Fundament</Marker> stellen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Pünktlich. Präzise. Zum vereinbarten Preis.</Marker>
|
||||
</Paragraph>
|
||||
173
apps/web/content/blog/gdpr-conformity-system-approach.mdx
Normal file
173
apps/web/content/blog/gdpr-conformity-system-approach.mdx
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: "DSGVO-Konformität: Ein systemischer Architektur-Ansatz"
|
||||
description: "Warum Datenschutz kein Banner-Problem ist: So bauen Sie rechtssichere Systeme durch kluges Engineering."
|
||||
date: "2026-02-10"
|
||||
tags: ["legal", "gdpr"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
DSGVO-Konformität wird oft als lästiges bürokratisches Hindernis
|
||||
wahrgenommen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect sehe ich sie jedoch als
|
||||
<Marker>das ultimative Qualitätsmerkmal für sauberes Engineering</Marker>.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ein System, das Daten schützt, ist ein gesundes System.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie ich Datenschutz architektonisch löse, statt ihn nur
|
||||
mit Bannern zu "flicken".
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Gegen das Abmahnrisiko – mit Systemarchitektur</H2>
|
||||
<Paragraph>
|
||||
Die meisten versuchen, die Anforderungen durch rechtliche Dokumente und
|
||||
nachträglich installierte Banner zu lösen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist so, als würde man ein brennendes Haus mit einer neuen
|
||||
Versicherungspolice löschen wollen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es schafft eine Schein-Sicherheit, bekämpft aber nicht die Ursache des
|
||||
Risikos.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Datenschutz muss bereits in der DNA des Codes verankert sein (Privacy by
|
||||
Design).
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn ein System keine unnötigen Daten sammelt,{" "}
|
||||
<Marker>verschwinden die Einfallstore für Probleme von selbst</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>Wahre Compliance ist technisch erzwungen.</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="gdpr-compliance-flow" title="DSGVO Compliance System" showShare={true}>
|
||||
graph TD
|
||||
Collection["Frontend Datenerhebung"] --> Minimize["Strikte Datenminimierung (SSO)"]
|
||||
Minimize --> Encrypt["End-to-End Verschlüsselung (TLS 1.3)"]
|
||||
Encrypt --> Access["Rollenbasierte Zugriffskontrolle (RBAC)"]
|
||||
Access --> Audit["Automatisches Compliance-Logging"]
|
||||
Audit --> Safe["Rechtssicheres & Robustes System"]
|
||||
style Safe fill:#4ade80,stroke:#333
|
||||
style Minimize fill:#4ade80,stroke:#333
|
||||
style Encrypt fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der Kreislauf der systemischen Sicherheit: Jede Stufe schützt Ihre
|
||||
Daten, Ihren Ruf und Ihren langfristigen Business-Value.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die ökonomische Logik des Datenschutz-Management</H3>
|
||||
<Paragraph>
|
||||
Wussten Sie, dass unsaubere Datensparsamkeit ein echtes finanzielles
|
||||
Risiko darstellt?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Daten, die Sie nicht besitzen, können nicht gestohlen werden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich helfe Ihnen, Ihre Prozesse so zu verschlanken, dass nur das{" "}
|
||||
<Marker>geschäftskritische Minimum</Marker> fließt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dies spart Speicher- und Rechenkapazitäten und reduziert Haftungsrisiken
|
||||
massiv.
|
||||
</Paragraph>
|
||||
<Paragraph>Ein schlankes Datensystem ist ein agiles System.</Paragraph>
|
||||
<Paragraph>
|
||||
Compliance ist kein Kostenfaktor, sondern eine Versicherung für Ihre
|
||||
digitale Zukunft.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Mein Prinzip: Privacy by Infrastructure</H2>
|
||||
<Paragraph>
|
||||
Ich betrachte Datenschutz nicht als Text auf der Unterseite "Impressum",
|
||||
sondern als Eigenschaft der Infrastruktur.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein Ziel ist ein System, bei dem die Einhaltung der Regeln{" "}
|
||||
<Marker>technisch unvermeidbar</Marker> ist.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Echtzeit-Anonymisierung:</strong> Logfiles und IPs werden am
|
||||
Punkt des Eingangs anonymisiert. Was technisch unkenntlich ist, fällt
|
||||
nicht unter die DSGVO-Strenge.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Geschlossene Datenkreisläufe:</strong> Wir vermeiden
|
||||
Blackbox-Server von Drittanbietern. Jedes Formular nutzt dedizierte
|
||||
Kanäle.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Compliance-as-Code:</strong> Automatisierte Tests prüfen bei
|
||||
jeder Änderung, ob neue Abhängigkeiten Ihre Richtlinien gefährden.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Zwei Ansätze der digitalen Compliance"
|
||||
negativeLabel="Reaktive Notlösung"
|
||||
negativeText="Nachträgliche Banner-Hacks, unsichere US-Dienste, permanente Angst vor der nächsten Abmahnwelle"
|
||||
positiveLabel="Proaktives Mintel Engineering"
|
||||
positiveText="Technisch verankerter Schutz, volle Datenhoheit, absolute rechtliche Sorgenfreiheit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Vertrauen als härteste Währung am Markt</H2>
|
||||
<Paragraph>
|
||||
Nutzer und Kunden sind heute sensibilisierter als jemals zuvor.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Eine Website, die respektvoll und ohne Banner-Nötigung mit Daten umgeht,
|
||||
schafft sofortiges Vertrauen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In einer Welt voller "Dark Patterns" ist Transparenz Ihr stärkstes
|
||||
Verkaufsargument.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe <Marker>souveräne digitale Räume</Marker>, die für Ihre Marke
|
||||
und Ihre Werte sprechen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein sauberes DSGVO-Konzept ist ein Investment in die Reputation Ihres
|
||||
Unternehmens.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Sicherheits-Festungen' baue</H2>
|
||||
<Paragraph>
|
||||
Mein architektonischer Ansatz ist ideal für Unternehmen in regulierten
|
||||
Branchen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Begreifen Sie{" "}
|
||||
<Marker>technische Exzellenz als Teil Ihrer Verantwortung</Marker>? Dann
|
||||
sind wir Partner.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich werde aktiv, wenn Sicherheit für Sie nicht verhandelbar ist.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Souveränität durch saubere Technik</H2>
|
||||
<Paragraph>
|
||||
Schaffen wir die Angst vor rechtlichen Fehltritten ab.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen ein System, das durch seine innere Ordnung besticht und den
|
||||
Schutz technically garantiert.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir Ihre digitale Basis für die
|
||||
<Marker>nächste Stufe der Professionalität</Marker> gemeinsam gießen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Seriosität ist planbar.</Marker>
|
||||
</Paragraph>
|
||||
169
apps/web/content/blog/green-it-sustainable-web.mdx
Normal file
169
apps/web/content/blog/green-it-sustainable-web.mdx
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: "Green IT: Warum nachhaltiger Code profitabler ist"
|
||||
description: "Effizienz als ökologischer und ökonomischer Hebel: Wie schlanke Web-Architekturen CO2 sparen und Ihr Budget schonen."
|
||||
date: "2026-02-03"
|
||||
tags: ["sustainability", "performance"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Das Internet verbraucht mehr Energie als der weltweite Flugverkehr.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Rolle als Digital Architect sehe ich Nachhaltigkeit nicht als
|
||||
"Nice-to-Have", sondern als Ausdruck technischer Reife.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Effizienter Code ist grüner Code.{" "}
|
||||
<Marker>
|
||||
Schlanke Systeme sparen nicht nur CO2, sondern auch bares Geld.
|
||||
</Marker>
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum ökologische Verantwortung und ökonomische
|
||||
Profitabilität Hand in Hand gehen.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Der ökologische Fußabdruck von schlechtem Code</H2>
|
||||
<Paragraph>
|
||||
Jedes unnötige Kilobyte, das durch das Netz geschickt wird, frisst Strom –
|
||||
im Rechenzentrum, in den Leitungen und am Endgerät des Nutzers.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Viele moderne Websites sind heute "Adipös". Sie schleppen Megabytes an
|
||||
Ballast mit sich herum.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das führt zu erhitzten Smartphones und überlasteten Servern.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>digitale Verschwendung</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Durch radikale Optimierung senken wir die CPU-Last um bis zu 80 %. Das
|
||||
schont die Umwelt und beschleunigt Ihre UX.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="green-it-efficiency" title="Green IT Effizienz-Kreislauf" showShare={true}>
|
||||
graph TD
|
||||
Code["Schlanke Code-Architektur"] --> Compute["Weniger CPU-Zyklen am Server"]
|
||||
Code --> Bytes["Weniger Traffic (CDN)"]
|
||||
Compute --> Energy["Niedrigerer Stromverbrauch"]
|
||||
Bytes --> Impact["Schnellere UX & Weniger CO2"]
|
||||
Energy --> Profit["Geringere Hosting-Kosten"]
|
||||
style Profit fill:#4ade80,stroke:#333
|
||||
style Impact fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Die grüne Rendite: Effizienz in der Software führt direkt zu
|
||||
ökologischen und finanziellen Einsparungen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Boutique-Engineering als Klimaschutz</H3>
|
||||
<Paragraph>
|
||||
Standard-Agenturen greifen oft zu überladenen Baukästen, um Zeit zu
|
||||
sparen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Der Preis dafür ist eine gigantische technische Ineffizienz.
|
||||
</Paragraph>
|
||||
<Paragraph>Ich investiere lieber Zeit in präzises Handwerk.</Paragraph>
|
||||
<Paragraph>
|
||||
Ein maßgeschneidertes System verbraucht nur einen Bruchteil der Ressourcen
|
||||
eines WordPress-Monolithen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>Nachhaltigkeit durch technologische Brillanz</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Hebel für eine nachhaltige Infrastruktur</H2>
|
||||
<Paragraph>Grüne IT beginnt bei der Wahl der Waffen.</Paragraph>
|
||||
<Paragraph>
|
||||
Hier sind drei Wege, wie ich Ihren digitalen Fußabdruck minimiere:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Static-First Architektur:</strong> Wir berechnen Seiten nicht
|
||||
bei jedem Aufruf neu. Einmal generiert, tausende Male effizient
|
||||
ausgeliefert.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Intelligentes Asset-Management:</strong> Keine unnötigen Fonts
|
||||
oder Tracking-Skripte. Wir senden nur das absolute Minimum an Daten an
|
||||
den Browser.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Grünes Server-Partnering:</strong> Ich wähle Hostinganbieter,
|
||||
die zu 100 % mit erneuerbaren Energien arbeiten.{" "}
|
||||
<Marker>Nachhaltigkeit über den gesamten Stack.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="energy-consumption-pie" title="Website Energie-Verbrauch Breakdown" showShare={true}>
|
||||
pie
|
||||
"Server Computing" : 40
|
||||
"Data Transfer" : 30
|
||||
"Client Rendering" : 20
|
||||
"Asset Storage" : 10
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Wo Ihre Website Energie verbraucht: Optimierungspotenzial in jedem Bereich.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact Ihres technologischen Fußabdrucks"
|
||||
negativeLabel="Standard Legacy-Website"
|
||||
negativeText="Hohe Serverlast, überladene Scripte, unnötiger CO2-Ausstoß"
|
||||
positiveLabel="Eco-Performance Architecture"
|
||||
positiveText="Minimale CPU-Last, schlanke Datenübertragung, 100% Green Hosting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Wirtschaftliche Vorteile von Green IT</H2>
|
||||
<Paragraph>Effizienz zahlt sich aus.</Paragraph>
|
||||
<Paragraph>
|
||||
Weniger Datentransfer und geringere Serverlast bedeuten niedrigere
|
||||
laufende Kosten.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Gleichzeitig belohnt Google schnelle, schlanke Seiten mit besseren
|
||||
Rankings.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Nachhaltigkeit ist also kein Verzicht, sondern ein{" "}
|
||||
<Marker>Wettbewerbsvorteil</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Positionieren Sie Ihr Unternehmen als Vorreiter einer neuen, bewussten
|
||||
digitalen Ära.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann macht Green IT für Sie Sinn?</H2>
|
||||
<Paragraph>
|
||||
Ich baue für Marken, die <Marker>Werte über kurzfristige Trends</Marker>{" "}
|
||||
stellen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie Ihre CSR-Ziele auch digital ernst nehmen, bin ich Ihr Architekt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Schaffen wir Systeme, die auch für die nächste Generation noch vorbildlich
|
||||
sind.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Weniger ist mehr Zukunft</H2>
|
||||
<Paragraph>Gutes Design ist immer auch sparsames Design.</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam Ihren digitalen Ballast abwerfen und stattdessen in
|
||||
echte Effizienz investieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Purer Output bei minimalem Input.</Marker> Das ist das Ziel meines
|
||||
Boutique-Ansatzes.
|
||||
</Paragraph>
|
||||
<Paragraph>Für Ihr Business und unseren Planeten.</Paragraph>
|
||||
159
apps/web/content/blog/hidden-costs-of-wordpress-plugins.mdx
Normal file
159
apps/web/content/blog/hidden-costs-of-wordpress-plugins.mdx
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: "Die versteckten Kosten von WordPress-Plugins"
|
||||
description: "Wie Sie die Plugin-Falle vermeiden und eine wartbare, sichere Plattform aufbauen."
|
||||
date: "2026-02-12"
|
||||
tags: ["wordpress", "performance"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
WordPress-Plugins werden oft als die ultimative Abkürzung zum Erfolg
|
||||
verkauft.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner täglichen Praxis als Digital Architect sehe ich jedoch meist das
|
||||
Gegenteil: Sie sind eine teure Umleitung in eine technische Sackgasse.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Die vermeintlich "schnelle Lösung" ist am Ende{" "}
|
||||
<Marker>oft die teuerste Entscheidung Ihrer digitalen Strategie</Marker>.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die "Frankenstein-Architektur" der Plugins</H2>
|
||||
<Paragraph>
|
||||
Die Versuchung ist menschlich: Ein Klick, und das neue Feature ist da.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Doch was Sie wirklich tun, ist fremden Code ungefiltert in Ihr
|
||||
geschäftskritisches System zu lassen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich sehe oft Instanzen, die unter der Last von 40+ Plugins förmlich
|
||||
zermalmt werden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Jedes Plugin verfolgt eine eigene Logik und kämpft mit anderen Komponenten
|
||||
um knappe Ressourcen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es entsteht eine instabile <Marker>"Frankenstein-Architektur"</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie geben die Kontrolle über Ihre Plattform ab und hängen von der Roadmap
|
||||
Dritter ab.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Verliert ein Plugin-Entwickler das Interesse, stehen Sie mit einer
|
||||
Sicherheitslücke da.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="plugin-dependency-trap" title="Plugin Dependency Trap" showShare={true}>
|
||||
graph TD
|
||||
P1["Plugin A (Slider)"] --> Core["WordPress Core"]
|
||||
P2["Plugin B (SEO)"] --> Core
|
||||
P3["Plugin C (Forms)"] --> Core
|
||||
Core --> Bloat["Asset-Overload (CSS/JS)"]
|
||||
Bloat --> Slow["Ladezeit > 4 Sek."]
|
||||
P1 -.-> P2["Konfliktmöglichkeit"]
|
||||
P2 -.-> P3["Konfliktmöglichkeit"]
|
||||
Slow --> Bounce["Besucher springen ab"]
|
||||
style Slow fill:#fca5a5,stroke:#333
|
||||
style Bounce fill:#ef4444,color:#fff
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Das Plugin-Paradoxon: Jedes 'Feature' erhöht die Wahrscheinlichkeit
|
||||
eines Systemkollapses.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die versteckten Kosten der "Gratis"-Features</H3>
|
||||
<Paragraph>
|
||||
Man sagt oft, WordPress-Plugins seien kostenlos. Das ist eine gefährliche
|
||||
Illusion.
|
||||
</Paragraph>
|
||||
<Paragraph>Die wahren Kosten entstehen bei der Wartung.</Paragraph>
|
||||
<Paragraph>
|
||||
In klassischen Agenturen zahlt ein Kunde meist hunderte Euro monatlich für
|
||||
"Updates installieren".
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>reaktives Hoffen statt proaktivem Engineering</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Echter, sauberer Code altert viel langsamer als zusammengeklickte
|
||||
Plugin-Konstrukte.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Technische und betriebswirtschaftliche Risiken</H2>
|
||||
<Paragraph>
|
||||
Aus meiner architektonischen Sicht gibt es drei kritische Hebel:
|
||||
</Paragraph>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Die Sicherheitsfalle:</strong> Über 90 % der Angriffe auf
|
||||
WordPress erfolgen über unsichere Plugins. Jede Erweiterung vergrößert
|
||||
Ihre Angriffsfläche.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Die Performance-Erosion:</strong> Viele Plugins laden ihre
|
||||
Scripte global – auch wenn sie gar nicht gebraucht werden.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Die technologische Sackgasse:</strong> Je mehr Plugins Sie
|
||||
nutzen, desto schwerer wird ein Wechsel.{" "}
|
||||
<Marker>Datenhoheit beginnt bei der Codehoheit.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der architektonische Vergleich"
|
||||
negativeLabel="Plugin-Chaos"
|
||||
negativeText="Abhängigkeit von Unbekannten, instabile Update-Zyklen, massiver Performance-Leck"
|
||||
positiveLabel="Bespoke Architecture"
|
||||
positiveText="100 % eigene Codebasis, maximale Kontrolle, Performance als Kernmerkmal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Mein Weg: Präziser Code statt Blackbox-Plugins</H2>
|
||||
<Paragraph>
|
||||
Anstatt ein tonnenschweres Plugin für eine einfache Funktion zu
|
||||
installieren, schreibe ich Ihnen diese Funktion direkt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist ein System, das exakt das tut, was Sie brauchen – und
|
||||
kein Byte mehr.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Kein Ballast, kein Sicherheitsrisiko, keine Abhängigkeit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue keine digitalen Kartenhäuser, sondern{" "}
|
||||
<Marker>echte digitale Assets</Marker>, die für Profis arbeiten.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann ist dieser Premium-Ansatz für Sie richtig?</H2>
|
||||
<Paragraph>
|
||||
Ich arbeite für die Wenigen, die{" "}
|
||||
<Marker>technische Brillanz zum entscheidenden Marktvorteil</Marker>
|
||||
machen wollen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Planen Sie, Ihre Marke über Jahre stabil im Netz zu führen? Dann sind wir
|
||||
Partner.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Investieren Sie in Ihr Fundament</H2>
|
||||
<Paragraph>
|
||||
Plugins sind wie billige Anbauwände: Sie wirken im Katalog gut, aber nach
|
||||
dem ersten Umzug wackeln sie.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam eine Plattform schaffen, die Ihr Business auch in
|
||||
fünf Jahren noch zuverlässig trägt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>
|
||||
Qualität ist die einzige Abkürzung, die wirklich funktioniert.
|
||||
</Marker>
|
||||
</Paragraph>
|
||||
157
apps/web/content/blog/maintenance-for-headless-systems.mdx
Normal file
157
apps/web/content/blog/maintenance-for-headless-systems.mdx
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: "Wartungsfrei durch Verzicht: Warum 'Kein CMS' oft das bessere CMS ist"
|
||||
description: "Sicherheit und Geschwindigkeit durch architektonische Reduktion: Warum Git-basierte Workflows klassische Admin-Backends schlagen."
|
||||
date: "2026-02-01"
|
||||
tags: ["maintenance", "architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Ein CMS wird oft als Befreiung verkauft. In der Realität ist es oft der
|
||||
Anfang einer teuren Abhängigkeit.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Praxis sehe ich, wie Unternehmen hunderte Stunden in die Pflege
|
||||
von Systemen investieren, die sie eigentlich entlasten sollten.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Content-Management ohne Ballast</Marker>{" "}
|
||||
der wahre Hebel für Geschwindigkeit und Fokus ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Der CMS-Wartungs-Albtraum</H2>
|
||||
<Paragraph>
|
||||
Klassische CMS-Lösungen (wie WordPress oder Typo3) sind komplexe
|
||||
Software-Monster.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie müssen ständig aktualisiert werden, weil wöchentlich neue
|
||||
Sicherheitslücken auftauchen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Diese Wartung frisst Zeit und Geld, ohne einen einzigen Cent Mehrwert für
|
||||
Ihr Business zu generieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>technische Sisyphusarbeit</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Warum ein ganzes Kraftwerk betreiben, wenn Sie nur eine Glühbirne zum
|
||||
Leuchten bringen wollen?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre Effizienz bedeutet, Komplexität radikal zu streichen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="maintenance-workflow" title="Wartungs-Workflow Vergleich" showShare={true}>
|
||||
graph TD
|
||||
Need["Änderung am Inhalt"] --> Path["Update-Prozess"]
|
||||
Path --> CMS["Klassisches CMS (Update/Backup/DB)"]
|
||||
Path --> Git["Git-based Workflow (Mintel)"]
|
||||
CMS --> Risk["Sicherheitslücken & Träge Ladezeit"]
|
||||
Git --> Speed["Instant Go-Live & Maximale Sicherheit"]
|
||||
Speed --> Focus["Fokus auf Kunden & Strategie"]
|
||||
style Git fill:#4ade80,stroke:#333
|
||||
style Focus fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der schlanke Workflow: Wir eliminieren die Datenbank-Ebene, um
|
||||
Angriffsflächen zu schließen und das Tempo zu verdoppeln.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Content as Code: Die Architektur der Profis</H3>
|
||||
<Paragraph>
|
||||
Anstatt sich mit unübersichtlichen Admin-Backends herumzuschlagen,
|
||||
integrieren wir Inhalte direkt in den Deployment-Prozess.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das bedeutet: Wenn Sie etwas ändern wollen, geschieht das in einer
|
||||
sauberen, versionierten Umgebung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Keine Datenbanken, die korrumpieren können. Kein Backend, das gehackt
|
||||
werden kann.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>Sicherheit durch Simplizität</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>Inhalte werden so stabil wie die Architektur selbst.</Paragraph>
|
||||
|
||||
<H2>Warum "Kein CMS" die beste CMS-Strategie ist</H2>
|
||||
<Paragraph>
|
||||
Vermeintlich "einfache" Editoren führen oft zu zerstückelten Layouts und
|
||||
inkonsistentem Design.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Durch meinen Code-basierten Ansatz bleibt Ihre Markenidentität zu 100 %
|
||||
geschützt.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>0 % Sicherheitsrisiko:</strong> Ohne Datenbank-Schnittstelle
|
||||
gibt es keine Login-Bereiche für Hacker. Ihre Seite ist faktisch immun.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Blitzschnelle Änderungen:</strong> Wir nutzen automatisierte
|
||||
Pipelines. Eine Textänderung ist in Sekunden weltweit live.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Reduzierte Fixkosten:</strong> Sie sparen sich teure
|
||||
Wartungsverträge für "Backend-Security".{" "}
|
||||
<Marker>Geld, das produktiver in Marketing fließen kann.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der ökonomische Vergleich Ihres Betriebsaufwands"
|
||||
negativeLabel="Standard CMS-Betrieb"
|
||||
negativeText="Wöchentliche Sicherheits-Updates, monatliche Hosting-Hacks, träge Performance"
|
||||
positiveLabel="Mintel Low-Maintenance"
|
||||
positiveText="0 Wartungs-Minuten pro Monat, felsenfeste Stabilität, Höchstgeschwindigkeit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Fokus auf das, was zählt: Ihre Botschaft</H2>
|
||||
<Paragraph>
|
||||
Die wertvollste Ressource in Ihrem Unternehmen ist Aufmerksamkeit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Verschwenden Sie diese nicht mit technischen Nebenschauplätzen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen ein System, das einfach funktioniert – im Hintergrund,
|
||||
lautlos und effizient.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Investieren Sie in <Marker>Inhaltliche Exzellenz</Marker> statt in
|
||||
technische Reparaturen.
|
||||
</Paragraph>
|
||||
<Paragraph>Befreien Sie Ihr Business von der CMS-Last.</Paragraph>
|
||||
|
||||
<H2>Wann ist dieser minimalistische Weg für Sie richtig?</H2>
|
||||
<Paragraph>
|
||||
Ich arbeite für Entscheider, deren Kerngeschäft nicht das Betreiben einer
|
||||
IT-Infrastruktur ist.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wollen Sie eine Website, die einfach{" "}
|
||||
<Marker>immer online und immer schnell</Marker> ist?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich bin der Architekt für alle, die Klarheit und Ergebnisorientierung über
|
||||
Feature-Listen stellen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Simplizität ist das neue High-End</H2>
|
||||
<Paragraph>Die besten Systeme sind die, die man nicht spürt.</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam den Ballast abwerfen und uns auf Ihren Erfolg
|
||||
konzentrieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Maximale Wirkung bei minimalem technischem Overhead.</Marker> Ihr
|
||||
Erfolg verdient dieses effiziente Fundament.
|
||||
</Paragraph>
|
||||
156
apps/web/content/blog/no-us-cloud-platforms.mdx
Normal file
156
apps/web/content/blog/no-us-cloud-platforms.mdx
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Warum ich keine US-Cloud-Plattformen nutze"
|
||||
description: "Souveränität durch lokale Infrastruktur: Wie Sie sich vor dem Zugriff fremder Behörden schützen."
|
||||
date: "2026-02-07"
|
||||
tags: ["cloud", "privacy"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Die Daten liegen sicher in der Cloud." – Dieser Satz ist heute oft eine
|
||||
gefährliche Halbwahrheit.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect begegne ich unzähligen Unternehmen,
|
||||
die die Kontrolle über ihre wichtigsten Assets verloren haben.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Sie sind abhängig von US-Infrastrukturen und rechtlichen Grauzonen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>lokale Datenhoheit</Marker> der wahre Hebel
|
||||
für Sicherheit und Geschwindigkeit ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Das Märchen von der sorglosen US-Cloud</H2>
|
||||
<Paragraph>Die großen Hyper-Scaler bieten Bequemlichkeit.</Paragraph>
|
||||
<Paragraph>
|
||||
Doch diese Bequemlichkeit hat einen Preis: Ihre Souveränität.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Durch Gesetze wie den Cloud Act können US-Behörden theoretisch Zugriff auf
|
||||
Daten verlangen, die auf US-Servern liegen – völlig egal, wo diese
|
||||
physisch stehen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Für europäische Unternehmen mit sensiblen Kunden- oder Prozessdaten ist
|
||||
das ein <Marker>untragbares strategisches Risiko</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer seine Datenhoheit aufgibt, macht sein Business erpressbar.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="local-cloud-hybrid" title="Local Cloud Strategie" showShare={true}>
|
||||
graph TD
|
||||
Data["Ihre geschäftskritischen Daten"] --> Hosting["Strategische Hosting-Wahl"]
|
||||
Hosting --> US["US Hyper-Scaler (Abhängigkeit)"]
|
||||
Hosting --> Local["European Local Cloud (Souveränität)"]
|
||||
US --> Risk["Rechtliche Unsicherheit & Cloud Act"]
|
||||
Local --> Compliance["DSGVO-Safe & Daten-Immunität"]
|
||||
Compliance --> Speed["Niedrige Latenz & Absolute Kontrolle"]
|
||||
style Local fill:#4ade80,stroke:#333
|
||||
style Risk fill:#fca5a5,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Architektonische Entscheidung: Geopolitische Risiken minimieren durch
|
||||
bewusste Standort-Wahl.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Technologie ist niemals neutral</H3>
|
||||
<Paragraph>
|
||||
Hinter jedem Server steht eine politische und rechtliche Realität.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Indem wir auf spezialisierte europäische Infrastrukturen setzen, gewinnen
|
||||
wir Immunität gegen fremde Gesetzgebungen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>digitaler Selbstschutz</Marker> auf höchstem Niveau.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Gleichzeitig profitieren wir von extrem niedrigen Latenzen und einer
|
||||
Performance, die US-Systeme oft nicht liefern können.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Nähe zum Kunden bedeutet hier auch messbare Geschwindigkeit.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Mein Ansatz: Die \"High-Fidelity\" Infrastruktur</H2>
|
||||
<Paragraph>
|
||||
Ich baue Systeme, die nicht nur technisch brillant, sondern auch
|
||||
strategisch unangreifbar sind.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lokalität ist bei mir kein Rückschritt, sondern{" "}
|
||||
<Marker>Premium-Protection</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Physische Souveränität:</strong> Wir nutzen Rechenzentren unter
|
||||
europäischem Recht. Ihre Daten verlassen niemals diesen Rechtsraum.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Entkoppelte Dienste:</strong> Ich vermeide proprietäre
|
||||
US-Schnittstellen. Wir nutzen offene Standards für maximale
|
||||
Architektur-Freiheit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Sovereign Operations:</strong> Wir kontrollieren jeden Layer –
|
||||
vom OS bis zur Applikation.{" "}
|
||||
<Marker>Keine Blackboxes, keine Hintertüren.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der strategische Impact Ihrer Standortwahl"
|
||||
negativeLabel="Standard US-Hosting"
|
||||
negativeText="Rechtliche Unsicherheit, Datenexport-Risiken, schleichende Abhängigkeit"
|
||||
positiveLabel="European Local Cloud"
|
||||
positiveText="Volle Compliance, geopolitische Sicherheit, maximale Geschwindigkeit vor Ort"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Souveränität als Wettbewerbsvorteil</H2>
|
||||
<Paragraph>
|
||||
In einer Welt, die immer instabiler wird, ist \"Daten-Sicherheit am
|
||||
Standort\" ein echtes Verkaufsargument.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Zeigen Sie Ihren Kunden, dass Sie ihre Informationen ernst nehmen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer heute proaktiv auf <Marker>lokale Cloud-Lösungen</Marker> setzt, spart
|
||||
sich morgen teure und hektische Migrationswellen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe Ihnen ein digitales Fundament, das politisch und rechtlich
|
||||
stabil steht.
|
||||
</Paragraph>
|
||||
<Paragraph>Investieren Sie in Immunität, nicht in Abhängigkeit.</Paragraph>
|
||||
|
||||
<H2>Wann macht lokale Exzellenz für Sie Sinn?</H2>
|
||||
<Paragraph>
|
||||
Suchen Sie nach maximaler Unabhängigkeit von globalen Playern?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich bin der richtige Partner für Unternehmen, die{" "}
|
||||
<Marker>eigene digitale Assets als Kern ihres Erfolgs</Marker> begreifen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Für Projekte \"ohne Anspruch\" gibt es Massenangebote. Ich baue für die, die
|
||||
keine Kompromisse machen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Ihr Business, Ihre Regeln</H2>
|
||||
<Paragraph>Holen Sie sich die Kontrolle über Ihre Daten zurück.</Paragraph>
|
||||
<Paragraph>
|
||||
Ich begleite Sie beim Aufbau einer Architektur, die so unabhängig ist wie
|
||||
Ihre unternehmerische Vision.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir Ihre Daten dort, wo sie sicher sind:{" "}
|
||||
<Marker>In Ihrem Einflussbereich.</Marker>
|
||||
</Paragraph>
|
||||
160
apps/web/content/blog/professional-hosting-operations.mdx
Normal file
160
apps/web/content/blog/professional-hosting-operations.mdx
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: "Professionelles Hosting: Das Fundament digitaler Exzellenz"
|
||||
description: "Beyond Shared Hosting: Warum Industrial-Grade Operations für Ihre Marke entscheidend sind und wie wir Hochverfügbarkeit garantieren."
|
||||
date: "2026-01-29"
|
||||
tags: ["hosting", "operations"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Ein brillanter Webauftritt ist wertlos, wenn er im entscheidenden Moment
|
||||
nicht erreichbar ist.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Hosting ist für mich kein notwendiges Übel, sondern das schlagende Herz
|
||||
Ihrer digitalen Präsenz.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie <Marker>Industrial-Grade Operations</Marker> dafür
|
||||
sorgen, dass Sie auch bei massiven Traffic-Spitzen ruhig schlafen können.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Das Märchen vom 'Billig-Hosting'</H2>
|
||||
<Paragraph>
|
||||
Viele Unternehmen sparen am falschen Ende und wählen Shared-Hosting für
|
||||
wenige Euro.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Der Preis dafür ist eine geteilte Performance und ein erhöhtes
|
||||
Sicherheitsrisiko.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist Ihr Nachbar auf dem Server Ziel einer Attacke, geht Ihre Seite mit
|
||||
unter.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist kein vernünftiges Geschäftsmodell, sondern ein digitales
|
||||
Glücksspiel.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In meiner Welt gibt es keine Kompromisse bei der Erreichbarkeit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Ihre Marke verdient eine eigene Umlaufbahn.</Marker>
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="cloud-native-operations" title="Cloud-Native Operations" showShare={true}>
|
||||
graph TD
|
||||
Deploy["Automatisches Deployment (Git Push)"] --> Build["Isolierte Container-Builds"]
|
||||
Build --> Global["Global CDN Verteilung"]
|
||||
Global --> User["Nutzer (Weltweit blitzschnell)"]
|
||||
Global --> Failover["Automatisches Failover (Sicherheit)"]
|
||||
style Global fill:#4ade80,stroke:#333
|
||||
style Failover fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Die Cloud-Native Architektur: Skalierung per Knopfdruck und
|
||||
Ausfallsicherheit durch globale Redundanz.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Infrastruktur als Code: Die moderne Festung</H3>
|
||||
<Paragraph>
|
||||
Ich konfiguriere Server nicht manuell durch Klicken in irgendwelchen
|
||||
Web-Interfaces.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schreibe die Infrastruktur als Code (IaC). Das bedeutet absolute
|
||||
Reproduzierbarkeit und Fehlerfreiheit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sollte ein Rechenzentrum ausfallen, "erwacht" Ihr System an einem anderen
|
||||
Ort in Minuten wieder zum Leben.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist das Niveau von <Marker>Hochverfügbarkeit</Marker>, das ich für
|
||||
meine Kunden realisiere.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ihre Website wird zu einem unverwüstlichen digitalen Asset.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Prinzipien für reibungslose Operations</H2>
|
||||
<Paragraph>Was macht eine professionelle Hosting-Strategie aus?</Paragraph>
|
||||
<Paragraph>Es sind die unsichtbaren Schutzschilde:</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Edge Computing & Caching:</strong> Ihre Seite wird direkt dort
|
||||
ausgeliefert, wo der Nutzer ist. Ob in New York oder Berlin – Ladezeiten
|
||||
unter 1 Sekunde sind der Standard.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Backups & Rollbacks:</strong> Mit einem Klick
|
||||
können wir jede Version der Vergangenheit wiederherstellen.{" "}
|
||||
<Marker>Keine Angst vor Fehlern.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Echtzeit-Monitoring:</strong> Ich sehe Probleme, bevor Ihre
|
||||
Kunden sie bemerken. Proaktives Handeln ist besser als reaktives
|
||||
Flicken.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact auf Ihre Betriebssicherheit"
|
||||
negativeLabel="Standard Shared-Hosting"
|
||||
negativeText="Träge Ladezeiten bei Last, unsichere Nachbarschaft, manuelle Wartung"
|
||||
positiveLabel="Bespoke Managed Infra"
|
||||
positiveText="Unbegrenzte Skalierung, maximale Isolation, 100 % automatisierte Sicherheit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Warum Sorgenfreiheit ein Investment ist</H2>
|
||||
<Paragraph>
|
||||
Was kostet Sie eine Stunde Website-Ausfall während einer wichtigen
|
||||
Kampagne?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Meist ist dieser Schaden weitaus höher als die Investition in eine solide
|
||||
Architektur.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schlage die Brücke zwischen Hochleistungstechnologie und
|
||||
geschäftlichem Erfolg.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns Ihre Plattform auf ein{" "}
|
||||
<Marker>industrielles Fundament</Marker> stellen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ruhe im Betrieb ist das Ergebnis von erstklassiger Planung.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'Unverwüstlichkeit' baue</H2>
|
||||
<Paragraph>
|
||||
Ich arbeite für Gründer, die über das Stadium von "es läuft irgendwie"
|
||||
hinaus sind.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist Ihre Website das Schaufenster Ihres Erfolgs? Dann sollte sie auf dem
|
||||
besten Fundament stehen, das die Technik bietet.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich bin der Architekt für <Marker>kompromisslose Verfügbarkeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Ihre Plattform verdient das Beste</H2>
|
||||
<Paragraph>Lassen wir das Basteln im Maschinenraum hinter uns.</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen eine Umgebung, die mit Ihren Ambitionen mitwächst.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Sicher. Schnell. Skalierbar.</Marker> Das ist modernes Hosting auf
|
||||
Boutique-Niveau.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Gönnen Sie Ihrem Business die technologische Souveränität, die es
|
||||
verdient.
|
||||
</Paragraph>
|
||||
142
apps/web/content/blog/responsive-design-high-fidelity.mdx
Normal file
142
apps/web/content/blog/responsive-design-high-fidelity.mdx
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "Responsive Design: Exzellenz auf jedem Endgerät"
|
||||
description: "Mehr als nur Boxen rücken: Warum architektonische Präzision und plattformübergreifende Ergonomie Ihre globale Conversion-Rate steigern."
|
||||
date: "2026-01-27"
|
||||
tags: ["design", "ux"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Responsive" bedeutet heute viel mehr als nur das Nebeneinander-Rücken von
|
||||
Boxen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Welt als Digital Architect ist jedes Endgerät eine eigene Bühne
|
||||
mit eigenen Regeln.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Responsive-Exzellenz</Marker> der Hebel für
|
||||
Ihre globale Conversion-Rate ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Jenseits der Standard-Breakpoints</H2>
|
||||
<Paragraph>
|
||||
Die meisten Agenturen nutzen simple Raster, die auf dem Desktop gut
|
||||
aussehen und auf dem Smartphone "irgendwie funktionieren".
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist oft frustrierend: Zu kleine Texte, unbedienbare Buttons
|
||||
und Bilder, die das Layout sprengen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich betrachte Responsive Design als{" "}
|
||||
<Marker>architektonische Präzisionsleistung</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir gestalten die Experience for den Nutzer im Zug ebenso perfekt wie für
|
||||
den Entscheider am 4K-Monitor.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Fokus und Hierarchie müssen auf jedem Screen neu definiert werden.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="responsive-ux-strategy" title="Responsive UX Strategie" showShare={true}>
|
||||
graph TD
|
||||
Logic["Zentrales Design-System"] --> Mobile["Mobile (Daumen-Optimiert)"]
|
||||
Logic --> Tablet["Tablet (Touch & Swipe)"]
|
||||
Logic --> Desktop["Desktop (Maus & Tastatur)"]
|
||||
Mobile --> UX["Perfekte UX auf jedem Gerät"]
|
||||
Tablet --> UX
|
||||
Desktop --> UX
|
||||
style UX fill:#4ade80,stroke:#333
|
||||
style Logic fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Plattformübergreifende Brillanz: Ein System, das sich nicht nur anpasst,
|
||||
sondern für jedes Endgerät optimiert wird.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Kontextsensitive Ergonomie</H3>
|
||||
<Paragraph>
|
||||
Ein mobiler Nutzer hat andere Bedürfnisse als ein Desktop-Nutzer.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In meiner Architektur passen wir nicht nur das Layout an, sondern oft auch
|
||||
die Interaktionslogik.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>digitale Ergonomie auf Boutique-Niveau</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Informationen müssen dort fließen, wo sie gebraucht werden – ohne
|
||||
Reibungsverluste durch das Endgerät.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Hebel für ein grenzenloses Web</H2>
|
||||
<Paragraph>Wie erreichen wir diese technische Perfektion?</Paragraph>
|
||||
<Paragraph>Durch den Einsatz modernster Engineering-Methoden:</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Fluid Typography & Spacing:</strong> Wir nutzen keine starren
|
||||
Pixel-Werte. Alles atmet und skaliert harmonisch mit der Viewport-Größe.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Adaptive Media:</strong> Bilder und Videos werden exakt in der
|
||||
Größe ausgeladen, die das Display erfordert.{" "}
|
||||
<Marker>Kein Byte wird verschwendet.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Intelligente Touch-Targets:</strong> Wir optimieren alle
|
||||
interaktiven Elemente für die menschliche Anatomie – auf jedem Gerät.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact auf Ihre globale Marktdurchdringung"
|
||||
negativeLabel="Standard Grid-Responsive"
|
||||
negativeText="Verlust an Übersichtlichkeit auf kleinen Screens, langsame Ladezeiten, 'Bastel'-Look"
|
||||
positiveLabel="Bespoke Multi-Screen Engine"
|
||||
positiveText="Perfekte Ergonomie überall, blitzschneller Content, konsistentes Markenbild"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Wirtschaftlicher Hebel: Mobiler Erfolg</H2>
|
||||
<Paragraph>
|
||||
Über 60 % des Web-Traffics findet heute auf mobilen Endgeräten statt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer hier patzt, verliert die Mehrheit seiner potenziellen Kunden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe Ihnen ein System, das <Marker>Zukunftssicherheit</Marker>{" "}
|
||||
garantiert – egal welche Geräte morgen auf den Markt kommen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ihre Website wird zu einem universellen Werkzeug Ihres Erfolgs.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Für wen ich 'grenzenlose' Welten baue</H2>
|
||||
<Paragraph>
|
||||
Mein Fokus liegt auf Marken mit einem internationalen, mobilen Publikum.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Verlangen Ihre Kunden Perfektion in jedem Moment? Dann bin ich Ihr
|
||||
Architekt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich schaffe <Marker>Barrierefreiheit durch Qualität</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Konsistenz ist die halbe Miete</H2>
|
||||
<Paragraph>Ihre Marke muss sich überall gleich wertig anfühlen.</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein digitales Ökosystem erschaffen, das auf jedem
|
||||
Screen glänzt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Präzision im Detail, Harmonie im Ganzen.</Marker> Ihr Erfolg
|
||||
verdient diese nahtlose Integration.
|
||||
</Paragraph>
|
||||
179
apps/web/content/blog/slow-loading-costs-customers.mdx
Normal file
179
apps/web/content/blog/slow-loading-costs-customers.mdx
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
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"
|
||||
tags: ["performance", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Zeit ist im modernen Web die härteste Währung.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Laufbahn als Digital Architect habe ich miterlebt, wie
|
||||
Millisekunden über den Erfolg von Geschäftsmodellen entscheiden.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Wer technische Altlasten ignoriert, zahlt Zinsen in Form von massiven
|
||||
Kundenverlusten.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
<Marker>
|
||||
Bis das digitale Business schließlich an technischer Zahlungsunfähigkeit
|
||||
scheitert
|
||||
</Marker>
|
||||
.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Zinsen auf schlechte technische Entscheidungen</H2>
|
||||
<Paragraph>
|
||||
Technik-Schulden (Technical Debt) entstehen oft schleichend.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Man wählt heute die schnelle, unsaubere Lösung, um vermeintlich Zeit zu
|
||||
sparen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Doch diese Entscheidung ist ein digitaler Kredit mit extrem hohen Zinsen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Bei der Performance äußert sich das in trägen Ladezeiten, die sich wie
|
||||
Blei auf Ihre Conversion-Rate legen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Jede Sekunde Verzögerung senkt die Abschlussquote im Schnitt um 20 %.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist kein bloßes IT-Thema – das ist ein{" "}
|
||||
<Marker>massives betriebswirtschaftliches Risiko</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich helfe Ihnen, diese Performance-Killer aufzuspüren und systematisch zu
|
||||
eliminieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Oft sind es kleine Versäumnisse, die in der Summe ein System unrettbar
|
||||
schwerfällig machen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="loading-debt-cycle" title="Ladezeit Teufelskreis" showShare={true}>
|
||||
graph LR
|
||||
Slow["Träge Website"] --> Frust["Nutzer-Frustration (Subliminal)"]
|
||||
Frust --> Bounce["Hohe Absprungrate"]
|
||||
Bounce --> Rank["Ranking-Verlust bei Google"]
|
||||
Rank --> Loss["Umsatz- & Reputationsverlust"]
|
||||
Loss --> Debt["Explodierende Akquisekosten"]
|
||||
style Loss fill:#ef4444,color:#fff
|
||||
style Debt fill:#ef4444,color:#fff
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der fatale Teufelskreis der Ladezeit: Technische Schulden führen zu
|
||||
immer höheren Opportunitätskosten.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die Psychologie des digitalen Wartens</H3>
|
||||
<Paragraph>
|
||||
Wussten Sie, dass die menschliche Wahrnehmung von Zeit im Internet völlig
|
||||
verzerrt ist?
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Eine Sekunde Wartezeit fühlt sich digital wie eine kleine Ewigkeit an.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn eine Seite nicht sofort reagiert, sendet dies ein Signal von{" "}
|
||||
<Marker>Unzuverlässigkeit und Inkompetenz</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Der Nutzer fragt sich unbewusst: "Wenn sie schon ihre Website nicht im
|
||||
Griff haben, wie gehen sie dann mit meinen Daten um?"
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich sorge für eine "Instant-Feel" Erfahrung durch intelligentes
|
||||
Pre-Loading.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das schafft Vertrauen, noch bevor das erste Wort gelesen wurde.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wo die Schulden in Ihrem System wirklich lauern</H2>
|
||||
<Paragraph>
|
||||
In meiner Analyse begegnen mir immer wieder die gleichen drei Quellen für
|
||||
technische Altlasten.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Diese zu bereinigen ist der Hebel für Ihre digitale Rendite:
|
||||
</Paragraph>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Legacy Script Bloat:</strong> Veraltete Tracker, die den Browser
|
||||
bei jedem Aufruf blockieren. Wir entfernen den Ballast radikal.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Architektonische Trägheit:</strong> Dynamische Abfragen, wo
|
||||
statische Antworten möglich wären. Wir liefern Antworten in
|
||||
Lichtgeschwindigkeit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Die mobile Vernachlässigung:</strong> Eine Seite, die im
|
||||
4G-Funkloch zur Qual wird.{" "}
|
||||
<Marker>Hier findet oft der Erstkontakt statt.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12 space-y-8">
|
||||
<ComparisonRow
|
||||
description="Status Quo (Altlasten) vs. Mintel Standard"
|
||||
negativeLabel="Historisch gewachsene Last"
|
||||
negativeText="8s Ladezeit, instabiles Layout (Lighthouse 30), hohe Wartungs-Sorgen"
|
||||
positiveLabel="Strategisches Refactoring"
|
||||
positiveText="< 1s Ladezeit, felsenfeste Architektur (Lighthouse 100), absolute Ruhe im Betrieb"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Refactoring als Rendite-Beschleuniger</H2>
|
||||
<Paragraph>
|
||||
Ich flicke nicht an Symptomen herum, ich optimiere die DNA Ihres Systems.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein strategisches Refactoring ist der mutige Schritt weg von
|
||||
schwerfälligen Datenbank-Monolithen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dies ist kein technisches Selbstverwirklichungsprojekt, sondern eine
|
||||
Investition mit knallhartem ROI.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer schneller ist als der Wettbewerb, gewinnt die Aufmerksamkeit zuerst.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In einem gesättigten Markt ist{" "}
|
||||
<Marker>Geschwindigkeit ein entscheidender Wettbewerbsvorteil</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann ist es Zeit für eine Schuldenbereinigung?</H2>
|
||||
<Paragraph>
|
||||
Wenn Sie spüren, dass Ihre aktuelle Technik Ihre Ambitionen ausbremst, bin
|
||||
ich Ihr Architekt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich verstehe, dass für junge Projekte ein pragmatischer Ansatz oft
|
||||
sinnvoll ist.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie jedoch an einem Punkt stehen, an dem Professionalität und
|
||||
Skalierung zählen, gießen wir das{" "}
|
||||
<Marker>technologische Fundament</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Befreien Sie Ihr Business</H2>
|
||||
<Paragraph>
|
||||
Technische Schulden sind eine unsichtbare Bremse für Ihr Wachstum.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen ein System, das durch{" "}
|
||||
<Marker>Effizienz und Klarheit</Marker> überzeugt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ihr Business verdient es, unbeschwert und frei von Altlasten zu skalieren.
|
||||
</Paragraph>
|
||||
22
apps/web/content/blog/test-mermaid.mdx
Normal file
22
apps/web/content/blog/test-mermaid.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "Test Mermaid"
|
||||
description: "Testing Mermaid rendering"
|
||||
date: "2026-01-01"
|
||||
tags: ["test"]
|
||||
---
|
||||
|
||||
Test 1: Plain string attribute (NO quotes in graph content):
|
||||
|
||||
<Mermaid id="test-1" title="Test Plain Multiline" showShare={false}>
|
||||
graph TD
|
||||
A-->B
|
||||
B-->C
|
||||
</Mermaid>
|
||||
|
||||
Test 2: Children as raw text (no template literal):
|
||||
|
||||
<Mermaid id="test-2" title="Test Raw Children" showShare={false}>
|
||||
graph TD
|
||||
D-->E
|
||||
E-->F
|
||||
</Mermaid>
|
||||
171
apps/web/content/blog/website-without-cookie-banners.mdx
Normal file
171
apps/web/content/blog/website-without-cookie-banners.mdx
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: "Websites ohne Cookie-Banner: Ein Haltungs-Statement"
|
||||
description: "Warum Cookie-Banner ein Zeichen für schlechtes Engineering sind und wie Sie UX und Datenschutz brillant vereinen."
|
||||
date: "2026-02-06"
|
||||
tags: ["privacy", "ux"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Ich halte Cookie-Banner für eine der größten Design-Sünden und
|
||||
Vertrauenskiller des modernen Webs.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Sie stören den Lesefluss und suggerieren eine Pseudo-Sicherheit.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Vor allem signalisieren sie eines: Ein mangelhaftes technisches Konzept.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir{" "}
|
||||
<Marker>vollständig ohne Banner auskommen</Marker> – bei 100 %
|
||||
DSGVO-Konformität.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Das Banner-Paradoxon: Warum wir uns das antun</H2>
|
||||
<Paragraph>
|
||||
Klassische Websites laden oft ungefragt Scripte von Drittanbietern – meist
|
||||
US-Konzerne.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Diese Tracker setzen Cookies, um Nutzer über verschiedene Seiten hinweg zu
|
||||
verfolgen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist das tägliche Banner-Chaos, das den Inhalt überdeckt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Viele Besucher klicken frustriert auf "Alle akzeptieren" oder verlassen
|
||||
die Seite sofort wieder.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich verfolge eine radikal andere Philosophie:{" "}
|
||||
<Marker>
|
||||
Wenn wir keine Daten abfließen lassen, brauchen wir auch keine
|
||||
Erlaubnis.
|
||||
</Marker>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es ist eine Frage der technischen Souveränität und des digitalen Anstands.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="cookie-free-architecture" title="Cookie-freie Architektur" showShare={true}>
|
||||
graph TD
|
||||
User["Nutzer besucht Website"] --> Logic["Mintel Privacy Engine"]
|
||||
Logic --> Assets["Lokale Assets (Fonts/Scripts)"]
|
||||
Logic --> Analytics["Aggregierte, anonyme Metriken"]
|
||||
Assets --> NoBanner["Kein Cookie-Banner nötig"]
|
||||
Analytics --> NoBanner
|
||||
NoBanner --> Experience["Sofortige Experience & Vertrauen"]
|
||||
style NoBanner fill:#4ade80,stroke:#333
|
||||
style Experience fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Privacy by Design: Wenn die Architektur den Schutz bereits garantiert,
|
||||
entfallen die rechtlichen Krücken.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die unsichtbare Transaktion des Vertrauens</H3>
|
||||
<Paragraph>
|
||||
Jedes Mal, wenn ein Nutzer Ihre Seite ohne Banner betreten kann, findet
|
||||
eine unsichtbare Transaktion statt: Vertrauensaufbau.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie signalisieren Ihrem Besucher: "Ich brauche deine persönlichen Daten
|
||||
nicht, um dich zu überzeugen."
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Privatsphäre ist heute ein <Marker>High-End Feature</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Systeme, die diesen Respekt technisch erzwingen und so die
|
||||
Markenbindung stärken.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir verzichten konsequent auf invasive Tracker und gewinnen dafür loyale
|
||||
Nutzer.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Mein Weg zu 100 % technischer Souveränität</H2>
|
||||
<Paragraph>
|
||||
Privacy-first bedeutet für mich nicht Verzicht, sondern intelligenteres
|
||||
Engineering.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir messen Ihren Erfolg – aber wir brauchen keine personenbezogenen
|
||||
Profile.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Full Local Hosting:</strong> Google Fonts und sämtliche Scripte
|
||||
liegen direkt auf Ihrer Infrastruktur. Kein Datentransfer zu US-Servern.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Ethische Telemetrie:</strong> Analysetools, die Nutzerwege
|
||||
messen, ohne Cookies zu setzen.{" "}
|
||||
<Marker>Volle Transparenz, ohne Verfolgung.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Zero-Tracking Default:</strong> Mein Framework ist so ausgelegt,
|
||||
dass "Consent-Einforderung" technisch gar nicht notwendig wird.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Nutzererfahrung im harten Fakten-Check"
|
||||
negativeLabel="Tracking-fokussierte Seite"
|
||||
negativeText="Banner-Dschungel, rechtliche Grauzonen, Performance-Verlust durch Tracker"
|
||||
positiveLabel="Cookie-Free Architecture"
|
||||
positiveText="Direkter Zugang zum Content, 100 % DSGVO-Safe, Premium-Look & Feel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Der ökonomische Vorteil von 'Banner-Freiheit'</H2>
|
||||
<Paragraph>
|
||||
Consent-Management-Tools verschlechtern oft selbst die Ladezeit Ihrer
|
||||
Website.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Sie laden schwere JavaScript-Dateien, noch bevor Ihre Botschaft erscheint.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Indem wir diese Tools eliminieren, verbessern wir die UX und Ihre
|
||||
PageSpeed-Werte.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Zudem vermeiden Sie Abo-Kosten und das Risiko von Abmahnungen durch
|
||||
falsche Skripte.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein <Marker>sauberes System ist wartungsarm und rechtssicher</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann macht dieser radikale Ansatz für Sie Sinn?</H2>
|
||||
<Paragraph>
|
||||
Ich verstehe mich als Partner für Marken, die Haltung zeigen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Datenschutz für Sie ein echtes Qualitätsmerkmal ist, ist dies mein
|
||||
absoluter Standard.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich arbeite für Unternehmen, die{" "}
|
||||
<Marker>durch Überzeugung und Relevanz</Marker> verkaufen, nicht durch
|
||||
Verfolgung.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Befreien Sie Ihre Inhalte</H2>
|
||||
<Paragraph>
|
||||
Eine Website ohne Banner wirkt sofort aufgeräumter, ehrlicher und
|
||||
wertiger.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es ist ein klares Statement für digitale Professionalität und Respekt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Ihnen die Brücke in eine{" "}
|
||||
<Marker>bannerfreie, souveräne Zukunft</Marker>.
|
||||
</Paragraph>
|
||||
207
apps/web/content/blog/why-agencies-are-slow.mdx
Normal file
207
apps/web/content/blog/why-agencies-are-slow.mdx
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
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"
|
||||
tags: ["architecture", "engineering"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Haben Sie sich schon einmal gefragt, warum eine einfache Textänderung bei
|
||||
Ihrer Agentur oft zwei Wochen dauert?
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich kenne die Antwort aus meiner jahrelangen Erfahrung: Es liegt an
|
||||
veralteten Hierarchien und einem technologischen Overhead, der Innovation
|
||||
im Keim erstickt.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
<Marker>Mein Modell radikaler Direktheit</Marker> beschleunigt Ihr
|
||||
Business, indem es unnötige Schnittstellen eliminiert. Wir lenken den
|
||||
Fokus zurück auf das fertige Produkt.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die "stille Post" der Agentur-Hierarchien</H2>
|
||||
<Paragraph>
|
||||
In einer klassischen Full-Service-Agentur landet Ihr Wunsch zuerst beim
|
||||
Account Manager.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dieser gibt ihn an den Projektleiter weiter, der ein Ticket erstellt,
|
||||
welches schließlich einem oft überarbeiteten Junior-Entwickler zugewiesen
|
||||
wird.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Jede dieser Stationen ist nicht nur ein potenzieller Flaschenhals, sondern
|
||||
auch eine Fehlerquelle.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Informationen gehen verloren, die Umsetzung dauert ewig und am Ende
|
||||
bezahlen Sie für Meetings und Ticket-Management statt für Output.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das die <Marker>"Agentur-Steuer"</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Während dort noch über Prioritäten diskutiert wird, könnte Ihre neue
|
||||
Lösung schon live sein und ersten Umsatz für Ihr Unternehmen generieren.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="agency-bottleneck" title="Agentur-Hierarchie Flaschenhals" showShare={true}>
|
||||
graph LR
|
||||
Request["Ihr Wunsch"] --> AM["Account Manager"]
|
||||
AM --> PM["Projektleiter (Meetings)"]
|
||||
PM --> Ticket["Veraltetes Ticketsystem"]
|
||||
Ticket --> Dev["Junior Entwickler"]
|
||||
Dev --> Review["Code Review (Wartezeit)"]
|
||||
Review --> QA["Manuelle QA"]
|
||||
QA --> Approval["Kundenseitige Freigabe"]
|
||||
Approval --> Prod["Mühsamer Live-Gang"]
|
||||
style AM fill:#fca5a5,stroke:#333
|
||||
style PM fill:#fca5a5,stroke:#333
|
||||
style Ticket fill:#fca5a5,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Die traditionelle 'Stille Post': Jede Schnittstelle kostet Sie Zeit,
|
||||
Präzision und bares Geld.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Der systemische Interessenkonflikt</H3>
|
||||
<Paragraph>
|
||||
Es gibt einen tieferen Grund für die Trägheit vieler Agenturen: Das
|
||||
Geschäftsmodell.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Agenturen nach abrechenbaren Stunden arbeiten und große Teams
|
||||
finanzieren müssen, fehlt oft der ökonomische Anreiz zur radikalen
|
||||
Vereinfachung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Langsame Prozesse bedeuten oft mehr abrechenbare Projektmanagement-Zeit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich hingegen habe mein gesamtes Business auf{" "}
|
||||
<Marker>Effizienz und technologische Souveränität</Marker> optimiert.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein Ziel ist die Maximierung Ihres Outputs, nicht die der Stunden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich investiere massiv in meine eigene Toolchain, um Aufgaben, die
|
||||
Agenturen Tage kosten, in Minuten zu erledigen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Handwerk statt Fließband: Mein Boutique-Ansatz</H2>
|
||||
<Paragraph>
|
||||
In meiner Welt gibt es keine Junioren, an die Arbeit "durchgereicht" wird.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie mit mir arbeiten, sprechen Sie direkt mit dem Experten, der die
|
||||
Architektur entwirft und den Code schreibt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich betrachte Softwareentwicklung nicht als anonymen Fließband-Job,
|
||||
sondern als <Marker>digitales Kunsthandwerk</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dieser Boutique-Ansatz erlaubt es mir, auf eine Weise flexibel zu sein,
|
||||
die für große, starre Strukturen unmöglich ist.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich kann Ideen sofort auf ihre technische Machbarkeit prüfen und
|
||||
prototypisieren, ohne erst eine interne Freigabewelle abwarten zu müssen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Zwei Welten der digitalen Umsetzung"
|
||||
negativeLabel="Struktur-Agentur"
|
||||
negativeText="Unnötige Meetings, langsame Kommunikationskanäle, personeller 'Verschleiß' Ihres Briefings"
|
||||
positiveLabel="Mintel Boutique Architect"
|
||||
positiveText="Direkter Experten-Draht, automatisierte Workflows, Fokus auf Pure Output"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Mein Modell: Radikale Direktheit</H2>
|
||||
<Paragraph>
|
||||
Ich nutze Technologien, die darauf ausgelegt sind, Reibung zu eliminieren.
|
||||
Statt mühsam "Tickets" zu schreiben, bauen wir sofort an Lösungen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Hier sind die drei Hebel, mit denen ich das Tempo Ihres Projekts
|
||||
verdopple:
|
||||
</Paragraph>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Preview-Deployments in Echtzeit:</strong> Jede Änderung wird
|
||||
sofort auf einer geheimen URL visualisiert. Sie sehen den Fortschritt
|
||||
live.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Komponentengestützte Evolution:</strong> Ich baue ein
|
||||
Design-System für Sie, das mitwächst. Neue Seiten entstehen in Stunden.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Schutzschilde:</strong> Statt fehleranfälliger
|
||||
manueller QA nutze ich automatisierte Tests.{" "}
|
||||
<Marker>Technik schützt Technik.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<DiagramGantt
|
||||
tasks={[
|
||||
{
|
||||
id: "agency-req",
|
||||
name: "Agentur: Anforderung bis Live",
|
||||
start: "2024-01-01",
|
||||
duration: "4w",
|
||||
},
|
||||
{
|
||||
id: "mintel-req",
|
||||
name: "Mintel: Anforderung bis Live",
|
||||
start: "2024-01-01",
|
||||
duration: "1w",
|
||||
},
|
||||
]}
|
||||
title="Zeitvergleich: Agentur vs. Boutique"
|
||||
caption="Direkte Kommunikation beschleunigt Ihr Business um den Faktor 4."
|
||||
id="agency-comparison-gantt"
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<H2>Für wen ich die Bremse löse</H2>
|
||||
<Paragraph>
|
||||
Mein Angebot richtet sich an Gründer und Entscheider, die{" "}
|
||||
<Marker>Ergebnisse über Hochglanz-Reports</Marker> stellen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Suchen Sie einen Partner, der so schnell denkt wie Ihr Business es
|
||||
erfordert? Dann passen wir zusammen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Unternehmen, die primär nach maximaler personeller Skalierung suchen, sind
|
||||
bei klassischen Großagenturen besser aufgehoben.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich arbeite für die Macher, die{" "}
|
||||
<Marker>Qualität und Geschwindigkeit durch Intelligenz</Marker> erreichen
|
||||
wollen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Ihre Zeit ist ein knappes Gut</H2>
|
||||
<Paragraph>
|
||||
Technologische Exzellenz bedeutet für mich auch, Ihnen keine Zeit zu
|
||||
stehlen. Ich baue Systeme, die fließen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie heute eine Vision haben, möchte ich, dass wir sie übermorgen
|
||||
bereits am Markt testen können.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns die Agentur-Steuer streichen und direkt in Ihren Erfolg
|
||||
investieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Echte Geschwindigkeit beginnt dort, wo Hierarchie endet.</Marker>
|
||||
</Paragraph>
|
||||
142
apps/web/content/blog/why-no-templates-matter.mdx
Normal file
142
apps/web/content/blog/why-no-templates-matter.mdx
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "Warum Templates Ihre Marken-Identität verwässern"
|
||||
description: "Fast-Food vs. Sterneküche: Warum wir auf Standard-Vorlagen verzichten und jedes Projekt als digitales Unikat für maximale Unterscheidbarkeit bauen."
|
||||
date: "2026-01-28"
|
||||
tags: ["design", "strategy"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Vorlagen sind die Fast-Food-Lösung des Web-Designs: Schnell verfügbar,
|
||||
aber auf Dauer ungesund für Ihre Marke.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect begegne ich ständig Unternehmen,
|
||||
die in der Beliebigkeit von Standard-Templates versinken.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Zero-Template-Architektur</Marker> der
|
||||
einzige Weg zu echter digitaler Distinktion ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die Falle der visuellen Gleichschaltung</H2>
|
||||
<Paragraph>
|
||||
Wenn Sie ein Template nutzen, nutzen Sie die gleiche Basis wie tausende
|
||||
andere Unternehmen weltweit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist eine "digitale Uniform", die Ihre Einzigartigkeit im Keim
|
||||
erstickt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Kunden spüren unbewusst, wenn eine Seite "von der Stange" kommt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Es signalisiert fehlende Investitionsbereitschaft und mangelnde Vision.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>ästhetische Kapitulation</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wahre Markenbildung braucht Raum zum Atmen und ein Fundament, das nur für
|
||||
Sie gegossen wurde.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="bespoke-vs-template" title="Bespoke vs. Template Vergleich" showShare={true}>
|
||||
graph TD
|
||||
Need["Individuelle Marken-Botschaft"] --> Path["Design-Entscheidung"]
|
||||
Path --> Temp["Fertig-Template (Masse)"]
|
||||
Path --> Custom["Bespoke Component Design (Mintel)"]
|
||||
Temp --> Bland["Visuelle Beliebigkeit & Hoher Ballast"]
|
||||
Custom --> Distinct["Maximale Unterscheidbarkeit & Pure Performance"]
|
||||
Bland --> NoTrust["Vertrauensverlust"]
|
||||
Distinct --> Authority["Marken-Autorität"]
|
||||
style Custom fill:#4ade80,stroke:#333
|
||||
style Authority fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Bespoke vs. Template: Investieren Sie in ein digitales Unikat, das Ihre
|
||||
Marktposition untermauert statt sie zu verwässern.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Boutique-Design: Jedes Pixel hat einen Zweck</H3>
|
||||
<Paragraph>
|
||||
Templates enthalten Code für hunderte Optionen, die Sie nie nutzen werden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dieser Ballast verlangsamt Ihre Seite und verwässert Ihre Botschaft.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In meinem Boutique-Ansatz entwickeln wir jede Komponente von Grund auf.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist eine <Marker>hochpräzise Maschine</Marker>, die exakt auf
|
||||
Ihre Ziele ausgerichtet ist.
|
||||
</Paragraph>
|
||||
<Paragraph>Schlank. Schnell. Unverwechselbar.</Paragraph>
|
||||
|
||||
<H2>Warum technisches Maßwerk rentabler ist</H2>
|
||||
<Paragraph>
|
||||
Templates are starr. Wenn Ihr Business wächst, wird das Template schnell
|
||||
zur Wachstumsbremse.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Anpassungen am Standard-Code sind oft teurer als ein kompletter Neubau.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Systeme, die <Marker>evolutionär</Marker> sind.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Unbegrenzte Design-Freiheit:</strong> Wir biegen den Code nach
|
||||
Ihrer Vision, nicht umgekehrt. Jede Interaktion folgt Ihrer Marken-DNA.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Zukunftssicheres Asset:</strong> Da wir keine
|
||||
Drittanbieter-Themen nutzen, gibt es keine Abhängigkeiten von deren
|
||||
Update-Zyklen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Maximale Konversions-Rate:</strong> Wir optimieren Layouts exakt
|
||||
nach dem Nutzerverhalten Ihres Zielmarktes.{" "}
|
||||
<Marker>Keine Kompromisse.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact auf Ihre digitale Reputation"
|
||||
negativeLabel="Premium Template-Kauf"
|
||||
negativeText="Gefahr der Verwechslung, technischer Overhead, eingeschränkte Flexibilität"
|
||||
positiveLabel="Bespoke Digital Object"
|
||||
positiveText="Absolut einzigartig, 100 % performant, unendlich skalierbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Wann ist 'Bespoke' der einzige Weg?</H2>
|
||||
<Paragraph>
|
||||
Ist Ihr Unternehmen Marktführer oder auf dem Weg dorthin? Dann darf Ihre
|
||||
Website nicht nach "Durchschnitt" aussehen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich arbeite für Entscheider, die{" "}
|
||||
<Marker>Qualität als Wettbewerbsvorteil</Marker> begreifen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Differenzierung über Ihren Erfolg entscheidet, bin ich Ihr Architekt.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Ein Unikat für Ihren Erfolg</H2>
|
||||
<Paragraph>
|
||||
Hören Sie auf, sich in fremde Formen pressen zu lassen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein digitales Denkmal setzen, das so unverwechselbar
|
||||
ist wie Ihr unternehmerischer Fingerabdruck.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Pures Handwerk. Maximale Distinktion.</Marker> Ihr Erfolg verdient
|
||||
dieses Niveau.
|
||||
</Paragraph>
|
||||
191
apps/web/content/blog/why-pagespeed-fails.mdx
Normal file
191
apps/web/content/blog/why-pagespeed-fails.mdx
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: "Warum Ihre Website bei Google PageSpeed scheitert"
|
||||
description: "Millisekunden entscheiden über Ihren Umsatz: So optimieren Sie Ihre Web-Performance für maximale Conversion."
|
||||
date: "2026-02-15"
|
||||
tags: ["performance", "seo"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das
|
||||
technische Fundament einer digitalen Ruine gleicht.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Wenn Ihre Website bei Google PageSpeed scheitert, verlieren Sie Kunden –{" "}
|
||||
<Marker>bevor diese Ihre Botschaft überhaupt wahrnehmen können</Marker>.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect ist die Geschwindigkeit der
|
||||
architektonische Gradmesser für Professionalität.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Der unsichtbare Umsatz-Verschleiß</H2>
|
||||
<Paragraph>
|
||||
Stellen Sie sich vor, Sie eröffnen ein Luxus-Geschäft in der besten Lage,
|
||||
aber die Eingangstür klemmt massiv.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Kunden müssen 10 Sekunden lang drücken, um einzutreten. Genau das passiert
|
||||
täglich auf tausenden Websites.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Millisekunden sind im digitalen Zeitalter die härteste Währung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Eine Verzögerung von nur einer Sekunde kann die{" "}
|
||||
<Marker>Conversion-Rate um bis zu 20 % senken</Marker>. Das ist kein
|
||||
technisches Detail, sondern ein unternehmerisches Risiko.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich betrachte Performance nicht als IT-Kennzahl, sondern als ökonomischen
|
||||
Hebel.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Google bewertet Websites heute primär nach den "Core Web Vitals". Das sind
|
||||
präzise Messgrößen für die Frustrationstoleranz Ihrer Nutzer.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wer hier rote Zahlen schreibt, wird vom Algorithmus unsichtbar gemacht –
|
||||
eine digitale Strafe für technische Nachlässigkeit.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact von Geschwindigkeit auf Ihre Bilanz"
|
||||
negativeLabel="Langsames Legacy-System"
|
||||
negativeText="Hohe Absprungraten, sinkendes Markenvertrauen, teure Akquise ohne Ertrag"
|
||||
positiveLabel="Mintel High-Performance"
|
||||
positiveText="Maximale Conversion, SEO-Vorsprung ab Tag 1, begeisterte Nutzer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Warum klassische Lösungen scheitern</H2>
|
||||
<Paragraph>
|
||||
Die Ursache liegt oft in der Verwendung von "All-in-One"-Lösungen wie
|
||||
WordPress oder überladenen Baukästen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Diese Systeme versuchen, alles für jeden zu sein. Das Ergebnis ist ein
|
||||
gigantischer "Ballast an Code".
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Jedes Byte muss durch das Nadelöhr der Internetverbindung gepresst werden,
|
||||
bevor das erste Bild erscheint.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In einer mobilen Welt mit oft instabilen Verbindungen ist das ein{" "}
|
||||
<Marker>architektonisches Todesurteil</Marker>. Wer hier spart, zahlt
|
||||
später doppelt durch verlorene Kunden.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="legacy-loading-bottleneck" title="Legacy System Ladezeit-Flaschenhals" showShare={true}>
|
||||
graph TD
|
||||
A["Anfrage des Browsers"] --> B["Server muss nachdenken (PHP/DB)"]
|
||||
B --> C["Hunderte Datenbank-Abfragen"]
|
||||
C --> D["HTML wird mühsam live konstruiert"]
|
||||
D --> E["Veraltetes Asset-Management lädt alles"]
|
||||
E --> F["Render-Blocking Code (Browser stoppt)"]
|
||||
F --> G["Seite endlich sichtbar (nach 3-5 Sek)"]
|
||||
style B fill:#fca5a5,stroke:#333
|
||||
style F fill:#fca5a5,stroke:#333
|
||||
style G fill:#fca5a5,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Der Flaschenhals der Standard-Systeme: Rechenzeit am Server raubt Ihnen
|
||||
wertvolle Kundenzeit.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H2>Meine Architektur der Geschwindigkeit</H2>
|
||||
<Paragraph>
|
||||
Ich verfolge einen radikal anderen Ansatz. Statt die Seite erst mühsam
|
||||
zusammenzubauen, wenn der Kunde sie anfragt, liefere ich fertig optimierte
|
||||
"digitale Objekte" aus.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein "Static-First" Framework sorgt dafür, dass die Antwortzeit Ihres
|
||||
Servers nahezu bei Null liegt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Völlig egal, ob gerade 10 oder 10.000 Menschen gleichzeitig auf Ihre Seite
|
||||
zugreifen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das ist <Marker>Skalierbarkeit durch Design</Marker>, nicht durch bloße
|
||||
Server-Power.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Die drei Säulen meiner Umsetzung</H3>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Zero-Computation am Edge:</strong> Durch Static Site Generation
|
||||
(SSG) liegen alle Inhalte fertig auf globalen CDNs. Keine Wartezeit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Präzises Asset-Engineering:</strong> Ich nutze Tree-Shaking. Ihr
|
||||
Kunde lädt exakt nur den Code, den er wirklich benötigt.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Next-Gen Media-Handling:</strong> Bilder werden automatisch in
|
||||
Formaten wie AVIF ausgeliefert. Qualität bleibt, Dateigröße schmilzt.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="performance-bottlenecks-pie" title="Typische Performance-Bottlenecks Verteilung" showShare={true}>
|
||||
pie
|
||||
"JavaScript Execution" : 35
|
||||
"Render Blocking CSS" : 25
|
||||
"Server Response Time" : 20
|
||||
"Image Loading" : 15
|
||||
"Third-Party Scripts" : 5
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Wo die Zeit wirklich verloren geht: Eine Analyse der häufigsten Ladezeit-Killer.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H2>Der wirtschaftliche Case</H2>
|
||||
<Paragraph>
|
||||
Baukästen wirken "auf den ersten Blick" günstiger. Doch das ist eine
|
||||
riskante Milchmädchenrechnung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wenn Sie monatlich 5.000 € in Marketing investieren, aber 30 % Ihrer Leads
|
||||
durch Ladezeiten verlieren, verbrennen Sie jedes Jahr 18.000 €.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein System ist kein Kostenfaktor, sondern ein{" "}
|
||||
<Marker>ROI-Beschleuniger</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Wir senken die Kosten pro Lead, indem wir die Reibungsverluste minimieren.
|
||||
Ein technisch überlegenes System ist immer die rentablere Wahl.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann meine Architektur für Sie Sinn macht</H2>
|
||||
<Paragraph>
|
||||
Ich bin Partner für Unternehmen, die über die "digitale Visitenkarte"
|
||||
hinausgewachsen sind.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ist Ihre Website ein geschäftskritisches Werkzeug für die Lead-Gen? Dann
|
||||
ist mein Ansatz alternativlos.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich steige dort ein, wo technologische{" "}
|
||||
<Marker>Exzellenz zum entscheidenden Wettbewerbsvorteil</Marker> wird.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Respekt vor der Zeit Ihrer Nutzer</H2>
|
||||
<Paragraph>
|
||||
Geschwindigkeit ist letztlich Ausdruck von Wertschätzung. Sie
|
||||
signalisieren Ihrem Kunden: "Ich respektiere deine Zeit."
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns Ihre Website in eine hochpräzise Wachstums-Maschine
|
||||
verwandeln.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Qualität zahlt sich aus</Marker> – in Millisekunden und in Euro.
|
||||
</Paragraph>
|
||||
171
apps/web/content/blog/why-websites-break-after-updates.mdx
Normal file
171
apps/web/content/blog/why-websites-break-after-updates.mdx
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: "Warum Ihre Website nach Updates nicht mehr funktioniert"
|
||||
description: "Stabilität durch Engineering: So beenden Sie den Teufelskreis aus Updates und Layout-Fehlern."
|
||||
date: "2026-02-11"
|
||||
tags: ["maintenance", "reliability"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Nach dem letzten Update war plötzlich das halbe Layout kaputt."
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Das ist der Satz, den ich am häufigsten von Neukunden höre.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Für mich ist eine Website ein technisches Präzisionswerkzeug. Es darf
|
||||
niemals einfach "auseinanderfallen".
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
<Marker>Stabilität ist kein glücklicher Zufall</Marker>, sondern das
|
||||
Ergebnis eines kompromisslosen Engineering-Systems.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die Entropie des Webs</H2>
|
||||
<Paragraph>Das Internet ist eine extrem dynamische Umgebung.</Paragraph>
|
||||
<Paragraph>
|
||||
Browser-Updates und neue Sicherheitsstandards nagen permanent an der
|
||||
Integrität Ihrer Website.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In herkömmlichen Systemen sind die Komponenten oft wie ein wackeliger
|
||||
Stapel Lego-Steine angeordnet.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Zieht man einen Stein heraus – etwa durch ein Plugin-Update –, gerät das
|
||||
gesamte Konstrukt ins Wanken.
|
||||
</Paragraph>
|
||||
<Paragraph>Ich nenne das den schleichenden "Software-Zerfall".</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue Architektur, die diesem Zerfall <Marker>aktiv widersteht</Marker>
|
||||
.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="deployment-safety-net" title="Deployment Sicherheitsnetz" showShare={true}>
|
||||
graph TD
|
||||
Update["Technische Änderung / Update"] --> Test["Automatisierte Qualitäts-Tests"]
|
||||
Test -->|OK| Deploy["Automatischer, sicherer Live-Gang"]
|
||||
Test -->|Fehler| Alert["Sofortiger Stopp & Fehler-Isolation"]
|
||||
Alert --> Fix["Manuelle Korrektur durch mich (Dev)"]
|
||||
Deploy --> Stable["Website bleibt 100% konsistent"]
|
||||
style Stable fill:#4ade80,stroke:#333
|
||||
style Alert fill:#ef4444,color:#fff
|
||||
style Deploy fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Mein defensives Sicherheitsnetz: Keine Änderung erreicht den Nutzer,
|
||||
ohne maschinell zertifiziert zu sein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H3>Die Kosten des menschlichen Versagens eliminieren</H3>
|
||||
<Paragraph>
|
||||
Die meisten Fehler entstehen durch manuelle Eingriffe oder das Übersehen
|
||||
von Seiteneffekten.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ein Entwickler ändert das Design auf einer Unterseite und merkt nicht,
|
||||
dass dadurch das Kontaktformular bricht.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
In meiner Welt gibt es solche Fehler nicht. Ich investiere in{" "}
|
||||
<Marker>automatisierte Wächtern</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Bevor eine Änderung live geht, prüft eine künstliche Instanz jedes Detail
|
||||
Ihrer gesamten Website.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Technik schützt hier den Menschen vor Flüchtigkeitsfehlern.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Die "Fortress-Mentalität": Drei Schichten der Sicherheit</H2>
|
||||
<Paragraph>
|
||||
Sorgen Sie sich nie wieder darum, ob Ihre Seite "das Wochenende überlebt
|
||||
hat".
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Mein Stabilitäts-System umfasst drei entscheidende Schutzschichten:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Visual Regression Testing:</strong> Mein System vergleicht nach
|
||||
jeder Änderung tausende Bildpunkte. Die Maschine sieht Fehler sofort.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Immutable Deployments:</strong> Ich überschreibe niemals
|
||||
Live-Dateien. Wir können in Millisekunden auf eine saubere Kopie
|
||||
zurückrollen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Entkoppelte Modul-Logik:</strong> Ich baue in isolierten
|
||||
Komponenten. Änderung an Punkt A gefährden niemals Punkt B.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="deployment-lifecycle-state" title="Website Deployment Lifecycle" showShare={true}>
|
||||
stateDiagram-v2
|
||||
["*"] --> Development
|
||||
Development --> Testing : Code Complete
|
||||
Testing --> Staging : Tests Pass
|
||||
Staging --> Production : Final Approval
|
||||
Production --> Rollback : Issue Detected
|
||||
Rollback --> Development : Fix Required
|
||||
Testing --> Development : Tests Fail
|
||||
Production --> ["*"]
|
||||
</Mermaid>
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
Jeder Zustand ist abgesichert: Keine Änderung erreicht Production ohne vollständige Validierung.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Hobby-Ansatz vs. Industrial-Grade Reliability"
|
||||
negativeLabel="Standard Agentur-Service"
|
||||
negativeText="Updates auf 'Gut Glück', Hotfixes am Live-System, schleichender Qualitätsverlust"
|
||||
positiveLabel="Mintel Engineering"
|
||||
positiveText="0 % Risiko-Deployments, proaktive Überwachung, technologische Sorgenfreiheit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Warum Stabilität die beste Wachstums-Strategie ist</H2>
|
||||
<Paragraph>
|
||||
Ein stabiles System ist kein technischer Selbstzweck. Es ist die
|
||||
Grundvoraussetzung für Skalierung.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Nur wenn Sie blind auf Ihr technologisches Rückgrat vertrauen können,
|
||||
können Sie mit voller Kraft investieren.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich baue keine "Schönwetter-Websites", sondern{" "}
|
||||
<Marker>industrielle Software-Systeme</Marker>.
|
||||
</Paragraph>
|
||||
<Paragraph>Stabilität bedeutet für Sie Fokus auf Ihr Business.</Paragraph>
|
||||
|
||||
<H2>Für wen ist meine 'Fortress-Architektur' richtig?</H2>
|
||||
<Paragraph>
|
||||
Gefährden optische Defekte oder Ausfälle direkt Ihren Ruf? Dann brauchen
|
||||
Sie eine ernsthafte Architektur.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich werde aktiv, wenn{" "}
|
||||
<Marker>Professionalität und Zuverlässigkeit</Marker> für Sie nicht
|
||||
verhandelbar sind.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Ihre digitale Ruhe ist mein Auftrag</H2>
|
||||
<Paragraph>
|
||||
Souveränität im Netz beginnt bei der Verlässlichkeit der eigenen
|
||||
Werkzeuge.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen wir die Zeit der "kaputten Layouts" ein für alle Mal beenden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Stabilität ist die Basis für Vertrauen.</Marker> Ihr Erfolg
|
||||
verdient dieses solide Fundament.
|
||||
</Paragraph>
|
||||
28
apps/web/contentlayer.config.ts
Normal file
28
apps/web/contentlayer.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineDocumentType, makeSource } from 'contentlayer2/source-files'
|
||||
|
||||
export const Post = defineDocumentType(() => ({
|
||||
name: 'Post',
|
||||
filePathPattern: `blog/**/*.mdx`,
|
||||
contentType: 'mdx',
|
||||
fields: {
|
||||
title: { type: 'string', required: true },
|
||||
date: { type: 'string', required: true },
|
||||
description: { type: 'string', required: true },
|
||||
tags: { type: 'list', of: { type: 'string' }, required: true },
|
||||
},
|
||||
computedFields: {
|
||||
slug: {
|
||||
type: 'string',
|
||||
resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx$/, ''),
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
resolve: (post) => `/blog/${post._raw.sourceFileName.replace(/\.mdx$/, '')}`,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
export default makeSource({
|
||||
contentDirPath: 'content',
|
||||
documentTypes: [Post],
|
||||
})
|
||||
11
apps/web/mdx-components.tsx
Normal file
11
apps/web/mdx-components.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// import type { MDXComponents } from 'mdx/types'; // Commented out due to resolution issue
|
||||
import { mdxComponents as registryComponents } from './src/content-engine/registry';
|
||||
|
||||
export const MDXComponents = registryComponents;
|
||||
|
||||
export function useMDXComponents(components: any): any {
|
||||
return {
|
||||
...components,
|
||||
...registryComponents,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { withContentlayer } from 'next-contentlayer2';
|
||||
import withMintelConfig from "@mintel/next-config";
|
||||
|
||||
import createMDX from '@next/mdx';
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
images: {
|
||||
@@ -25,4 +29,8 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withMintelConfig(nextConfig);
|
||||
const withMDX = createMDX({
|
||||
// Add markdown plugins here, as desired
|
||||
});
|
||||
|
||||
export default withContentlayer(withMintelConfig(withMDX(nextConfig)));
|
||||
|
||||
@@ -30,8 +30,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@directus/sdk": "21.0.0",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@mintel/cloner": "^1.8.0",
|
||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||
"@mintel/pdf": "^1.8.0",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.1.0",
|
||||
"@opentelemetry/core": "^2.1.0",
|
||||
@@ -52,12 +57,16 @@
|
||||
"axios": "^1.13.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"clsx": "^2.1.1",
|
||||
"contentlayer2": "^0.5.8",
|
||||
"crawlee": "^3.15.3",
|
||||
"esbuild": "^0.27.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"ioredis": "^5.9.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "^16.1.6",
|
||||
"next-contentlayer2": "^0.5.8",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"nodemailer": "^8.0.1",
|
||||
"playwright": "^1.58.1",
|
||||
"prismjs": "^1.30.0",
|
||||
|
||||
46
apps/web/scripts/clean-mermaid-format.ts
Normal file
46
apps/web/scripts/clean-mermaid-format.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
function cleanMermaidFormatting(content: string): string {
|
||||
// Fix: The repair script reformatted <Mermaid> tags badly with extra blank lines
|
||||
// Pattern: <Mermaid\n \n graph={`...`}\n \n />
|
||||
// Should be: <Mermaid\n graph={`...`}\n id="..."\n .../>
|
||||
|
||||
// Remove empty lines between <Mermaid and graph=
|
||||
let result = content.replace(/<Mermaid\s*\n\s*\n\s*graph=/g, '<Mermaid\n graph=');
|
||||
|
||||
// Remove trailing empty space before />
|
||||
result = result.replace(/`\}\s*\n\s*\n\s*\/>/g, '`}\n />');
|
||||
|
||||
// Fix: Remove trailing whitespace-only lines before id= or title= or showShare=
|
||||
result = result.replace(/`\}\s*\n\s*\n\s*(id=)/g, '`}\n $1');
|
||||
result = result.replace(/`\}\s*\n\s*\n\s*(title=)/g, '`}\n $1');
|
||||
result = result.replace(/`\}\s*\n\s*\n\s*(showShare=)/g, '`}\n $1');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const cleaned = cleanMermaidFormatting(content);
|
||||
|
||||
if (content !== cleaned) {
|
||||
fs.writeFileSync(filePath, cleaned);
|
||||
fixCount++;
|
||||
console.log(`✅ Cleaned formatting in ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} OK`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal cleaned: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
365
apps/web/scripts/convert-diagrams-to-children.ts
Normal file
365
apps/web/scripts/convert-diagrams-to-children.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Convert <Mermaid graph={`...`} id="..." ... /> to <Mermaid id="..." ...>{`...`}</Mermaid>
|
||||
* This fixes the RSC serialization issue where template literal props are stripped.
|
||||
*/
|
||||
function convertMermaidToChildren(content: string): string {
|
||||
// Match <Mermaid ... graph={`...`} ... /> (self-closing)
|
||||
// We need a multi-pass approach since the graph prop can appear anywhere in the tag
|
||||
|
||||
const mermaidBlockRegex = /<Mermaid\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(mermaidBlockRegex, (match) => {
|
||||
// Extract the graph prop value (template literal)
|
||||
const graphMatch = match.match(/graph=\{`([\s\S]*?)`\}/);
|
||||
if (!graphMatch) return match; // No graph prop, skip
|
||||
|
||||
const graphContent = graphMatch[1];
|
||||
|
||||
// Remove the graph prop from the tag
|
||||
let cleanedTag = match.replace(/\s*graph=\{`[\s\S]*?`\}\s*/g, ' ');
|
||||
|
||||
// Remove the self-closing /> and add children
|
||||
cleanedTag = cleanedTag.replace(/\s*\/>$/, '');
|
||||
|
||||
// Clean up extra whitespace
|
||||
cleanedTag = cleanedTag.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return `${cleanedTag}>\n{\`${graphContent}\`}\n</Mermaid>`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert <DiagramPie data={[...]} ... /> to <Mermaid ...>{`pie\n "label": value\n...`}</Mermaid>
|
||||
*/
|
||||
function convertDiagramPie(content: string): string {
|
||||
const pieRegex = /<DiagramPie\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(pieRegex, (match) => {
|
||||
// Extract data array
|
||||
const dataMatch = match.match(/data=\{\[([\s\S]*?)\]\}/);
|
||||
if (!dataMatch) return match;
|
||||
|
||||
const dataStr = dataMatch[1];
|
||||
|
||||
// Parse the array items: { label: "...", value: ... }
|
||||
const items: { label: string; value: number }[] = [];
|
||||
const itemRegex = /\{\s*label:\s*"([^"]+)"\s*,\s*value:\s*(\d+)\s*\}/g;
|
||||
let itemMatch;
|
||||
while ((itemMatch = itemRegex.exec(dataStr)) !== null) {
|
||||
items.push({ label: itemMatch[1], value: parseInt(itemMatch[2]) });
|
||||
}
|
||||
|
||||
if (items.length === 0) return match;
|
||||
|
||||
// Build pie chart Mermaid syntax
|
||||
const pieLines = items.map(item => ` "${item.label}" : ${item.value}`).join('\n');
|
||||
const pieGraph = `pie\n${pieLines}`;
|
||||
|
||||
// Extract other props
|
||||
const titleMatch = match.match(/title="([^"]+)"/);
|
||||
const captionMatch = match.match(/caption="([^"]+)"/);
|
||||
const idMatch = match.match(/id="([^"]+)"/);
|
||||
const showShareMatch = match.match(/showShare=\{(true|false)\}/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const caption = captionMatch ? captionMatch[1] : '';
|
||||
const id = idMatch ? idMatch[1] : '';
|
||||
const showShare = showShareMatch ? showShareMatch[1] : 'true';
|
||||
|
||||
// Build replacement with Mermaid component wrapped in div
|
||||
let result = `<div className="my-12">\n <Mermaid`;
|
||||
if (id) result += ` id="${id}"`;
|
||||
if (title) result += ` title="${title}"`;
|
||||
result += ` showShare={${showShare}}>`;
|
||||
result += `\n{\`${pieGraph}\`}\n</Mermaid>`;
|
||||
if (caption) {
|
||||
result += `\n <div className="text-center text-xs text-slate-400 mt-4 italic">\n ${caption}\n </div>`;
|
||||
}
|
||||
result += `\n</div>`;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert <DiagramGantt tasks={[...]} ... /> to <Mermaid ...>{`gantt\n...`}</Mermaid>
|
||||
*/
|
||||
function convertDiagramGantt(content: string): string {
|
||||
const ganttRegex = /<DiagramGantt\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(ganttRegex, (match) => {
|
||||
// Extract tasks array
|
||||
const tasksMatch = match.match(/tasks=\{\[([\s\S]*?)\]\}/);
|
||||
if (!tasksMatch) return match;
|
||||
|
||||
const tasksStr = tasksMatch[1];
|
||||
|
||||
// Parse the task items
|
||||
const taskRegex = /\{\s*id:\s*"([^"]+)"\s*,\s*name:\s*"([^"]+)"\s*,\s*start:\s*"([^"]+)"\s*,\s*duration:\s*"([^"]+)"\s*(?:,\s*dependencies:\s*\[([^\]]*)\])?\s*\}/g;
|
||||
const tasks: { id: string; name: string; start: string; duration: string; deps?: string }[] = [];
|
||||
let taskMatch;
|
||||
while ((taskMatch = taskRegex.exec(tasksStr)) !== null) {
|
||||
tasks.push({
|
||||
id: taskMatch[1],
|
||||
name: taskMatch[2],
|
||||
start: taskMatch[3],
|
||||
duration: taskMatch[4],
|
||||
deps: taskMatch[5] || '',
|
||||
});
|
||||
}
|
||||
|
||||
if (tasks.length === 0) return match;
|
||||
|
||||
// Build gantt chart Mermaid syntax
|
||||
const ganttLines = tasks.map(task => {
|
||||
const deps = task.deps ? `, after ${task.deps.replace(/"/g, '').trim()}` : '';
|
||||
return ` ${task.name} :${task.id}, ${task.start}, ${task.duration}${deps}`;
|
||||
}).join('\n');
|
||||
const ganttGraph = `gantt\n dateFormat YYYY-MM-DD\n${ganttLines}`;
|
||||
|
||||
// Extract other props
|
||||
const titleMatch = match.match(/title="([^"]+)"/);
|
||||
const captionMatch = match.match(/caption="([^"]+)"/);
|
||||
const idMatch = match.match(/id="([^"]+)"/);
|
||||
const showShareMatch = match.match(/showShare=\{(true|false)\}/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const caption = captionMatch ? captionMatch[1] : '';
|
||||
const id = idMatch ? idMatch[1] : '';
|
||||
const showShare = showShareMatch ? showShareMatch[1] : 'true';
|
||||
|
||||
let result = `<div className="my-12">\n <Mermaid`;
|
||||
if (id) result += ` id="${id}"`;
|
||||
if (title) result += ` title="${title}"`;
|
||||
result += ` showShare={${showShare}}>`;
|
||||
result += `\n{\`${ganttGraph}\`}\n</Mermaid>`;
|
||||
if (caption) {
|
||||
result += `\n <div className="text-center text-xs text-slate-400 mt-4 italic">\n ${caption}\n </div>`;
|
||||
}
|
||||
result += `\n</div>`;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert <DiagramSequence participants={[...]} messages={[...]} ... /> to <Mermaid ...>{`sequenceDiagram\n...`}</Mermaid>
|
||||
*/
|
||||
function convertDiagramSequence(content: string): string {
|
||||
const seqRegex = /<DiagramSequence\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(seqRegex, (match) => {
|
||||
// Extract participants array
|
||||
const participantsMatch = match.match(/participants=\{\[([\s\S]*?)\]\}/);
|
||||
if (!participantsMatch) return match;
|
||||
|
||||
// Extract messages array
|
||||
const messagesMatch = match.match(/messages=\{\[([\s\S]*?)\]\}/);
|
||||
if (!messagesMatch) return match;
|
||||
|
||||
const participantsStr = participantsMatch[1];
|
||||
const messagesStr = messagesMatch[1];
|
||||
|
||||
// Parse participants
|
||||
const participants = participantsStr.match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
|
||||
|
||||
// Parse messages
|
||||
const msgRegex = /\{\s*from:\s*"([^"]+)"\s*,\s*to:\s*"([^"]+)"\s*,\s*message:\s*"([^"]+)"(?:\s*,\s*type:\s*"([^"]+)")?\s*\}/g;
|
||||
const messages: { from: string; to: string; message: string; type?: string }[] = [];
|
||||
let msgMatch;
|
||||
while ((msgMatch = msgRegex.exec(messagesStr)) !== null) {
|
||||
messages.push({
|
||||
from: msgMatch[1],
|
||||
to: msgMatch[2],
|
||||
message: msgMatch[3],
|
||||
type: msgMatch[4],
|
||||
});
|
||||
}
|
||||
|
||||
if (participants.length === 0 || messages.length === 0) return match;
|
||||
|
||||
// Build sequence diagram Mermaid syntax
|
||||
const getArrow = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'dotted': return '-->>';
|
||||
case 'async': return '->>';
|
||||
default: return '->>';
|
||||
}
|
||||
};
|
||||
|
||||
const participantLines = participants.map(p => ` participant ${p}`).join('\n');
|
||||
const messageLines = messages.map(m => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join('\n');
|
||||
const seqGraph = `sequenceDiagram\n${participantLines}\n${messageLines}`;
|
||||
|
||||
// Extract other props
|
||||
const titleMatch = match.match(/title="([^"]+)"/);
|
||||
const captionMatch = match.match(/caption="([^"]+)"/);
|
||||
const idMatch = match.match(/id="([^"]+)"/);
|
||||
const showShareMatch = match.match(/showShare=\{(true|false)\}/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const caption = captionMatch ? captionMatch[1] : '';
|
||||
const id = idMatch ? idMatch[1] : '';
|
||||
const showShare = showShareMatch ? showShareMatch[1] : 'true';
|
||||
|
||||
let result = `<div className="my-12">\n <Mermaid`;
|
||||
if (id) result += ` id="${id}"`;
|
||||
if (title) result += ` title="${title}"`;
|
||||
result += ` showShare={${showShare}}>`;
|
||||
result += `\n{\`${seqGraph}\`}\n</Mermaid>`;
|
||||
if (caption) {
|
||||
result += `\n <div className="text-center text-xs text-slate-400 mt-4 italic">\n ${caption}\n </div>`;
|
||||
}
|
||||
result += `\n</div>`;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert <DiagramTimeline events={[...]} ... /> to <Mermaid ...>{`timeline\n...`}</Mermaid>
|
||||
*/
|
||||
function convertDiagramTimeline(content: string): string {
|
||||
const timelineRegex = /<DiagramTimeline\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(timelineRegex, (match) => {
|
||||
const eventsMatch = match.match(/events=\{\[([\s\S]*?)\]\}/);
|
||||
if (!eventsMatch) return match;
|
||||
|
||||
const eventsStr = eventsMatch[1];
|
||||
|
||||
const eventRegex = /\{\s*year:\s*"([^"]+)"\s*,\s*title:\s*"([^"]+)"\s*\}/g;
|
||||
const events: { year: string; title: string }[] = [];
|
||||
let eventMatch;
|
||||
while ((eventMatch = eventRegex.exec(eventsStr)) !== null) {
|
||||
events.push({ year: eventMatch[1], title: eventMatch[2] });
|
||||
}
|
||||
|
||||
if (events.length === 0) return match;
|
||||
|
||||
const titleMatch = match.match(/title="([^"]+)"/);
|
||||
const captionMatch = match.match(/caption="([^"]+)"/);
|
||||
const idMatch = match.match(/id="([^"]+)"/);
|
||||
const showShareMatch = match.match(/showShare=\{(true|false)\}/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const caption = captionMatch ? captionMatch[1] : '';
|
||||
const id = idMatch ? idMatch[1] : '';
|
||||
const showShare = showShareMatch ? showShareMatch[1] : 'true';
|
||||
|
||||
const eventLines = events.map(e => ` ${e.year} : ${e.title}`).join('\n');
|
||||
const timelineGraph = `timeline\n title ${title || 'Timeline'}\n${eventLines}`;
|
||||
|
||||
let result = `<div className="my-12">\n <Mermaid`;
|
||||
if (id) result += ` id="${id}"`;
|
||||
if (title) result += ` title="${title}"`;
|
||||
result += ` showShare={${showShare}}>`;
|
||||
result += `\n{\`${timelineGraph}\`}\n</Mermaid>`;
|
||||
if (caption) {
|
||||
result += `\n <div className="text-center text-xs text-slate-400 mt-4 italic">\n ${caption}\n </div>`;
|
||||
}
|
||||
result += `\n</div>`;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert <DiagramState states={[...]} transitions={[...]} ... /> to <Mermaid ...>{`stateDiagram-v2\n...`}</Mermaid>
|
||||
*/
|
||||
function convertDiagramState(content: string): string {
|
||||
const stateRegex = /<DiagramState\s+([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(stateRegex, (match) => {
|
||||
// Extract transitions
|
||||
const transitionsMatch = match.match(/transitions=\{\[([\s\S]*?)\]\}/);
|
||||
if (!transitionsMatch) return match;
|
||||
|
||||
const transitionsStr = transitionsMatch[1];
|
||||
|
||||
const transRegex = /\{\s*from:\s*"([^"]+)"\s*,\s*to:\s*"([^"]+)"(?:\s*,\s*label:\s*"([^"]+)")?\s*\}/g;
|
||||
const transitions: { from: string; to: string; label?: string }[] = [];
|
||||
let transMatch;
|
||||
while ((transMatch = transRegex.exec(transitionsStr)) !== null) {
|
||||
transitions.push({
|
||||
from: transMatch[1],
|
||||
to: transMatch[2],
|
||||
label: transMatch[3],
|
||||
});
|
||||
}
|
||||
|
||||
if (transitions.length === 0) return match;
|
||||
|
||||
// Extract initialState
|
||||
const initialStateMatch = match.match(/initialState="([^"]+)"/);
|
||||
const initialState = initialStateMatch ? initialStateMatch[1] : '';
|
||||
|
||||
// Extract finalStates
|
||||
const finalStatesMatch = match.match(/finalStates=\{\[([^\]]*)\]\}/);
|
||||
const finalStatesStr = finalStatesMatch ? finalStatesMatch[1] : '';
|
||||
const finalStates = finalStatesStr.match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
|
||||
|
||||
const titleMatch = match.match(/title="([^"]+)"/);
|
||||
const captionMatch = match.match(/caption="([^"]+)"/);
|
||||
const idMatch = match.match(/id="([^"]+)"/);
|
||||
const showShareMatch = match.match(/showShare=\{(true|false)\}/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const caption = captionMatch ? captionMatch[1] : '';
|
||||
const id = idMatch ? idMatch[1] : '';
|
||||
const showShare = showShareMatch ? showShareMatch[1] : 'true';
|
||||
|
||||
let stateLines = 'stateDiagram-v2';
|
||||
if (initialState) {
|
||||
stateLines += `\n [*] --> ${initialState}`;
|
||||
}
|
||||
stateLines += '\n' + transitions.map(t => {
|
||||
const label = t.label ? ` : ${t.label}` : '';
|
||||
return ` ${t.from} --> ${t.to}${label}`;
|
||||
}).join('\n');
|
||||
stateLines += '\n' + finalStates.map(s => ` ${s} --> [*]`).join('\n');
|
||||
|
||||
let result = `<div className="my-12">\n <Mermaid`;
|
||||
if (id) result += ` id="${id}"`;
|
||||
if (title) result += ` title="${title}"`;
|
||||
result += ` showShare={${showShare}}>`;
|
||||
result += `\n{\`${stateLines}\`}\n</Mermaid>`;
|
||||
if (caption) {
|
||||
result += `\n <div className="text-center text-xs text-slate-400 mt-4 italic">\n ${caption}\n </div>`;
|
||||
}
|
||||
result += `\n</div>`;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const original = content;
|
||||
|
||||
content = convertMermaidToChildren(content);
|
||||
content = convertDiagramPie(content);
|
||||
content = convertDiagramGantt(content);
|
||||
content = convertDiagramSequence(content);
|
||||
content = convertDiagramTimeline(content);
|
||||
content = convertDiagramState(content);
|
||||
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✅ Converted diagrams in ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no changes needed)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles();
|
||||
56
apps/web/scripts/convert-to-children-clean.ts
Normal file
56
apps/web/scripts/convert-to-children-clean.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Convert ugly single-line graph="..." props to clean multi-line children.
|
||||
*
|
||||
* FROM:
|
||||
* <Mermaid graph="graph TD\n A-->B\n B-->C" id="..." title="..." />
|
||||
*
|
||||
* TO:
|
||||
* <Mermaid id="..." title="...">
|
||||
* graph TD
|
||||
* A-->B
|
||||
* B-->C
|
||||
* </Mermaid>
|
||||
*/
|
||||
function convertToChildren(content: string): string {
|
||||
// Match <Mermaid graph="..." ... />
|
||||
const mermaidRegex = /<Mermaid\s+graph="([^"]*)"([^>]*?)\/>/g;
|
||||
|
||||
return content.replace(mermaidRegex, (match, graphValue, otherProps) => {
|
||||
// Unescape \n to real newlines
|
||||
const cleanGraph = graphValue.replace(/\\n/g, '\n');
|
||||
|
||||
// Clean up other props
|
||||
const cleanProps = otherProps.trim();
|
||||
|
||||
// Build the new format with children
|
||||
return `<Mermaid${cleanProps ? ' ' + cleanProps : ''}>\n${cleanGraph}\n</Mermaid>`;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = convertToChildren(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Converted to children: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no changes needed)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal converted: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
46
apps/web/scripts/convert-to-plain-prop.ts
Normal file
46
apps/web/scripts/convert-to-plain-prop.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Convert graph={"..."} to graph="..." (plain string prop without JSX expression wrapper).
|
||||
* MDXRemote RSC strips JSX expression props but keeps plain string props.
|
||||
*
|
||||
* But we also need the escape sequences to go through.
|
||||
* The plain string prop `graph="graph TD\nA-->B"` will have the \n treated as
|
||||
* literal characters by MDX's parser, not as a newline. The Mermaid component
|
||||
* then unescapes them.
|
||||
*/
|
||||
function convertToPlainStringProps(content: string): string {
|
||||
// Match graph={" ... "} and convert to graph="..."
|
||||
// The content inside should already have escaped newlines and quotes
|
||||
const pattern = /graph=\{"((?:[^"\\]|\\.)*)"\}/g;
|
||||
|
||||
return content.replace(pattern, (match, graphContent) => {
|
||||
return `graph="${graphContent}"`;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = convertToPlainStringProps(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Converted: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no changes needed)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal converted: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
60
apps/web/scripts/convert-to-plain-string.ts
Normal file
60
apps/web/scripts/convert-to-plain-string.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Convert all Mermaid children syntax back to graph prop,
|
||||
* BUT use a regular double-quoted string with escaped newlines instead of template literals.
|
||||
*
|
||||
* MDXRemote RSC strips template literals!
|
||||
*
|
||||
* Convert:
|
||||
* <Mermaid id="..." title="..." showShare={true}>
|
||||
* {`graph TD
|
||||
* A --> B`}
|
||||
* </Mermaid>
|
||||
*
|
||||
* To:
|
||||
* <Mermaid graph={"graph TD\n A --> B"} id="..." title="..." showShare={true} />
|
||||
*/
|
||||
function convertToPlainStringProp(content: string): string {
|
||||
// Match <Mermaid ...>{\`...\`}</Mermaid>
|
||||
const mermaidChildrenRegex = /<Mermaid\s+([^>]*?)>\s*\{`([\s\S]*?)`\}\s*<\/Mermaid>/g;
|
||||
|
||||
return content.replace(mermaidChildrenRegex, (match, propsStr, graphContent) => {
|
||||
// Escape double quotes in the graph content
|
||||
const escapedGraph = graphContent
|
||||
.replace(/\\/g, '\\\\') // escape backslashes first
|
||||
.replace(/"/g, '\\"') // escape double quotes
|
||||
.replace(/\n/g, '\\n'); // escape newlines
|
||||
|
||||
// Clean up props string
|
||||
const cleanProps = propsStr.trim();
|
||||
|
||||
return `<Mermaid graph={"${escapedGraph}"} ${cleanProps} />`;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = convertToPlainStringProp(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Converted to plain string: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no Mermaid children found)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal converted: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
47
apps/web/scripts/final-diagram-fix.ts
Normal file
47
apps/web/scripts/final-diagram-fix.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* FINAL ATTEMPT: Standardize EVERYTHING in Mermaid blocks to double quotes.
|
||||
*
|
||||
* 1. Find all text inside <Mermaid>...</Mermaid>.
|
||||
* 2. Replace any ['Label'] or ['Label's'] or ["Label"] patterns.
|
||||
* 3. Enforce ["Label"] for all labels.
|
||||
* 4. Remove any internal single quotes that break parsing.
|
||||
*/
|
||||
function finalMermaidFix(content: string): string {
|
||||
const mermaidRegex = /(<Mermaid[^>]*>)([\s\S]*?)(<\/Mermaid>)/g;
|
||||
|
||||
return content.replace(mermaidRegex, (match, open, body, close) => {
|
||||
let fixedBody = body;
|
||||
|
||||
// Convert common label syntax to clean double quotes
|
||||
// Match: [followed by optional space and any quote, capture content, end with optional quote and space]
|
||||
fixedBody = fixedBody.replace(/\[\s*['"]?([^\]'"]+?)['"]?\s*\]/g, (m, label) => {
|
||||
// Clean the label: remove any internal quotes that could cause issues
|
||||
const cleanLabel = label.replace(/['"]/g, "").trim();
|
||||
return `["${cleanLabel}"]`;
|
||||
});
|
||||
|
||||
// Also handle Pie charts which use 'Label' : value
|
||||
fixedBody = fixedBody.replace(/^\s*'([^']+)'\s*:/gm, (m, label) => {
|
||||
const cleanLabel = label.replace(/['"]/g, "").trim();
|
||||
return ` "${cleanLabel}" :`;
|
||||
});
|
||||
|
||||
return open + fixedBody + close;
|
||||
});
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
for (const file of files) {
|
||||
const fp = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(fp, 'utf8');
|
||||
const fixed = finalMermaidFix(content);
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(fp, fixed);
|
||||
console.log(`✅ Fixed potentially problematic syntax in: ${file}`);
|
||||
}
|
||||
}
|
||||
86
apps/web/scripts/fix-graph-quotes.ts
Normal file
86
apps/web/scripts/fix-graph-quotes.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Fix escaped double quotes in Mermaid graph props.
|
||||
*
|
||||
* The graph prop contains \" which is invalid in MDX attribute syntax.
|
||||
* Replace \" with ' (single quote) - Mermaid supports both.
|
||||
*
|
||||
* Also fix \\n to just \n (single backslash) - the MDX parser
|
||||
* will treat \n as literal characters, and the Mermaid component
|
||||
* will unescape them.
|
||||
*/
|
||||
function fixGraphQuotes(content: string): string {
|
||||
// Match graph="..." prop (the entire value including escaped content)
|
||||
// This is tricky because the value can contain escaped quotes
|
||||
// We need to match from graph=" to the closing " that ends the prop value
|
||||
|
||||
// Strategy: Find graph=" then scan forward, tracking escape sequences
|
||||
const graphPropStart = 'graph="';
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < content.length) {
|
||||
const idx = content.indexOf(graphPropStart, i);
|
||||
if (idx === -1) {
|
||||
result += content.slice(i);
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy everything up to and including graph="
|
||||
result += content.slice(i, idx + graphPropStart.length);
|
||||
|
||||
// Now scan the value, replacing \" with '
|
||||
let j = idx + graphPropStart.length;
|
||||
let graphValue = '';
|
||||
|
||||
while (j < content.length) {
|
||||
if (content[j] === '\\' && content[j + 1] === '"') {
|
||||
// Replace \" with '
|
||||
graphValue += "'";
|
||||
j += 2;
|
||||
} else if (content[j] === '\\' && content[j + 1] === '\\') {
|
||||
// Keep \\ as is
|
||||
graphValue += '\\\\';
|
||||
j += 2;
|
||||
} else if (content[j] === '"') {
|
||||
// End of attribute value
|
||||
break;
|
||||
} else {
|
||||
graphValue += content[j];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
result += graphValue;
|
||||
i = j; // Continue from the closing quote (will be added in next iteration)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = fixGraphQuotes(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Fixed quotes: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no changes needed)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal fixed: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
123
apps/web/scripts/fix-mdx-whitespace.ts
Normal file
123
apps/web/scripts/fix-mdx-whitespace.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Fix missing whitespace in MDX files by comparing with TSX originals
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Mapping of TSX component names to MDX slugs
|
||||
const TSX_TO_MDX_MAP: Record<string, string> = {
|
||||
'PageSpeedFails': 'why-pagespeed-fails',
|
||||
'SlowLoadingDebt': 'slow-loading-costs-customers',
|
||||
'AgencySlowdown': 'why-agencies-are-slow',
|
||||
'WordPressPlugins': 'hidden-costs-of-wordpress-plugins',
|
||||
'WebsiteStability': 'why-websites-break-after-updates',
|
||||
'CookieFreeDesign': 'website-without-cookie-banners',
|
||||
'LocalCloud': 'no-us-cloud-platforms',
|
||||
'GDPRSystem': 'gdpr-conformity-system-approach',
|
||||
'VendorLockIn': 'builder-systems-threaten-independence',
|
||||
'PrivacyAnalytics': 'analytics-without-tracking',
|
||||
'GreenIT': 'green-it-sustainable-web',
|
||||
'FixedPrice': 'fixed-price-digital-projects',
|
||||
'BuildFirst': 'build-first-digital-architecture',
|
||||
'MaintenanceNoCMS': 'maintenance-for-headless-systems',
|
||||
'Longevity': 'digital-longevity-architecture',
|
||||
'CleanCode': 'clean-code-for-business-value',
|
||||
'ResponsiveDesign': 'responsive-design-high-fidelity',
|
||||
'HostingOps': 'professional-hosting-operations',
|
||||
'NoTemplates': 'why-no-templates-matter',
|
||||
'CRMSync': 'crm-synchronization-headless',
|
||||
};
|
||||
|
||||
const TSX_BASE = path.join(process.cwd(), 'src/components/blog/posts');
|
||||
const MDX_BASE = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
function findTsxFile(componentName: string): string | null {
|
||||
for (const group of ['Group1', 'Group2', 'Group3', 'Group4']) {
|
||||
const tsxPath = path.join(TSX_BASE, group, `${componentName}.tsx`);
|
||||
if (fs.existsSync(tsxPath)) {
|
||||
return tsxPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fixWhitespace() {
|
||||
let totalFixed = 0;
|
||||
|
||||
for (const [tsxName, mdxSlug] of Object.entries(TSX_TO_MDX_MAP)) {
|
||||
const tsxPath = findTsxFile(tsxName);
|
||||
const mdxPath = path.join(MDX_BASE, `${mdxSlug}.mdx`);
|
||||
|
||||
if (!tsxPath || !fs.existsSync(mdxPath)) {
|
||||
console.log(`⚠️ Skipping ${tsxName}: files not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tsxContent = fs.readFileSync(tsxPath, 'utf-8');
|
||||
let mdxContent = fs.readFileSync(mdxPath, 'utf-8');
|
||||
|
||||
// Count occurrences of {" "} in both files
|
||||
const tsxSpaces = (tsxContent.match(/\{" "\}/g) || []).length;
|
||||
const mdxSpacesBefore = (mdxContent.match(/\{" "\}/g) || []).length;
|
||||
|
||||
if (tsxSpaces === 0) {
|
||||
console.log(`✓ ${mdxSlug}: No whitespace needed`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract all lines with {" "} from TSX
|
||||
const tsxLines = tsxContent.split('\n');
|
||||
const spacedLines: Array<{ lineNum: number; content: string }> = [];
|
||||
|
||||
tsxLines.forEach((line, idx) => {
|
||||
if (line.includes('{" "}')) {
|
||||
spacedLines.push({ lineNum: idx, content: line.trim() });
|
||||
}
|
||||
});
|
||||
|
||||
// For each spaced line in TSX, find similar content in MDX and add {" "}
|
||||
let fixCount = 0;
|
||||
for (const { content } of spacedLines) {
|
||||
// Extract the text pattern before {" "}
|
||||
const match = content.match(/(.+?)\{" "\}/);
|
||||
if (!match) continue;
|
||||
|
||||
const textBefore = match[1].trim();
|
||||
|
||||
// Find this pattern in MDX without the space
|
||||
const searchPattern = new RegExp(
|
||||
textBefore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?!\\{" "\\})',
|
||||
'g'
|
||||
);
|
||||
|
||||
const newMdxContent = mdxContent.replace(searchPattern, (matched) => {
|
||||
// Only add {" "} if it's not already there and if it's followed by a tag
|
||||
if (mdxContent.indexOf(matched + '{" "}') === -1 &&
|
||||
mdxContent.indexOf(matched) < mdxContent.indexOf('<', mdxContent.indexOf(matched))) {
|
||||
fixCount++;
|
||||
return matched + '{" "}';
|
||||
}
|
||||
return matched;
|
||||
});
|
||||
|
||||
mdxContent = newMdxContent;
|
||||
}
|
||||
|
||||
const mdxSpacesAfter = (mdxContent.match(/\{" "\}/g) || []).length;
|
||||
|
||||
if (fixCount > 0) {
|
||||
fs.writeFileSync(mdxPath, mdxContent, 'utf-8');
|
||||
console.log(`✓ ${mdxSlug}: Fixed ${fixCount} whitespace issues (${mdxSpacesBefore} → ${mdxSpacesAfter})`);
|
||||
totalFixed += fixCount;
|
||||
} else {
|
||||
console.log(`✓ ${mdxSlug}: Already correct (${mdxSpacesBefore} spaces)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Total whitespace fixes: ${totalFixed}`);
|
||||
}
|
||||
|
||||
fixWhitespace();
|
||||
61
apps/web/scripts/fix-mermaid-apostrophes.ts
Normal file
61
apps/web/scripts/fix-mermaid-apostrophes.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Fix apostrophes in Mermaid labels by removing them.
|
||||
*
|
||||
* Mermaid parser treats ' as a quote delimiter even inside ["..."].
|
||||
* Replace ' with nothing or use HTML entity ' (but simpler to just remove).
|
||||
*/
|
||||
function fixMermaidApostrophes(content: string): string {
|
||||
// Find all Mermaid blocks
|
||||
const mermaidBlockRegex = /(<Mermaid[^>]*>)([\s\S]*?)(<\/Mermaid>)/g;
|
||||
|
||||
return content.replace(mermaidBlockRegex, (match, openTag, mermaidContent, closeTag) => {
|
||||
// Within Mermaid content, find all ["..."] labels and remove apostrophes
|
||||
const fixed = mermaidContent.replace(/\["([^"]*)"\]/g, (m: string, label: string) => {
|
||||
// Remove all apostrophes from the label
|
||||
const cleanLabel = label.replace(/'/g, '');
|
||||
return `["${cleanLabel}"]`;
|
||||
});
|
||||
|
||||
return openTag + fixed + closeTag;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
let totalApostrophes = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Count apostrophes in Mermaid blocks before fixing
|
||||
const mermaidBlocks = content.match(/<Mermaid[^>]*>[\s\S]*?<\/Mermaid>/g) || [];
|
||||
for (const block of mermaidBlocks) {
|
||||
const apostrophes = (block.match(/\["[^"]*'[^"]*"\]/g) || []).length;
|
||||
if (apostrophes > 0) {
|
||||
totalApostrophes += apostrophes;
|
||||
}
|
||||
}
|
||||
|
||||
const fixed = fixMermaidApostrophes(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Fixed apostrophes: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no apostrophes found)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal files fixed: ${fixCount}`);
|
||||
console.log(`Total apostrophes removed: ${totalApostrophes}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
69
apps/web/scripts/fix-mermaid-labels-strict.ts
Normal file
69
apps/web/scripts/fix-mermaid-labels-strict.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* STRICTLY fixes Mermaid node labels.
|
||||
*
|
||||
* Goal:
|
||||
* 1. Find all content inside [...] in Mermaid blocks.
|
||||
* 2. Strip ALL outer quotes (single or double).
|
||||
* 3. Sanitize the inner text:
|
||||
* - Remove/Replace internal double quotes
|
||||
* - Remove/Replace internal single quotes (to avoid any ambiguity)
|
||||
* 4. Wrap strictly in ["..."].
|
||||
*/
|
||||
function fixMermaidLabels(content: string): string {
|
||||
// Find Mermaid blocks
|
||||
return content.replace(/(<Mermaid[^>]*>)([\s\S]*?)(<\/Mermaid>)/g, (match, open, body, close) => {
|
||||
|
||||
// Process the body line by line to be safe, or just regex the labels.
|
||||
// Regex for labels: \[ followed by anything until \]
|
||||
// Note: We assume labels don't contain nested brackets for now (Mermaid usually doesn't).
|
||||
const fixedBody = body.replace(/\[([^\]]+)\]/g, (labelMatch, innerContent) => {
|
||||
let text = innerContent.trim();
|
||||
|
||||
// Check if it looks like a quoted label
|
||||
const hasOuterQuotes = /^['"]|['"]$/.test(text);
|
||||
if (hasOuterQuotes) {
|
||||
// Remove ALL starting/ending quotes (handling multiple if messed up)
|
||||
text = text.replace(/^['"]+|['"]+$/g, '');
|
||||
}
|
||||
|
||||
// Sanitize internal text
|
||||
// Replace " with ' to avoid breaking the outer double quotes
|
||||
text = text.replace(/"/g, "'");
|
||||
|
||||
// Verify parsing safety:
|
||||
// Replace ' with space or nothing if we want to be super safe,
|
||||
// but "Text with 'quotes'" SHOULD be valid in Mermaid.
|
||||
// However, the previous error might have been due to MDX interference.
|
||||
// Let's keep single quotes inside, but ensure outer are double.
|
||||
|
||||
// WAIT: The specific error was `B['Server ... 'inner' ...']`.
|
||||
// If we convert to `B["Server ... 'inner' ..."]`, it should work.
|
||||
|
||||
return `["${text}"]`;
|
||||
});
|
||||
|
||||
return open + fixedBody + close;
|
||||
});
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = fixMermaidLabels(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
console.log(`✅ Fixed labels in: ${file}`);
|
||||
fixedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFixed ${fixedCount} files.`);
|
||||
49
apps/web/scripts/fix-mermaid-quotes.ts
Normal file
49
apps/web/scripts/fix-mermaid-quotes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
/**
|
||||
* Fix ALL quote variations in Mermaid labels to use consistent double quotes.
|
||||
*
|
||||
* Handles:
|
||||
* - ['Label'] → ["Label"]
|
||||
* - ["Label'] → ["Label"]
|
||||
* - ['Label"] → ["Label"]
|
||||
* - ["Label"] → ["Label"] (already correct)
|
||||
*/
|
||||
function fixMermaidQuotes(content: string): string {
|
||||
// Find all Mermaid blocks (between <Mermaid> and </Mermaid>)
|
||||
const mermaidBlockRegex = /(<Mermaid[^>]*>)([\s\S]*?)(<\/Mermaid>)/g;
|
||||
|
||||
return content.replace(mermaidBlockRegex, (match, openTag, mermaidContent, closeTag) => {
|
||||
// Replace all variations: [' or [" at start, '] or "] at end
|
||||
// Match pattern: [ followed by ' or ", then content, then ' or ", then ]
|
||||
const fixed = mermaidContent.replace(/\[['"]([^'"]*)['"]\]/g, '["$1"]');
|
||||
|
||||
return openTag + fixed + closeTag;
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
let fixCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = fixMermaidQuotes(content);
|
||||
|
||||
if (content !== fixed) {
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
fixCount++;
|
||||
console.log(`✅ Fixed Mermaid quotes: ${file}`);
|
||||
} else {
|
||||
console.log(`- ${file} (no changes needed)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal fixed: ${fixCount}`);
|
||||
}
|
||||
|
||||
processFiles();
|
||||
53
apps/web/scripts/repair-mermaid.ts
Normal file
53
apps/web/scripts/repair-mermaid.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MDX_DIR = path.join(process.cwd(), 'content/blog');
|
||||
|
||||
function repairMermaidSyntax(content: string): string {
|
||||
// 1. Convert <Mermaid graph={`...`} /> to <Mermaid graph={`...`}>...</Mermaid> style or just fix the graph prop
|
||||
// Actually, let's keep the graph prop but make sure the content is VERY safe.
|
||||
|
||||
const mermaidRegex = /<Mermaid\s+([\s\S]*?)graph=\{(`[\s\S]*?`)\}([\s\S]*?)\/>/g;
|
||||
|
||||
return content.replace(mermaidRegex, (match, before, graphLiteral, after) => {
|
||||
let graphContent = graphLiteral.slice(1, -1);
|
||||
|
||||
// Replace all {Label} with ["Label"]
|
||||
graphContent = graphContent.replace(/\{([^{}]+)\}/g, '["$1"]');
|
||||
|
||||
// Sometimes people use double {{Label}}
|
||||
graphContent = graphContent.replace(/\{\{([^{}]+)\}\}/g, '["$1"]');
|
||||
|
||||
// Remove any trailing backticks inside that might have been accidentally added
|
||||
graphContent = graphContent.trim();
|
||||
|
||||
return `<Mermaid\n ${before.trim()}\n graph={\`${graphContent}\`}\n ${after.trim()}\n />`;
|
||||
});
|
||||
}
|
||||
|
||||
// Additional fix for other diagram components that might have similar issues with props
|
||||
function repairOtherDiagrams(content: string): string {
|
||||
// For DiagramSequence, DiagramTimeline etc., we often pass arrays of objects.
|
||||
// MDX handles these better, but let's make sure there are no weird backticks.
|
||||
return content;
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const files = fs.readdirSync(MDX_DIR).filter(f => f.endsWith('.mdx'));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(MDX_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
let repaired = repairMermaidSyntax(content);
|
||||
repaired = repairOtherDiagrams(repaired);
|
||||
|
||||
if (content !== repaired) {
|
||||
fs.writeFileSync(filePath, repaired);
|
||||
console.log(`✅ Repaired Mermaid syntax in ${file}`);
|
||||
} else {
|
||||
console.log(`- Checked ${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles();
|
||||
@@ -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
|
||||
@@ -5,6 +5,9 @@
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"contentlayer/generated": [
|
||||
"./.contentlayer/generated"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -12,7 +15,8 @@
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
mintel-me-app:
|
||||
image: registry.infra.mintel.me/mintel/mintel.me:${IMAGE_TAG:-latest}
|
||||
build: .
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
@@ -11,59 +10,59 @@ services:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# HTTP ⇒ HTTPS redirect
|
||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-web.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-web.entrypoints=web"
|
||||
# - "traefik.http.routers.${PROJECT_NAME:-mintel-me}-web.middlewares=redirect-https"
|
||||
- 'traefik.http.routers.mintel-me-web.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
||||
- "traefik.http.routers.mintel-me-web.entrypoints=web"
|
||||
# - "traefik.http.routers.mintel-me-web.middlewares=redirect-https"
|
||||
# HTTPS router (Standard)
|
||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.service=${PROJECT_NAME:-mintel-me}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-mintel-me}-ratelimit,${PROJECT_NAME:-mintel-me}-forward}"
|
||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}.loadbalancer.server.port=3000"
|
||||
- 'traefik.http.routers.mintel-me.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
||||
- "traefik.http.routers.mintel-me.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.mintel-me.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.mintel-me.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.mintel-me.service=mintel-me-app-svc"
|
||||
- "traefik.http.routers.mintel-me.middlewares=${AUTH_MIDDLEWARE:-mintel-me-ratelimit,mintel-me-forward}"
|
||||
- "traefik.http.services.mintel-me-app-svc.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=infra"
|
||||
- "caddy=${TRAEFIK_HOST:-mintel.localhost}"
|
||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||
|
||||
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.service=${PROJECT_NAME:-mintel-me}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-${PROJECT_NAME:-mintel-me}-ratelimit,${PROJECT_NAME:-mintel-me}-forward}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.priority=2000"
|
||||
- 'traefik.http.routers.mintel-me-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
|
||||
- "traefik.http.routers.mintel-me-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.mintel-me-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.mintel-me-public.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.mintel-me-public.service=mintel-me-app-svc"
|
||||
- "traefik.http.routers.mintel-me-public.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-mintel-me-ratelimit,mintel-me-forward}"
|
||||
- "traefik.http.routers.mintel-me-public.priority=2000"
|
||||
|
||||
# Middlewares
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-ratelimit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-ratelimit.ratelimit.burst=50"
|
||||
- "traefik.http.middlewares.mintel-me-ratelimit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.mintel-me-ratelimit.ratelimit.burst=50"
|
||||
|
||||
# Gatekeeper Router (Path-based)
|
||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-gatekeeper.rule=(Host("${TRAEFIK_HOST:-mintel.localhost}") && PathPrefix("/gatekeeper"))'
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-gatekeeper.service=${PROJECT_NAME:-mintel-me}-gatekeeper"
|
||||
- 'traefik.http.routers.mintel-me-gatekeeper.rule=(Host("${TRAEFIK_HOST:-mintel.localhost}") && PathPrefix("/gatekeeper"))'
|
||||
- "traefik.http.routers.mintel-me-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.mintel-me-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.mintel-me-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.mintel-me-gatekeeper.service=mintel-me-gatekeeper-svc"
|
||||
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-auth.forwardauth.address=http://${PROJECT_NAME:-mintel-me}-gatekeeper:3000/gatekeeper/api/verify"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||
- "traefik.http.middlewares.mintel-me-auth.forwardauth.address=http://mintel-me-gatekeeper:3000/gatekeeper/api/verify"
|
||||
- "traefik.http.middlewares.mintel-me-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.mintel-me-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
|
||||
- "traefik.http.middlewares.mintel-me-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||
|
||||
# Forwarded Headers
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mintel-me}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||
- "traefik.http.middlewares.mintel-me-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||
- "traefik.http.middlewares.mintel-me-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||
|
||||
gatekeeper:
|
||||
profiles: ["gatekeeper"]
|
||||
mintel-me-gatekeeper:
|
||||
profiles: [ "gatekeeper" ]
|
||||
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
||||
container_name: ${PROJECT_NAME:-mintel-me}-gatekeeper
|
||||
restart: always
|
||||
networks:
|
||||
infra:
|
||||
aliases:
|
||||
- ${PROJECT_NAME:-mintel-me}-gatekeeper
|
||||
- mintel-me-gatekeeper
|
||||
env_file:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
@@ -75,12 +74,12 @@ services:
|
||||
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-mintel}
|
||||
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
|
||||
labels:
|
||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-gatekeeper.loadbalancer.server.port=3000"
|
||||
- "traefik.http.services.mintel-me-gatekeeper-svc.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=infra"
|
||||
- "caddy=gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||
|
||||
directus:
|
||||
mintel-me-cms:
|
||||
image: registry.infra.mintel.me/mintel/directus:latest
|
||||
restart: always
|
||||
networks:
|
||||
@@ -92,7 +91,7 @@ services:
|
||||
KEY: ${DIRECTUS_KEY}
|
||||
SECRET: ${DIRECTUS_SECRET}
|
||||
DB_CLIENT: "pg"
|
||||
DB_HOST: "directus-db"
|
||||
DB_HOST: "mintel-me-db"
|
||||
DB_PORT: "5432"
|
||||
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
|
||||
DB_USER: ${DIRECTUS_DB_USER:-directus}
|
||||
@@ -106,20 +105,26 @@ services:
|
||||
- ./directus/extensions:/directus/extensions
|
||||
- ./directus/schema:/directus/schema
|
||||
- ./directus/migrations:/directus/migrations
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8055/server/ping" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.service=${PROJECT_NAME:-mintel-me}-directus"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.middlewares=${PROJECT_NAME:-mintel-me}-forward"
|
||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
||||
- 'traefik.http.routers.mintel-me-cms.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
||||
- "traefik.http.routers.mintel-me-cms.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.mintel-me-cms.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.mintel-me-cms.tls=${TRAEFIK_TLS:-false}"
|
||||
- "traefik.http.routers.mintel-me-cms.service=mintel-me-cms-svc"
|
||||
- "traefik.http.routers.mintel-me-cms.middlewares=mintel-me-forward"
|
||||
- "traefik.http.services.mintel-me-cms-svc.loadbalancer.server.port=8055"
|
||||
- "traefik.docker.network=infra"
|
||||
- "caddy=${DIRECTUS_HOST:-cms.mintel.localhost}"
|
||||
- "caddy.reverse_proxy={{upstreams 8055}}"
|
||||
|
||||
directus-db:
|
||||
mintel-me-db:
|
||||
image: postgres:15-alpine
|
||||
restart: always
|
||||
networks:
|
||||
@@ -142,3 +147,5 @@ networks:
|
||||
|
||||
volumes:
|
||||
directus-db-data:
|
||||
external: true
|
||||
name: mintel-me_directus-db-data
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"dependencies": {
|
||||
"@directus/sdk": "21.0.0",
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@mintel/acquisition": "link:../at-mintel/packages/acquisition-library"
|
||||
"@mintel/acquisition": "link:../at-mintel/packages/acquisition-library",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
17340
pnpm-lock.yaml
generated
17340
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user