尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

badseo.dev:OpenSEO 审计引擎的 e2e 测试靶场——从 SEO 缺陷 Fixture 到可运行审计断言

badseo.dev:OpenSEO 审计引擎的 e2e 测试靶场——从 SEO 缺陷 Fixture 到可运行审计断言 badseo.devOpenSEO 审计引擎的 e2e 测试靶场——从 SEO 缺陷 Fixture 到可运行审计断言【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seobadseo.dev 是 OpenSEO 仓库中一个“故意做坏”的网站每一页只犯一类常见技术 SEO 错误同时它也是 OpenSEO site audit 的端到端测试夹具——一个 harness 驱动真实审计引擎爬取站点断言每页恰好触发它声明的问题。读完本文你可以理解这套“缺陷即测试”的 e2e 架构、Fixture 的类型化契约与字节级响应控制实现并能按步骤在本地运行审计、按规范新增自己的缺陷页面。1. 定位SEO 反面教材 审计引擎的回归测试仓库根目录下的 badseo/README.md 对它的定义是A test site full of SEO mistakes.badseo.dev is a set of open-source web pages. Each page breaks one common technical-SEO rule: a missingtitle, a redirect loop, a page nothing links to, thin content. Point an SEO crawler at it and check what the crawler catches.它承担两个角色可人工浏览的反面教材每个缺陷页面都在页面上展示一个“What this page tests”测试面板含该页应触发的审计问题 chips读者可以直接用浏览器或任意第三方爬虫验证“错误长什么样、后果是什么”。OpenSEO site audit 的 e2e 夹具每个 Fixture 用expectedIssues声明自己应触发的审计问题 idharness 对运行中的副本跑真实的 OpenSEO 爬取 问题检测函数逐页核对“恰好触发且仅触发”声明的问题任何偏差都以非零退出码失败。package.json的描述也明确了这一双重身份见 badseo/package.jsona deliberately broken website full of SEO mistakes, used as e2e test fixtures for the OpenSEO site audit。2. Fixture 覆盖的完整问题矩阵README 按类别给出了覆盖总表并强调“审计引擎的每一种问题类型都至少被一个页面覆盖由 harness 强制”。以下是 README 原文的类别矩阵首页/#issues可浏览全部页面类别页面Head tags headingsmissing title、title 过长/过短、missing meta、meta 过长、missing H1、multiple H1、heading-level skipContent qualitythin content、images missing alt、duplicate content、duplicate title、duplicate meta descriptionIndexability canonicalnoindexmeta X-Robots-Tagheader、canonicalized to another URL、conflicting canonicalsHTTP status links404、500、403 (blocked)、broken internal linkRedirectsredirect chain、redirect loop、trailing-slash canonicalredirect-cycle trapPerformanceslow server response (TTFB)Site structureorphan page、deep click-pathKitchen sink一页同时犯 6 种错误从 src/shared/audit-issues.ts 中 OpenSEO 共享的问题注册表看引擎共定义 25 种 issue 类型severity 分三级critical/warning/infofixture 的expectedIssues就是对这些 id 的引用critical4blocked-page、server-error5xx、broken-internal-link、missing-titlewarning12broken-page4xx、duplicate-title、duplicate-meta-description、duplicate-content、missing-meta-description、missing-h1、multiple-h1、redirect-chain、redirect-loop、canonical-conflict、thin-content、images-missing-alt、orphan-page、no-outgoing-linksinfo9title-too-long、title-too-short、meta-description-too-long、meta-description-too-short、heading-order-skip、slow-responseTTFB 超过 1.5s、noindex-page、canonicalized-page、deep-page距首页 5 次点击以上该注册表同时被服务端问题引擎、MCP 工具与客户端问题 UI、CSV 导出共享badseo.dev 的测试面板正是直接引用注册表里的title与explanation渲染 chips见 badseo/src/lib.ts 的issueChips保证页面声明与引擎措辞一致。3. 架构TanStack Start Cloudflare Worker 字节级响应控制3.1 整体技术栈README 的 “How its built” 一节给出的实现要点如下均与源码对应badseo 是一个TanStack Start 应用部署在 Cloudflare Worker 上与仓库的web/应用采用相同的 Vite Cloudflare 配置TanStack React 路由只负责健康的首页与隐私政策badseo/src/routes/index.tsx、badseo/src/routes/privacy.tsx一个TanStack catch-all server 路由承载所有刻意的 fixture以“原始响应”形式返回从而获得对状态码、重定向、响应头X-Robots-Tag、Link: …; relcanonical、时序TTFB 延迟以及畸形head状态的字节级控制权。catch-all 的实现只有 10 行badseo/src/routes/$.tsexport const Route createFileRoute(/$)({ server: { handlers: { GET: ({ request }) handleFixtureRequest(request), }, }, });3.2 Fixture 契约每个缺陷页就是一个带断言的对象Fixture 的类型定义在 badseo/src/fixtures/types.ts核心字段export interface Fixture { path: string; // 规范 URL 路径如 /head/missing-title必须以 / 开头 extraPaths?: string[]; // 字节级完全相同的额外 URL用于建模多 URL 的重复页 category: string; // 目录页展示的分组 name: string; // 人类可读名称 summary: string; // 页面测试面板中的一行描述 lesson?: string; // 为什么重要 / 如何修复 expectedIssues: IssueId[]; // 该页被“工程化”触发的问题 id 集合 // 同时是 e2e harness 的断言真值空数组 “该页必须干净” support?: boolean; // 仅用于支撑其它 fixture 可达的辅助页如重定向链的中转跳 // 被爬取但不展示在目录中harness 断言其为 clean inSitemap?: boolean; // 是否写入 sitemap.xml默认 true linkedFromCatalog?: boolean; // 目录页是否链接它孤儿页设为 false handler: (ctx: FixtureContext) Response | PromiseResponse; // 完整控制 status、headers、timing 的响应生产函数 }其中IssueId直接类型导入自主项目同一行注释说明了原因// 从 OpenSEO 审计引擎直接引入问题 id 联合类型 // 使每个 fixture 的 expectedIssues 都针对真实注册表做类型检查。 // 类型级导入——构建期擦除不会打进 Worker。 import type { AuditIssueType } from ../../../src/shared/audit-issues; export type IssueId AuditIssueType;这意味着expectedIssues中写一个不存在的 id或引擎新增/改名 id 后忘记同步会在npm run typecheck阶段直接报错——类型系统替 harness 挡下了第一道错误。Fixture 按类别分文件存放badseo/src/fixtures/下head-tags.ts、content.ts、indexability.ts、http-status.ts、redirects.ts、performance.ts、structure.ts、kitchen-sink.ts再由 badseo/src/fixtures/registry.ts 聚合并导出派生集合allFixtures目录顺序的全部 fixturecatalogLinkedFixtureslinkedFromCatalog ! false目录页会链接到的 fixture——链接即“内链”让爬虫可达且不变成孤儿页sitemapFixturesinSitemap ! false写入 sitemap.xml 的路径duplicateUrlLinks各 fixture 的extraPaths重复/备选 URL。目录页也链接它们使重复页被爬取且不会被误判为孤儿页。以 head 类为例badseo/src/fixtures/head-tags.ts 中“缺失 title”的 fixture 展示了“刻意省略一个head元素即制造缺陷”的惯用手法const missingTitle: Fixture { path: /head/missing-title, category: Head tags headings, name: Missing title tag, summary: This page has no title element at all., lesson: The title is the strongest signal of what a page is about, …, expectedIssues: [missing-title], handler: () htmlResponse( renderPage({ fixture: missingTitle, // title intentionally omitted metaDescription: This page is fine except for one thing: …, bodyHtml: article({ h1: A page with no title, lede: …, sections: [/* … */] }), }), ), };同类中还包含 title 过长99 字符的longTitle、title 过短Hi、meta description 缺失/过长/过短、缺失 H1、空 H1expectedIssues: [missing-h1]与“无 H1”共享同一 issue id、多个 H1、标题层级跳级H1→H4等页面。3.3 请求分发与爬虫发现响应badseo/src/server/badseo.ts 的handleFixtureRequest是 fixture 的分发中枢把 pathname 归一化非根路径去掉末尾斜杠后查routeTablepath与所有extraPaths都注册进同一张表命中则调用fixture.handler(context)FixtureContext提供origin由请求的host头推导因此同一个站点在 localhost 与 badseo.dev 上生成的绝对 URL 都正确、request、path特殊拦截命中TRAILING_SLASH_CANONICAL/redirect/trailing-slash常量见 badseo/src/fixtures/redirects.ts时301 跳转到带斜杠形式——这是“尾斜杠规范化页”陷阱的前半段未命中则返回一个手写的 404 页no-store。同文件还提供爬虫发现所需的两个端点robotsResponseUser-agent: * / Allow: /并动态输出Sitemap: ${origin}/sitemap.xml——“broken on purpose, but it lets crawlers in”sitemapResponse把/、/privacy加上全部sitemapFixtures的pathextraPaths拼成标准urlsetXMLbadseo/src/routes/robots[.]txt.ts 与 badseo/src/routes/sitemap[.]xml.ts 分别对接。3.4 SEO 中立的渲染层让审计只量到注入的缺陷badseo/src/lib.ts 是全部 fixture 共用的渲染原语其头部注释点明了设计纪律Everything the shared chrome emits (nav, footer, and the what this page tests panel) is deliberatelySEO-NEUTRAL: noh1–h6and noimg. That way each fixtures headings and images are fully under the fixtures own control, and the audit measures exactly the defect we injected — not accidental noise from the layout.关键函数renderDocument(opts)构建完整 HTML 文档对head有逐字段控制权——title/metaDescription整个省略即不输出对应元素用于 missing-title / missing-meta可选canonical、robotsMetameta namerobots、headExtra原始head注入用于 JSON-LD 等附加标签、langhtmlResponse(html, { status?, headers?, delayMs? })返回Response默认200text/html; charsetutf-8cache-control: no-storedelayMs在首字节前人为延迟是 slow-TTFB 类 fixture 的实现基础redirect(location, status 301)手工 3xx 跳转。README 特别注明“爬虫会把每一跳记录为独立页面行”这正是 redirect-chain 检测的输入renderPage带测试面板与renderShell首页/目录用的无面板外壳lorem(words)确定性填充文本让页面词数能越过 thin-content 阈值避免“测别的缺陷时误伤薄内容检查”。“性能”与“组合错误”两个 fixture 恰好展示了delayMs与多缺陷叠加的用法badseo/src/fixtures/performance.ts/perf/slow-response用{ delayMs: 1700 }制造约 1.7 秒 TTFB审计阈值 1.5s见注册表slow-response描述expectedIssues: [slow-response]badseo/src/fixtures/kitchen-sink.ts/kitchen-sink一页同时具备 6 个缺陷——过长的 title、缺失 meta description、两个 H1、H1→H4 跳级、无 alt 的img、以及{ delayMs: 1700 }的慢响应expectedIssues列出全部 6 个 id。它的lesson点明了设计意图“真实坏页面通常不止一个问题审计应当报告其中每一个而不是停在第一个。”redirects 类中最具工程价值的是尾斜杠陷阱badseo/src/fixtures/redirects.ts规范形式/redirect/trailing-slash/返回 200无斜杠形式 301 到它——与 WordPress 等 CMS 的常规行为一致。一个会把/foo/归一化为/foo的爬虫会陷入“取回 301 → 再去/foo→ 301 回去 → 再 strip”的死循环508 Loop Detected 一类 bug注释中指向主仓库 PR #61 的回归背景。该 fixture 的expectedIssues是空数组正确行为是规范页被爬一次并返回 200且不误报 redirect-loop。harness 还为此设了显式回归守卫见下节。4. 端到端审计运行器驱动真实引擎逐页精确断言harness 是 badseo/scripts/run-audit.ts。其文件头注释准确概括了策略This drives theREALOpenSEO audit engine (the same crawl issue-detection functions the production Worker uses) against a running badseo.dev… It reimplementsonly the crawl frontier loop— deliberately, so it can crawl localhost (the production frontiers SSRF policy blocks private hosts). Every actual detection call below is imported straight from../src.即只有爬取 frontier 循环是本地重写的为了绕过生产爬虫对私有主机的 SSRF 拦截从而支持 localhost所有真正的问题检测调用都从主仓库直接导入import { crawlPage } from ../../src/server/workflows/site-audit-workflow-helpers; import { discoverUrls, parseRobotsTxt } from ../../src/server/lib/audit/discovery; import { normalizeUrl, isSameOrigin } from ../../src/server/lib/audit/url-utils; import { runPageReporters } from ../../src/server/lib/audit/issues/page-reporters; import { findDuplicates, findRedirectChainsAndLoops, type SlimPage } from ../../src/server/lib/audit/issues/multipage-checks; import { AUDIT_ISSUE_TYPES } from ../../src/shared/audit-issues;运行参数与流程目标 origin 取命令行参数默认http://localhost:8787MAX_PAGES 200、CONCURRENCY 10预热正式爬取前并发请求首页、/privacy和所有 fixture 路径redirect: manual避免 dev server 冷启动让健康页被误判为慢响应BFS frontier链接发现的 URL 优先于仅 sitemap 可达的 URLsitemap-only 的页面depth null与真实审计区分robots 不允许的 URL 不入队每个页面的内部链接收集为CrawlLinksource → target供多页检查使用检测阶段逐页runPageReporters(page)得到页面级问题再把页面压成SlimPage跑findDuplicates重复 title/meta/content与findRedirectChainsAndLoops两个依赖生产环境 D1 存储的多页检查以内存等价实现替代——findBrokenInternalLinks目标页 4xx/5xx 的内部链接与findOrphanPages无任何非自引用入链、且非重定向目标的 2xx 页仅在爬取完整未截断时执行断言语义核心对每个 fixture要求实际检测到的问题集合 expectedIssues缺一个missing或多一个extra都判失败首页与隐私页必须完全干净check(Homepage, /, [], false)等标记support: true的辅助页只允许携带 info 级噪音例如深层链路上的中转点自身可能就是 deep page出现 critical/warning 即失败尾斜杠显式回归守卫断言TRAILING_SLASH_CANONICAL的两种形式中至少有一个被爬成 200且相关 URL 无redirect-loop、无 5xx/抓取错误。注释说明该守卫“故意对具体修复方式保持中立”——无论修复后 200 落在斜杠形式保留斜杠的根因修复还是非斜杠形式旧版 strip 后 inline-follow 的行为都算通过若爬虫仍会 strip 且不 follow则表现为 508 或自环必然失败输出与退出码打印逐页 pass/fail 矩阵ANSI 着色✓/✗以及一行问题类型覆盖率issue-type coverage: N/25枚举AUDIT_ISSUE_TYPES的全部 key列出未被任何 fixture 覆盖的 idfailures 0时process.exit(1)——因此它可以原样作为审计引擎的 CI 门禁。5. 本地运行、添加 Fixture 与部署5.1 本地运行与跑审计# from the badseo/ directory npm run dev # serves on http://localhost:8787badseo/vite.config.ts 把 dev server 绑定在127.0.0.1:8787插件为cloudflare()tanstackStart()react()SSR 解析条件为[worker, import, module, default]。# with npm run dev running in another terminal: npm run audit -- http://localhost:8787audit脚本的实际定义是cd .. tsx badseo/scripts/run-audit.tstest:e2e与之相同即从仓库根目录用tsx直跑 harness也可传任意 origin 参数对部署后的站点复跑。5.2 添加一个新 Fixture新 Fixture 新回归测试README 的 “Add a fixture” 一节给出了最小对象模板可直接对照types.ts的完整字段使用const myFixture: Fixture { path: /category/my-mistake, category: Content quality, name: My SEO mistake, summary: One-line description shown in the on-page test panel., lesson: Why it matters / how to fix it., expectedIssues: [thin-content], // the audit issue ids this page must trigger handler: () htmlResponse( renderPage({ fixture: myFixture, title: …, metaDescription: …, bodyHtml: …, }), ), };然后把它加入所属类别的导出数组headTagFixtures等即可被registry.ts聚合。README 给出的三条守则值得原样保留每页只隔离一个问题。主题页除了展示的缺陷外必须在其它一切方面健康保证审计结果无歧义kitchen-sink 页是刻意例外全站保持 title 与 meta description 唯一否则会意外制造 duplicate-title / duplicate-meta 分组刻意的重复对除外它们用extraPaths建模文案保持朴素说清页面做了什么、为什么这个错误重要不要渲染。类型层面expectedIssues对照真实审计注册表做类型检查harness 在运行时再“按它办事”——静态与动态双重约束。5.3 构建与部署npm run build # Vite build typecheck npm run deploy # build wrangler deploy → badseo.devbuildvite build tsc --noEmit先构建、后类型检查deploy在其基础上追加wrangler deploy。badseo/wrangler.jsonc 的关键配置{ name: badseo, compatibility_date: 2026-02-19, compatibility_flags: [nodejs_compat], main: tanstack/react-start/server-entry, // TanStack server 入口 assets: { directory: ./dist/client, // 构建后的客户端静态资源 html_handling: none, not_found_handling: none, }, routes: [ { pattern: badseo.dev, custom_domain: true }, { pattern: www.badseo.dev, custom_domain: true } ] }html_handling: none表示静态资源命中不了时全部交给 Worker 处理——这正是 catch-all fixture 路由能接住任意 URL 的前提。6. 分析统计Plausible 带同意的 Google AnalyticsREADME 的 “Analytics” 一节与源码实现一致Plausible无 cookie 的聚合基线通过站点专属脚本加载于每页见 badseo/src/plausible.ts 导出的PLAUSIBLE_SCRIPT_SRC/PLAUSIBLE_INIT_SCRIPT并被renderDocument写入每个 fixture 文档的headGoogle Analytics使用 measurement IDG-7MXV9FH7SS。badseo/public/analytics.js 是 TanStack 页面与原始 fixture 文档共享的小型同意脚本在访客点击 Accept 之前不请求 Google 的 tag、不写入分析 cookie选择granted/denied只存在访客浏览器localStorage的badseo.analyticsConsent键中可从页脚Cookie settings重新打开横幅更改拒绝时还会主动清除_ga*cookie。7. 小结这套“靶场”设计可复用的三点缺陷即断言每个 fixture 的expectedIssues既是页面文档测试面板又是 e2e 断言真值配合“恰好匹配missing 与 extra 都算失败”的语义能同时抓出检测器的漏报与误报。单一事实源issue id 联合类型、severity、标题与说明全部来自主仓库共享注册表 src/shared/audit-issues.tsbadseo 侧零复制——引擎改名一个 id类型检查立刻在 fixture 侧报警。字节级可控的原始响应只让框架渲染“健康页”让 fixture 用Response直接生产状态码、头、时序与畸形head并用 SEO 中立的共享 chrome 排除布局噪音爬虫发现文件robots/sitemap按 fixture 元数据动态生成使 orphan、duplicate、deep-click 这类结构性缺陷也能被精确构造。对想给自己的爬虫/审计工具做回归验证的团队这套“fixture 声明期望问题 harness 驱动真实引擎 覆盖率强制 100%”的模式可以直接借鉴新增一个缺陷页就是免费新增一个回归测试。【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表