Imported from kahobusiness/insights (
AGENTS.md). Install upstream withnpx skills add kahobusiness/insights. Copyright stays with the author.
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Project Overview
This is a bilingual (English/Chinese) blog built with Nextra 4 and Next.js 16, deployed on Vercel. The site covers AI, product design, and travel topics.
Development Commands
pnpm dev # Start dev server (runs `next dev --webpack` — see gotcha below)
pnpm build # Build for production (includes pagefind search index generation)
pnpm start # Start production server
pnpm dev intentionally uses webpack, not Turbopack. Under Turbopack, editing an .mdx file triggers an incremental recompile that intermittently resolves a barrel-exported component (e.g. Callout from nextra/components, which Nextra force-adds to optimizePackageImports) to undefined, 500-ing the page with "Element type is invalid ... got: undefined" until a full restart. Webpack's MDX pipeline doesn't have this HMR race. Trade-off: slightly slower cold start.
Architecture
The whole site is a single Nextra 4 catch-all route. There are no hand-written page files for articles — every .mdx under content/{locale}/ is compiled on demand and served through one dynamic route. Understanding how a URL turns into a rendered, SEO-complete page requires tracing across proxy.ts → app/[lang]/[[...mdxPath]]/page.tsx → mdx-components.js, which is the core flow described below.
Content Structure
content/en/andcontent/zh/- MDX content files mirrored for both localescontent/{locale}/_meta.js- Navigation structure and page titles for Nextra- Navigation supports separators and hidden pages via
_meta.jsconfiguration - Frontmatter is authored as
export const metadata = { title, description, publishedAt, ... }at the top of each.mdx(not YAML). See the commit gate below for required fields.
Internationalization
- Locales:
en(default),zh i18n-config.ts- Locale configuration and types (imported by routing, sitemap, and metadata code — the single source of truth for the locale list)proxy.ts- Handles locale detection (cookie → browser preference), redirects root to/say-hello(Next.js 16 proxy convention)dictionaries/- UI strings for each locale (JSON files)get-dictionary.ts- Server-only dictionary loader
Routing & rendering flow
- Dynamic route:
app/[lang]/[[...mdxPath]]/page.tsxhandles all MDX pages via Nextra'simportPage;generateStaticParamspre-renders every path - Root path and locale roots redirect to
/{locale}/say-hello;proxy.tsauto-prepends locale to paths missing it app/[lang]/layout.tsxwraps everything in the NextraLayout(Navbar/Footer/Search/Banner), loads theme + KaTeX +globals.css(order matters — project CSS must come last), and mounts the auto-hide chrome components. It rejects non-locale first segments withnotFound()(e.g. DevTools probing/.well-known/...) before callinggetPageMap
SEO pipeline (spread across three files — change them together)
page.tsxemits per-page JSON-LD: articles renderBlogPosting, while top-level non-articles listed inNON_ARTICLE_SLUGS(say-hello,logs,japan-gallery) renderWebPageto avoid missing-datePublishedwarnings. It also buildscanonical+hreflangalternates ingenerateMetadatalayout.tsxsets site-level defaults:metadataBase, title template%s | Insights, OG/Twitter cards using/preview.pngapp/sitemap.tswalkscontent/{locale}/at build time, emitting hreflang alternates per path and skippingHIDDEN_SEGMENTS. Pages hidden from nav or non-indexable must be added to that set or they leak into the sitemapnext.config.mjssetsunstable_shouldAddLocaleToLinks: trueso Nextra emits locale-prefixed internal hrefs (/zh/...). Without it, links are locale-less, every crawl hop goes through aproxy.ts307, and Googlebot files pages under "Page with redirect" instead of indexing them- The commit gate's SEO checks (description / alt / anchor text) exist because these fields feed directly into the JSON-LD and cards above
MDX components & interactivity
mdx-components.jsextends the Nextra theme and globally registersImageRowandRecentPosts(usable in any.mdxwithout import). ItsWrappertranslates author-controlledmetadata.publishedAtinto Nextra'stimestamp(see commit gate check 6 for whytimestampcan't be used directly)- Heavier interactive widgets (
CostBatchSimulator,TComputeMemorySimulator, both recharts-based) are imported per-file in the MDX that uses them via the@app/*alias, not globally registered app/components/gallery.tsx- LightGallery-based image gallery for photo pages- LaTeX is enabled (
latex: trueinnext.config.mjs), rendered with KaTeX
Build & config gotchas
- Do NOT add
turbopack.resolveAlias['next-mdx-import-source-file']tonext.config.mjs. Nextra already aliases it to@vercel/turbopack-next/mdx-import-source(its HMR-stable MDX provider); overriding it with a raw./mdx-components.jspath replaces that machinery and 500s on cold compile. This alias was removed — seenext.config.mjscomment. (Dev on webpack sidesteps the Turbopack MDX HMR issues entirely; see Development Commands.) pnpm buildrunsnext buildthenpagefindto generate the client-side search index intopublic/_pagefind— search is broken in a plainnext buildwithout this step- Package manager is pnpm locally and on Vercel, and
pnpm-lock.yamlis committed. Vercel installs in CI mode (frozen lockfile), so any dependency change must regenerate the lockfile in the same commit or the build fails withERR_PNPM_OUTDATED_LOCKFILE. The project env varENABLE_EXPERIMENTAL_COREPACK=1(Production + Preview) pins Vercel to thepackageManagerversion (pnpm 11.9.0); without it Vercel would pick pnpm 10 from the lockfile version, not 11.pnpm-workspace.yaml(not.npmrc, per pnpm 11) holds the dependency overrides (zod: ~4.1.12,@xmldom/xmldom: ^0.9.12), duplicated inpackage.jsonoverridesas a safety net for npm — keep both in sync; the yaml comments explain each one. Nextra 4.6.1's<Layout>crashes the whole site under zod 4.2+ (a build that misses the override fails at prerender withexpected nonoptional, received undefined → at children). Dependabot (.github/dependabot.yml) opens weekly grouped version-update PRs againstdev; security updates always targetmain
Path Alias
@app/*maps to./app/*
Commit 门禁检测机制
在每次帮助用户提交 commit 时,必须先执行以下三项检查,确保双语内容同步和导航配置正确。
检查 1: MDX 文档双语对应检查
触发条件: 本次 commit 在 content/en/ 或 content/zh/ 下新增或修改了 .mdx 文件
检查步骤:
- 通过
git status或git diff --cached识别新增/修改的 MDX 文件 - 若在
content/en/下有变更,检查content/zh/对应路径是否存在相同文件名 - 若在
content/zh/下有变更,检查content/en/对应路径是否存在相同文件名
若文件缺失:
- 提示用户缺少对应语言版本
- 询问是否需要在对应位置创建文件并填充翻译内容
若文件存在,检查内容对应性:
- 读取双语版本文件内容
- 比较核心结构:标题层级、图片引用、代码块数量
- 若本次修改的内容在另一语言版本中未同步更新,提醒用户
检查 2: _meta.js 导航配置检查
关键文件:
content/en/_meta.js/content/zh/_meta.js- 根目录导航content/{locale}/{subfolder}/_meta.js- 子目录导航
检查步骤:
- 识别新增 MDX 文件所在的文件夹
- 读取该文件夹对应的
_meta.js文件 - 检查新文件的 basename (不含扩展名) 是否已在 meta 配置中定义
若未定义:
- 提示用户该文件未在导航中配置
- 建议添加配置格式:
"new-file": "页面标题"
检查 3: logs.mdx 更新日志检查
文件位置:
content/en/logs.mdx- 英文更新日志content/zh/logs.mdx- 中文更新日志
日志格式:
### YYYY-MM
* Published: [文章标题](../path/to/file.mdx)
* Updated: [文章标题](../path/to/file.mdx)
检查步骤:
- 检查当前年月 (如
### 2026-01) 下是否有本次新增/修改文件的记录 - /en 目录下,新增文件应使用
Published:,修改文件应使用Updated: - /zh 目录下,新增文件应使用
新增文档:,修改文件应使用更新文档:
检查 4: SEO frontmatter 完整性检查
触发条件: 本次 commit 在 content/en/ 或 content/zh/ 下新增 .mdx 文件
检查步骤:
- 读取新增文件,确认顶部存在
export const metadata = { description: '...' } description字段不能为空字符串,且建议长度在 60–160 字符(搜索结果摘要常见上限)- 该
description会被BlogPostingJSON-LD schema 与 OG/Twitter card 复用,缺失会让搜索引擎与社交分享退化为站点级默认描述
若缺失:
- 提示用户该文件缺少
description - 建议在
import之后、H1 之前补充export const metadata = { description: '一句话描述本文要点' }
检查 5: 图片 alt 与内链锚文本检查
触发条件: 本次 commit 在 content/en/ 或 content/zh/ 下新增或修改 .mdx 文件
检查步骤:
- 扫描本次变更内容中的
图片,确认alt文本非空且具有描述性 - 扫描
[文本](url)链接,确认锚文本不是「点击这里」「这里」「click here」「here」「link」「了解更多」等无描述性的占位词 - 这两项直接影响搜索引擎理解图片含义和链接目标,也是无障碍体验的基础
若发现问题:
- 列出具体行号与问题类型(EMPTY_ALT / GENERIC_ANCHOR)
- 建议改写为描述图片内容或链接目的的文本
检查 6: 文章 publishedAt 是否需要更新
背景:
站点底部显示的「最后更新于 xxx」取自每篇文章 metadata.publishedAt 字段(手动维护,由 mdx-components.js 里的 wrapper 转换为 Nextra 的 timestamp 渲染)。注意:不能直接用 timestamp 字段 —— Nextra 的 remark-assign-frontmatter 会用每个文件的 git 最后修改时间无条件覆盖 timestamp,导致 SEO 批量改一行也会让所有文章显示同一天。
这个时间是给读者看的「内容代表更新日期」,应该只在文章内容实质性更新时才往后调,避免 SEO 改一行字、修一个错别字也让所有文章看起来像刚翻新过。
触发条件: 本次 commit 在 content/en/ 或 content/zh/ 下新增或修改 .mdx 文件
判断规则:
- ✅ 应该更新 publishedAt:新增文章、重写章节、补充新内容、修复事实错误、推翻论证、加入新比喻或新数据;中英文版应同步更新到同一日期
- ❌ 不应该更新 publishedAt:批量改
metadata.description(SEO)、统一术语大小写、修错别字、调标点、只改一两个翻译腔短语、改图片 alt、改链接锚文本、纯格式化(如 prettier)、文件改名/重新编号 - ⚖️ 边界情况:如果一次改动既包含实质内容、又包含批量小修,按"是否值得让读者重新看一眼"判断;倾向更新
检查步骤:
- 通过
git diff --cached看本次每个 mdx 的实际改动 - 对每个文件按上面规则判断是否属于"实质内容更新"
- 对应该更新的文件,确认
metadata.publishedAt已被改到今天的日期(格式:'YYYY-MM-DD') - 对不应更新的文件,确认 publishedAt 保持原值
- 新增 mdx 必须含
publishedAt字段,否则 frontmatter 检查不通过
若不一致:
- 列出"应该更新但未更新"和"不应更新但已更新"的文件
- 询问用户每篇文章是否需要调整 publishedAt,再继续 commit
检查结果处理
| 检查项 | 通过 | 未通过 |
|---|---|---|
| 双语对应 | 继续 | 询问是否创建对应语言文件 |
| _meta.js | 继续 | 提示并建议配置 |
| logs.mdx | 继续 | 提示并建议日志内容 |
| SEO description | 继续 | 提示并建议补充 metadata.description |
| 图片 alt / 链接锚文本 | 继续 | 列出问题并建议改写 |
| publishedAt 是否应更新 | 继续 | 列出建议、与用户确认每篇取值 |
执行时机: 用户请求 commit 时,先执行以上检查,全部通过或用户确认处理完问题后,再执行 commit 操作。
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.