Ben Senescu d6d0f210ba Redesign the BadSEO home and landing lab experience (#381)
* 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
2026-07-15 12:39:31 -04:00

140 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.