feat: molzi3d.de v1.0.0 — Komplettes Redesign mit Next.js 16

- WordPress durch Next.js 16 + Tailwind CSS v4 + Framer Motion ersetzt
- 44 Guides + 15 Seiten aus WordPress migriert (HTML -> Markdown)
- Emerald Design-System mit Light/Dark Mode Toggle
- Sidebar-First Navigation (Dokumentations-Stil)
- Difficulty-Badges, Lesezeit, verwandte Guides
- Statischer Export fuer Plesk-Hosting
- WordPress-DB Backup gesichert (6.2 MB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Klaus Molzberger
2026-03-29 01:37:57 +01:00
commit 35b4ddde00
223 changed files with 24950 additions and 0 deletions

95
app/src/lib/content.ts Normal file
View File

@@ -0,0 +1,95 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { remark } from "remark";
import html from "remark-html";
import readingTime from "reading-time";
export interface GuideMetadata {
title: string;
slug: string;
excerpt: string;
category: string;
difficulty: "einsteiger" | "fortgeschritten" | "experte";
readingTime: string;
}
export interface Guide extends GuideMetadata {
contentHtml: string;
}
const GUIDES_DIR = path.join(process.cwd(), "src/content/guides");
const PAGES_DIR = path.join(process.cwd(), "src/content/pages");
const ensureDir = (dir: string) => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
};
export const getAllGuides = (): GuideMetadata[] => {
ensureDir(GUIDES_DIR);
const files = fs.readdirSync(GUIDES_DIR).filter((f) => f.endsWith(".md"));
return files
.map((filename) => {
const filePath = path.join(GUIDES_DIR, filename);
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const stats = readingTime(content);
return {
title: data.title ?? filename.replace(".md", ""),
slug: data.slug ?? filename.replace(".md", ""),
excerpt: data.excerpt ?? "",
category: data.category ?? "Allgemein",
difficulty: data.difficulty ?? "fortgeschritten",
readingTime: stats.text.replace("read", "Lesezeit"),
};
})
.sort((a, b) => a.title.localeCompare(b.title, "de"));
};
export const getGuideBySlug = async (slug: string): Promise<Guide | null> => {
ensureDir(GUIDES_DIR);
const filePath = path.join(GUIDES_DIR, `${slug}.md`);
if (!fs.existsSync(filePath)) return null;
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const stats = readingTime(content);
const processed = await remark().use(html).process(content);
return {
title: data.title ?? slug,
slug: data.slug ?? slug,
excerpt: data.excerpt ?? "",
category: data.category ?? "Allgemein",
difficulty: data.difficulty ?? "fortgeschritten",
readingTime: stats.text.replace("read", "Lesezeit"),
contentHtml: processed.toString(),
};
};
export const getGuideCategories = (): string[] => {
const guides = getAllGuides();
const categories = [...new Set(guides.map((g) => g.category))];
return categories.sort((a, b) => a.localeCompare(b, "de"));
};
export const getPageContent = async (
slug: string
): Promise<{ title: string; contentHtml: string } | null> => {
ensureDir(PAGES_DIR);
const filePath = path.join(PAGES_DIR, `${slug}.md`);
if (!fs.existsSync(filePath)) return null;
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const processed = await remark().use(html).process(content);
return {
title: data.title ?? slug,
contentHtml: processed.toString(),
};
};

28
app/src/lib/theme.ts Normal file
View File

@@ -0,0 +1,28 @@
"use client";
import { useEffect, useState } from "react";
type Theme = "light" | "dark";
export const useTheme = () => {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
const stored = localStorage.getItem("m3d-theme") as Theme | null;
const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
const initial = stored ?? preferred;
setTheme(initial);
document.documentElement.classList.toggle("dark", initial === "dark");
}, []);
const toggleTheme = () => {
const next = theme === "light" ? "dark" : "light";
setTheme(next);
localStorage.setItem("m3d-theme", next);
document.documentElement.classList.toggle("dark", next === "dark");
};
return { theme, toggleTheme };
};