brianchitester.com文章

用 Next.js、TypeScript 和 Nextra 4 打造現代部落格

概觀

這份教學會帶你做出一個和這個部落格類似、功能齊全的部落格,包含:

如果想快點上手,你可以直接複製這篇文章的內容貼進 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

  1. 把程式碼推到 GitHub
  2. 在 Vercel 連上你的儲存庫
  3. 自動部署!

第十二步:自訂與延伸的點子

加上更多功能

  1. 電子報訂閱:串接 ConvertKit 或 Mailchimp
  2. 留言:加入 Giscus 或 Disqus 留言
  3. 分析:Google Analytics 或 Plausible
  4. 圖片最佳化:Next.js 的 Image 元件
  5. 閱讀進度:長文的進度條
  6. 相關文章:推薦相似內容
  7. 作者頁面:支援多位作者
  8. 系列文章:把相關文章歸成一組

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", }, };

結語

你現在有了一個功能完整的部落格,具備:

你的部落格已經可以開始寫內容,之後也能隨著需求增長輕鬆加上更多功能!

寫得開心!🚀 想快點上手的話,可以複製這篇文章的內容貼進 Cursor。

2026 © Brian Chitester.