From 386a07aa53fef851cf32695640ddfc9ea9c365e3 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 15 Feb 2026 18:52:48 +0100 Subject: [PATCH] feat(blog): complete blog experience overhaul - Implemented minimalist vertical teaser list (MediumCard) - Consolidated and refined 20 engineering-focused blog posts - Rebuilt blog overview with narrow, centered layout (max-w-3xl) - Introduced BlogCommandBar for integrated search and tag filtering - Consolidated tags to 6-8 core technical categories - Redesigned blog detail pages with industrial 'Technical Frame' layout - Added SectionHeader component for consistent industrial titling - Optimized vertical space by removing redundant PageHeaders --- apps/web/app/blog/[slug]/page.tsx | 241 ++++------------- apps/web/app/blog/embed-demo/page.tsx | 242 ------------------ apps/web/app/blog/page.tsx | 205 ++++++++------- apps/web/src/components/ArticleHeading.tsx | 22 +- apps/web/src/components/ArticleParagraph.tsx | 22 +- .../src/components/Landing/ComparisonRow.tsx | 52 ++-- apps/web/src/components/MediumCard.tsx | 71 +++-- apps/web/src/components/Mermaid.tsx | 34 ++- apps/web/src/components/PageHeader.tsx | 61 ++++- apps/web/src/components/SearchBar.tsx | 36 +-- apps/web/src/components/Section.tsx | 86 ++++--- apps/web/src/components/SectionHeader.tsx | 55 ++++ .../src/components/blog/BlogCommandBar.tsx | 80 ++++++ .../web/src/components/blog/BlogFilterBar.tsx | 69 +++++ .../blog/posts/Group1/AgencySlowdown.tsx | 191 ++++++++++++++ .../blog/posts/Group1/PageSpeedFails.tsx | 182 +++++++++++++ .../blog/posts/Group1/SlowLoadingDebt.tsx | 184 +++++++++++++ .../blog/posts/Group1/WebsiteStability.tsx | 159 ++++++++++++ .../blog/posts/Group1/WordPressPlugins.tsx | 164 ++++++++++++ .../blog/posts/Group2/CookieFreeDesign.tsx | 176 +++++++++++++ .../blog/posts/Group2/GDPRSystem.tsx | 178 +++++++++++++ .../blog/posts/Group2/LocalCloud.tsx | 161 ++++++++++++ .../blog/posts/Group2/PrivacyAnalytics.tsx | 155 +++++++++++ .../blog/posts/Group2/VendorLockIn.tsx | 157 ++++++++++++ .../blog/posts/Group3/BuildFirst.tsx | 179 +++++++++++++ .../blog/posts/Group3/FixedPrice.tsx | 167 ++++++++++++ .../components/blog/posts/Group3/GreenIT.tsx | 161 ++++++++++++ .../blog/posts/Group3/Longevity.tsx | 157 ++++++++++++ .../blog/posts/Group3/MaintenanceNoCMS.tsx | 162 ++++++++++++ .../components/blog/posts/Group4/CRMSync.tsx | 164 ++++++++++++ .../blog/posts/Group4/CleanCode.tsx | 159 ++++++++++++ .../blog/posts/Group4/HostingOps.tsx | 165 ++++++++++++ .../blog/posts/Group4/NoTemplates.tsx | 147 +++++++++++ .../blog/posts/Group4/ResponsiveDesign.tsx | 147 +++++++++++ apps/web/src/components/blog/posts/index.ts | 49 ++++ apps/web/src/data/blogPosts.ts | 189 ++++++++++++-- 36 files changed, 4141 insertions(+), 688 deletions(-) delete mode 100644 apps/web/app/blog/embed-demo/page.tsx create mode 100644 apps/web/src/components/SectionHeader.tsx create mode 100644 apps/web/src/components/blog/BlogCommandBar.tsx create mode 100644 apps/web/src/components/blog/BlogFilterBar.tsx create mode 100644 apps/web/src/components/blog/posts/Group1/AgencySlowdown.tsx create mode 100644 apps/web/src/components/blog/posts/Group1/PageSpeedFails.tsx create mode 100644 apps/web/src/components/blog/posts/Group1/SlowLoadingDebt.tsx create mode 100644 apps/web/src/components/blog/posts/Group1/WebsiteStability.tsx create mode 100644 apps/web/src/components/blog/posts/Group1/WordPressPlugins.tsx create mode 100644 apps/web/src/components/blog/posts/Group2/CookieFreeDesign.tsx create mode 100644 apps/web/src/components/blog/posts/Group2/GDPRSystem.tsx create mode 100644 apps/web/src/components/blog/posts/Group2/LocalCloud.tsx create mode 100644 apps/web/src/components/blog/posts/Group2/PrivacyAnalytics.tsx create mode 100644 apps/web/src/components/blog/posts/Group2/VendorLockIn.tsx create mode 100644 apps/web/src/components/blog/posts/Group3/BuildFirst.tsx create mode 100644 apps/web/src/components/blog/posts/Group3/FixedPrice.tsx create mode 100644 apps/web/src/components/blog/posts/Group3/GreenIT.tsx create mode 100644 apps/web/src/components/blog/posts/Group3/Longevity.tsx create mode 100644 apps/web/src/components/blog/posts/Group3/MaintenanceNoCMS.tsx create mode 100644 apps/web/src/components/blog/posts/Group4/CRMSync.tsx create mode 100644 apps/web/src/components/blog/posts/Group4/CleanCode.tsx create mode 100644 apps/web/src/components/blog/posts/Group4/HostingOps.tsx create mode 100644 apps/web/src/components/blog/posts/Group4/NoTemplates.tsx create mode 100644 apps/web/src/components/blog/posts/Group4/ResponsiveDesign.tsx create mode 100644 apps/web/src/components/blog/posts/index.ts diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index 05c093c..5f0fcf2 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -1,19 +1,11 @@ import * as React from "react"; import { notFound } from "next/navigation"; import { blogPosts } from "../../../src/data/blogPosts"; -import { Tag } from "../../../src/components/Tag"; -import { CodeBlock } from "../../../src/components/ArticleBlockquote"; -import { H2 } from "../../../src/components/ArticleHeading"; -import { - Paragraph, - LeadParagraph, -} from "../../../src/components/ArticleParagraph"; -import { UL, LI } from "../../../src/components/ArticleList"; -import { FileExamplesList } from "../../../src/components/FileExamplesList"; -import { FileExampleManager } from "../../../src/data/fileExamples"; -import { BlogPostClient } from "../../../src/components/BlogPostClient"; import { PageHeader } from "../../../src/components/PageHeader"; import { Section } from "../../../src/components/Section"; +import { BlogPostClient } from "../../../src/components/BlogPostClient"; +import { PostComponents } from "../../../src/components/blog/posts"; +import { Card } from "../../../src/components/Layout"; export async function generateStaticParams() { return blogPosts.map((post) => ({ @@ -33,201 +25,78 @@ export default async function BlogPostPage({ notFound(); } - const formattedDate = new Date(post.date).toLocaleDateString("en-US", { + const formattedDate = new Date(post.date).toLocaleDateString("de-DE", { month: "long", day: "numeric", year: "numeric", }); - const wordCount = post.description.split(/\s+/).length + 100; + const wordCount = post.description.split(/\s+/).length + 300; // Average post length const readingTime = Math.max(1, Math.ceil(wordCount / 200)); - const showFileExamples = post.tags?.some((tag) => - [ - "architecture", - "design-patterns", - "system-design", - "docker", - "deployment", - ].includes(tag), - ); - - // Load file examples for the post - let groups: any[] = []; - if (showFileExamples) { - const allGroups = await FileExampleManager.getAllGroups(); - groups = allGroups - .map((group) => ({ - ...group, - files: group.files.filter((file) => { - if (file.postSlug !== slug) return false; - return true; - }), - })) - .filter((group) => group.files.length > 0); - } + const PostContent = PostComponents[slug]; return ( -
+
-
-
-
- - - {readingTime} min read -
+
+
+ + {/* Decorative background grid inside the card */} +
- {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((tag, index) => ( - - ))} +
+
+
+ + +
+
+ {readingTime} min Lesezeit + | + + {slug.substring(0, 4).toUpperCase()}- + {Math.floor(Math.random() * 999)} + +
+
+ + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag, index) => ( + + #{tag} + + ))} +
+ )} + + {PostContent ? ( + + ) : ( +
+ Inhalt wird bald veröffentlicht... +
+ )}
- )} - - {slug === "first-note" && ( - <> - - This blog is a public notebook. It's where I document things I - learn, problems I solve, and tools I test. - -

Why write in public?

- - I forget things. Writing them down helps. Making them public - helps me think more clearly and might help someone else. - -

What to expect

-
    -
  • Short entries, usually under 500 words
  • -
  • Practical solutions to specific problems
  • -
  • Notes on tools and workflows
  • -
  • Mistakes and what I learned
  • -
- - )} - - {slug === "debugging-tips" && ( - <> - - Sometimes the simplest debugging tool is the best one. Print - statements get a bad reputation, but they're often exactly - what you need. - -

Why print statements work

- - Debuggers are powerful, but they change how your code runs. - Print statements don't. - - - {`def process_data(data): - print(f"Processing {len(data)} items") - result = expensive_operation(data) - print(f"Operation result: {result}") - return result`} - - -

Complete examples

- - Here are some practical file examples you can copy and - download. These include proper error handling and logging. - - -
- -
- - )} - - {slug === "architecture-patterns" && ( - <> - - Good software architecture is about making the right decisions - early. Here are some patterns I've found useful in production - systems. - -

Repository Pattern

- - The repository pattern provides a clean separation between - your business logic and data access layer. It makes your code - more testable and maintainable. - - -

Service Layer

- - Services orchestrate business logic and coordinate between - repositories and domain events. They keep your controllers - thin and your business rules organized. - - -

Domain Events

- - Domain events help you decouple components and react to - changes in your system. They're essential for building - scalable, event-driven architectures. - - -

Complete examples

- - These TypeScript examples demonstrate modern architecture - patterns for scalable applications. You can copy them directly - into your project. - - -
- -
- - )} - - {slug === "docker-deployment" && ( - <> - - Docker has become the standard for containerizing - applications. Here's how to set up production-ready - deployments that are secure, efficient, and maintainable. - -

Multi-stage builds

- - Multi-stage builds keep your production images small and - secure by separating build and runtime environments. This - reduces attack surface and speeds up deployments. - - -

Health checks and monitoring

- - Proper health checks ensure your containers are running - correctly. Combined with restart policies, this gives you - resilient, self-healing deployments. - - -

Orchestration with Docker Compose

- - Docker Compose makes it easy to manage multi-service - applications in development and production. Define services, - networks, and volumes in a single file. - - -

Complete examples

- - These Docker configurations are production-ready. Use them as - a starting point for your own deployments. - - -
- -
- - )} +
diff --git a/apps/web/app/blog/embed-demo/page.tsx b/apps/web/app/blog/embed-demo/page.tsx deleted file mode 100644 index 7a4ea6d..0000000 --- a/apps/web/app/blog/embed-demo/page.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import React from 'react'; -import { Tag } from '../../../src/components/Tag'; -import { H2 } from '../../../src/components/ArticleHeading'; -import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph'; -import { UL, LI } from '../../../src/components/ArticleList'; -import { CodeBlock } from '../../../src/components/ArticleBlockquote'; -import { YouTubeEmbed } from '../../../src/components/YouTubeEmbed'; -import { TwitterEmbed } from '../../../src/components/TwitterEmbed'; -import { GenericEmbed } from '../../../src/components/GenericEmbed'; -import { Mermaid } from '../../../src/components/Mermaid'; -import { BlogPostClient } from '../../../src/components/BlogPostClient'; - -export default function EmbedDemoPage() { - const post = { - title: "Rich Content Embedding Demo", - description: "Testing our new free embed components for YouTube, Twitter, Mermaid diagrams, and other platforms", - date: "2024-02-15", - slug: "embed-demo", - tags: ["embeds", "components", "tutorial", "mermaid"] - }; - - const formattedDate = new Date(post.date).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric' - }); - - const readingTime = 5; - - return ( -
- - -
-
-
-
-

- {post.title} -

- -
- - - - - - - {readingTime} min - -
- -

- {post.description} -

- -
- {post.tags.map((tag, index) => ( - - ))} -
-
-
-
- -
-
- - This post demonstrates our new free embed components that give you full styling control over YouTube videos, Twitter tweets, and other rich content - all generated at build time. - - -

YouTube Embed Example

- - Here's a YouTube video embedded with full styling control. The component uses build-time generation for optimal performance. - - -
- -
- - - You can customize the appearance using CSS variables or data attributes: - - - -{``} - - -

Twitter/X Embed Example

- - Twitter embeds use the official Twitter iframe embed for reliable display. - - -
- -
- - -{``} - - -

Generic Embed Example

- - The generic component supports direct embeds for Vimeo, CodePen, GitHub Gists, and other platforms. - - -
- -
- - -{``} - - -

Mermaid Diagrams

- - We've added support for Mermaid diagrams! You can now create flowcharts, sequence diagrams, and more using a simple text-based syntax. - - -
- B[Load Balancer] - B --> C[App Server 1] - B --> D[App Server 2] - C --> E[(Database)] - D --> E`} - /> -
- - - Usage is straightforward: - - - -{` B[Load Balancer] - B --> C[App Server 1] - B --> D[App Server 2] - C --> E[(Database)] - D --> E\`} -/>`} - - -

Styling Control

- - All components use CSS variables for easy customization: - - - -{`.youtube-embed { - --aspect-ratio: 56.25%; - --bg-color: #000000; - --border-radius: 12px; - --shadow: 0 4px 12px rgba(0,0,0,0.15); -} - -/* Data attribute variations */ -.youtube-embed[data-style="minimal"] { - --border-radius: 4px; - --shadow: none; -}`} - - -

Benefits

-
    -
  • Free: No paid services required
  • -
  • Fast: Build-time generation, no runtime API calls
  • -
  • Flexible: Full styling control via CSS variables
  • -
  • Self-hosted: Complete ownership and privacy
  • -
  • SEO-friendly: Static HTML content
  • -
- -

Integration

- - Simply import the components in your blog posts: - - - -{`import { YouTubeEmbed } from '../components/YouTubeEmbed'; -import { TwitterEmbed } from '../components/TwitterEmbed'; -import { GenericEmbed } from '../components/GenericEmbed'; - - -`} - -
-
-
-
- ); -} diff --git a/apps/web/app/blog/page.tsx b/apps/web/app/blog/page.tsx index fe58b3c..90400af 100644 --- a/apps/web/app/blog/page.tsx +++ b/apps/web/app/blog/page.tsx @@ -3,19 +3,19 @@ import * as React from "react"; import { useState, useEffect } from "react"; import { MediumCard } from "../../src/components/MediumCard"; -import { SearchBar } from "../../src/components/SearchBar"; -import { Tag } from "../../src/components/Tag"; +import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar"; import { blogPosts } from "../../src/data/blogPosts"; import { PageHeader } from "../../src/components/PageHeader"; +import { SectionHeader } from "../../src/components/SectionHeader"; import { Reveal } from "../../src/components/Reveal"; import { Section } from "../../src/components/Section"; import { AbstractCircuit, GradientMesh } from "../../src/components/Effects"; -import { Label } from "../../src/components/Typography"; export default function BlogPage() { const [searchQuery, setSearchQuery] = useState(""); + const [activeTags, setActiveTags] = useState([]); - // Memoize allPosts to prevent infinite re-render loop + // Memoize allPosts const allPosts = React.useMemo( () => [...blogPosts].sort( @@ -32,110 +32,123 @@ export default function BlogPage() { [allPosts], ); - useEffect(() => { - const query = searchQuery.toLowerCase().trim(); - if (query.startsWith("#")) { - const tag = query.slice(1); - setFilteredPosts( - allPosts.filter((post) => - post.tags?.some((t) => t.toLowerCase() === tag.toLowerCase()), - ), - ); - } else { - setFilteredPosts( - allPosts.filter((post) => { - const title = post.title.toLowerCase(); - const description = post.description.toLowerCase(); - const tags = (post.tags || []).join(" ").toLowerCase(); - return ( - title.includes(query) || - description.includes(query) || - tags.includes(query) - ); - }), - ); - } - }, [searchQuery, allPosts]); + const [visibleCount, setVisibleCount] = useState(8); - const filterByTag = (tag: string) => { - setSearchQuery(`#${tag}`); + const handleTagToggle = (tag: string) => { + setActiveTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], + ); + setVisibleCount(8); // Reset pagination on filter change }; + useEffect(() => { + const query = searchQuery.toLowerCase().trim(); + + let filtered = allPosts; + + if (query) { + filtered = filtered.filter((post) => { + const title = post.title.toLowerCase(); + const description = post.description.toLowerCase(); + const pTagString = (post.tags || []).join(" ").toLowerCase(); + return ( + title.includes(query) || + description.includes(query) || + pTagString.includes(query) + ); + }); + } + + if (activeTags.length > 0) { + filtered = filtered.filter((post) => + post.tags?.some((tag) => activeTags.includes(tag)), + ); + } + + setFilteredPosts(filtered); + }, [searchQuery, activeTags, allPosts]); + + const loadMore = () => { + setVisibleCount((prev) => prev + 6); + }; + + const hasMore = visibleCount < filteredPosts.length; + const postsToShow = filteredPosts.slice(0, visibleCount); + return ( -
+
- - Blog
- & Notes. - - } - description="Ein technisches Notizbuch über Lösungen, Fehler und Werkzeuge." - backLink={{ href: "/", label: "Zurück" }} - backgroundSymbol="B" - /> -
} + effects={} + className="pb-32 pt-12 md:pt-20" + containerVariant="wide" > -
- {/* Sidebar / Filter area */} -
-
- -
- - -
-
- - {allTags.length > 0 && ( - -
- -
- {allTags.map((tag, index) => ( - - ))} -
-
-
- )} -
+
+ {/* Section Header & Filters - Centered & Compact */} +
+ + + +
- {/* Posts area */} -
-
- {filteredPosts.length === 0 ? ( -
-

- Keine Beiträge gefunden. -

-
- ) : ( - filteredPosts.map((post, i) => ( - + {/* Posts List (Vertical & Minimal) */} +
+ {postsToShow.length === 0 ? ( +
+

+ Keine Beiträge gefunden. +

+ +
+ ) : ( +
+ {postsToShow.map((post, i) => ( + - )) - )} -
+ ))} +
+ )} + + {/* Pagination */} + {hasMore && ( +
+ + + +
+ )}
diff --git a/apps/web/src/components/ArticleHeading.tsx b/apps/web/src/components/ArticleHeading.tsx index f866b89..8336c11 100644 --- a/apps/web/src/components/ArticleHeading.tsx +++ b/apps/web/src/components/ArticleHeading.tsx @@ -1,24 +1,30 @@ -import React from 'react'; +import React from "react"; interface HeadingProps { children: React.ReactNode; className?: string; } -export const H1: React.FC = ({ children, className = '' }) => ( -

+export const H1: React.FC = ({ children, className = "" }) => ( +

{children}

); -export const H2: React.FC = ({ children, className = '' }) => ( -

+export const H2: React.FC = ({ children, className = "" }) => ( +

{children}

); -export const H3: React.FC = ({ children, className = '' }) => ( -

+export const H3: React.FC = ({ children, className = "" }) => ( +

{children}

-); \ No newline at end of file +); diff --git a/apps/web/src/components/ArticleParagraph.tsx b/apps/web/src/components/ArticleParagraph.tsx index df70dd4..1194b13 100644 --- a/apps/web/src/components/ArticleParagraph.tsx +++ b/apps/web/src/components/ArticleParagraph.tsx @@ -1,18 +1,28 @@ -import React from 'react'; +import React from "react"; interface ParagraphProps { children: React.ReactNode; className?: string; } -export const Paragraph: React.FC = ({ children, className = '' }) => ( -

+export const Paragraph: React.FC = ({ + children, + className = "", +}) => ( +

{children}

); -export const LeadParagraph: React.FC = ({ children, className = '' }) => ( -

+export const LeadParagraph: React.FC = ({ + children, + className = "", +}) => ( +

{children}

-); \ No newline at end of file +); diff --git a/apps/web/src/components/Landing/ComparisonRow.tsx b/apps/web/src/components/Landing/ComparisonRow.tsx index 27987fa..131bbfc 100644 --- a/apps/web/src/components/Landing/ComparisonRow.tsx +++ b/apps/web/src/components/Landing/ComparisonRow.tsx @@ -2,8 +2,10 @@ import * as React from "react"; import { ArrowRight } from "lucide-react"; import { Reveal } from "../Reveal"; import { Label, H3, LeadText } from "../Typography"; +import { cn } from "../../utils/cn"; interface ComparisonRowProps { + description?: string; negativeLabel: string; negativeText: React.ReactNode; positiveLabel: string; @@ -13,6 +15,7 @@ interface ComparisonRowProps { } export const ComparisonRow: React.FC = ({ + description, negativeLabel, negativeText, positiveLabel, @@ -22,27 +25,40 @@ export const ComparisonRow: React.FC = ({ }) => { return ( -
-
-