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:
95
app/src/lib/content.ts
Normal file
95
app/src/lib/content.ts
Normal 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
28
app/src/lib/theme.ts
Normal 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user