{T}

Next.js 博客项目开发

使用 Next.js App Router 构建技术博客的核心方案:MDX 内容管理实现写作与渲染分离,Metadata API 实现 SEO 优化,next-themes 实现深色模式,@next/bundle-analyzer 实现性能分析。

技术选型

功能方案说明
内容管理MDX + 文件系统无需 CMS,Git 管理内容
样式Tailwind CSS原子化 CSS,暗色模式原生支持
SEONext.js Metadata API动态生成 OG Image、结构化数据
深色模式next-themes跟随系统/手动切换,无闪烁
国际化next-intl路由级 i18n
性能分析@next/bundle-analyzer可视化打包体积
评论Giscus / Twikoo基于 GitHub Discussions

项目结构

code
blog/
├── app/
│   ├── layout.tsx           # 根布局(主题 Provider)
│   ├── page.tsx             # 首页(文章列表)
│   ├── blog/
│   │   ├── page.tsx         # 文章列表页
│   │   └── [slug]/
│   │       └── page.tsx     # 文章详情页
│   └── about/
│       └── page.tsx         # 关于页
├── content/
│   └── posts/               # MDX 文章
│       ├── hello-world.mdx
│       └── nextjs-app-router.mdx
├── components/
│   ├── ThemeToggle.tsx      # 主题切换按钮
│   ├── PostCard.tsx         # 文章卡片
│   └── MDXComponents.tsx    # MDX 自定义组件
├── lib/
│   └── posts.ts             # 文章读取/解析工具
└── next.config.ts

MDX 内容管理

文章 Frontmatter 规范

mdx
---
title: "Next.js App Router 深度指南"
date: "2025-06-15"
tags: ["Next.js", "React", "SSR"]
summary: "全面解析 App Router 的路由、数据获取和渲染策略"
cover: "/images/nextjs-guide.png"
---

正文内容...支持 JSX 组件嵌入。

文章解析工具

typescript
// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

const postsDir = path.join(process.cwd(), 'content/posts');

export interface Post {
  slug: string;
  title: string;
  date: string;
  tags: string[];
  summary: string;
}

export function getAllPosts(): Post[] {
  const files = fs.readdirSync(postsDir).filter((f) => f.endsWith('.mdx'));

  return files
    .map((filename) => {
      const slug = filename.replace(/\.mdx$/, '');
      const raw = fs.readFileSync(path.join(postsDir, filename), 'utf-8');
      const { data } = matter(raw);
      return { slug, ...data } as Post;
    })
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

export function getPostBySlug(slug: string) {
  const raw = fs.readFileSync(path.join(postsDir, `${slug}.mdx`), 'utf-8');
  const { data, content } = matter(raw);
  return { metadata: data as Post, content };
}

SEO 优化

动态 Metadata

tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPostBySlug } from '@/lib/posts';

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const { metadata } = getPostBySlug(params.slug);

  return {
    title: `${metadata.title} | My Blog`,
    description: metadata.summary,
    openGraph: {
      title: metadata.title,
      description: metadata.summary,
      type: 'article',
      publishedTime: metadata.date,
      tags: metadata.tags,
      images: [{ url: metadata.cover }],
    },
    twitter: {
      card: 'summary_large_image',
      title: metadata.title,
      description: metadata.summary,
    },
  };
}

Sitemap 与 RSS

typescript
// app/sitemap.ts
import { getAllPosts } from '@/lib/posts';

export default function sitemap() {
  const posts = getAllPosts().map((post) => ({
    url: `https://myblog.com/blog/${post.slug}`,
    lastModified: new Date(post.date),
    changeFrequency: 'monthly' as const,
    priority: 0.8,
  }));

  return [
    { url: 'https://myblog.com', lastModified: new Date(), priority: 1 },
    ...posts,
  ];
}

深色模式

next-themes 集成

tsx
// app/layout.tsx
import { ThemeProvider } from 'next-themes';

export default function RootLayout({ children }) {
  return (
    <html lang="zh" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
tsx
// components/ThemeToggle.tsx
'use client';

import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

export default function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
}

性能分析

Bundle Analyzer 配置

typescript
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

export default withBundleAnalyzer({
  // 其他配置
});
bash
# 运行分析
ANALYZE=true npm run build

性能优化清单

优化项措施效果
图片next/image + WebP/AVIF减少 50%+ 图片体积
字体next/font 本地加载消除 FOUT/FOIT
代码分割按路由自动分割 + 动态 import减少首屏 JS
缓存ISR + CDN 边缘缓存TTFB < 100ms
组件Server Component 优先减少客户端 JS

评论系统(Giscus)

tsx
// components/Comments.tsx
'use client';

import { useTheme } from 'next-themes';

export default function Comments({ slug }: { slug: string }) {
  const { theme } = useTheme();

  return (
    <div
      className="giscus"
      data-repo="username/blog-comments"
      data-repo-id="R_xxx"
      data-category="Announcements"
      data-category-id="DIC_xxx"
      data-mapping="specific"
      data-term={slug}
      data-theme={theme === 'dark' ? 'dark' : 'light'}
      data-lang="zh-CN"
    />
  );
}

参考资源