用 Next.js、TypeScript 和 Nextra 4 打造現代部落格
概觀
這份教學會帶你做出一個和這個部落格類似、功能齊全的部落格,包含:
- ✅ Nextra 4 搭配 blog 主題
- ✅ Next.js 15 搭配 TypeScript
- ✅ MDX 支援豐富的內容
- ✅ TailwindCSS 4 處理樣式
- ✅ 標籤系統與篩選
- ✅ RSS feed 產生
- ✅ 用 Pagefind 做搜尋功能
- ✅ 深色/淺色主題切換
- ✅ 閱讀時間計算
- ✅ SEO 最佳化
如果想快點上手,你可以直接複製這篇文章的內容貼進 Cursor,claude-4-sonnet 能輕鬆幫你產生程式碼。
第一步:專案設定
初始化專案
npx create-next-app@latest my-blog --typescript --tailwind --app
cd my-blog安裝相依套件
npm install nextra@^4.2.17 nextra-theme-blog@^4.2.17
npm install -D pagefind@^1.3.0更新 package.json
{
"name": "my-blog",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"postbuild": "pagefind --site .next/server/app --output-path public/_pagefind",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^15.3.5",
"nextra": "^4.2.17",
"nextra-theme-blog": "^4.2.17",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.5",
"tailwindcss": "^4",
"typescript": "^5",
"pagefind": "^1.3.0"
}
}第二步:設定 Next.js 和 Nextra
建立 next.config.ts
import nextra from "nextra";
const withNextra = nextra({
defaultShowCopyCode: true,
readingTime: true,
});
export default withNextra({
reactStrictMode: true,
cleanDistDir: true,
});更新 tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}第三步:建立檔案結構
建立以下目錄結構:
src/
├── app/
│ ├── components/
│ │ └── ActiveNavLink.jsx
│ ├── posts/
│ │ ├── get-posts.js
│ │ ├── page.jsx
│ │ └── [your-first-post]/
│ │ └── page.mdx
│ ├── rss.xml/
│ │ └── route.js
│ ├── tags/
│ │ └── [tag]/
│ │ └── page.jsx
│ ├── _meta.global.js
│ ├── globals.css
│ ├── layout.jsx
│ └── page.jsx
└── mdx-components.tsx第四步:建立核心元件
建立 src/app/layout.jsx
import { Footer, Layout, Navbar, ThemeSwitch } from "nextra-theme-blog";
import { Head, Search } from "nextra/components";
import { getPageMap } from "nextra/page-map";
import { ActiveNavLink } from "./components/ActiveNavLink";
import "nextra-theme-blog/style.css";
import "./globals.css";
export const metadata = {
title: "Your Blog Name",
description: "Your blog description",
};
export default async function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<Head backgroundColor={{ dark: "#0f172a", light: "#fefce8" }} />
<body>
<Layout>
<Navbar pageMap={await getPageMap()}>
<ThemeSwitch />
</Navbar>
{children}
<Footer>{new Date().getFullYear()} © Your Name.</Footer>
</Layout>
</body>
</html>
);
}建立 src/app/components/ActiveNavLink.jsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export function ActiveNavLink({ href, children, ...props }) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={isActive ? "nextra-nav-link active" : "nextra-nav-link"}
{...props}
>
{children}
</Link>
);
}建立 src/app/_meta.global.js
export default {
index: {
type: "page",
},
posts: {
type: "page",
},
};第五步:設定 MDX
建立 mdx-components.tsx
import { useMDXComponents as getBlogMDXComponents } from "nextra-theme-blog";
import type { MDXComponents } from "mdx/types";
const blogComponents = getBlogMDXComponents({
h1: ({ children }) => <h1>{children}</h1>,
DateFormatter: ({ date }) =>
`Last updated at ${date.toLocaleDateString("en", {
day: "numeric",
month: "long",
year: "numeric",
})}`,
});
export function useMDXComponents(components: MDXComponents) {
return {
...blogComponents,
...components,
};
}第六步:建立文章管理系統
建立 src/app/posts/get-posts.js
import { normalizePages } from "nextra/normalize-pages";
import { getPageMap } from "nextra/page-map";
export async function getPosts() {
const { directories } = normalizePages({
list: await getPageMap("/posts"),
route: "/posts",
});
return directories
.filter((post) => post.name !== "index")
.sort(
(a, b) => new Date(b.frontMatter.date) - new Date(a.frontMatter.date)
);
}
export async function getTags() {
const posts = await getPosts();
const tags = posts.flatMap((post) => post.frontMatter.tags || []);
return tags;
}第七步:建立頁面
建立 src/app/page.jsx(首頁)
import Link from "next/link";
import { getPosts } from "./posts/get-posts";
export const metadata = {
title: "Your Blog Name",
description: "Welcome to my blog about technology and life",
};
export default async function HomePage() {
const posts = await getPosts();
const recentPosts = posts.slice(0, 3);
return (
<div>
<div>
<h1>Your Name</h1>
<div>
<p>
Welcome to my blog! I write about technology, programming, and life.
</p>
</div>
<hr />
</div>
{/* Recent Posts Section */}
<div>
<h2>Recent Posts</h2>
<div>
{recentPosts.map((post) => (
<div key={post.route}>
<h3>
<Link href={post.route}>{post.frontMatter.title}</Link>
</h3>
{post.frontMatter.description && (
<p>{post.frontMatter.description}</p>
)}
<div>
{post.frontMatter.date && (
<time dateTime={post.frontMatter.date}>
{new Date(post.frontMatter.date).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "long",
day: "numeric",
}
)}
</time>
)}
</div>
</div>
))}
</div>
<div style={{ margin: "32px 0" }}>
<Link href="/posts">View all posts →</Link>
</div>
</div>
</div>
);
}建立 src/app/posts/page.jsx
import Link from "next/link";
import { PostCard } from "nextra-theme-blog";
import { getPosts, getTags } from "./get-posts";
export const metadata = {
title: "All Posts",
};
export default async function PostsPage() {
const tags = await getTags();
const posts = await getPosts();
const allTags = Object.create(null);
for (const tag of tags) {
allTags[tag] ??= 0;
allTags[tag] += 1;
}
return (
<div data-pagefind-ignore="all">
<h1>{metadata.title}</h1>
<div
className="not-prose"
style={{ display: "flex", flexWrap: "wrap", gap: ".5rem" }}
>
{Object.entries(allTags).map(([tag, count]) => (
<Link key={tag} href={`/tags/${tag}`} className="nextra-tag">
{tag} ({count})
</Link>
))}
</div>
{posts.map((post) => (
<PostCard key={post.route} post={post} />
))}
</div>
);
}建立 src/app/tags/[tag]/page.jsx
import { PostCard } from "nextra-theme-blog";
import { getPosts, getTags } from "../../posts/get-posts";
export async function generateMetadata(props) {
const params = await props.params;
return {
title: `Posts Tagged with "${decodeURIComponent(params.tag)}"`,
};
}
export async function generateStaticParams() {
const allTags = await getTags();
return [...new Set(allTags)].map((tag) => ({ tag }));
}
export default async function TagPage(props) {
const params = await props.params;
const { title } = await generateMetadata({ params });
const posts = await getPosts();
return (
<>
<h1>{title}</h1>
{posts
.filter((post) =>
post.frontMatter.tags?.includes(decodeURIComponent(params.tag))
)
.map((post) => (
<PostCard key={post.route} post={post} />
))}
</>
);
}第八步:建立 RSS Feed
建立 src/app/rss.xml/route.js
import { getPosts } from "../posts/get-posts.js";
const CONFIG = {
title: "Your Blog Name",
siteUrl: "https://yourdomain.com",
description: "Latest blog posts",
lang: "en-us",
};
export async function GET() {
const allPosts = await getPosts();
const posts = allPosts
.map(
(post) => ` <item>
<title>${post.frontMatter.title}</title>
<description>${post.frontMatter.description || ""}</description>
<link>${CONFIG.siteUrl}${post.route}</link>
<pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>
</item>`
)
.join("\n");
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${CONFIG.title}</title>
<link>${CONFIG.siteUrl}</link>
<description>${CONFIG.description}</description>
<language>${CONFIG.lang}</language>
${posts}
</channel>
</rss>`;
return new Response(xml, {
headers: {
"Content-Type": "application/rss+xml",
},
});
}第九步:建立你的第一篇部落格文章
建立 src/app/posts/my-first-post/page.mdx
---
title: My First Blog Post
date: 2024/01/15
description: Welcome to my new blog built with Next.js and Nextra!
tags: [web development, next.js, blogging]
author: Your Name
---
# My First Blog Post
Welcome to my new blog! This post demonstrates the power of MDX for creating rich, interactive content.
## Features
This blog includes:
- **MDX Support**: Write JSX directly in your markdown
- **Syntax Highlighting**: Beautiful code blocks
- **Tag System**: Organize posts by topic
- **RSS Feed**: Keep readers updated
- **Search**: Find content quickly
- **Dark Mode**: Easy on the eyes
## Code Example
Here's a simple React component:
```jsx
function HelloWorld() {
return <h1>Hello, World!</h1>;
}
```
## What's Next?
Stay tuned for more posts about web development, programming tips, and technology insights!第十步:加上自訂樣式
建立 src/app/globals.css
/* Override Nextra blog theme for custom styling */
/* Make prose content wider on desktop */
@media (min-width: 768px) {
.x\:prose {
max-width: 90ch !important; /* Increase from default ~65ch to 90ch */
}
}
/* Custom tag styling */
.nextra-tag {
@apply px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors;
}
.nextra-tag.active {
@apply bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200;
}
/* Custom navigation link styling */
.nextra-nav-link {
@apply px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors;
}
.nextra-nav-link.active {
@apply bg-gray-100 dark:bg-gray-800 font-medium;
}第十一步:執行與部署
開發
npm run dev開啟 http://localhost:3000 就能看到你的部落格!
建置正式版
npm run build
npm start部署到 Vercel
- 把程式碼推到 GitHub
- 在 Vercel 連上你的儲存庫
- 自動部署!
第十二步:自訂與延伸的點子
加上更多功能
- 電子報訂閱:串接 ConvertKit 或 Mailchimp
- 留言:加入 Giscus 或 Disqus 留言
- 分析:Google Analytics 或 Plausible
- 圖片最佳化:Next.js 的 Image 元件
- 閱讀進度:長文的進度條
- 相關文章:推薦相似內容
- 作者頁面:支援多位作者
- 系列文章:把相關文章歸成一組
SEO 強化
// Add to your layout.jsx
export const metadata = {
title: {
default: "Your Blog Name",
template: "%s | Your Blog Name",
},
description: "Your blog description",
keywords: ["next.js", "blog", "typescript"],
authors: [{ name: "Your Name" }],
creator: "Your Name",
openGraph: {
type: "website",
locale: "en_US",
url: "https://yourdomain.com",
siteName: "Your Blog Name",
},
twitter: {
card: "summary_large_image",
creator: "@yourusername",
},
};結語
你現在有了一個功能完整的部落格,具備:
- ✅ 現代的 Next.js 15 + TypeScript 設定
- ✅ 用 Nextra 4 輕鬆管理 MDX 內容
- ✅ 整理內容的標籤系統
- ✅ 給訂閱者的 RSS feed
- ✅ 搜尋功能
- ✅ 深色/淺色主題切換
- ✅ SEO 最佳化
- ✅ 響應式行動裝置設計
你的部落格已經可以開始寫內容,之後也能隨著需求增長輕鬆加上更多功能!
寫得開心!🚀 想快點上手的話,可以複製這篇文章的內容貼進 Cursor。