这篇是「从零开始」实录,不是教科书。我会把实际踩过的选型、每一个配置、写文章和上线的最短路径讲清楚,顺带把我用 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 shikivelite:把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 / MDXtitle:标题(字符串,必填)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 写一篇新文章的最短路径
- 在
content/posts/新建<slug>.mdx,填好 frontmatter。 - 本地跑
pnpm dev,velite Watch 模式自动把新文章编进.velite,首页立即可见。 - 想加图片:丢进
public/,正文用<Image src="/xxx.jpg" width={...} height={...} />(已映射next/image)。 - 提交前跑
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 main3.2 关联 Vercel(零配置)
- 登录 Vercel → New Project → Import 你的仓库。
- Vercel 自动识别这是 Next.js 项目,框架预设和输出目录基本不用改。
- 因为仓库根有
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 的生命周期
- 认领:
linear issue update JEV-XXX --assignee self(unassigned = 没人做,assigned = 在做)。 - 开工:改代码,本地
pnpm dev/pnpm build自测。 - 关单前 checklist:提交代码 → 对照验收标准 → 读评论/子 issue 看有无要同步 → 加完成评论 → 改 state 为 Done。
- 收尾:提交走
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 的事了。
