计算机学习笔记
2 分钟阅读 · 492 字

App Router 里把博客做成纯静态:SSG、元数据与 RSS

这个博客的核心只有一句话:在构建期把 Markdown 变成 HTML。下面按数据流动的顺序拆开讲。

第一步:把 Markdown 读成数据

文章放在 content/posts/ 下,用 gray-matter 把 frontmatter 和正文分开:

import fs from 'node:fs';
import path from 'node:path';
import matter from 'gray-matter';

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

export function getPostBySlug(slug: string) {
  const raw = fs.readFileSync(path.join(POSTS_DIR, `${slug}.mdx`), 'utf8');
  const { data, content } = matter(raw);

  return {
    slug,
    title: String(data.title ?? slug),
    date: new Date(data.date).toISOString(),
    tags: Array.isArray(data.tags) ? data.tags : [],
    content,
  };
}

因为 process.cwd() 在构建时就是项目根目录,node:fs 可以直接用——这些代码只在服务端跑,永远不会进浏览器。

第二步:把文章路径预先算出来

export function generateStaticParams() {
  return getAllPostSlugs().map((slug) => ({ slug }));
}

// 只放行构建期生成过的路径,其余一律 404
export const dynamicParams = false;

next build 的输出里,这几行会变成一张静态路径清单:

● /blog/hello-world
● /blog/mdx-writing-guide
● /blog/nextjs-app-router-ssg

为什么要 dynamicParams = false

不写的话,Next 会为没预渲染的 slug 保留按需渲染的能力——对有数据库的站点是好事,对纯文件博客是多余的运行时风险:URL 拼错了不进 404,而是走一遍读文件的逻辑。

第三步:SEO 交给 generateMetadata

export async function generateMetadata({ params }) {
  const { slug } = await params;      // Next 15 起 params 是 Promise
  const post = getPostBySlug(slug);

  return {
    title: post.title,
    description: post.summary,
    alternates: { canonical: `/blog/${post.slug}` },
    openGraph: { type: 'article', publishedTime: post.date, tags: post.tags },
  };
}

第四步:RSS 不需要装插件

RSS 就是一个 XML 字符串,用 Route Handler 拼出来最省事:

export const dynamic = 'force-static';

export function GET() {
  const items = getAllPosts().map((post) => `
    <item>
      <title>${escapeXml(post.title)}</title>
      <link>${site.url}/blog/${post.slug}</link>
      <pubDate>${new Date(post.date).toUTCString()}</pubDate>
    </item>`).join('');

  return new Response(`<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>${items}</channel></rss>`, {
    headers: { 'Content-Type': 'application/xml; charset=utf-8' },
  });
}

关于缓存的取舍

方案何时重新生成适合
默认 SSG只在 next build 时文章型博客(本站)
revalidate = 3600每小时有评论、有外部数据
完全动态每次请求需要登录态、实时数据

静态方案的代价很直白:改一个字也要重新部署。对个人博客来说这笔交易划算——部署只要二十秒,而换来的是不需要服务器、不需要监控、几乎不可能挂掉。

踩过的坑

← 返回首页