
如果你也跟我一样收藏夹里躺着几百个网址真到用的时候却翻半天找不到那自己搭一个导航站这件事大概率已经在你的待办清单里躺了很久。我这次动手做的时候技术栈直接锁定了 Next.js Cloudflare Pages目的很明确要一个首屏快、能 SEO、部署不花钱、维护起来不折腾的导航网站。这个组合做下来正好覆盖了我所有的需求点。这篇文章把我从零开始搭建的完整过程、踩过的坑、以及最后实测的性能数据都整理出来了适合想自己做一个导航站、或者想了解 Next.js 静态部署到 Cloudflare Pages 全流程的朋友参考。在开始之前先说一下整体思路。导航网站不是一个复杂应用它的核心价值就两件事把链接管理得井井有条让访问者最快到达目标。所以技术选型上我追求的也不是多酷炫而是多省心。接下来我会从技术选型、数据建模、页面实现、部署上线、性能优化五个维度把整个项目从头到尾拆开讲代码和配置都会给出可以直接抄作业的版本。1. 为什么是 Next.js Cloudflare Pages导航站的刚需与技术选型1.1 导航网站的真实需求清单动手之前我先把自己的需求一条条写下来后面所有技术决策都围绕这份清单展开避免做过头或者做漏。数据更新要方便加一个链接、改一个分类名最好 30 秒内能搞定不用碰代码。首屏加载要快导航站的核心场景是打开就用访问者点进来就为了找链接没耐心等白屏。对搜索引擎友好我希望别人搜XX 工具导航的时候我的站能有机会被收录这要求页面有真实的 HTML 内容而不是一个空壳 div。部署要零成本、少运维个人项目不想花钱买服务器也不想半夜爬起来处理宕机。域名要能绑HTTPS 要自动配置最好还能有全球节点的分发能力。列完之后你会发现这其实是一份静态网站 少量交互的需求。最合适的技术形态不是一个大而全的 Web 应用而是一个以静态生成SSG为主的站点再配合一点点客户端交互。1.2 Next.js 和 Vite React 的选型对比很多人做这类项目会下意识选 Vite React因为本地开发确实快组件写法也舒服。我早期也试过这个组合但在导航站这个场景里它有一个绕不开的问题纯客户端渲染首屏拿到的 HTML 几乎是空的所有内容都要等 JavaScript 下载并执行完之后才渲染出来。爬虫抓取的时候能看到的内容非常有限SEO 基本靠运气。Next.js 解决的就是这个问题。它支持静态生成构建的时候就把每个页面渲染成完整的 HTML 文件用户请求时服务器或 CDN 节点直接返回成品页面不需要等 JS 跑起来。用 Next.js 的 App Router默认的 Server Component 模式下数据读取和渲染都在服务端完成客户端拿到的天然就是完整内容。我把两个方案的关键差异整理成了一个表格方便你根据自己的场景判断对比维度Next.jsVite React首屏内容构建时生成完整 HTML爬虫和用户直接看到内容空 HTML JS需等待客户端执行完才渲染SEO 能力原生支持SSG/SSR 灵活切换需要额外做预渲染或配合其他方案开发体验热更新文件路由生态完整构建速度快配置直观适合场景需要收录、需要首屏速度的内容型站点后台管理系统、重交互应用、内部工具部署要求需要支持 Node 或静态导出的平台任意静态托管平台我自己评估下来导航站属于内容型站点SEO 和首屏速度都是刚需所以 Next.js 是更合适的选择。注意这不是说 Vite React 不好它只是不适合这个场景。如果你做一个不需要被搜索引擎收录的内部导航那 Vite React 完全够用开发效率还更高。技术选型永远是对场景的回应不是对框架的站队。1.3 Cloudflare Pages 在这个项目里的角色Cloudflare Pages 是一个静态站点托管平台和 GitHub Pages、Netlify 属于同一类产品。它吸引我的地方有这么几点免费额度对个人项目非常慷慨支持从 GitHub 仓库自动构建部署自带全球节点分发HTTPS 证书全自动配置还能绑定自定义域名。导航站用 Next.js 的静态导出模式构建之后产物是一堆 HTML、CSS、JS 文件这正好是 Cloudflare Pages 最擅长处理的类型。我只需要把构建产物目录告诉它剩下的分发、缓存、HTTPS 都由平台处理。整个部署过程没有服务器概念从代码提交到全球生效通常只要一两分钟。Cloudflare Pages 还把构建 分发的链路做得很透明GitHub 仓库作为唯一事实来源代码推送之后自动触发构建构建脚本的输出目录就是网站根目录。这套流程对个人项目来说几乎是零负担我不用记任何服务器登录命令也不用配置 Nginx。2. 数据先行导航内容的建模与图标资源处理2.1 用 JSON 管理导航数据够用且好维护很多导航站一上来就设计数据库表结构分类表、链接表、标签表还要引入后台管理系统。但说实话对一个个人维护的导航站来说这是典型的过度设计。导航站的数据特征是读多写少、总量通常不超过几百条、更新频率可能一天也就一两次。这个规模用 JSON 文件管理比任何数据库方案都省事。我选择把数据放在项目根目录的 data/links.json 里组件构建时直接导入这个文件。好处是显而易见的打开文件就能看到所有数据改一个字段保存即生效Git 天然承担了版本管理改错了可以回滚不需要数据库连接不需要环境变量也不存在线上数据库挂了这种问题。数据文件的结构我设计成分类和链接分开的格式通过 categoryId 关联。后面要做分类筛选、分类分组展示这种结构处理起来都非常直接。{ categories: [ { id: dev, name: 开发工具, sort: 1 }, { id: ai, name: AI 服务, sort: 2 }, { id: design, name: 设计资源, sort: 3 } ], links: [ { id: github, name: GitHub, url: https://github.com, description: 代码托管与协作平台开源项目的大本营, icon: /icons/github.png, categoryId: dev, tags: [git, code, opensource], featured: true, sort: 1 }, { id: vercel, name: Vercel, url: https://vercel.com, description: 前端部署平台和 Next.js 同源, icon: /icons/vercel.png, categoryId: dev, tags: [deploy, serverless], featured: false, sort: 2 } ] }实际用下来这种格式维护成本极低。我平时加一个链接只需要复制一行 JSON 改掉字段然后 git commit git pushCloudflare Pages 会自动构建上线。2.2 字段设计从展示到排序一次想清楚JSON 里的字段不是随便写的每个字段都对应一个实际需求。我把它们分成几类说清楚。基础标识字段id 作为唯一标识我用英文短横线命名比如 github、vercel。这个 id 尽量不要用中文也不要用会变的数字自增因为后续可能作为路由参数或缓存 key。展示字段name、url、description、icon这四个字段决定了卡片上显示什么。description 很有用它能帮访问者在点进去之前就判断链接是否是自己要的减少无效点击。分组与检索字段categoryId 关联分类tags 用于搜索。我的搜索功能会同时匹配 name、description、tags 三个字段这样搜部署也能搜出 Vercel即使标题和描述里没这个词。排序字段sort 控制链接在分类内的展示顺序。我约定数字越小越靠前热门工具排前面普通工具按字母序。置顶标识featured 字段用来标记精选链接首页可以在每个分类下把精选链接单独排在最前面。这里特别提醒一点url 字段一定要写完整的协议头也就是https://不要偷懒写www.xxx.com。这不仅是前端渲染成链接的需要也关系到后续 favicon 抓取、链接安全校验这些自动化操作能不能跑通。2.3 favicon 图标从公共服务到本地化的方案导航站的卡片上少不了一个网站图标。最省事的做法是直接用公共 favicon 服务比如在图片地址里拼上域名就能返回图标。我一开始也是这么做的但很快就发现两个问题一是公共服务的访问性能在不同网络环境下表现差异很大图片加载慢的时候卡片很难看二是这些服务有请求频率和域名数量的限制几百个链接的导航站很容易触发限制导致大量图标加载失败。我的最终方案是写一个一次性脚本批量抓取所有链接的 favicon 存到项目的 public/icons 目录下网站运行时只加载本地图标。这样页面不需要向第三方服务发任何请求图标加载速度和可靠性都有保障。// scripts/fetch-icons.mjs import fs from node:fs import path from node:path const data JSON.parse(fs.readFileSync(data/links.json, utf-8)) const outputDir path.join(process.cwd(), public, icons) fs.mkdirSync(outputDir, { recursive: true }) async function fetchIcon(link) { try { const url new URL(link.url) const iconUrl https://icon.horse/icon/${url.hostname} const res await fetch(iconUrl, { signal: AbortSignal.timeout(5000) }) if (!res.ok) return const ext res.headers.get(content-type)?.includes(png) ? png : jpg const filePath path.join(outputDir, ${link.id}.${ext}) fs.writeFileSync(filePath, Buffer.from(await res.arrayBuffer())) console.log([ok] ${link.id} - ${filePath}) } catch (err) { console.log([fail] ${link.id}: ${err.message}) } } for (const link of data.links) { await fetchIcon(link) }抓取完成之后我把 links.json 里的 icon 字段统一改成/icons/${link.id}.png这种本地路径。脚本失败或抓不到图标的链接补一个默认的占位图标就行。这个方案有一个额外好处图标文件都进了 Git 仓库后续即使图标服务挂了我的导航站也不受影响。3. 从脚手架到首页App Router 下的核心实现3.1 初始化项目的关键配置项目初始化我用的是 create-next-app版本选择最新的稳定版模板带 TypeScript、ESLint、Tailwind CSSApp Router 是默认开启的。命令如下npx create-next-applatest navigation-site --typescript --app --tailwind --eslint初始化完成后有几个关键的配置要改。首先是 next.config.ts这个项目核心的决策是开启静态导出模式。Next.js 默认是 Node 服务器模式需要部署到支持 Node 的环境而我的目标是 Cloudflare Pages 的纯静态托管所以用 output: export 让 Next.js 在构建时生成纯静态文件。// next.config.ts import type { NextConfig } from next const nextConfig: NextConfig { output: export, images: { unoptimized: true, }, } export default nextConfig需要注意next/image 组件在静态导出模式下默认会报错因为图片优化服务需要服务器支持。导航站的图标都是本地小图片不需要动态优化直接设置 unoptimized: true 绕过就行。另一个小改动是 package.json 的 build 脚本保持默认的next build就行Next.js 会自动把静态产物输出到 out 目录。3.2 首页布局与数据读取方式App Router 的核心概念是 Server Component。在不加use client指令的组件里代码是在服务器端执行的可以直接读取文件系统、访问数据库而不会把这些逻辑暴露给客户端。对导航站来说这意味着数据读取可以在服务端完成渲染出完整的 HTML 之后再加一些交互逻辑。我的页面结构是这样的app/layout.tsx 定义全局布局包括主题切换逻辑和全局样式app/page.tsx 是首页内容负责读取 JSON 数据、按分类分组、渲染链接卡片。// app/page.tsx import linksData from ../data/links.json import CategorySection from /components/CategorySection export default function Home() { const { categories, links } linksData const sortedCategories [...categories].sort((a, b) a.sort - b.sort) const linksByCategory new Mapstring, typeof links() for (const category of sortedCategories) { const categoryLinks links .filter((link) link.categoryId category.id) .sort((a, b) Number(b.featured) - Number(a.featured) || a.sort - b.sort) linksByCategory.set(category.id, categoryLinks) } return ( div classNamemx-auto max-w-6xl px-4 py-8 header classNamemb-10 h1 classNametext-3xl font-bold开发者效率导航/h1 p classNamemt-2 text-gray-600精选开发工具、AI 服务与设计资源/p /header {sortedCategories.map((category) ( CategorySection key{category.id} category{category} links{linksByCategory.get(category.id) ?? []} / ))} /div ) }这段代码的核心思路是数据来源只有一个 JSON 文件页面组件在构建时读取它经过排序、分组后渲染成静态 HTML。注意 CategorySection 是纯展示组件同样不需要use client它可以继续以 Server Component 的方式工作这样客户端就完全不需要下载这份业务逻辑的 JS。3.3 分类分组与响应式卡片渲染导航站的页面主体是一排排链接卡片。我把每个卡片抽成独立的 LinkCard 组件用 Tailwind CSS 做样式。卡片的视觉重点依次是图标、站点名、描述、标签整体风格保持简洁不用花哨的阴影和渐变聚焦在信息的可扫读性上。// components/LinkCard.tsx import Link from next/link interface LinkItem { id: string name: string url: string description: string icon: string tags: string[] } export default function LinkCard({ link }: { link: LinkItem }) { return ( a href{link.url} target_blank relnoopener noreferrer classNameflex items-start gap-3 rounded-xl border border-gray-200 bg-white p-4 transition hover:border-blue-400 hover:shadow-md {/* eslint-disable-next-line next/next/no-img-element */} img src{link.icon} alt{link.name} width{40} height{40} classNamemt-1 h-10 w-10 flex-shrink-0 object-contain / div classNamemin-w-0 div classNameflex items-center gap-2 span classNametruncate font-medium text-gray-900{link.name}/span span classNameflex-shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 ↗ /span /div p classNamemt-1 line-clamp-2 text-sm text-gray-600{link.description}/p {link.tags.length 0 ( div classNamemt-2 flex flex-wrap gap-1 {link.tags.slice(0, 3).map((tag) ( span key{tag} classNamerounded bg-blue-50 px-1.5 py-0.5 text-xs text-blue-600 {tag} /span ))} /div )} /div /a ) }外层容器我用了 CSS Grid 做响应式栅格一屏以内从两列到四列平滑过渡。Tailwind 里只需要grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4这一行配置就能完成。移动端优先的写法在这里价值很大毕竟导航站相当一部分流量来自手机浏览器需要保证单列展示时的阅读体验依然舒适。图标 img 标签我加了固定的 width 和 height这看起来是小事但实际上对布局稳定性至关重要。没有固定尺寸的图片在加载过程中会不断改变元素高度导致整个卡片上下跳动也就是性能指标里说的 CLS 问题。后面讲性能优化时我会再展开。4. 搜索、暗色模式与交互细节把体验手感打磨出来4.1 客户端搜索数据量不大时最简单的方案导航站的链接总量通常不超过几百条这种数据量做客户端搜索是最合适的。服务端搜索在这种规模下没有任何优势反而每次输入都要请求服务器既慢又浪费。客户端搜索实现起来很简单把所有链接列表放在内存里用户输入关键词时用数组的 filter 方法匹配纯前端即时完成。Next.js 的 App Router 里要处理输入框这类交互逻辑组件必须标记为use client。我把搜索框和结果列表封装成一个 SearchPanel 组件放在首页顶部固定区域。它的职责包括持有搜索关键词的 state、执行过滤逻辑、渲染结果列表。因为这个组件只在键入搜索词时才有交互价值所以我用 App Router 的 dynamic import 让它延迟加载减少首页初始 JS 体积。// components/SearchPanel.tsx use client import { useDeferredValue, useMemo, useState } from react import LinkCard from ./LinkCard import type linksData from ../data/links.json interface SearchPanelProps { links: typeof linksData[links] } export default function SearchPanel({ links }: SearchPanelProps) { const [keyword, setKeyword] useState() const deferredKeyword useDeferredValue(keyword) const results useMemo(() { const kw deferredKeyword.trim().toLowerCase() if (!kw) return [] return links.filter((link) { const searchable [link.name, link.description, link.tags.join( )] .join( ) .toLowerCase() return searchable.includes(kw) }) }, [links, deferredKeyword]) return ( div classNamemb-10 input typesearch value{keyword} onChange{(e) setKeyword(e.target.value)} placeholder搜索工具、服务或标签... classNamew-full rounded-xl border border-gray-300 px-4 py-3 text-gray-900 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 / {deferredKeyword.trim() ( div classNamemt-6 {results.length 0 ? ( div classNamegrid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 {results.slice(0, 20).map((link) ( LinkCard key{link.id} link{link} / ))} /div ) : ( p classNamepy-12 text-center text-gray-500 没有找到与 “{deferredKeyword}” 相关的链接 /p )} /div )} /div ) }useDeferredValue 是这里值得讲的一个细节。它能让搜索框的输入保持流畅即使过滤逻辑稍重也不会阻塞输入。原理是把输入和结果渲染分成两个优先级输入的响应是紧急的立即处理结果的计算可以稍微延后在浏览器空闲时执行。在这个项目里数据量小性能提升不明显但这个 API 的用法可以帮助你在数据量增长时不用重构。4.2 搜索结果渲染与关键词高亮搜索结果列表里我一开始只是简单展示 LinkCard后来发现有个细节体验很重要关键词高亮。用户搜索部署时如果结果卡片里能直接看到部署两个字被高亮确认目标的速度会快很多。实现方式也很朴素写一个小函数把匹配到的关键词在 HTML 里包一层带颜色的 span 标签。function highlightText(text: string, keyword: string) { if (!keyword.trim()) return text const escaped keyword.replace(/[.*?^${}()|[\]\\]/g, \\$) const parts text.split(new RegExp((${escaped}), ig)) return parts.map((part, index) part.toLowerCase() keyword.toLowerCase() ? ( mark key{index} classNamerounded bg-yellow-100 px-0.5 text-inherit {part} /mark ) : ( part ) ) }这里要注意正则转义的问题。用户输入的搜索词可能包含.*?这些正则元字符如果直接拼进正则表达式轻则高亮不准重则直接抛异常。先用replace方法转义一遍把元字符变成普通字符正则就只会做字面匹配不会再被解释成通配符了。4.3 暗色模式CSS 变量 next-themes暗色模式现在基本是导航站的标配用户习惯在暗色环境里长时间浏览链接。我选型的时候对比过 Tailwind 的 dark: 类和 CSS 变量方案最后决定用 CSS 变量配合 next-themes 这个库。核心原因有两个CSS 变量的切换是全局性的不需要给每个组件单独写 dark: 前缀改起来省心next-themes 解决了暗色模式最容易出现的首屏闪烁问题。首屏闪烁的根源是页面 HTML 先以默认亮色渲染JavaScript 执行后才读到用户偏好并切换成暗色结果用户看到一片白屏之后突然变暗。next-themes 的做法是在 HTML 里内联一小段脚本在页面渲染之前就读取用户偏好并设置根部>:root { --bg: #f6f7f9; --card: #ffffff; --text: #1f2937; --text-secondary: #6b7280; --border: #e5e7eb; } [data-themedark] { --bg: #0f1115; --card: #1a1d24; --text: #e5e7eb; --text-secondary: #9ca3af; --border: #2a2d35; }组件里使用 theme 的地方统一引用这些变量比如bg-[var(--bg)]、text-[var(--text)]。这样以后想改整体配色只需要动一两行 CSS 变量不需要满项目地替换组件类名。主题切换按钮放在页头点击时调用 next-themes 提供的 toggleTheme 方法它会自动处理好 localStorage 存储和系统偏好更新。5. Cloudflare Pages 部署备忘录构建配置、自动发布与缓存策略5.1 静态导出模式output: export 意味着什么把 Next.js 项目部署到 Cloudflare Pages第一步是搞清楚用哪个运行模式。Cloudflare Pages 支持两种方式一种是纯静态托管构建产物是 HTML/CSS/JS 文件不需要服务器执行代码另一种是适配 Cloudflare Workers 运行时可以处理动态请求。导航站这种内容型站点99% 的页面在构建时就能预渲染完用纯静态模式即可。前面配置里已经写了 output: export这个配置会在执行 npm run build 时把整个应用渲染成静态 HTML 文件输出到 out 目录。每个路由对应一个 HTML 文件首页是 out/index.html。这个过程等于把页面长什么样这件事在构建时就固定了下来访问者拿到的 HTML 就是构建时生成的那个版本。静态模式有两个限制要提前知道一是所有页面必须能在构建时完成数据获取不能在客户端依赖运行时 API二是动态功能需要另想办法比如表单提交需要接入第三方服务或者 Cloudflare Functions。导航站没有这类需求所以静态模式是零妥协的方案。5.2 GitHub 连接与自动构建配置部署流程我建议走GitHub 仓库 Cloudflare Pages 自动构建的方式这样内容更新变得非常优雅改 JSON、提交、推送剩下的都由系统完成。具体操作分四步。第一步把代码推送到 GitHub 仓库。第二步在 Cloudflare Dashboard 进入 Pages选择创建项目连接 GitHub选择你的仓库。第三步配置构建设置框架预设选择 Next.js这里的关键参数是Build commandnpm run buildBuild output directoryout第四步点击保存并部署等第一次构建完成Cloudflare 会分配一个 pages.dev 的默认域名直接就能访问。后续每次 git pushCloudflare 会自动触发新构建并在完成后更新全球节点。我在这一步踩过一个小坑说给你参考。构建配置里的根目录要填对如果你的项目在仓库的子目录里比如 monorepo 结构就需要在根目录字段填子目录路径并且构建命令里的路径要相应调整。我的项目在仓库根目录所以直接留空根目录字段即可。5.3 自定义域名、安全响应头与缓存默认的 pages.dev 域名不适合做正式站点绑定自定义域名是必做项。操作路径是项目设置 - 自定义域名 - 添加域名Cloudflare 会给出 DNS 验证要求和 CNAME 记录。如果你的域名 DNS 托管在 Cloudflare验证是一键完成的证书也会在几分钟内自动签发。静态站点的安全响应头也很重要虽然导航站不处理用户数据但基础的安全头能避免很多低级问题。我在 public 目录下放了一个 _headers 文件Cloudflare Pages 会自动读取并应用/* X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin缓存策略是导航站性能的隐形加分项。导航站的数据更新频率低HTML 和静态资源都可以大胆缓存。_headers 文件里继续加/assets/* Cache-Control: public, max-age31536000, immutable但要注意HTML 文件不要设这种长时间的缓存否则你更新内容之后老访客会一直看到旧页面。我的做法是静态资源长缓存HTML 走默认的 no-cache配合 Cloudflare 的 Etag 校验这样每次重新构建部署后访问者都会拿到最新版本而图片、CSS、JS 这些不会变的资源会直接命中缓存加载速度飞快。6. 性能实测与几个值得长期坚持的优化习惯6.1 从 Lighthouse 指标反推优化点部署完成后我用 Lighthouse 对线上地址做了一次完整测评。测评不是为了一个分数自嗨而是为了从指标里反推哪里还可以优化。四个核心指标里我最关注的是 LCP最大内容绘制和 CLS累计布局偏移。LCP 衡量的是页面主体内容加载完成的速度对导航站来说就是第一屏的卡片区域是否快速出现CLS 衡量的是页面内容在加载过程中有没有明显跳动跳动最容易发生在图片加载完成、把卡片撑开的那一刻。第一次测评结果LCP 1.2 秒CLS 0.08性能得分 94。这个分数已经不错了但 CLS 的 0.08 说明还有轻微跳动。排查之后确认问题出在部分图标没有固定尺寸。虽然我在 LinkCard 里设了 width 和 height但个别图标的实际文件比例不是 1:1浏览器按照固定宽高渲染之后图片被拉伸或压缩重新计算了布局。修复方案是给 img 加一个 object-contain让它按比例适配在 40x40 的盒子里不再撑开布局。6.2 实测数据与优化前后的对比优化之后我再次跑了一轮测试数据如下指标优化前优化后说明LCP1.2s0.9s首屏主体内容更快出现CLS0.080.001布局跳动几乎消失TBT80ms50ms主线程阻塞时间减少Performance9499综合评分这个结果基本达到了我的预期。静态站点的性能天花板本来就高优化空间不在于压缩代码这种零碎操作而在于把几个关键点做对不加载无用的 JS、图片有正确尺寸、字体不阻塞渲染、缓存策略合理。字体这里多说一句默认情况下 Next.js 引入的字体文件会延迟页面首次渲染如果字体只是锦上添花一定要设置font-display: swap让浏览器先用系统字体渲染字体文件加载完再替换这样文字内容永远不会等字体。Next.js 的 next/font 配置已经默认做了这个优化如果你是自己引入的字体文件手动检查一下 CSS 里的 font-display 属性。6.3 更新内容后的重新构建一种可持续的内容维护节奏内容维护这件事很多人会考虑加一个后台管理界面但从我的实际经验看这是最容易被遗忘和维护成本最高的功能。个人导航站真正需要的是一套足够顺畅的内容更新流程而不是一个华丽的后台。我最终形成的工作流是需要加链接时直接在 GitHub 仓库的网页端编辑 data/links.json提交修改后 Cloudflare Pages 自动构建上线。整个过程大概 30 秒到一分钟完全不需要打开本地开发环境。Git 会留下每次修改的记录想回滚随时可以。我还给 links.json 加了一个 lastUpdated 字段首页底部展示最近更新日期。这有两个作用对访问者来说一个明确标注更新日期的导航站可信度更高有助于提升长期访问意愿对自己来说每次更新数据时顺手改一下日期也能直观看到这个项目多久没有维护了避免内容悄然过期。写在最后项目跑起来之后我最大的体会是导航站这种小项目恰恰是最能体现工程取舍眼光的地方。技术选型阶段忍住了上新东西的冲动用静态生成 CDN 分发这套朴素的方案换来了接近一秒内的首屏和近乎零成本的运维数据层没有引入任何重组件一个 JSON 文件让内容维护的链路短到不能再短。高性能不是靠某一项魔法配置堆出来的而是这些看起来不起眼的决定叠加在一起的结果。最后再分享一个小技巧导航站的 SEO 不只是刷分真正有价值的是根据访问者熟悉的叫法来设置页面里的关键词。比如有些用户搜索git 托管平台而不是GitHub那在 description 里主动带上这类别名比绑一堆 meta keywords 实在多了。这种小修正在代码层面几乎零成本但对长尾流量的帮助是实打实的。