lesssagoo

This commit is contained in:
rattatwinko
2025-06-16 17:06:26 +02:00
parent 037c7cd31e
commit 439d673e8f
9 changed files with 323 additions and 52 deletions

95
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.16",
"autoprefixer": "^10.4.17",
"chokidar": "^4.0.3",
"date-fns": "^3.3.1",
"gray-matter": "^4.0.3",
"next": "14.1.0",
@@ -2785,39 +2786,18 @@
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chownr": {
@@ -8102,15 +8082,16 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/reflect.getprototypeof": {
@@ -9275,6 +9256,42 @@
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
@@ -9310,6 +9327,18 @@
}
}
},
"node_modules/tailwindcss/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",

View File

@@ -13,6 +13,7 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.16",
"autoprefixer": "^10.4.17",
"chokidar": "^4.0.3",
"date-fns": "^3.3.1",
"gray-matter": "^4.0.3",
"next": "14.1.0",

22
posts/test-post.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: "Testing Hot Reloading"
date: "2024-03-10"
tags: ["test", "feature"]
summary: "A test post to demonstrate hot reloading and date-based sorting"
---
# Testing Hot Reloading
This is a test post to demonstrate the hot reloading feature of our blog system. When you add or modify a post, the changes should appear immediately without needing to refresh the page.
## Features Being Tested
1. Hot reloading of new posts
2. File creation date sorting
3. Real-time updates
## How It Works
The system uses `chokidar` to watch the `posts/` directory for changes. When a new file is added or modified, the blog automatically updates to show the latest content.
The posts are sorted by their file creation date, so newer posts appear at the top of the list.

View File

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'posts');
// Function to get file creation date
function getFileCreationDate(filePath: string): Date {
const stats = fs.statSync(filePath);
return stats.birthtime;
}
async function getPostBySlug(slug: string) {
const realSlug = slug.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const createdAt = getFileCreationDate(fullPath);
const processedContent = await remark()
.use(html)
.process(content);
return {
slug: realSlug,
title: data.title,
date: data.date,
tags: data.tags || [],
summary: data.summary,
content: processedContent.toString(),
createdAt: createdAt.toISOString(),
};
}
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
try {
const post = await getPostBySlug(params.slug);
return NextResponse.json(post);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 });
}
}

View File

@@ -0,0 +1,61 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'posts');
// Function to get file creation date
function getFileCreationDate(filePath: string): Date {
const stats = fs.statSync(filePath);
return stats.birthtime;
}
async function getPostBySlug(slug: string) {
const realSlug = slug.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const createdAt = getFileCreationDate(fullPath);
const processedContent = await remark()
.use(html)
.process(content);
return {
slug: realSlug,
title: data.title,
date: data.date,
tags: data.tags || [],
summary: data.summary,
content: processedContent.toString(),
createdAt: createdAt.toISOString(),
};
}
async function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = await Promise.all(
fileNames
.filter((fileName) => fileName.endsWith('.md'))
.map(async (fileName) => {
const slug = fileName.replace(/\.md$/, '');
return getPostBySlug(slug);
})
);
return allPostsData.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}
export async function GET() {
try {
const posts = await getAllPosts();
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });
}
}

View File

@@ -5,8 +5,8 @@ import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'My Markdown Blog',
description: 'A blog built with Next.js and Markdown',
title: 'Sebastian Zinkls - Blog',
description: 'Ein Blog von Sebastian Zinkl, gebaut mit Next.js und Markdown',
};
export default function RootLayout({
@@ -15,7 +15,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<html lang="de">
<body className={inter.className}>{children}</body>
</html>
);

View File

@@ -1,20 +1,54 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { getAllPosts } from '@/lib/markdown';
import { format } from 'date-fns';
export default async function Home() {
const posts = await getAllPosts();
interface Post {
slug: string;
title: string;
date: string;
tags: string[];
summary: string;
content: string;
createdAt: string;
}
export default function Home() {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
// Initial load
loadPosts();
// Set up polling for changes
const interval = setInterval(loadPosts, 2000);
// Cleanup
return () => clearInterval(interval);
}, []);
const loadPosts = async () => {
try {
const response = await fetch('/api/posts');
const data = await response.json();
setPosts(data);
} catch (error) {
console.error('Fehler beim Laden der Beiträge:', error);
}
};
return (
<main className="min-h-screen p-8 max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-8">My Blog</h1>
<h1 className="text-4xl font-bold mb-8">Sebastian Zinkls - Blog</h1>
<div className="grid gap-8">
{posts.map((post) => (
<article key={post.slug} className="border rounded-lg p-6 hover:shadow-lg transition-shadow">
<Link href={`/posts/${post.slug}`}>
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
<div className="text-gray-600 mb-4">
{format(new Date(post.date), 'MMMM d, yyyy')}
<div>Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}</div>
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
</div>
<p className="text-gray-700 mb-4">{post.summary}</p>
<div className="flex gap-2">

View File

@@ -1,26 +1,57 @@
import { getPostBySlug, getAllPosts } from '@/lib/markdown';
'use client';
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import Link from 'next/link';
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
interface Post {
slug: string;
title: string;
date: string;
tags: string[];
summary: string;
content: string;
createdAt: string;
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
export default function PostPage({ params }: { params: { slug: string } }) {
const [post, setPost] = useState<Post | null>(null);
useEffect(() => {
// Initial load
loadPost();
// Set up polling for changes
const interval = setInterval(loadPost, 2000);
// Cleanup
return () => clearInterval(interval);
}, [params.slug]);
const loadPost = async () => {
try {
const response = await fetch(`/api/posts/${params.slug}`);
const data = await response.json();
setPost(data);
} catch (error) {
console.error('Fehler beim Laden des Beitrags:', error);
}
};
if (!post) {
return <div>Lädt...</div>;
}
return (
<article className="min-h-screen p-8 max-w-4xl mx-auto">
<Link href="/" className="text-blue-600 hover:underline mb-8 inline-block">
Back to posts
Zurück zu den Beiträgen
</Link>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600 mb-8">
{format(new Date(post.date), 'MMMM d, yyyy')}
<div>Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}</div>
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
</div>
<div className="flex gap-2 mb-8">

View File

@@ -3,6 +3,7 @@ import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import chokidar from 'chokidar';
export interface Post {
slug: string;
@@ -11,15 +12,23 @@ export interface Post {
tags: string[];
summary: string;
content: string;
createdAt: Date;
}
const postsDirectory = path.join(process.cwd(), 'posts');
// Function to get file creation date
function getFileCreationDate(filePath: string): Date {
const stats = fs.statSync(filePath);
return stats.birthtime;
}
export async function getPostBySlug(slug: string): Promise<Post> {
const realSlug = slug.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const createdAt = getFileCreationDate(fullPath);
const processedContent = await remark()
.use(html)
@@ -32,6 +41,7 @@ export async function getPostBySlug(slug: string): Promise<Post> {
tags: data.tags || [],
summary: data.summary,
content: processedContent.toString(),
createdAt,
};
}
@@ -46,10 +56,45 @@ export async function getAllPosts(): Promise<Post[]> {
})
);
return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
// Sort by creation date (newest first)
return allPostsData.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
export async function getPostsByTag(tag: string): Promise<Post[]> {
const allPosts = await getAllPosts();
return allPosts.filter((post) => post.tags.includes(tag));
}
// File watcher setup
let watcher: chokidar.FSWatcher | null = null;
let onChangeCallback: (() => void) | null = null;
export function watchPosts(callback: () => void) {
if (watcher) {
watcher.close();
}
onChangeCallback = callback;
watcher = chokidar.watch(postsDirectory, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true
});
watcher
.on('add', handleFileChange)
.on('change', handleFileChange)
.on('unlink', handleFileChange);
}
function handleFileChange() {
if (onChangeCallback) {
onChangeCallback();
}
}
export function stopWatching() {
if (watcher) {
watcher.close();
watcher = null;
}
onChangeCallback = null;
}