'use client'; import { useEffect, useState } from 'react'; import { format } from 'date-fns'; import Link from 'next/link'; interface Post { slug: string; title: string; date: string; tags: string[]; summary: string; content: string; createdAt: string; } export default function PostPage({ params }: { params: { slug: string[] } }) { const [post, setPost] = useState(null); // Join the slug array to get the full path const slugPath = Array.isArray(params.slug) ? params.slug.join('/') : params.slug; useEffect(() => { // Initial load loadPost(); // Set up polling for changes const interval = setInterval(loadPost, 2000); // Cleanup return () => clearInterval(interval); }, [slugPath]); // On post load or update, scroll to anchor in hash if present useEffect(() => { // Scroll to anchor if hash is present const scrollToHash = () => { if (window.location.hash) { const id = window.location.hash.substring(1); const el = document.getElementById(id); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }; // On initial load scrollToHash(); // Listen for hash changes window.addEventListener('hashchange', scrollToHash); return () => { window.removeEventListener('hashchange', scrollToHash); }; }, [post]); // Intercept anchor clicks in rendered markdown to ensure smooth scrolling to headings useEffect(() => { // Find the rendered markdown container const prose = document.querySelector('.prose'); if (!prose) return; /** * Handles clicks on anchor links (e.g. Table of Contents links) inside the markdown. * - If the link is an in-page anchor (starts with #), prevent default navigation. * - Try to find an element with the corresponding id and scroll to it. * - If not found, search all headings for one whose text matches the anchor (case-insensitive, ignoring spaces/punctuation). * - If a match is found, scroll to that heading. * - Update the URL hash without reloading the page. */ const handleClick = (e: Event) => { if (!(e instanceof MouseEvent)) return; let target = e.target as HTMLElement | null; // Traverse up to find the closest anchor tag while (target && target.tagName !== 'A') { target = target.parentElement; } if (target && target.tagName === 'A' && target.getAttribute('href')?.startsWith('#')) { e.preventDefault(); const id = target.getAttribute('href')!.slice(1); let el = document.getElementById(id); if (!el) { // Try to find a heading whose text matches the id (case-insensitive, ignoring spaces/punctuation) const headings = prose.querySelectorAll('h1, h2, h3, h4, h5, h6'); const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]+/g, ''); const normId = normalize(id); const found = Array.from(headings).find(h => normalize(h.textContent || '') === normId); el = (found as HTMLElement) || null; } if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); history.replaceState(null, '', `#${id}`); } } }; prose.addEventListener('click', handleClick); return () => { prose.removeEventListener('click', handleClick); }; }, [post]); const loadPost = async () => { try { const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`); const data = await response.json(); setPost(data); } catch (error) { console.error('Fehler beim Laden des Beitrags:', error); } }; if (!post) { return
Lädt...
; } return (
← Zurück zu den Beiträgen

{post.title}

{post.date ? (
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
) : (
⚙️ ⚙️
In Bearbeitung
)}
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}
{post.tags.map((tag) => ( {tag} ))}
); }