Next.js 15 全栈开发系列之markdown静态博客开发

文摘   2024-11-16 20:40   湖北  



本文将带你一步步实现一个基于 Next.js 14 的现代博客系统,包含完整代码示例和最佳实践。


项目初始化与基础配置

1.1 创建项目

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

1.2 目录结构设置

my-blog/
├── app/
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ ├── (subpages)/ # 子页面分组
│ │ ├── about/
│ │ │ └── page.tsx # 关于页面
│ │ ├── blog/
│ │ │ ├── [slug]/ # 动态博客页面
│ │ │ │ ├── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ └── page.tsx # 博客列表页面
│ │ └── components/ # 共享组件
├── components/ # 全局组件
├── lib/ # 工具函数
├── posts/ # markdown文章
└── public/ # 静态资源

核心页面实现

2.1 根布局设置

// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
title: {
template: '%s | My Blog',
default: 'My Blog',
},
description: '技术博客分享',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}
) {
return (
<html lang="zh">
<body className={inter.className}>
<main className="max-w-4xl mx-auto px-4">
{children}
</main>
</body>
</html>
)
}

2.2 首页实现

// app/page.tsx
import Link from 'next/link'
import { getPosts } from '@/lib/posts'

export default async function HomePage() {
const posts = await getPosts()

return (
<div className="py-8">
<h1 className="text-4xl font-bold mb-8">我的博客</h1>
<div className="grid gap-6">
{posts.map((post) => (
<article key={post.slug} className="p-6 bg-white rounded-lg shadow">
<Link href={`/blog/${post.slug}`}>
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600">{post.description}</p>
<time className="text-sm text-gray-500">
{new Date(post.date).toLocaleDateString('zh-CN')}
</time>
</Link>
</article>
))}
</div>
</div>
)
}

Markdown 博客系统实现

3.1 文章数据获取

// lib/posts.ts
import matter from 'gray-matter'
import path from 'path'
import fs from 'fs/promises'
import { cache } from 'react'

export type Post = {
slug: string
title: string
date: string
description: string
body: string
}

export const getPosts = cache(async () => {
const postsDirectory = path.join(process.cwd(), 'posts')
const files = await fs.readdir(postsDirectory)

const posts = await Promise.all(
files
.filter((file) => path.extname(file) === '.mdx')
.map(async (file) => {
const filePath = path.join(postsDirectory, file)
const fileContent = await fs.readFile(filePath, 'utf8')
const { data, content } = matter(fileContent)

return {
slug: path.basename(file, '.mdx'),
title: data.title,
date: data.date,
description: data.description,
body: content,
} as Post
})
)

return posts.sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime()
})
})

export async function getPost(slug: string) {
const posts = await getPosts()
return posts.find((post) => post.slug === slug)
}

3.2 博客详情页实现

// app/(subpages)/blog/[slug]/page.tsx
import { getPost, getPosts } from '@/lib/posts'
import { PostBody } from './components/post-body'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return notFound()

return {
title: post.title,
description: post.description,
}
}

export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return notFound()

return (
<article className="py-8 prose prose-lg max-w-none">
<h1>{post.title}</h1>
<time className="text-gray-500">
{new Date(post.date).toLocaleDateString('zh-CN')}
</time>
<PostBody>{post.body}</PostBody>
</article>
)
}

3.3 Markdown 渲染组件

// app/(subpages)/blog/[slug]/components/post-body.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import { Code } from 'bright'

const components = {
pre: Code,
// 自定义其他组件...
}

export function PostBody({ children }: { children: string }) {
return (
<MDXRemote
source={children}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
},
}}
components={components}
/>

)
}

SEO 和性能优化

4.1 站点地图生成

// app/sitemap.ts
import { getPosts } from '@/lib/posts'

export default async function sitemap() {
const posts = await getPosts()
const baseUrl = 'https://your-domain.com'

const postUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
}))

const routes = ['', '/about', '/blog'].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
}))

return [...routes, ...postUrls]
}

4.2 RSS 订阅支持

// scripts/generate-rss.ts
import fs from 'fs'
import RSS from 'rss'
import { getPosts } from '../lib/posts'

async function generateRssFeed() {
const posts = await getPosts()
const site_url = 'https://your-domain.com'

const feedOptions = {
title: '我的技术博客',
description: '分享 Web 开发技术与经验',
site_url: site_url,
feed_url: `${site_url}/feed.xml`,
language: 'zh-CN',
}

const feed = new RSS(feedOptions)

posts.forEach((post) => {
feed.item({
title: post.title,
description: post.description,
url: `${site_url}/blog/${post.slug}`,
date: post.date,
})
})

fs.writeFileSync('./public/feed.xml', feed.xml({ indent: true }))
}

generateRssFeed()

部署配置

5.1 生产环境配置

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 开启图片优化
images: {
domains: ['your-image-domain.com'],
},
// 开启静态页面导出
output: 'export',
// 禁用未使用的功能
experimental: {
appDir: true,
},
}

module.exports = nextConfig

5.2 Vercel 部署

  1. 在 Vercel 上创建新项目

  2. 连接 GitHub 仓库

  3. 配置环境变量(如果需要)

  4. 部署命令:


# package.json
{
"scripts": {
"build": "next build",
"postbuild": "node scripts/generate-rss.ts"
}
}

六、实用功能扩展

6.1 评论系统集成

// components/comments.tsx
'use client'

import Giscus from '@giscus/react'

export default function Comments() {
return (
<Giscus
repo="username/repo"
repoId="R_xxx"
category="Comments"
categoryId="xxx"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
theme="light"
/>

)
}

6.2 阅读时间估算

// lib/readingTime.ts
export function getReadingTime(content: string) {
const wordsPerMinute = 200
const wordCount = content.trim().split(/\s+/).length
const readingTime = Math.ceil(wordCount / wordsPerMinute)
return `${readingTime} min read`
}

使用建议

  1. 开发流程

  • 使用 TypeScript 保证代码质量

  • 遵循 React Server Components 最佳实践

  • 保持组件的单一职责

  • 性能优化

    • 使用 cache() 缓存数据获取

    • 合理使用客户端和服务端组件

    • 实现增量静态再生成(ISR)

  • 内容管理


      • 建立清晰的文章分类系统

      • 使用 Git 进行内容版本控制

      • 定期备份数据


      常见问题解决

      1. 图片优化

      // next.config.js
      const nextConfig = {
      images: {
      domains: ['your-domain.com'],
      formats: ['image/avif', 'image/webp'],
      },
      }
      1. 开发环境热更新

      // next.config.js
      const nextConfig = {
      webpack: (config) => {
      config.watchOptions = {
      poll: 1000,
      aggregateTimeout: 300,
      }
      return config
      },
      }


      如果你觉得这篇文章有帮助,欢迎点赞、关注,后续会持续更新更多 Next.js 相关内容!


      字节笔记本
      专注于科技领域的分享,AIGC,全栈开发,产品运营
       最新文章