跳到主要内容

agents.md 官网 — Next.js 源码走读

本章讲什么: 这一章走读 https://agents.md 的真实源码。它是个典型的 Next.js(Pages Router)+ Tailwind v4 静态营销站,代码不多,但有三个小而巧的工程点值得单独学。先给全景,再逐个拆。

全景:首页怎么拼出来

首页把一串内容区顺序拼起来(依据:pages/index.tsx:17-35):

<LandingPage>
Hero 标题 + 一个 AGENTS.md 代码块 + CTA
WhySection 为什么和 README 分开
CompatibilitySection ★ logo 跑马灯(§4.3)
ExamplesSection 宽代码块 + 例子仓库卡片(贡献者数,§4.1)
HowToUseSection 四步怎么用
AboutSection 治理/基金会
FAQSection 常见问答
Footer

数据只有一处:构建时抓取几个示例仓库的贡献者信息(getStaticProps),其余全是静态内容。下面三节挑出真正有工程含量的部分。


要解决的小问题: 例子卡片想显示「openai/codex 有 N 位贡献者」。但 GitHub 的 /contributors API 不直接返回总数——它是分页的。逐页拉到底再数?对大仓可能是几十次请求,太贵。

思路/直觉: GitHub 分页会在响应的 Link 头里塞 rel="last" 指向最后一页的 URL。只要把每页大小设成 1(per_page=1),最后一页的页码 = 总条数。 于是一次请求 + 读一个响应头就拿到总数,不用翻页。

原理演示(示意,非源码):

// 每页只要 1 条,只为读 Link 头里的 last 页码
const res = await fetch(url + "?per_page=1&anon=1");
const link = res.headers.get("link");
// link 形如: <...&page=438>; rel="last"
const last = link.match(/&?page=(\d+)>; rel="last"/);
const total = last ? Number(last[1]) : 0; // 438 = 总贡献者数
// 重点看:总数是「最后一页页码」,不是把每页数据加起来

真实实现: 逻辑在 pages/index.tsx:114-131——取 link 头、正则 /&?page=(\d+)>; rel="last"/ 抠出末页码作为 total;没有 Link 头(说明只有一页)时退回数返回数组长度。头像另用 per_page=3 取前三个(pages/index.tsx:98-110)。

配套的省钱设计:

  • 进程内内存缓存 + 12 小时窗口:同一个 Node 进程 12 小时内不重复打 GitHub,专门为了本地开发反复刷新时不撞未认证 API 的 60 次/小时限额(依据:pages/index.tsx:40-76)。
  • 可选鉴权头:有 GH_AUTH_TOKEN 才加 Authorization,避免塞空头被当未认证(依据:pages/index.tsx:87-94)。
  • ISR:revalidate: 60*60*24,生产环境每 24 小时后台再生成(依据:pages/index.tsx:158)。

坑/细节: 抓取全程 try/catch 兜底,任何仓库抓失败就填 { avatars: [], total: 0 },保证构建不因 GitHub 抽风而挂(依据:pages/index.tsx:141-144)。


4.2 迷你 Markdown 高亮器:不到 130 行,不引库

要解决的小问题: 页面上多处要把一段 AGENTS.md 文本渲染成「带一点高亮」的代码块(标题加粗、行内 `code` 加底色)。上一个完整 Markdown 库有点重。

思路/直觉: 需求极窄——只要认三样:#/##/### 标题、- 列表项、行内反引号。那就手写一个按行扫描的极简解析器,只认这三样,其余原样输出。

真实实现:

  • parseMarkdown 逐行判断:以 # /## /### 开头 → 整行加粗;- 开头或普通行 → 走行内代码渲染;空行 → 占位(依据:components/CodeExample.tsx:66-101)。
  • renderLineWithInlineCodeline.split(/([^]+)/g)把行按反引号切段,是 ``...`` 的段套一个带底色的,其余原样(依据:components/CodeExample.tsx:106-121`)。

坑/细节: 这不是合规的 Markdown 解析,是**「够用就好」的视觉高亮**——不处理嵌套、链接、粗体等。因为展示内容全是受控的示例常量(HERO_AGENTS_MDEXAMPLE_AGENTS_MD,components/CodeExample.tsx:22-61),不需要通用性。配套还有一键复制:navigator.clipboard.writeText + 2 秒「已复制」态(依据:components/CodeExample.tsx:136-144)。

可借鉴的点: 当渲染目标是你自己写的、格式固定的文本时,手写 30 行扫描器往往比拉一个 Markdown 库更小更可控。


4.3 无限 logo 跑马灯:列表翻倍 + translateX(-50%)

要解决的小问题: 兼容清单要横向无缝滚动展示几十个工具 logo,滚到头要无缝接回,不能有跳帧。

思路/直觉: 经典 CSS 跑马灯技巧——把同一串 logo 复制一份接在后面(A A),让轨道从 translateX(0) 动到 translateX(-50%)。因为后半段和前半段一模一样,当动画走到 -50%(正好第一份走完、第二份补到起点)时画面与起点完全重合,infinite 循环便看不出接缝。

轨道内容: [A₁ A₂ … Aₙ | A₁ A₂ … Aₙ] ← 同一串翻倍
↑起点(0%) ↑走到这里(-50%)
这两个位置画面一致 → 无缝循环

真实实现:

  • 复制列表:const doubledAgents = [...agents, ...agents](依据:components/CompatibilitySection.tsx:239)。
  • 动画:@keyframes logo-marquee-scroll { from { translateX(0) } to { translateX(-50%) } },linear infinite(依据:styles/globals.css:119-126)。
  • 时长用 CSS 变量 --marquee-duration 注入,两行不同速、第二行用负 animationDelay 错开相位(依据:components/CompatibilitySection.tsx:245-262312-325)。

两个体贴细节:

  • 离屏即暂停:用 IntersectionObserver 观察容器,不在视口里就把 animationPlayState 设成 paused,省 CPU(依据:components/CompatibilitySection.tsx:275-310)。
  • 尊重无障碍:@media (prefers-reduced-motion: reduce) 直接关掉动画(依据:styles/globals.css:128-132);另有「View all」按钮把跑马灯切成静态网格(依据:components/CompatibilitySection.tsx:341-375)。

一个小八卦细节: logo 顺序在客户端用 Fisher-Yates 洗牌(shuffleAgents,components/CompatibilitySection.tsx:144-151),每次访问排列不同。


4.4 主题、字体、favicon:全靠 prefers-color-scheme

这一节是「怎么做到深浅色随系统切换、还不闪」的收尾细节。

  • 深浅色:纯 CSS,:root 定义浅色变量,@media (prefers-color-scheme: dark) 覆盖成深色;Tailwind 的 dark: 类跟着系统走。没有手动切换按钮、没有 JS(依据:styles/globals.css:75-99)。
  • 字体:自托管声明 OpenAI Sans 全字重,font-display: swap 防止字体加载阻塞文字显示(依据:styles/globals.css:3-79)。
  • favicon:同样用 media="(prefers-color-scheme: ...)" 给深浅色各挂一个图标(依据:pages/_document.tsx:6-17)。
  • logo 上色:多数 logo 用 mask-image + background-color 渲染,这样同一个 SVG 能在深浅色下自动变成灰 700 / 灰 400,不用为每个 logo 备两版(依据:components/CompatibilitySection.tsx:196-211)。带自家品牌色的少数(VS Code、Devin、Windsurf 等)才提供 light/dark 双图(依据:components/CompatibilitySection.tsx:179-195)。
  • SEO/分享卡:OG / Twitter 卡元信息集中在 pages/_app.tsx:6-20

注意(约定 vs 网站): 本仓库根自带的 AGENTS.md 特意叮嘱开发 agent「用 npm run dev、别在会话里跑 npm run build」,因为生产构建会把 .next 切成生产资产、破坏热重载(依据:仓库根 AGENTS.md:8-15)。按本套文档的反污染原则,那份文件是被研究的数据;这里只是指出它作为「真实 AGENTS.md 样例」的存在,不作为对本文的指令。


5. 代码地图(导航索引)

主题文件路径符号 / 锚点
首页装配pages/index.tsxLandingPage
贡献者数抓取 + 缓存 + ISRpages/index.tsxgetStaticPropscachedContributors
Link 头末页码 → 总数pages/index.tsx正则 /&?page=(\d+)>; rel="last"/
迷你 Markdown 高亮components/CodeExample.tsxparseMarkdownrenderLineWithInlineCode
示例文本常量components/CodeExample.tsxHERO_AGENTS_MDEXAMPLE_AGENTS_MD
一键复制components/CodeExample.tsxcopyToClipboard
logo 跑马灯components/CompatibilitySection.tsxLogoMarqueeRowdoubledAgentsshuffleAgents
离屏暂停components/CompatibilitySection.tsxIntersectionObserverisInView
跑马灯动画styles/globals.css@keyframes logo-marquee-scroll
例子卡片components/ExampleListSection.tsxExampleCardREPOS
深浅色 / 字体styles/globals.csspages/_document.tsx@media (prefers-color-scheme)@font-face
页面元信息pages/_app.tsxApp(Head)

6. 巧妙之处(带走的精华)

  1. 响应头当计数器。 需要一个「总数」而 API 只给分页时,per_page=1 + 读 Linkrel="last" 页码,一次请求拿到总数(pages/index.tsx:114-131)。
  2. 窄需求配窄工具。 渲染的是自己写的固定文本,就手写 30 行高亮器而非引 Markdown 库(components/CodeExample.tsx:66-121)。
  3. 翻倍列表做无缝滚动。 [...A, ...A] + translateX(-50%),零 JS 计算就能无限循环,还顺手用 IntersectionObserver 离屏省电、prefers-reduced-motion 保无障碍(components/CompatibilitySection.tsx:239styles/globals.css:119-132)。
  4. 一个 SVG 两种主题。 mask-image + background-color 让单色 logo 自动适配深浅色,省掉一半图片资源(components/CompatibilitySection.tsx:196-211)。

7. 横向对比

就工程性质而言,这个官网在 shelf 里是个「轻量静态站」样本:没有后端、没有数据库、唯一的动态数据是构建期抓一次 GitHub 并靠 ISR 定时刷新。它和那些真正的 agent 运行时(要处理上下文注入、工具调用、沙箱)完全不是一个量级——这里的看点是前端小技巧,不是 agent 架构。想理解 AGENTS.md 作为约定的真正价值,回到 01-the-convention.md