使用 Next.js 15 搭建现代化博客系统

详细介绍如何使用 Next.js 15、TypeScript 和 Tailwind CSS 从零开始搭建一个功能完整的现代化博客系统。

使用 Next.js 15 搭建现代化博客系统

在这篇文章中,我将详细介绍如何使用 Next.js 15TypeScriptTailwind CSS 从零开始搭建一个功能完整的现代化博客系统。

为什么选择 Next.js?

Next.js 是一个基于 React 的全栈框架,具有以下优势:

  • 静态网站生成 (SSG) - 更好的性能和 SEO
  • 服务端渲染 (SSR) - 首屏加载速度快
  • 文件系统路由 - 简化路由配置
  • 内置优化 - 图片优化、代码分割等
  • TypeScript 支持 - 开箱即用的类型安全

项目初始化

首先创建一个新的 Next.js 项目:

npx create-next-app@latest my-blog --typescript --tailwind --eslint --app
cd my-blog

安装必要依赖

安装处理 Markdown 文件所需的依赖:

npm install gray-matter remark remark-html date-fns @tailwindcss/typography
npm install -D @types/node

依赖说明

  • gray-matter - 解析 Markdown 文件的 frontmatter
  • remark - Markdown 处理器
  • remark-html - 将 Markdown 转换为 HTML
  • date-fns - 日期格式化工具
  • @tailwindcss/typography - 美化文章内容的样式

项目结构设计

my-blog/
├── src/
│   ├── app/
│   │   ├── page.tsx          # 首页
│   │   ├── posts/[id]/       # 文章详情页
│   │   ├── tags/             # 标签页面
│   │   ├── search/           # 搜索页面
│   │   └── about/            # 关于页面
│   ├── components/           # 组件
│   │   └── Navigation.tsx    # 导航组件
│   └── lib/
│       ├── posts.ts          # 文章处理逻辑
│       └── utils.ts          # 工具函数
├── posts/                    # Markdown 文章
└── public/                   # 静态资源

核心功能实现

1. 文章数据处理

创建 src/lib/posts.ts 文件:

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')

export interface PostData {
  id: string
  title: string
  date: string
  excerpt?: string
  tags?: string[]
  content: string
}

export function getSortedPostsData(): PostData[] {
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames
    .filter(fileName => fileName.endsWith('.md'))
    .map((fileName) => {
      const id = fileName.replace(/\.md$/, '')
      const fullPath = path.join(postsDirectory, fileName)
      const fileContents = fs.readFileSync(fullPath, 'utf8')
      const matterResult = matter(fileContents)

      return {
        id,
        content: matterResult.content,
        ...(matterResult.data as {
          title: string
          date: string
          excerpt?: string
          tags?: string[]
        }),
      }
    })

  return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1))
}

export async function getPostData(id: string): Promise<PostData> {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const matterResult = matter(fileContents)

  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  return {
    id,
    content: contentHtml,
    ...(matterResult.data as {
      title: string
      date: string
      excerpt?: string
      tags?: string[]
    }),
  }
}

2. 导航组件

创建 src/components/Navigation.tsx

'use client'

import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function Navigation() {
  const [searchQuery, setSearchQuery] = useState('')
  const router = useRouter()

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    if (searchQuery.trim()) {
      router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
    }
  }

  return (
    <nav className="bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50">
      <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <Link href="/" className="flex items-center space-x-2">
            <div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
              <span className="text-white font-bold text-sm">展</span>
            </div>
            <span className="font-bold text-xl text-gray-900">技术博客</span>
          </Link>

          {/* Navigation Links */}
          <div className="hidden md:flex items-center space-x-8">
            <Link href="/" className="text-gray-700 hover:text-blue-600 transition-colors duration-200">
              首页
            </Link>
            <Link href="/tags" className="text-gray-700 hover:text-blue-600 transition-colors duration-200">
              标签
            </Link>
            <Link href="/about" className="text-gray-700 hover:text-blue-600 transition-colors duration-200">
              关于
            </Link>
          </div>

          {/* Search */}
          <form onSubmit={handleSearch} className="flex items-center">
            <input
              type="text"
              placeholder="搜索文章..."
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              className="hidden sm:block w-64 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
            />
            <button
              type="submit"
              className="ml-2 p-2 text-gray-600 hover:text-blue-600 transition-colors duration-200"
            >
              <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
              </svg>
            </button>
          </form>
        </div>
      </div>
    </nav>
  )
}

高级功能

标签分类系统

实现标签页面,让用户可以按标签浏览文章:

// src/app/tags/page.tsx
export default function TagsPage() {
  const allPostsData = getSortedPostsData()
  
  const tagCounts = allPostsData.reduce((acc, post) => {
    if (post.tags) {
      post.tags.forEach(tag => {
        acc[tag] = (acc[tag] || 0) + 1
      })
    }
    return acc
  }, {} as Record<string, number>)

  // 渲染标签列表...
}

搜索功能

实现全文搜索功能:

// src/app/search/page.tsx
'use client'

export default function SearchPage() {
  const [searchResults, setSearchResults] = useState([])
  
  const performSearch = (searchTerm: string) => {
    const allPosts = getSortedPostsData()
    const results = allPosts.filter(post => {
      return post.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
             post.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
             post.tags?.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
    })
    setSearchResults(results)
  }

  // 渲染搜索结果...
}

部署配置

静态导出配置

next.config.ts 中配置静态导出:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  trailingSlash: true,
  images: {
    unoptimized: true
  }
}

export default nextConfig

GitHub Actions 自动部署

创建 .github/workflows/deploy.yml

name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Build
      run: npm run build
      
    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./out

性能优化

1. 图片优化

  • 使用 Next.js 的 Image 组件
  • 配置适当的图片格式和尺寸

2. 代码分割

  • 利用 Next.js 的自动代码分割
  • 使用动态导入优化组件加载

3. SEO 优化

  • 配置 metadata
  • 添加结构化数据
  • 生成 sitemap 和 robots.txt

总结

通过这个教程,我们成功搭建了一个功能完整的现代化博客系统,包括:

  • ✅ 响应式设计
  • ✅ Markdown 文章支持
  • ✅ 标签分类系统
  • ✅ 全文搜索功能
  • ✅ SEO 优化
  • ✅ 自动部署

这个博客系统不仅功能丰富,而且性能优秀,是学习 Next.js 和现代前端开发的绝佳实践项目。

下一步

你可以继续扩展以下功能:

  • 评论系统
  • 文章阅读统计
  • 深色模式
  • 多语言支持
  • RSS 订阅

希望这篇文章对你有所帮助!如果有任何问题,欢迎在评论区讨论。

喜欢这篇文章?分享给更多人吧!

相关文章