* Redesign BadSEO homepage and add design lab routes * Migrate badseo.dev to TanStack Start * Refine BadSEO landing page design * Migrate badseo.dev to TanStack Start * Refine BadSEO field guide homepage * Simplify BadSEO example catalog * Remove BadSEO issue section label * Remove All issues from BadSEO header * Reorder BadSEO examples and simplify footer * Reduce BadSEO desktop hero type * Narrow BadSEO homepage content * Further reduce BadSEO hero type * Add BadSEO brand to footer * Tighten Kitchen sink description * Reorder BadSEO example categories
140 lines
6.0 KiB
Markdown
140 lines
6.0 KiB
Markdown
# badseo.dev
|
||
|
||
**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 missing `<title>`, a redirect loop, a page nothing links
|
||
to, thin content. Point an SEO crawler at it and check what the crawler catches.
|
||
|
||
It is also the end-to-end test fixture for the
|
||
[OpenSEO](https://openseo.so) site audit. Every page lists the audit issues it
|
||
should trigger, and a harness runs the real audit engine against a running copy
|
||
to check that it does.
|
||
|
||
Maintained by the team behind [OpenSEO](https://openseo.so), an open-source SEO
|
||
tool.
|
||
|
||
---
|
||
|
||
## What's covered
|
||
|
||
Every issue type in the OpenSEO audit engine is exercised by at least one page
|
||
(the harness enforces this). Pages are grouped by category:
|
||
|
||
| Category | Pages |
|
||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||
| **Head tags & headings** | missing title, title too long/short, missing meta, meta too long, missing H1, multiple H1, heading-level skip |
|
||
| **Content quality** | thin content, images missing alt, duplicate content, duplicate title, duplicate meta description |
|
||
| **Indexability & canonical** | noindex (meta + `X-Robots-Tag` header), canonicalized to another URL, conflicting canonicals |
|
||
| **HTTP status & links** | 404, 500, 403 (blocked), broken internal link |
|
||
| **Redirects** | redirect chain, redirect loop, trailing-slash canonical (redirect-cycle trap) |
|
||
| **Performance** | slow server response (TTFB) |
|
||
| **Site structure** | orphan page, deep click-path |
|
||
| **Kitchen sink** | one page that breaks six ways at once |
|
||
|
||
Browse them all on the homepage at `/#issues`.
|
||
|
||
## How it's built
|
||
|
||
badseo.dev is a TanStack Start app deployed to a Cloudflare Worker, following
|
||
the same Vite and Cloudflare setup as the repository's `web/` app.
|
||
|
||
TanStack React routes render the healthy homepage and privacy policy.
|
||
A TanStack catch-all server route keeps the deliberate fixtures as raw
|
||
responses with byte-level control over status codes, redirects, headers
|
||
(`X-Robots-Tag`, `Link: …; rel=canonical`), timing, and the malformed `<head>`
|
||
states the audit needs to observe.
|
||
|
||
- `src/routes/` — TanStack pages plus raw server routes for fixtures,
|
||
`robots.txt`, and `sitemap.xml`.
|
||
- `src/server/badseo.ts` — fixture dispatch and crawler-discovery responses.
|
||
- `src/lib.ts` — raw fixture HTML rendering. Its shared chrome is deliberately
|
||
**SEO-neutral**: it emits no `<h1>`–`<h6>` and no `<img>`.
|
||
- `src/fixtures/*.ts` — the fixtures, one file per category.
|
||
|
||
## Analytics
|
||
|
||
Plausible Analytics loads on every page using the site-specific script supplied
|
||
for badseo.dev. It provides the cookieless aggregate baseline without changing
|
||
the Google Analytics consent choice.
|
||
|
||
Google Analytics uses measurement ID `G-7MXV9FH7SS`. The small consent script in
|
||
`public/analytics.js` is shared by TanStack pages and raw fixture documents. It
|
||
does not request Google's tag or set analytics cookies until a visitor accepts.
|
||
The visitor can reject analytics or revisit the choice from **Cookie settings**
|
||
in the footer. The choice is stored only in the visitor's browser.
|
||
|
||
## Run it locally
|
||
|
||
```bash
|
||
# from the badseo/ directory
|
||
npm run dev # serves on http://localhost:8787
|
||
```
|
||
|
||
## Run the end-to-end audit
|
||
|
||
The harness drives the **real** OpenSEO crawl + issue-detection functions
|
||
(imported straight from `../src`) against a running badseo.dev, then asserts every
|
||
fixture triggers exactly the issues it declares — and that the homepage,
|
||
privacy policy, and support pages come back clean.
|
||
|
||
```bash
|
||
# with `npm run dev` running in another terminal:
|
||
npm run audit -- http://localhost:8787
|
||
```
|
||
|
||
It prints a per-page pass/fail matrix and an issue-type coverage line, and exits
|
||
non-zero on any mismatch — so it works as a CI gate for the audit engine.
|
||
|
||
## Add a fixture
|
||
|
||
Contributions are welcome — a new fixture _is_ a new regression test. Each is a
|
||
small object:
|
||
|
||
```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: "…",
|
||
}),
|
||
),
|
||
};
|
||
```
|
||
|
||
Then add it to its category's exported array. `expectedIssues` is type-checked
|
||
against the real audit registry, and the harness will hold you to it.
|
||
|
||
Guidelines:
|
||
|
||
- **Isolate one issue per page.** A themed page should be healthy in every way
|
||
_except_ the defect it demonstrates, so the audit result is unambiguous. (The
|
||
kitchen-sink page is the deliberate exception.)
|
||
- **Keep titles and meta descriptions unique** across the site, or you'll create
|
||
accidental duplicate-title / duplicate-meta groups. The exceptions are the
|
||
intentional duplicate pairs.
|
||
- **Keep the copy plain.** Say what the page does and why the mistake matters.
|
||
No hype.
|
||
|
||
## Deploy
|
||
|
||
Vite builds the TanStack Start client and Worker bundles, then TypeScript checks
|
||
the project before Wrangler deploys it:
|
||
|
||
```bash
|
||
npm run build # Vite build + typecheck
|
||
npm run deploy # build + wrangler deploy → badseo.dev
|
||
```
|
||
|
||
The custom-domain routes for `badseo.dev` and `www.badseo.dev` live in
|
||
`wrangler.jsonc`, alongside the TanStack server entry and built asset directory.
|