diff options
| author | schererleander <leander@schererleander.de> | 2025-06-25 01:00:49 +0200 |
|---|---|---|
| committer | schererleander <leander@schererleander.de> | 2025-06-25 01:00:59 +0200 |
| commit | 64564a6fee02708d375a349d75ce49d515e66f8d (patch) | |
| tree | fa7685e6f51e90af9f8bd45fae1f2133b1237772 /src/pages | |
| parent | 12aa0baac380149cdbb36803605be42368e736ad (diff) | |
add markdown-based blog
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/Blog.tsx | 50 | ||||
| -rw-r--r-- | src/pages/Post.tsx | 75 |
2 files changed, 125 insertions, 0 deletions
diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx new file mode 100644 index 0000000..5db87c8 --- /dev/null +++ b/src/pages/Blog.tsx @@ -0,0 +1,50 @@ +import { useMemo } from "react"; +import matter from "gray-matter"; +import { Link } from "react-router-dom"; +import CardLink from "../components/CardLink"; + +interface PostMeta { + slug: string; + title: string; + date: string; + excerpt: string; + cover?: string; +} + +export default function Blog() { + const posts = useMemo<PostMeta[]>(() => { + const files = import.meta.glob("../blog/*.md", { + eager: true, + query: "?raw", + import: "default", + }) as Record<string, string>; + + return Object.entries(files) + .map(([path, raw]) => { + const { data } = matter(raw); + const slug = path.split("/").pop()!.replace(".md", ""); + return { slug, ...data } as PostMeta; + }) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + }, []); + + return ( + <section className="container mx-auto px-4 py-10"> + <h1 className="text-4xl font-bold mb-8">Blog</h1> + + <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> + {posts.map((post) => ( + /* 1) Link für internes Routing */ + /* 2) CardLink bekommt genau deine Prop-Namen */ + <Link key={post.slug} to={`/blog/${post.slug}`} className="block"> + <CardLink + title={post.title} + body={post.excerpt} // <— body statt description + imgSrc={post.cover} // <— imgSrc statt image + /> + </Link> + ))} + </div> + </section> + ); +} diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx new file mode 100644 index 0000000..17022d2 --- /dev/null +++ b/src/pages/Post.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "react"; +import { useParams, Link } from "react-router-dom"; +import matter from "gray-matter"; +import ReactMarkdown from "react-markdown"; +import CodeSnippet from "../components/CodeSnippet"; +import LinkWithIcon from "../components/LinkWithIcon"; +import NotFoundPage from "./404Page"; + +interface PostMeta { + title: string; + date: string; + cover?: string; +} + +export default function Post() { + const { slug } = useParams<{ slug: string }>(); + const [meta, setMeta] = useState<PostMeta | null>(null); + const [content, setContent] = useState(""); + const [notFound, setNotFound] = useState(false); + + useEffect(() => { + import(`../blog/${slug}.md?raw`) + .then((m) => { + const { data, content } = matter(m.default); + setMeta(data as PostMeta); + setContent(content); + }) + .catch(() => setNotFound(true)); + }, [slug]); + + if (!meta) return null; + if (notFound) return <NotFoundPage />; + + return ( + <article className="prose prose-zinc dark:prose-invert mx-auto px-4 py-10"> + <Link to="/blog" className="no-underline"> + ← Zurück zum Blog + </Link> + + {meta.cover && ( + <img + src={meta.cover} + alt={meta.title} + className="w-full h-60 object-cover rounded-lg my-6" + /> + )} + + <h1>{meta.title}</h1> + <p className="text-sm text-zinc-500 mb-8"> + {new Date(meta.date).toLocaleDateString("de-DE")} + </p> + + <article className="markdown-body"> + <ReactMarkdown + components={{ + code({ children }) { + const text = String(children).replace(/\n$/, ""); + return <CodeSnippet code={text} initialLines={5} />; + }, + + a({ href = "", children, ...props }) { + return ( + <LinkWithIcon href={href} {...props}> + {children} + </LinkWithIcon> + ); + }, + }} + > + {content} + </ReactMarkdown> + </article> + </article> + ); +}
\ No newline at end of file |
