📘 建站教程

用 Next.js 从零搭一个博客:目录结构与渲染策略

搭博客最容易走偏的地方,不是样式,而是一开始就没想清楚"什么该静态、什么该动态"。这篇先把目录和渲染策略定下来,后面的功能都只是往里加东西。

目录结构#

这是本项目实际使用的分层,核心原则是:内容与代码分离、读内容的能力收敛到一个文件里。

text
moyu-blog/
├─ app/                  # 页面与路由(App Router)
│  ├─ page.tsx           # 首页
│  ├─ posts/             # 全部文章列表
│  ├─ section/[section]/ # 栏目页
│  ├─ tag/[tag]/         # 标签页
│  ├─ post/[slug]/       # 文章详情
│  ├─ admin/             # 后台(写/改/删)
│  └─ api/               # 评论、搜索、后台接口
├─ components/           # 组件(服务端为主,交互单独成客户端组件)
├─ content/posts/        # ★ 文章 Markdown(带 frontmatter)
├─ data/comments.json    # ★ 评论数据
├─ lib/                  # 读内容 / 渲染 / 鉴权
└─ site.config.ts        # 站点与栏目配置

好处很直接:备份 = 复制 content/ 和 data/;换主题 = 只动 app/ 与 components/;加栏目 = 改 site.config.ts。

渲染策略:默认静态,例外才动态#

页面 策略 原因
首页 / 列表 / 栏目 / 标签 ISR(revalidate = 300) 内容变化不频繁,静态 HTML 抓取最快
文章详情 ISR 正文要出现在首屏 HTML 里
后台管理 强制动态 依赖登录 Cookie
评论接口 强制动态 每次读写文件

判断标准就一句话:搜索引擎和访客看到的内容,必须能在不执行 JS 的情况下拿到。

读取内容只有一层#

不要在页面里到处 fs.readFileSync,把读取集中到 lib/posts.ts:

ts
export function getAllPosts(options: ListOptions = {}): PostMeta[] {
  const { includeDrafts = false, section, tag, query, limit, offset } = options;
  let posts: PostMeta[] = [];
  for (const file of markdownFiles()) {
    const post = readPostFile(file);
    if (!includeDrafts && post.draft) continue;
    posts.push(post);
  }
  // 过滤 + 排序 + 分页
  return posts;
}

页面只负责"拿数据 → 拼 HTML",这样搜索、RSS、sitemap 都能复用同一份数据源,不会出现"列表有、RSS 没有"的不一致。

经验:每多一个直接读文件的地方,就多一个数据不一致的隐患。

什么时候需要用客户端组件#

只有三类必须 'use client':

  1. 需要浏览器状态:深色模式开关、阅读进度条、目录高亮;
  2. 需要事件委托:图片灯箱、代码复制按钮;
  3. 需要请求接口:评论区、搜索框。

其余全部保持服务端组件。把 useState 塞进列表卡片这类组件,只会白白增大客户端包体积。

上线前自查#

  • curl 首页能看到文章标题与摘要(说明是服务端渲染)
  • 每个文章页 <title> 与 canonical 正确
  • /sitemap.xml 与 /rss.xml 可访问
  • /search/ 设置了 noindex
  • 后台 /admin/ 未登录时看不到内容

下一篇会把"内容更新后立刻生效"这一块讲透。

评论

正在加载评论…

评论保存在本站 data/comments.json,不经过第三方。