diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx
new file mode 100644
index 0000000..6637a89
--- /dev/null
+++ b/app/blog/[slug]/page.tsx
@@ -0,0 +1,88 @@
+import { notFound } from 'next/navigation'
+import Link from 'next/link'
+import { getPosts, getPost } from '@/lib/posts'
+import { tagToSlug } from '@/lib/tags'
+import { TableOfContents } from '@/components/posts/TableOfContents'
+import { ScrollToTop } from '@/components/ui/ScrollToTop'
+import { ReadingProgress } from '@/components/ui/ReadingProgress'
+
+export const dynamicParams = false
+export const dynamic = 'force-static'
+
+export async function generateStaticParams() {
+ const posts = await getPosts()
+ if (posts.length === 0) {
+ return [{ slug: '__no-posts__' }]
+ }
+ return posts.map((post) => ({ slug: post.slug }))
+}
+
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
+ const slug = (await params).slug
+ const post = await getPost(slug)
+ if (!post) return { title: 'Not Found' }
+ return { title: post.title }
+}
+
+export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
+ const slug = (await params).slug
+
+ if (slug === '__no-posts__') notFound()
+
+ const post = await getPost(slug)
+
+ if (!post) notFound()
+
+ const { default: PostContent } = await import(
+ /* turbopackOptional: true */
+ `@/content/posts/${slug}.mdx`
+ )
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {post.title}
+
+
+ {post.author && by {post.author}}
+ {post.readingTime} min read
+
+ {post.coverImage && (
+
+ )}
+ {post.tags.length > 0 && (
+
+ {post.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx
new file mode 100644
index 0000000..fd556a7
--- /dev/null
+++ b/app/blog/layout.tsx
@@ -0,0 +1,16 @@
+import { Header } from '@/components/layout/Header'
+import { Footer } from '@/components/layout/Footer'
+import { ScrollProgress } from '@/components/ui/ScrollProgress'
+
+export default function BlogLayout({ children }: { children: React.ReactNode }) {
+ return (
+ <>
+
+
+
+ {children}
+
+
+ >
+ )
+}
diff --git a/app/blog/page.tsx b/app/blog/page.tsx
new file mode 100644
index 0000000..0c84a5a
--- /dev/null
+++ b/app/blog/page.tsx
@@ -0,0 +1,30 @@
+import { getPosts } from '@/lib/posts'
+import { PostSearch } from '@/components/posts/PostSearch'
+
+export const metadata = { title: 'Field Notes' }
+
+export default async function PostsPage() {
+ const posts = await getPosts()
+
+ return (
+ <>
+
+
+ {posts.length > 0 ? (
+
+ ) : (
+
+ No posts yet
+ The archive is empty.
+
+ Publish an MDX post to populate this list and enable archive search.
+
+
+ )}
+ >
+ )
+}
diff --git a/app/blog/tags/[tag]/page.tsx b/app/blog/tags/[tag]/page.tsx
new file mode 100644
index 0000000..a590e1c
--- /dev/null
+++ b/app/blog/tags/[tag]/page.tsx
@@ -0,0 +1,61 @@
+import type { Metadata } from 'next'
+import { notFound } from 'next/navigation'
+import { PostCard } from '@/components/posts/PostCard'
+import { getAllTags, getPostsByTag } from '@/lib/posts'
+
+type TagPageProps = {
+ params: Promise<{ tag: string }>
+}
+
+export const dynamicParams = false
+export const dynamic = 'force-static'
+
+export async function generateStaticParams() {
+ const tags = await getAllTags()
+ if (tags.length === 0) {
+ return [{ tag: '__no-tags__' }]
+ }
+ return tags.map((tag) => ({ tag: tag.slug }))
+}
+
+export async function generateMetadata({ params }: TagPageProps): Promise {
+ const slug = (await params).tag
+ const tags = await getAllTags()
+ const tag = tags.find((item) => item.slug === slug)
+
+ if (!tag) return { title: 'Not Found' }
+
+ return { title: `${tag.label} posts` }
+}
+
+export default async function TagPage({ params }: TagPageProps) {
+ const slug = (await params).tag
+
+ if (slug === '__no-tags__') notFound()
+
+ const tags = await getAllTags()
+ const tag = tags.find((item) => item.slug === slug)
+
+ if (!tag) notFound()
+
+ const posts = await getPostsByTag(slug)
+
+ return (
+
+
+
+ {posts.map((post, index) => (
+ -
+
+
+ ))}
+
+
+ )
+}
diff --git a/app/blog/template.tsx b/app/blog/template.tsx
new file mode 100644
index 0000000..c684ac4
--- /dev/null
+++ b/app/blog/template.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import { m } from "motion/react";
+
+export default function Template({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}