从零开始建个人博客 ———— 基于Vercel的MVP实现

从零开始建个人博客 ———— 基于Vercel的MVP实现

这篇是「从零开始」实录,不是教科书。我会把实际踩过的选型、每一个配置、写文章和上线的最短路径讲清楚,顺带把我用 Linear(项目管理工具) 管理这个博客开发的过程也记下来。

核心结论先放前面:技术栈选 Next.js 16(App Router)+ React 19 + Tailwind CSS v4 + velite(内容层)+ Vercel(部署)。下面拆开说为什么、怎么搭、文章写哪、怎么上线。

技术选型(30 秒版)

  • 框架:Next.js 16 App Router — React 19、文件即路由、Vercel 零配置部署
  • 样式:Tailwind CSS v4 — 设计 token 写进 @theme,无配置文件
  • 内容层:velite — Zod 校验 frontmatter,构建期生成类型安全的数据层
  • MDX 渲染:@next/mdx + next-mdx-remote — 列表页用 velite 数据,详情页用 compileMDX 渲染
  • 代码高亮:rehype-pretty-code + shiki — 双主题(亮/暗)语法着色
  • 包管理:pnpm — 快、省磁盘,Vercel 自动识别 pnpm-lock.yaml
  • 部署:Vercel — 推上去就上线,Preview 部署天然支持
  • 项目管理:Linear — 迭代式博客开发用 issue tracker 追进度

选型依据我单独写了调研文档(docs/research/ 下 content-pipeline-comparison.md、nextjs-15-vercel-baseline.md 等)。关键取舍:contentlayer 已停更、next-mdx-remote 上游已归档,所以内容层走 velite;纯博客场景不需要 fumadocs 那种全家桶。

说明:本项目详情页当前用 next-mdx-remote@6 的 compileMDX。该包上游已于 2026-04 归档,新项目若从零开始,可改用活跃维护的 next-mdx-remote-client;这里保留现有接线是为了不破坏 MVP。


一、如何建项目

1.1 初始化 Next.js

用官方脚手架起项目,按提示选:TypeScript / Tailwind / App Router / 包管理器选 pnpm。Next 16 的 --tailwind 会自动装 Tailwind v4。

pnpm create next-app@latest personal-blog --ts --tailwind --app --no-src-dir --import-alias "@/*" --use-pnpm
cd personal-blog

本项目 app/ 直接放在仓库根(不用 src/),tsconfig.json 里 @/* 指向根目录,所以后面能用 @/.velite 这种绝对路径导入。

1.2 安装内容层与高亮依赖

pnpm add velite @next/mdx @mdx-js/loader @mdx-js/react rehype-pretty-code shiki
  • velite:把 content/ 下的 MDX 编译成带类型的 JSON 数据层(输出到 .velite)。
  • @next/mdx + @mdx-js/loader:让 Next 能处理 .mdx 文件。
  • rehype-pretty-code + shiki:代码块语法高亮。

1.3 三个关键配置

(1)velite.config.ts — 定义内容集合

import { defineConfig, s } from 'velite'
 
export default defineConfig({
  collections: {
    posts: {
      name: 'Post',
      pattern: 'posts/**/*.mdx',
      schema: s.object({
        title: s.string(),
        tags: s.array(s.string()),
        slug: s.slug('posts'),
        content: s.raw(), // 保留 MDX 原文,详情页用 compileMDX 渲染
      }),
    },
  },
})

(2)next.config.ts — 注入 MDX 并在开发期联动 velite

import type { NextConfig } from 'next';
import createMDX from '@next/mdx';
 
const isDev = process.env.NODE_ENV !== 'production'
if (!process.env.VELITE_STARTED && isDev) {
  process.env.VELITE_STARTED = '1'
  import('velite').then((m) => m.build({ watch: isDev, clean: !isDev }))
}
 
const withMDX = createMDX({ options: { remarkPlugins: [], rehypePlugins: [] } })
 
const nextConfig: NextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}
 
export default withMDX(nextConfig);

注意这里有个关键设计:开发模式下(pnpm dev)next.config.ts 会 spawn 一个 velite 进程并开启 watch,所以改完文章内容 .velite 会自动刷新;而生产构建靠 package.json 里的 build 脚本显式先跑 velite。

(3)根目录 mdx-components.tsx — MDX 组件映射(Next 约定文件名)

import type { MDXComponents } from 'mdx/types';
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return { ...components };
}

package.json 的脚本长这样(构建必须 velite 先于 next):

{
  "scripts": {
    "dev": "next dev",
    "build": "velite build --strict && next build",
    "start": "next start",
    "type-check": "tsc --noEmit"
  }
}

样式方面,Tailwind v4 不再有 tailwind.config.js,设计 token 直接写在 app/globals.css 的 @theme 块里(中性灰、品牌色、字体栈、间距、圆角、阴影都在这定义),入口只需一行 @import "tailwindcss";,postcss.config.mjs 配 @tailwindcss/postcss 即可。

1.4 项目目录结构

personal-blog/
├─ app/
│  ├─ layout.tsx          # 根布局:`<html>`/`<body>` + 字体 + 全局样式
│  ├─ page.tsx            # 首页:文章列表(读 @/.velite)
│  ├─ globals.css         # Tailwind v4 入口 + @theme 设计 token
│  ├─ design-system/      # 开发期专属:可视化设计 token(生产隐藏)
│  └─ posts/[slug]/
│     └─ page.tsx         # 文章详情:compileMDX 渲染
├─ content/
│  └─ posts/*.mdx         # ← 你的文章都在这
├─ velite.config.ts
├─ next.config.ts
├─ mdx-components.tsx
└─ tsconfig.json          # paths: { "@/*": ["./*"] }

app/design-system 是个只给开发期看的页面:在 app/page.tsx 里用 process.env.NODE_ENV !== 'production' 条件渲染,访客在生产环境看不到,本地调试 token 很方便。


二、如何找到写文章的位置

2.1 文章放哪

所有文章都是 content/posts/ 下的 .mdx 文件。文件名随意,但建议和 slug 一致,方便定位。

2.2 frontmatter 规范

每篇顶部用 --- 包一段 YAML,字段由 velite.config.ts 的 schema 约束(写错 pnpm build 会因 --strict 直接报错):

---
title: 从零开始建个人博客
slug: blog-from-scratch
tags: ['建站笔记', 'Next.js']
---
 
# 正文从这里开始写 Markdown / MDX
  • title:标题(字符串,必填)
  • slug:URL 片段,最终访问地址是 /posts/<slug>
  • tags:标签数组,列表页和详情页顶部会展示

2.3 列表页与详情页怎么读数据

列表页 app/page.tsx 直接从 velite 生成的类型安全数据导入:

import { posts } from '@/.velite'
 
export default function PostsPage() {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/posts/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

详情页 app/posts/[slug]/page.tsx 用 compileMDX 把 raw MDX 渲染成 React:

import { compileMDX } from 'next-mdx-remote/rsc'
import rehypePrettyCode from 'rehype-pretty-code'
import { posts } from '@/.velite'
 
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = posts.find((p) => p.slug === slug)
  if (!post) notFound()
 
  const { content } = await compileMDX({
    source: post.content,
    options: { mdxOptions: { rehypePlugins: [[rehypePrettyCode, { theme: { light: 'github-light', dark: 'github-dark' } }]] } },
  })
  return <main><article>{content}</article></main>
}

2.4 写一篇新文章的最短路径

  1. 在 content/posts/ 新建 <slug>.mdx,填好 frontmatter。
  2. 本地跑 pnpm dev,velite Watch 模式自动把新文章编进 .velite,首页立即可见。
  3. 想加图片:丢进 public/,正文用 <Image src="/xxx.jpg" width={...} height={...} />(已映射 next/image)。
  4. 提交前跑 pnpm build 验证(velite strict 校验 + next 构建一起过)。

三、如何部署上 Vercel

3.1 推到 Git 仓库

Vercel 从 Git 仓库拉代码,先有个仓库(GitHub / GitLab 都行):

git init
git add .
git commit -m "feat: blog MVP"
git remote add origin <你的仓库地址>
git push -u origin main

3.2 关联 Vercel(零配置)

  1. 登录 Vercel → New Project → Import 你的仓库。
  2. Vercel 自动识别这是 Next.js 项目,框架预设和输出目录基本不用改。
  3. 因为仓库根有 pnpm-lock.yaml,Vercel 自动用 pnpm 安装依赖。

3.3 设置构建命令(重要)

Next.js 预设的构建命令默认是 next build,但本项目的 .velite 必须先由 velite 生成。所以在 Project Settings → Build & Development 把构建命令显式设为:

velite build --strict && next build

或者直接填 pnpm build(它等价于上面这条)。不要留默认的纯 next build,否则 next build 时找不到 @/.velite 会失败。

环境变量如果本地用了 .env.local(如 API Key),在 Vercel 同页面的 Environment Variables 里按 Production / Preview / Development 三档补上,注意敏感变量不要加 NEXT_PUBLIC_ 前缀。

3.4 Preview 部署与自定义域名

  • 每次往非主分支推送,Vercel 自动生成一个 Preview 部署(独立 URL),PR 里还会自动评论链接,方便先预览再合。
  • 主分支合入即上线到 Production。
  • 想用自己的域名:Settings → Domains 里加,按提示改 DNS 解析即可。

3.5 验证上线

部署完成后打开 Production URL,确认:文章列表有内容、点进详情页排版/代码高亮正常、移动端不贴边。后续每次 git push 主分支就自动重新部署,无需手动操作。


四、用 Linear 管理博客开发

博客不是一次写完就完,是个持续迭代的东西。我用 Linear(项目管理/issue tracker 工具) 追每个改进点,避免「想改但忘了改什么」。

4.1 为什么用 Linear

  • 轻量、键盘流操作快,issue 之间能建依赖关系(blocked-by),一眼看清哪些能动手(frontier)。
  • 和 Git 心智一致:一个 ticket 对应一个改动,做完关掉,进度天然可见。

4.2 团队 / 项目 / 标签体系

本项目在 Linear 里的配置(详见 docs/agents/issue-tracker.md):

  • Team:Jevei Persenal,key 是 JEV(issue 编号形如 JEV-123)。

  • Project:「个人博客」。

  • 标签(wayfinder 体系):

  • wayfinder:map(紫):地图 issue(总体规划)

  • wayfinder:research(蓝):调研型 ticket

  • wayfinder:prototype(橙):原型型 ticket

  • wayfinder:grilling(绿):讨论型 ticket

  • wayfinder:task(红):直接执行型 ticket

4.3 CLI 优先工作流

默认用 linear-cli(装一次后用 linear 命令),稳定且和 git 集成好:

npx @schpet/linear-cli --version   # 验证安装

常用命令(节选):

  • 列 issue:linear issue list --team JEV --sort priority --no-pager
  • 看单个:linear issue view JEV-123
  • 建 issue:linear issue create --title "..." --description-file body.md --label wayfinder:task --team JEV --project "个人博客" --no-interactive
  • 开始做:linear issue start JEV-123
  • 认领:linear issue update JEV-123 --assignee self
  • 设依赖:linear issue relation add JEV-123 blocked-by JEV-122
  • 加评论:linear issue comment add JEV-123 --body-file note.md

规则:正文/评论一律用 --description-file / --body-file 传文件,别用 inline 参数,避免转义和 Markdown 格式问题。issue 标题、正文、评论都用纯中文。

4.4 一个 ticket 的生命周期

  1. 认领:linear issue update JEV-XXX --assignee self(unassigned = 没人做,assigned = 在做)。
  2. 开工:改代码,本地 pnpm dev / pnpm build 自测。
  3. 关单前 checklist:提交代码 → 对照验收标准 → 读评论/子 issue 看有无要同步 → 加完成评论 → 改 state 为 Done。
  4. 收尾:提交走 git-commit 流程,再去 Linear 关 ticket。

4.5 踩坑提醒

MCP(mcp_linear)实测有 schema 与实际行为不一致的问题:比如 labels 字段 server 实际要单个 string,但 schema 写的是数组;blockedBy 要传 JSON 字符串 "[\"JEV-101\"]" 而非数组;连续建标签还可能报「已存在」的假阳性。所以优先用 linear-cli,其次 linear api 走 GraphQL,最后才考虑 MCP。


小结

从零到上线的最短闭环就是:Next.js 16 起项目 → velite 管内容 → 文章写进 content/posts/*.mdx → git push 到 Vercel 自动部署。中间用 Linear 把每个迭代点盯住,不靠记忆。

剩下都是润色:暗色模式开关、RSS、评论、搜索……这些都是下一个 JEV-xxx 的事了。