游戏站模板化器 (Game Site Template Maker)
核心原则
三层分离,是模板化的唯一心法。 一套框架要服务好几个不同的游戏,每个游戏有不同的内容分类、主题色、官方链接、文章。如果全揉在一起,换一个游戏就要把代码翻一遍,又慢又容易改漏。把一个站拆成三层,各管各的:
| 层 | 内容 | 改动频率 | 模板化目标 |
|---|---|---|---|
| 框架层 | 整体布局、页面结构、组件、技术栈、SEO/多语言生成逻辑 | 一次写好,以后基本不动 | 所有站共用的"地基" |
| 配置层 | 游戏名、主题色、官方链接、导航分类、SEO 标题描述、支持语言 | 每开一个新站,改几个地方 | 集中放进几个配置文件 |
| 内容层 | 所有内页文章、首页各模块文案数据、各语言翻译 | 每个站完全独立替换 | 动态读取,新增/删除自动跟随 |
一句话:改内容不动代码,改配置不重写框架。换新游戏只动配置层和内容层,框架层一行不改。
模板成了没的验证标准(来自源文档):换个游戏名,看看是不是只动配置 + 固定提示词,就能让整站切过去。能做到,模板就成了。这是本技能唯一的"毕业考试",必须实测通过。
本技能由 AI 直接执行重构。 AI 自主完成:扫描现有站点 → 分类三层 → 抽取配置 → 改造动态读取 → 换皮验证 → 封装模板。不需要用户手动改代码。
前置条件
用户需提供:
- 第一个游戏站的项目路径(已用
yxz-game-site-builder跑通、能正常npm run build出out/的 Next.js 项目) - 该站的游戏名(用于识别哪些是"游戏特定信息"需要抽进配置)
- 对标站游戏名(若复刻时参照了对标站,需告知以便一并清理残留)
技能关系
yxz-hot-word-miner → 选定游戏主词
↓
yxz-keyword-miner → 产出关键词清单 + 页面矩阵
↓
yxz-game-info-collector → 产出首页信息 + 内页素材
↓
yxz-game-site-builder → 搭出第一个站(单站,硬编码)
↓
yxz-game-site-template-maker ← 本技能:把第一个站打磨成模板(本技能)
↓
(后续)新站只需:复制模板 + 改配置 + 换内容 → 直接上线
↓
(站内放大)yxz-bulk-page-factory ← 模板成型后,把页面矩阵/GSC补页面清单批量铺成内页,放大单站流量
本技能是
yxz-game-site-builder的下游。前者负责"做一个站",本技能负责"把一个站变成十个站的模板"。是路线 A「批量放大」的落点。
工具与数据获取方式
文件操作:Read 读取项目源码与素材文档;Grep 全项目扫描硬编码值与残留;Glob 按模式列出文件;Edit 重构代码(把硬编码改为读配置);Write 生成配置文件、模板文档、脚手架。
命令执行:Bash 执行 npm run build(注意绕过 WorkBuddy 构建挂死,见踩坑)、git 命令、Python 脚本。
Python 运行时:脚本均为纯 Python 标准库,无第三方依赖。Managed 解释器路径为 C:/Users/Brahma/.workbuddy/binaries/python/versions/3.13.12/python.exe(若 envs/default/python.exe 这个 venv 不存在,直接用带版本号的解释器即可,无需 pip install)。
浏览器操作(仅换皮验证阶段可选):mcp__playwright-extension__browser_* 访问本地静态服务,确认换皮后页面正常渲染。本地静态服务用 python -m http.server <port> --directory out --bind 127.0.0.1。
执行流程
第一阶段:三层分类扫描(AI 自动执行)
AI 扫描现有项目,把每一个"值"分类到三层之一,产出分类清单。这是后续重构的依据。
步骤 1.1:识别框架层(冻结不动)
框架层是"换游戏不会变"的部分。AI 用 Glob + Read 通读项目,标记以下为框架层:
- 技术栈:
package.json、next.config.js/mjs、tsconfig.json、构建配置 - 全局布局:
app/layout.tsx(结构骨架,但其中的游戏名/导航文案属配置层,需抽出) - 组件结构:
components/下各组件的结构(不是其中写死的文案) - 路由生成逻辑:
app/[lang]/[slug]/page.tsx的动态路由读取逻辑 - SEO/sitemap 生成逻辑:
lib/seo.ts、next-sitemap.config.js的生成函数(不是写死的标题) - 样式骨架:
app/globals.css的设计 token 结构(变量名),但变量值属配置层 - 构建脚本:
scripts/下的 mirror/validate 脚本
判定规则:问自己"换一个游戏,这个文件/这段代码要不要改?"——不要改的就是框架层。
步骤 1.2:识别配置层(需抽取)
配置层是"换游戏就变、但量很小"的信息。AI 用 Grep + 人工判断,标记以下为配置层:
| 配置项 | 常见硬编码位置 | 示例 |
|---|---|---|
| 游戏名 | app/layout.tsx、components/Hero.tsx、components/Footer.tsx、lib/seo.ts |
"Big Walk" |
| 主题色 HSL | app/globals.css 的 :root 变量、组件内联 style |
--primary: 25 95% 53% |
| 官方链接 | components/Footer.tsx、components/Sidebar.tsx |
官网/Discord/YouTube URL |
| 导航分类 | components/Navbar.tsx |
Guide/Classes/Codes/Tier List |
| SEO title/description/keywords | lib/seo.ts、各页 metadata 导出 |
含游戏名的标题 |
| 支持语言 | content/i18n.json、next.config.js i18n 配置 |
['en','de','es'] |
| 首页 Hero stats | components/Hero.tsx |
发行日期/Metacritic/玩家数 |
| 兑换码 | components/Sidebar.tsx |
真实码或"暂无" |
| 底部号召文案 | app/page.tsx |
CTA 文字 |
判定规则:问自己"换游戏时,这个值要不要换?"——要换的就是配置层。
步骤 1.3:识别内容层(需独立化)
内容层是"每个站完全不一样、量大"的部分:
- 内页文章:
content/en/*.mdx(每个关键词一篇文章) - 首页模块文案数据:新手引导卡片内容、游戏介绍段落(若写在
app/page.tsx里则属硬编码,需抽到内容层) - 多语言翻译:
content/de/、content/es/等
判定规则:问自己"换游戏时,这部分是不是要完全重新写?"——是的就是内容层。
步骤 1.4:产出三层分类清单
AI 把扫描结果写成 {项目目录}/_template_audit.md:
# 三层分类审计 - {游戏名}
## 框架层(冻结不动)
- app/layout.tsx(结构骨架)
- components/Navbar.tsx(结构)
- ...
## 配置层(需抽取 → site.config.json)
| 配置项 | 当前硬编码位置 | 当前值 |
|--------|---------------|--------|
| gameName | app/layout.tsx:12 | Big Walk |
| theme.primary | app/globals.css:3 | 25 95% 53% |
| officialLinks.homepage | components/Footer.tsx:8 | https://bigwalkgame.com |
| ...
## 内容层(需动态读取)
- content/en/guide.mdx
- content/en/tips.mdx
- (首页新手引导卡片文案目前硬编码在 app/page.tsx:45-60,需抽出)
第二阶段:配置层抽取(AI 重构代码)
把步骤 1.2 识别的所有配置层值,从代码里抽出来集中到一个配置文件,代码改为"读配置"不"写死"。
步骤 2.1:定义配置文件 site.config.json
AI 在项目根目录创建 site.config.json,结构如下(所有游戏特定信息的唯一真相源):
{
"gameName": "Big Walk",
"gameSlug": "your-game",
"tagline": "Co-op Adventure Guide",
"theme": {
"primary": "25 95% 53%",
"primaryForeground": "0 0% 100%",
"background": "0 0% 100%",
"foreground": "222 47% 11%",
"muted": "210 40% 96%",
"border": "214 32% 91%",
"accent": "142 71% 45%"
},
"officialLinks": {
"homepage": "https://bigwalkgame.com",
"discord": "https://discord.gg/bigwalk",
"youtube": "https://youtube.com/@bigwalk",
"steam": "https://store.steampowered.com/app/xxx"
},
"nav": [
{ "label": "Guide", "href": "/guide" },
{ "label": "Classes", "href": "/classes" },
{ "label": "Codes", "href": "/codes" },
{ "label": "Tier List", "href": "/tier-list" }
],
"seo": {
"title": "Big Walk Wiki & Guide | Tips, Codes, Classes",
"description": "Complete Big Walk guide with beginner tips, all class rankings, active codes and co-op walkthroughs.",
"keywords": "big walk guide, big walk wiki, big walk codes, big walk classes"
},
"languages": ["en", "de", "es"],
"hero": {
"title": "Big Walk Wiki",
"subtitle": "Your complete guide to the co-op adventure",
"stats": [
{ "label": "Release", "value": "2025" },
{ "label": "Metacritic", "value": "82" },
{ "label": "Steam Reviews", "value": "Mostly Positive" },
{ "label": "Players", "value": "10K+" },
{ "label": "Achievements", "value": "40" }
]
},
"sidebar": {
"codes": [
{ "code": "BIGWALK2025", "reward": "500 Coins", "status": "active" }
]
},
"cta": {
"title": "Start Your Adventure",
"text": "Pick a guide below and jump in.",
"buttonText": "Read the Beginner Guide"
}
}
关键:这是换新站时唯一需要改的文件(内容层另算)。所有游戏特定信息都在这里,代码里不允许再出现任何游戏名、主题色、官方链接的硬编码。
步骤 2.2:重构代码读配置
AI 用 Edit 逐个重构,把硬编码改为读 site.config.json:
// lib/config.ts —— 配置加载工具(框架层,所有站共用)
import config from "../site.config.json";
export const siteConfig = config;
export const { gameName, theme, officialLinks, nav, seo, hero, sidebar, cta } = config;
// app/layout.tsx —— 改造前
export const metadata = { title: "Big Walk Wiki", ... };
// 改造后
import { seo, gameName } from "@/lib/config";
export const metadata = { title: seo.title, ... };
// components/Hero.tsx —— 改造前
<h1>Big Walk Wiki</h1>
<p>Your complete guide to the co-op adventure</p>
// 改造后
import { hero } from "@/lib/config";
<h1>{hero.title}</h1>
<p>{hero.subtitle}</p>
/* app/globals.css —— 主题色集中到变量,值来自配置 */
/* 改造前::root { --primary: 25 95% 53%; } 写死 */
/* 改造后:CSS 变量名是框架层,值通过内联 style 从配置注入 */
// app/layout.tsx —— 主题色从配置注入到 CSS 变量
import { theme } from "@/lib/config";
export default function RootLayout({ children }) {
return (
<html lang="en" style={{
"--primary": theme.primary,
"--primary-foreground": theme.primaryForeground,
"--background": theme.background,
// ...
} as React.CSSProperties}>
<body>{children}</body>
</html>
);
}
重构检查清单(AI 用 Grep 验证零硬编码):
# 游戏名硬编码(应只剩 site.config.json + lib/config.ts 两处)
Grep(pattern="Big Walk", path="{项目目录}", glob="*.{tsx,ts,css,json,mdx}")
# 期望:只有 site.config.json 命中
# 主题色硬编码(globals.css 的 :root 不应再有具体 HSL 值)
Grep(pattern="--primary:\s*\d", path="{项目目录}/app/globals.css")
# 官方链接硬编码
Grep(pattern="bigwalkgame\.com|discord\.gg/bigwalk", path="{项目目录}")
步骤 2.2b:可见文案的占位符替换(换皮真正生效的机制)
光把配置拆出来还不够。换皮验证是扫产物 HTML 找真实游戏名残留(见第四阶段),而配置里里外外到处是游戏名。要让"换游戏只动配置"成立,必须区分两类字符串:
- SEO 元数据(
metadata.title/description/keywords、sitemap/robots 文案):这些是给搜索引擎看的真实字段,应保留字面游戏名(对真实站是正确的;换皮测试时会被虚拟配置整文件覆盖,所以不会残留)。 - 可见正文(hero 标题、About 段落、四塔卡片、页面 H1/lede、footer 文案):这些是给人看的渲染文案,必须用占位符
{gameName}/{developer}/{publisher},运行时由gt()替换成配置值。不能写死游戏名——否则换皮后产物 HTML 里仍残留旧名,测试 FAIL。
配置加载器(框架层,所有站共用)——注意 import 绑定名不要和导出 const 同名(同名会触发 Identifier 'siteConfig' has already been declared 的构建报错):
// lib/config.ts ← 推荐 JS 版 lib/config.js
import rawConfig from "../site.config.json"; // 绑定名用 rawConfig,避开冲突
export const siteConfig = rawConfig;
export const { gameName, gameSlug, domain, developer, publisher, tagline,
theme, officialLinks, seo, nav, hero, about, fourTowers,
startJourney, finalCta, footer, pages } = siteConfig;
// 占位符替换:把 "{gameName}" 等就地展开成当前配置值
export function gt(text: unknown): string {
if (!text) return text as string;
return String(text)
.split("{gameName}").join(gameName)
.split("{developer}").join(developer)
.split("{publisher}").join(publisher);
}
export default siteConfig;
配置里的可见字符串一律写占位符:
// site.config.json
{
"gameName": "Big Walk",
"hero": { "title": "{gameName} Wiki", "subtitle": "Your complete {gameName} guide" },
"about": { "title": "What is {gameName}?", "paragraphs": ["{gameName} is made by {developer} and published by {publisher}."] },
"pages": {
"guide": { "h1": "Complete {gameName} Guide", "lede": "Everything about {gameName}." }
}
}
渲染时两路都过 gt():
// app/page.tsx
<h1>{gt(hero.title)}</h1> {/* "{gameName} Wiki" → "Big Walk Wiki" */}
<p>{gt(hero.subtitle)}</p>
// components/Sections.jsx 的 PageHead/SectionTitle/QuickFacts/FinalCta 全部用 gt() 包裹文案
// 内页:title/lede 从配置读,含占位符
<PageHead title={pages["guide"].h1} lede={pages["guide"].lede} /> {/* 经 gt() 渲染 */}
正文里嵌游戏名用 JSX Token 组件(避免和周围文字的空格/标点拼接出错):
// components/Tokens.tsx
import { gameName, developer, publisher } from "@/lib/config";
export function GameName({ children }: { children?: React.ReactNode }) {
return <>{gameName}{children}</>;
}
export function Developer({ children }: { children?: React.ReactNode }) {
return <>{developer}{children}</>;
}
export function Publisher({ children }: { children?: React.ReactNode }) {
return <>{publisher}{children}</>;
}
// 页面正文
<p><GameName /> is developed by <Developer /> and published by <Publisher />.</p>
<p>Open <GameName />'s settings to change the language.</p> {/* 所有格 's 紧贴组件不换行 */}
核心记忆点:换皮 = 把"真实游戏名"从所有可见文案里抽成
{gameName}占位符;gt()+ Token 组件在渲染时展开。这样换配置时整站文案自动跟随,不需要改任何一格代码,产物 HTML 里自然零残留。
步骤 2.3:导航/路由动态化
导航菜单从配置读(已在 2.2 完成)。路由结构改为根据内容目录自动生成:
# 改造前:导航和路由是写死的
app/guide/page.tsx
app/classes/page.tsx
# 改造后:动态路由根据 content/ 目录自动生成
app/[lang]/[slug]/page.tsx ← 读取 content/{lang}/*.mdx,新增/删除文章自动跟随
AI 检查 app/[lang]/[slug]/page.tsx 的 generateStaticParams 是否扫描 content/ 目录:
// lib/content.ts —— 框架层,根据 content/ 目录动态生成路由参数
import fs from "fs";
import path from "path";
export function getAllSlugs(lang = "en") {
const dir = path.join(process.cwd(), "content", lang);
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.endsWith(".mdx"))
.map(f => f.replace(/\.mdx$/, ""));
}
export async function getArticle(slug, lang = "en") {
const file = path.join(process.cwd(), "content", lang, `${slug}.mdx`);
// ...读取并解析 MDX
}
步骤 2.4:SEO 文件(sitemap/robots/manifest)生成
血的教训(本技能实战踩坑):Next.js 14 +
output: "export"下,app/sitemap.js/app/robots.js/app/manifest.js这类 metadata route 会要求generateStaticParams(),在next build里能生成静态文件,但在next dev下直接 HTTP 500。当你"把站跑起来验证"(第四阶段 4.4 或用户要求本地起服务)时,/sitemap.xml、/robots.txt、/manifest.webmanifest全挂,体验极差。模板化正确做法:不要依赖 metadata route,改用静态生成脚本。用一个
scripts/gen-seo.mjs(纯 Node,无依赖)从site.config.json生成public/sitemap.xml+public/robots.txt,再用package.json的predev/prebuild钩子自动跑。静态文件在next dev和next build下都正常(build 会原样拷进out/)。PWA manifest 同理——用public/favicons/site.webmanifest静态文件,别用app/manifest.js。
scripts/gen-seo.mjs(已随技能分发,复制进模板的 scripts/ 即可):
// scripts/gen-seo.mjs —— 从 site.config.json 生成 public/sitemap.xml + public/robots.txt
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const cfg = JSON.parse(fs.readFileSync(path.join(root, "site.config.json"), "utf-8"));
const base = process.env.BASE_URL || `https://${cfg.domain}`;
const slugs = new Set(Object.keys(cfg.pages || {}));
for (const row of cfg.nav || []) {
const href = row?.href || row?.url;
if (href && href.startsWith("/") && href.length > 1) slugs.add(href.replace(/^\//, "").replace(/\/$/, ""));
}
const urls = [`${base}/`, ...[...slugs].map(s => `${base}/${s}`)];
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset ...>${urls.map(u => ` <url><loc>${u}</loc></url>`).join("\n")}</urlset>`;
const robots = `User-Agent: *\nAllow: /\n\nSitemap: ${base}/sitemap.xml\n`;
fs.writeFileSync(path.join(root, "public", "sitemap.xml"), xml, "utf-8");
fs.writeFileSync(path.join(root, "public", "robots.txt"), robots, "utf-8");
package.json 钩子(确保每次 dev/build 前都刷新):
{
"scripts": {
"predev": "node scripts/gen-seo.mjs",
"dev": "next dev",
"prebuild": "node scripts/gen-seo.mjs",
"build": "next build",
"gen-seo": "node scripts/gen-seo.mjs"
}
}
若原站已有
app/sitemap.js/app/robots.js/app/manifest.js,用mv移出app/(见踩坑 #8,rm会被安全删除 shim 拦截),改由静态文件 + 脚本提供。SEO 元数据(title/description/keywords)仍从site.config.json的seo字段读,不受影响。
第三阶段:内容层独立化
确保内容层完全独立——新增/删除一篇文章,前端自动跟随,不用手动改任何代码。
步骤 3.1:内容目录结构标准化
content/
├── en/
│ ├── guide.mdx
│ ├── tips.mdx
│ ├── codes.mdx
│ └── ...
├── de/
│ └── (同结构,德语翻译)
└── i18n.json ← 语言映射,从 site.config.json.languages 派生
步骤 3.2:首页模块文案抽到内容层
若首页新手引导卡片、游戏介绍段落的文案硬编码在 app/page.tsx,AI 抽到 content/en/home.json:
// content/en/home.json
{
"beginnerCards": [
{ "title": "Beginner Guide", "desc": "First 2 hours walkthrough", "href": "/guide" },
{ "title": "Best Classes", "desc": "Rankings for every playstyle", "href": "/classes" }
],
"aboutGame": "Big Walk is a co-op adventure where you..."
}
// app/page.tsx 改造后
import home from "@/content/en/home.json";
{home.beginnerCards.map(card => <Card {...card} />)}
步骤 3.3:验证内容动态跟随
AI 新增一个测试文章 content/en/test-dynamic.mdx,确认:
- 不改任何其他代码
npm run build后out/en/test-dynamic/index.html自动生成- 导航/ sitemap 自动包含该页
验证通过后删除测试文章。
第四阶段:换皮验证(毕业考试)
这是源文档定义的"模板成了没"的验证:换个游戏名,只动配置 + 固定提示词,让整站切过去。
步骤 4.1:准备虚拟游戏配置
AI 创建一个虚拟游戏的配置 site.config.test.json(用一个明显是测试的名字,如 "TestGame XYZ"):
{
"gameName": "TestGame XYZ",
"gameSlug": "testgame-xyz",
"theme": { "primary": "280 80% 55%", ... },
"officialLinks": { "homepage": "https://example.com", ... },
"nav": [ { "label": "Guide", "href": "/guide" } ],
"seo": { "title": "TestGame XYZ Wiki | Guide & Tips", ... },
...
}
步骤 4.2:应用虚拟配置并构建
# 1. 备份真实配置
cp site.config.json site.config.real.bak
# 2. 替换为虚拟配置
cp site.config.test.json site.config.json
# 3. 构建(绕过 WorkBuddy 挂死)
NODE_OPTIONS="" npm run build
# 4. 用自带脚本扫描残留(--no-seo 跳过 SEO 长度噪声,只看换皮残留)
python scripts/swap_test.py out/ --real-game "Big Walk" --ref-game "farever" --no-seo
# 5. 恢复真实配置
cp site.config.real.bak site.config.json
步骤 4.3:换皮测试通过标准
scripts/swap_test.py 扫描 out/ 全部 HTML,断言:
| 检查项 | 要求 | 说明 |
|---|---|---|
| 真实游戏名残留 | 0 处 | "Big Walk" 不应出现在任何产物 HTML(说明框架层无硬编码) |
| 对标站游戏名残留 | 0 处 | "farever" 等不应残留 |
| 虚拟游戏名出现 | ≥1 处 | "TestGame XYZ" 应出现(说明配置已生效) |
| 主题色已切换 | 是 | 产物 CSS 中 primary 应是虚拟配置的紫色,不是原来的橙色 |
| CJK 泄漏 | 0 处 | 英文站无中文 |
| 占位符 | 0 处 | 无 {GAME_NAME}/TODO/WIP |
| 内页可访问 | 全部 200 | 所有 slug 页面正常渲染 |
全部通过 = 模板成了。 任一失败,AI 回到第二/三阶段排查硬编码残留,修复后重测。
实战要点:
- 用
--no-seo拿干净闸门。虚拟测试配置里的 title/description/keywords 往往是占位短句,swap_test.py的 SEO 长度检查(title≤60/desc 140-160/kw≤100)会刷出一堆 WARN——这些是换皮无关噪声,不是框架层缺陷。跑毕业考试用swap_test.py out/ --real-game "Big Walk" --old-name "House House" --old-name "Panic" --test-game "TestGame XYZ 7841" --no-seo,只看真实名/对标名/CJK/占位符零残留 + 虚拟名出现,exit 0 即 "TEMPLATE IS READY."。 - 虚拟名要明显到不像真词:用 "TestGame XYZ 7841" 这类带随机后缀的,避免和真实代码/库名撞车导致误判。
- 常见英文短语会误报:
swap_test.py是子串/词匹配,"still got a big walk to go"这种双关句会命中 "Big Walk","Don't panic"会命中 "Panic"。每一条命中都必须人工复核:确属游戏名残留就修;确属普通英文就改写措辞(如 "big walk to go"→"long way to go"、"Don't panic"→"Don't freak out")。 --no-placeholder/--no-color也可按需关闭对应检查(占位符 WARN、主题色 INFO)。
步骤 4.4:(可选)浏览器渲染验证
# 起静态服务
python -m http.server 4321 --bind 127.0.0.1 --directory out
# 用 Playwright MCP 访问,确认虚拟配置已生效
browser_navigate("http://127.0.0.1:4321/")
browser_take_screenshot(...) # 标题应是 TestGame XYZ,主题色应是紫色
第五阶段:模板封装与文档
换皮验证通过后,AI 把项目封装成可复用模板,并产出新站脚手架。
步骤 5.1:清理真实数据,写入模板示例配置
# 1. 配置文件保留为"示例配置"(带注释说明每项含义)
# site.config.json 改名为 site.config.example.json,游戏名改为占位 "YOUR_GAME_NAME"
# 2. content/ 下保留 1-2 篇示例文章(占位内容),删除真实内容
# content/en/guide.mdx → 改为占位示例
# 3. 恢复 site.config.json 为示例配置
cp site.config.example.json site.config.json
步骤 5.2:编写模板 README
AI 用 Write 生成 TEMPLATE_README.md:
# {游戏名} Wiki —— 站点模板
这是一个三层分离的游戏攻略站模板。开新站只需 3 步:
## 快速开新站
1. 复制本模板到新目录
2. 编辑 `site.config.json`,填入新游戏信息
3. 把内页文章放进 `content/{lang}/`,执行 `new_site.py` 校验
## 三层结构
- **框架层**(不要动):app/layout.tsx, components/, lib/, 构建配置
- **配置层**(改这里):site.config.json
- **内容层**(换这里):content/{lang}/*.mdx
## 配置项说明
| 字段 | 含义 | 示例 |
|------|------|------|
| gameName | 游戏全名 | "Big Walk" |
| theme.primary | 主题色 HSL | "25 95% 53%" |
| ...
## 换皮验证
改完配置后,跑换皮测试确认零残留:
python scripts/swap_test.py out/ --real-game "旧游戏名"
步骤 5.3:新站脚手架脚本
AI 确认 scripts/new_site.py 可用(本技能自带,见下文"复用脚本")。开新站时:
python scripts/new_site.py --template {模板目录} --dest {新站目录} --config {新游戏配置.json}
脚本自动:复制框架层 → 写入新配置 → 创建空 content 目录 → 首次构建校验。
复用脚本
scripts/extract_config.py <project_dir> [--game-name X]:扫描现有项目,识别硬编码的游戏特定信息(游戏名、主题色、官方链接、导航分类),产出候选site.config.json+ 硬编码位置清单_template_audit.md。第一阶段扫描用。scripts/swap_test.py <out_dir> --real-game <真实游戏名> [--ref-game <对标站名>] [--old-name <旧名>] [--test-game X] [--no-seo] [--no-placeholder] [--no-color]:换皮测试闸门。扫描 build 产物 HTML,断言真实游戏名/对标站名/CJK/占位符零残留 + 虚拟游戏名已生效 + 主题色已切换。第四阶段毕业考试用。跑毕业考试加--no-seo拿干净闸门(跳过 SEO 长度 WARN 噪声)。scripts/new_site.py --template <模板目录> --dest <新站目录> --config <新配置.json>:从模板脚手架新站。复制框架层、写入配置、初始化 content 目录、首次构建校验。必填字段从模板自身 config 派生(不写死)。第五阶段及后续开新站用。scripts/gen-seo.mjs(纯 Node,无依赖):从site.config.json生成public/sitemap.xml+public/robots.txt。替代app/sitemap.js/app/robots.js(后者在output:"export"+next dev下 500)。package.json用predev/prebuild钩子自动跑。
Python 脚本均纯标准库,用 managed 解释器运行:
C:/Users/Brahma/.workbuddy/binaries/python/versions/3.13.12/python.exe scripts/xxx.py ...(若envs/default/python.exevenv 不存在,用带版本号的解释器即可)。
执行要点
- 三层分离是唯一心法:框架层冻结、配置层集中、内容层动态。每改一处先问"这属哪一层"。
- 配置层是唯一真相源:所有游戏特定信息只活在
site.config.json,代码里零硬编码。换新站只改这一个文件。 - 内容层动态读取:新增/删除文章不改任何代码,前端自动跟随。用
generateStaticParams扫描 content 目录。 - 主题色集中管理:CSS 变量名是框架层,变量值从配置注入。换皮只改
site.config.json的theme字段。 - 毕业考试必测:用虚拟游戏名换皮,build 后扫描产物零真实游戏名残留。不测不算模板成。
- 扫产物不扫源码:源码合规不代表产物合规(build 会把残留带进 HTML,CSS 注释里的游戏名会泄漏)。用
swap_test.py扫out/*.html。 - AI 直接执行重构:扫描 → 抽配置 → 改代码 → 换皮验证 → 封装,全程 AI 自主,不需用户手动改代码。
- 不破坏现有站:重构前
cp -r整站备份,重构后换皮验证通过再删备份。
浏览器抓取要点
- 换皮验证可选浏览器渲染:静态扫描(
swap_test.py)通过后,可选起服务用 Playwright MCP 截图确认主题色/标题已切换。 - 统一用
mcp_playwright-extension:复用已装 Chrome,不另下 Chromium。 - 两种起服务方式:
- A.
next dev(推荐,可交互验证):NODE_OPTIONS="" npx next dev -p 3000,访问http://localhost:3000/。前提是按步骤 2.4 把 sitemap/robots/manifest 改成静态生成——否则/sitemap.xml在 dev 下 500。改完源码 dev 会热更新,最适合"把站跑起来看效果"。 - B. 静态产物:
python -m http.server <port> --directory out --bind 127.0.0.1,再 navigatehttp://127.0.0.1:<port>/。适合只验证 build 产物。 - MCP 禁
file://:必须用 http 服务,不要直接打开本地 html。
- A.
- 截图限 MCP 工作目录:
browser_take_screenshot落在 MCP runtime 目录(如C:/Users/Brahma/.workbuddy/logs/mcp-runtime/custom-mcp_playwright-extension-*/),用Read直接看那个路径即可,或cp出来。 - 路由验证用
navigate()别用点导航:Playwright 点顶部<a>偶发 actionability timeout——常见于原站残留的 off-screen 浮动元素(button/generic在[-20,0]之类坐标)干扰稳定判定。直接browser_navigate("http://localhost:3000/<slug>")验证每个路由 200 + 标题正确,比点导航稳。 - dev 缓存坏了先清
.next:改了路由/配置后若 dev 报MODULE_NOT_FOUND之类诡异错,杀掉 3000 端口进程,mv .next .next.bak.<日期>(用mv不用rm),重启next dev。
输出文件
执行完成后产出:
模板化的 Next.js 项目(原项目原地重构)
- 框架层代码零硬编码(所有游戏特定信息已抽进配置)
site.config.json作为唯一配置真相源content/{lang}/动态读取,新增/删除文章自动跟随- 导航/路由/sitemap/SEO 全部根据配置 + 内容自动生成
TEMPLATE_README.md说明如何开新站
三层分类审计文档
_template_audit.md:记录每个值归哪一层、硬编码位置、重构方案
换皮测试报告
_swap_test_report.txt:虚拟游戏名换皮结果,证明框架层零残留
配套脚本(已自带,随模板分发)
scripts/extract_config.py:配置抽取scripts/swap_test.py:换皮闸门scripts/new_site.py:新站脚手架
示例配置
site.config.example.json:带注释的配置模板,开新站时复制改名填值
模板级
.gitignore(随模板分发,只提交源码)- 忽略:
node_modules/、.next/、.next.bak*/、out/、backup_*/、*.bak、site.config.*.bak、site.config.test.json、public/sitemap.xml、public/robots.txt(脚本生成,不提交)、.env*.local、.DS_Store。 - 提交:
app/、components/、lib/、scripts/、site.config.json(真实配置)、site.config.example.json、public/下非生成的素材。 - 见本技能实战:临时备份
backup_*/、构建产物out//.next/、换皮测试配置site.config.test.json、脚本生成的sitemap.xml/robots.txt都不应入库。
- 忽略:
三层分类审计文档结构
# 三层分类审计 - {游戏名}
> 审计日期:{日期}
> 项目路径:{路径}
## 框架层(冻结不动)
| 文件 | 说明 |
|------|------|
| app/layout.tsx | 全局布局骨架 |
| components/Navbar.tsx | 导航栏结构 |
| ...
## 配置层(→ site.config.json)
| 配置项 | 当前硬编码位置 | 当前值 | 配置字段路径 |
|--------|---------------|--------|-------------|
| 游戏名 | app/layout.tsx:12 | Big Walk | gameName |
| 主题色 | app/globals.css:3 | 25 95% 53% | theme.primary |
| 官网链接 | components/Footer.tsx:8 | https://... | officialLinks.homepage |
| ...
## 内容层(→ content/{lang}/)
| 内容 | 当前位置 | 目标位置 |
|------|----------|----------|
| 新手引导文章 | content/en/guide.mdx | 保持 |
| 首页新手卡片文案 | app/page.tsx:45-60 | content/en/home.json |
| ...
## 重构方案
1. 创建 site.config.json,填入配置层所有值
2. 创建 lib/config.ts 加载配置
3. 逐个 Edit 重构:硬编码 → 读配置
4. 首页文案抽到 content/en/home.json
5. 换皮测试验证
实战踩坑教训
配置层要"全"不要"漏":最容易漏的是 footer 法律页的游戏名、sidebar 的兑换码、CSS 注释里的游戏名。对策:第一阶段用
extract_config.py扫描,再人工核对照清单逐项确认,宁多勿漏。主题色不能只在 globals.css 改:如果组件里用了内联
style={{ color: "#ff6b35" }},换皮时 globals.css 变了但内联没变。对策:所有颜色必须走 CSS 变量var(--primary),内联色值一律 Grep 出来改成变量引用。换皮测试必须扫产物不扫源码:源码里
gameName只在site.config.json和lib/config.ts出现,看似合规。但 build 后产物 HTML 可能在 SEO 结构化数据、og 标签、JSON-LD 里残留旧值。对策:swap_test.py扫out/**/*.html,不扫app/。HTML 实体膨胀让源码合规的 meta 在产物里超长:
'→'会让源码 60 字符的 title 在产物里变成 67 字符。对策:换皮测试的 SEO 长度检查应扫产物,源码长度只作参考。动态路由的
generateStaticParams必须扫 content 目录:若仍写死[{ params: { slug: "guide" } }, ...],新增文章不会自动生成页面。对策:检查lib/content.ts的getAllSlugs()是fs.readdirSync(content/{lang}),不是硬编码数组。导航分类硬编码在 Navbar 组件里:
<Link href="/guide">Guide</Link>写死,换游戏后导航不跟随。对策:导航项从site.config.json的nav数组.map()渲染。多语言配置两层易脱节:
site.config.json的languages和content/目录结构、next.config.js的 i18n locales 三处容易不同步。对策:以site.config.json.languages为唯一源,lib/config.ts派生 i18n 配置,new_site.py校验 content 目录与配置一致。WorkBuddy 构建/清理拦截(genie-safe-delete shim):同一 shim 既拦截
fs.unlink/rm导致next build收尾卡死,也拦截普通rm/PowerShellRemove-Item删文件。对策:① 构建用NODE_OPTIONS="" npm run build(最稳);② 清理临时文件/备份用mv改名移走(mv foo.py _backup/),不要用rm;③ 含中文的路径在 PowerShell 下Remove-Item会因编码乱码失败,一律走mv或 Git Bash。out/404.html文件锁:build 退出后 404.html 残留句柄删不动(EPERM)。对策:开新目录用 junction 软链 node_modules 重建,或换皮测试时直接在新 out 目录扫。重构前必须整站备份:三层分离重构改动面大,万一改坏要能回退。对策:
cp -r {项目} {项目}_backup_$(date +%s),换皮验证通过后再删备份。配置字段命名要稳定:一旦
site.config.json的字段名定下,框架层代码就依赖它。开第二个站时若改字段名,框架层代码要跟着改——违背"框架层不动"原则。对策:字段名一次定死,开新站只改值不改结构。new_site.py校验配置 schema 完整性。首页文案混在配置层和内容层边界:Hero 的 title/subtitle 量小、换游戏就变,属配置层;但新手引导卡片内容量大、每站独立重写,属内容层。对策:量小且结构稳定的进
site.config.json(hero/cta/sidebar.codes);量大且每站重写的进content/{lang}/home.json。判定标准:换游戏时是"改值"还是"重写"——改值进配置,重写进内容。换皮测试的虚拟游戏名要够"假":用 "TestGame" 可能和真实测试代码冲突。对策:用带随机后缀的名字如 "TestGame XYZ 7841",确保不会误判为残留。
对标站游戏名是最易漏的残留:从
yxz-game-site-builder复刻来的代码,对标站游戏名(如 farever)常藏在 CSS 注释/* Structure mirrored from farever */、JS 字符串里。对策:extract_config.py把对标站游戏名也列为扫描目标,换皮测试--ref-game参数必传。内容层动态读取的 MDX 路径必须匹配路由:
content/en/guide.mdx必须能被app/[lang]/[slug]/page.tsx通过getArticle("guide","en")读到,否则 404。对策:new_site.py校验 content 目录结构与路由配置一致,缺文件报错。Next.js 14 +
output:"export"下 metadata route 在 dev 模式 500:app/sitemap.js/app/robots.js/app/manifest.js会要求generateStaticParams(),next build能出静态文件但next dev直接 500(报错is missing exported function "generateStaticParams()")。用户"把站跑起来看效果"时/sitemap.xml等全挂。对策:模板一律不用 metadata route,改用scripts/gen-seo.mjs静态生成public/sitemap.xml+public/robots.txt,PWA manifest 用public/favicons/site.webmanifest静态文件;package.json接predev/prebuild钩子自动跑。dev 和 build 都能用。swap_test.py子串匹配会误报普通英文:脚本对游戏名/旧名做大小写无关子串匹配,普通英文短语如"still got a big walk to go"(双关)、"Don't panic"会分别命中 "Big Walk"/"Panic"。对策:每条命中都人工复核——确属残留就修;确属普通英文就改写措辞("big walk to go"→"long way to go"、"Don't panic"→"Don't freak out")。虚拟测试名仍要够"假"以免误判。new_site.py的必填字段要跟着模板走:脚本原先用写死的REQUIRED_KEYS(如languages/sidebar/cta),但不同模板的site.config.jsonschema 并不一样(本模板用developer/publisher/domain/pages,没有languages/sidebar/cta),硬校验会让脚手架对新模板直接 FAIL。对策:必填字段改为从模板自身的site.config.json(或site.config.example.json)派生——新配置必须含同样的顶层 key 与theme子 key,schema 自然跟随模板,不再写死。