# Statischer Markdown-Blog in SvelteKit - Implementierungsguide ## Übersicht: 3 Ansätze für statische Markdown-Blogs ### 1. **mdsvex** - Markdown als Svelte-Komponenten (Empfohlen) ### 2. **Vite Glob Import** - Dynamisches Laden zur Build-Zeit ### 3. **Content Collections** - Strukturierte Markdown-Verwaltung --- ## Ansatz 1: mdsvex (Empfohlen) ⭐ mdsvex verwandelt Markdown-Dateien in Svelte-Komponenten. Du kannst Svelte-Components direkt im Markdown verwenden! ### Installation & Setup ```bash npm install -D mdsvex npm install -D rehype-slug rehype-autolink-headings ``` ### Konfiguration in `vite.config.js` ```javascript import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], }); ``` ### Konfiguration in `svelte.config.js` ```javascript import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { mdsvex } from 'mdsvex'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; /** @type {import('mdsvex').MdsvexOptions} */ const mdsvexOptions = { extensions: ['.md', '.mdx'], layout: { blog: './src/lib/layouts/BlogLayout.svelte', }, rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]], }; /** @type {import('@sveltejs/kit').Config} */ const config = { extensions: ['.svelte', '.md', '.mdx'], preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)], kit: { adapter: adapter(), }, }; export default config; ``` ### Blog-Layout erstellen ```svelte {title} | uload Blog {#if image} {/if}

{title}

{author}
{#if tags.length > 0}
{#each tags as tag} {tag} {/each}
{/if}
``` ### Blog-Post Beispiel ```markdown --- title: Die Psychologie kurzer URLs date: 2024-01-15 author: Till Schneider excerpt: Warum 42% weniger Menschen auf lange URLs klicken tags: [Psychology, Marketing, URLs] image: /blog/psychology-urls.jpg --- # Die Psychologie kurzer URLs 42% weniger Klicks bei langen URLs – diese Zahl zeigt... ## Warum kurze URLs funktionieren Unser Gehirn verarbeitet kurze Informationen schneller... ``` ### Routing-Struktur ``` src/routes/ └── blog/ ├── +page.svelte # Blog-Übersicht ├── +page.js # Posts laden └── [slug]/ └── +page.js # Dynamisches Routing ``` ### Blog-Übersicht mit allen Posts ```javascript // src/routes/blog/+page.js export async function load() { const posts = import.meta.glob('/src/content/blog/*.md'); const postPromises = Object.entries(posts).map(async ([path, resolver]) => { const { metadata } = await resolver(); const slug = path.split('/').pop().replace('.md', ''); return { slug, ...metadata, }; }); const allPosts = await Promise.all(postPromises); // Nach Datum sortieren allPosts.sort((a, b) => new Date(b.date) - new Date(a.date)); return { posts: allPosts, }; } ``` ```svelte

Blog

{#each data.posts as post}

{post.title}

{post.excerpt}

{new Date(post.date).toLocaleDateString('de-DE')}
{/each}
``` --- ## Ansatz 2: Vite Glob Import (Einfacher) Dieser Ansatz lädt alle Markdown-Dateien zur Build-Zeit ohne mdsvex. ### Setup ```bash npm install -D gray-matter marked ``` ### Markdown-Parser Utility ```javascript // src/lib/utils/markdown.js import { marked } from 'marked'; import matter from 'gray-matter'; export function parseMarkdown(content) { const { data, content: markdown } = matter(content); const html = marked(markdown); return { metadata: data, html, }; } ``` ### Posts laden ```javascript // src/routes/blog/+page.server.js import { parseMarkdown } from '$lib/utils/markdown'; export async function load() { const postFiles = import.meta.glob('/src/content/blog/*.md', { query: '?raw', import: 'default', }); const posts = await Promise.all( Object.entries(postFiles).map(async ([path, resolver]) => { const content = await resolver(); const { metadata, html } = parseMarkdown(content); const slug = path.split('/').pop().replace('.md', ''); return { slug, ...metadata, content: html, }; }) ); return { posts: posts.sort((a, b) => new Date(b.date) - new Date(a.date)), }; } ``` ### Einzelner Blog-Post ```javascript // src/routes/blog/[slug]/+page.server.js import { parseMarkdown } from '$lib/utils/markdown'; import { error } from '@sveltejs/kit'; export async function load({ params }) { try { const post = await import(`../../../content/blog/${params.slug}.md?raw`); const { metadata, html } = parseMarkdown(post.default); return { ...metadata, content: html, slug: params.slug, }; } catch (e) { throw error(404, 'Post nicht gefunden'); } } ``` ```svelte {data.title}

{data.title}

{@html data.content}
``` --- ## Ansatz 3: Content Collections (Strukturiert) Ein strukturierterer Ansatz mit Typ-Sicherheit und besserer Organisation. ### Content-Struktur ``` src/content/ ├── blog/ │ ├── 2024-01-15-psychologie-urls.md │ ├── 2024-01-20-link-tracking.md │ └── _drafts/ │ └── upcoming-post.md └── config.js ``` ### Content-Config ```javascript // src/content/config.js import { z } from 'zod'; export const blogSchema = z.object({ title: z.string(), date: z.date(), author: z.string(), excerpt: z.string(), tags: z.array(z.string()), image: z.string().optional(), draft: z.boolean().default(false), }); export const collections = { blog: { schema: blogSchema, directory: 'src/content/blog', }, }; ``` ### Content-Loader ```javascript // src/lib/content.js import { blogSchema } from '../content/config'; import matter from 'gray-matter'; import { marked } from 'marked'; export async function getCollection(collection) { const posts = import.meta.glob('/src/content/blog/*.md', { query: '?raw', import: 'default', }); const entries = await Promise.all( Object.entries(posts).map(async ([path, resolver]) => { // Drafts überspringen if (path.includes('_drafts')) return null; const content = await resolver(); const { data, content: markdown } = matter(content); // Schema validieren const metadata = blogSchema.parse({ ...data, date: new Date(data.date), }); // Draft-Posts in Production ausblenden if (metadata.draft && import.meta.env.PROD) return null; const slug = path.split('/').pop().replace('.md', ''); const html = marked(markdown); return { slug, ...metadata, content: html, }; }) ); return entries.filter(Boolean); } export async function getEntry(collection, slug) { const posts = await getCollection(collection); return posts.find((post) => post.slug === slug); } ``` --- ## Features für alle Ansätze ### 1. RSS Feed Generation ```javascript // src/routes/blog/rss.xml/+server.js import { getCollection } from '$lib/content'; export async function GET() { const posts = await getCollection('blog'); const site = 'https://ulo.ad'; const xml = ` uload Blog ${site}/blog Insights über URLs, Marketing und Psychologie ${posts .map( (post) => ` ${post.title} ${site}/blog/${post.slug} ${post.excerpt} ${new Date(post.date).toUTCString()} ` ) .join('')} `; return new Response(xml, { headers: { 'Content-Type': 'application/xml', }, }); } ``` ### 2. Sitemap Generation ```javascript // src/routes/sitemap.xml/+server.js export async function GET() { const posts = await getCollection('blog'); const site = 'https://ulo.ad'; const xml = ` ${posts .map( (post) => ` ${site}/blog/${post.slug} ${new Date(post.date).toISOString()} monthly 0.8 ` ) .join('')} `; return new Response(xml, { headers: { 'Content-Type': 'application/xml', }, }); } ``` ### 3. Syntax Highlighting ```bash npm install -D shiki ``` ```javascript // src/lib/utils/highlight.js import { codeToHtml } from 'shiki'; export async function highlightCode(code, lang = 'javascript') { return await codeToHtml(code, { lang, theme: 'github-dark', }); } ``` ### 4. Reading Time ```javascript // src/lib/utils/reading-time.js export function calculateReadingTime(content) { const wordsPerMinute = 200; const words = content.split(/\s+/).length; const minutes = Math.ceil(words / wordsPerMinute); return { minutes, words, text: `${minutes} Min. Lesezeit`, }; } ``` ### 5. Table of Contents ```javascript // src/lib/utils/toc.js export function extractHeadings(html) { const regex = /]*id="([^"]*)"[^>]*>(.*?)<\/h\1>/g; const headings = []; let match; while ((match = regex.exec(html)) !== null) { headings.push({ level: parseInt(match[1]), id: match[2], text: match[3].replace(/<[^>]*>/g, ''), }); } return headings; } ``` --- ## Performance-Optimierungen ### 1. Prerendering aktivieren ```javascript // src/routes/blog/+page.js export const prerender = true; // Statisch zur Build-Zeit generieren ``` ### 2. Lazy Loading für Bilder ```svelte
(isInView = true)}> {#if isInView} {:else}
{/if}
``` ### 3. Content Caching ```javascript // src/lib/cache.js const cache = new Map(); const CACHE_DURATION = 1000 * 60 * 5; // 5 Minuten export function getCached(key, fetcher) { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } const data = fetcher(); cache.set(key, { data, timestamp: Date.now() }); return data; } ``` --- ## Deployment-Tipps ### Build-Optimierungen ```javascript // vite.config.js export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { markdown: ['marked', 'gray-matter'], highlight: ['shiki'], }, }, }, }, }); ``` ### Static Adapter für volle statische Generierung ```bash npm install -D @sveltejs/adapter-static ``` ```javascript // svelte.config.js import adapter from '@sveltejs/adapter-static'; export default { kit: { adapter: adapter({ pages: 'build', assets: 'build', fallback: null, precompress: true, }), prerender: { entries: ['*'], // Alle Seiten prerendern }, }, }; ``` --- ## Schnellstart-Anleitung ### 1. mdsvex installieren & konfigurieren ```bash npm install -D mdsvex gray-matter rehype-slug rehype-autolink-headings ``` ### 2. Blog-Struktur erstellen ```bash mkdir -p src/content/blog mkdir -p src/lib/layouts mkdir -p src/routes/blog/\[slug\] ``` ### 3. Ersten Post erstellen ```bash cat > src/content/blog/hello-world.md << 'EOF' --- title: Hello World date: 2024-01-15 author: Till Schneider excerpt: Mein erster Blog-Post tags: [Announcement] --- # Hello World Dies ist mein erster Blog-Post mit Markdown! EOF ``` ### 4. Dev-Server starten ```bash npm run dev ``` Besuche `http://localhost:5173/blog` --- ## Vorteile des statischen Ansatzes ✅ **Performance**: Zur Build-Zeit generiert = ultraschnell ✅ **SEO**: Vollständig gerenderte HTML-Seiten ✅ **Versionskontrolle**: Alle Posts in Git ✅ **Einfachheit**: Kein CMS, keine Datenbank ✅ **Sicherheit**: Keine dynamischen Vulnerabilities ✅ **Hosting**: Überall deploybar (Vercel, Netlify, etc.) ## Nachteile ❌ **Rebuild nötig**: Bei jedem neuen Post ❌ **Keine Kommentare**: Ohne externe Services ❌ **Kein WYSIWYG**: Markdown-Kenntnisse nötig ❌ **Keine Scheduled Posts**: Ohne CI/CD-Automation --- **Empfehlung**: Starte mit **mdsvex** (Ansatz 1) - es bietet die beste Balance zwischen Einfachheit und Features, plus du kannst Svelte-Components direkt in Markdown verwenden!