{post.title}
{post.summary}
diff --git a/package-lock.json b/package-lock.json index 0899a99..4add222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bdfd272..7c5979f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/posts/test-post.md b/posts/test-post.md new file mode 100644 index 0000000..4e082e8 --- /dev/null +++ b/posts/test-post.md @@ -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. \ No newline at end of file diff --git a/src/app/api/posts/[slug]/route.ts b/src/app/api/posts/[slug]/route.ts new file mode 100644 index 0000000..7f03b89 --- /dev/null +++ b/src/app/api/posts/[slug]/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts new file mode 100644 index 0000000..1fbd9a0 --- /dev/null +++ b/src/app/api/posts/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 060780f..5543e0d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - +
{children} ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 69db60d..d27c4fc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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.summary}