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
This commit is contained in:
parent
987e5494a2
commit
d6d0f210ba
@ -6,10 +6,15 @@ This is not a completed-work log or a bug tracker. Never include secrets, creden
|
||||
|
||||
## Open
|
||||
|
||||
- [ ] `2026-07-11T21:38:29Z` — `codex` — While visually testing the BadSEO design lab, a stale local process still held the configured port 8787, so Vite silently moved the app to 8788. Make the active preview URL more visible in the normal workflow or provide a reliable task-local server cleanup step.
|
||||
- [ ] `2026-07-10T21:32:49Z` — `codex` — While starting the BadSEO TanStack/Cloudflare dev server in a sandbox, the Cloudflare Vite plugin failed with `listen EPERM` while probing its inspector port, even though the app port was otherwise valid. Document the need for elevated local port binding or provide an inspector-disabled validation command.
|
||||
- [ ] `2026-07-10T21:28:46Z` — `codex` — While validating the standalone BadSEO package after merging its TanStack migration, `pnpm --dir badseo run typecheck` appeared to work through the root toolchain but `pnpm --dir badseo run build` could not find Vite because `badseo/node_modules` was absent. Document or enforce the required package-local install before validation.
|
||||
- [ ] `2026-07-10T22:32:40Z` — `codex` — While running the badseo audit from the workspace, sandboxed TSX failed with `listen EPERM` when creating its IPC socket under the temporary directory. Rerunning the same audit with local IPC permission succeeded; provide a sandbox-compatible TSX invocation for validation scripts.
|
||||
- [ ] `2026-07-10T22:23:50Z` — `codex` — While pushing the consent-banner refinement to PR #380, the SSH push hung without output and a separate remote verification hung as well; retrying the same push succeeded immediately. Surface an actionable SSH timeout or connection error instead of waiting indefinitely.
|
||||
- [ ] `2026-07-10T21:36:27Z` — `codex` — While using the repository's `webapp-testing` skill, its required Python Playwright import was unavailable in both system and bundled Python. The bundled Node Playwright runtime completed the check; document or provide that fallback, including Cloudflare Vite's default port 8787 rather than Vite's usual 5173.
|
||||
- [ ] `2026-07-10T21:32:10Z` — `codex` — While formatting the standalone `badseo` workspace, `pnpm exec prettier` failed because Prettier is only available from the repository root. Document the root-only formatter command or expose a workspace-local formatting script.
|
||||
- [ ] `2026-07-11T15:13:43Z` — `codex` — While inspecting a BadSEO merge with login-shell commands, zsh printed `(eval):5: parse error near \`end'` before otherwise successful Git output. Running the same commands with login-shell initialization disabled avoided the noise; identify and fix the malformed shell startup hook.
|
||||
- [ ] `2026-07-11T15:15:13Z` — `codex` — While resolving a merge from the sandboxed BadSEO subdirectory, `git add` could not create the parent worktree's `index.lock` despite the merge itself being allowed. Staging required the approved elevated Git path; align the workspace write boundary with the actual Git worktree root.
|
||||
- [ ] `2026-07-10T21:09:27Z` — `codex` — While building the TanStack/Cloudflare badseo app, Wrangler reported an EPERM writing its debug log under the user preferences directory even though the build succeeded. Set `WRANGLER_LOG_PATH` to a writable temporary path in sandboxed build commands or make the logging failure non-fatal and quiet.
|
||||
- [ ] `2026-07-10T17:53:20Z` — `codex` — While validating `.greptile/`, both `pnpm exec prettier --check` and the existing `pnpm format:check` attempted to reconcile `node_modules` and aborted because no TTY was available. Calling `node_modules/.bin/prettier` performed the non-installing check successfully; the agent/CI path needs a stable way to run package scripts without an interactive modules purge.
|
||||
- [ ] `2026-07-10T18:12:35Z` — `codex` — While validating referenced files in zsh, using `path` as a loop variable overwrote zsh's special `path` array and made commands such as `git`, `jq`, and `sed` appear missing later in the same shell. Use a neutral name such as `file_path` in shell loops.
|
||||
|
||||
@ -32,14 +32,14 @@ Every issue type in the OpenSEO audit engine is exercised by at least one page
|
||||
| **Site structure** | orphan page, deep click-path |
|
||||
| **Kitchen sink** | one page that breaks six ways at once |
|
||||
|
||||
Browse them all at `/catalog`.
|
||||
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, catalog, and privacy policy.
|
||||
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>`
|
||||
@ -76,7 +76,7 @@ npm run dev # serves on http://localhost:8787
|
||||
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,
|
||||
catalog, privacy policy, and support pages come back clean.
|
||||
privacy policy, and support pages come back clean.
|
||||
|
||||
```bash
|
||||
# with `npm run dev` running in another terminal:
|
||||
|
||||
@ -1,417 +1,152 @@
|
||||
:root {
|
||||
--canvas: #f5f1ec;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #ebe7e1;
|
||||
--ink: #111111;
|
||||
--ink-muted: #626260;
|
||||
--ink-subtle: #7b7b78;
|
||||
--hairline: #d8d1c8;
|
||||
--hairline-soft: #ebe7e1;
|
||||
--orange: #ff5600;
|
||||
--footer-bg: #eee8de;
|
||||
--critical: #d23b1f;
|
||||
--warning: #b26a00;
|
||||
--info: #6b7280;
|
||||
--mono:
|
||||
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--sans:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
color-scheme: light;
|
||||
--navy: #0c1b3a;
|
||||
--blue: #1745d1;
|
||||
--paper: #e7ecf4;
|
||||
--paper-deep: #d9e1ef;
|
||||
--surface: #f7f9fc;
|
||||
--line: #93a5c8;
|
||||
--line-soft: #bdc8da;
|
||||
--muted: #526584;
|
||||
--critical: #d94040;
|
||||
--warning: #cf7d18;
|
||||
--info: #6f82a5;
|
||||
--mono: "IBM Plex Mono", monospace;
|
||||
--sans: "IBM Plex Sans", sans-serif;
|
||||
--serif: "Newsreader", Georgia, serif;
|
||||
--condensed: "Archivo Narrow", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { min-height: 100%; background: var(--paper); scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
|
||||
body { min-height: 100%; margin: 0; background: var(--paper); color: var(--navy); font-family: var(--sans); font-size: 16px; line-height: 1.55; -webkit-font-smoothing: antialiased; }
|
||||
button, input { font: inherit; }
|
||||
a { color: inherit; text-underline-offset: 4px; }
|
||||
a:focus-visible, button:focus-visible { outline: 3px solid #ff6a57; outline-offset: 3px; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--canvas);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.wrap,
|
||||
.main,
|
||||
.panel,
|
||||
.nav,
|
||||
.foot-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* ── nav ── */
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.brand {
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
font-size: 17px;
|
||||
}
|
||||
.brand:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link {
|
||||
color: var(--ink-muted);
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
font-size: 15px;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* ── content ── */
|
||||
.main {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
.main h1 {
|
||||
font-size: clamp(30px, 4.6vw, 46px);
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
margin: 12px 0 18px;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.main h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.017em;
|
||||
margin: 40px 0 12px;
|
||||
}
|
||||
.main h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
margin: 26px 0 8px;
|
||||
}
|
||||
.main h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0 6px;
|
||||
}
|
||||
.main p {
|
||||
color: #2c2c2b;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.lede {
|
||||
font-size: 19px;
|
||||
line-height: 1.45;
|
||||
letter-spacing: -0.006em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.main a {
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--hairline);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.main a:hover {
|
||||
text-decoration-color: var(--ink);
|
||||
}
|
||||
.main img {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--hairline);
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
background: var(--surface);
|
||||
}
|
||||
.main code {
|
||||
font-family: var(--mono);
|
||||
font-size: 13.5px;
|
||||
background: var(--surface-2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.main ul,
|
||||
.main ol {
|
||||
color: #2c2c2b;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.main li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.next {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.next a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── test panel ── */
|
||||
.panel {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 32px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.panel-kicker {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-subtle);
|
||||
}
|
||||
.panel-cat {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--ink-subtle);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
.panel-summary {
|
||||
margin: 10px 0 0;
|
||||
font-size: 15px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.panel-lesson {
|
||||
margin: 6px 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.panel-chips {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.chips-label {
|
||||
font-size: 12px;
|
||||
color: var(--ink-subtle);
|
||||
font-weight: 500;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
padding: 3px 11px 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--canvas);
|
||||
border: 1px solid var(--hairline);
|
||||
}
|
||||
.chip::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
background: var(--info);
|
||||
}
|
||||
.chip-critical::before {
|
||||
background: var(--critical);
|
||||
}
|
||||
.chip-warning::before {
|
||||
background: var(--warning);
|
||||
}
|
||||
.chip-info::before {
|
||||
background: var(--info);
|
||||
}
|
||||
.chip-clean {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.chip-clean::before {
|
||||
background: #3a9d5d;
|
||||
}
|
||||
|
||||
/* ── hero + home bits ── */
|
||||
.hero {
|
||||
padding: 20px 0 4px;
|
||||
}
|
||||
.stat-row {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
margin: 26px 0 8px;
|
||||
}
|
||||
.stat b {
|
||||
font-size: 30px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.02em;
|
||||
display: block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.stat span {
|
||||
color: var(--ink-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.callout {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.callout p {
|
||||
margin: 0;
|
||||
color: #2c2c2b;
|
||||
}
|
||||
|
||||
/* ── catalog index (no cards) ── */
|
||||
.index-group {
|
||||
margin: 40px 0;
|
||||
}
|
||||
.index-group > h2 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.index-list {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.index-list .index-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 240px) 1fr auto;
|
||||
gap: 20px;
|
||||
align-items: baseline;
|
||||
padding: 14px 12px;
|
||||
margin: 0 -12px;
|
||||
border-radius: 8px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
.index-list .index-row:hover {
|
||||
background: var(--surface-2);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.index-list .index-row:hover .row-name {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--ink);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.row-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.row-sum {
|
||||
color: var(--ink-muted);
|
||||
font-size: 14.5px;
|
||||
}
|
||||
.row-sev {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
flex: none;
|
||||
}
|
||||
.dot-critical {
|
||||
background: var(--critical);
|
||||
}
|
||||
.dot-warning {
|
||||
background: var(--warning);
|
||||
}
|
||||
.dot-info {
|
||||
background: var(--info);
|
||||
}
|
||||
.legend {
|
||||
display: inline-flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: var(--ink-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.legend span {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.index-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
.row-sum {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── footer + OpenSEO badge ── */
|
||||
/* The band fills to the bottom of the page (no body padding beneath it). Extra
|
||||
bottom padding keeps the footer text clear of the fixed badge, which floats
|
||||
in the empty band space below the row. */
|
||||
.foot {
|
||||
background: var(--footer-bg);
|
||||
border-top: 1px solid var(--hairline);
|
||||
margin-top: 64px;
|
||||
}
|
||||
.foot-inner {
|
||||
padding: 26px 24px 76px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 14px;
|
||||
/* Shared site chrome. No headings or images are introduced here, keeping the
|
||||
fixture pages' audit signals entirely under fixture control. */
|
||||
.site-header, .nav {
|
||||
display: flex;
|
||||
min-height: 92px;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 8px 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 28px;
|
||||
padding: 31px 3.5vw 19px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.foot-inner a {
|
||||
color: var(--ink-muted);
|
||||
.site-brand, .brand { color: var(--navy); font-family: var(--condensed); font-size: 18px; font-weight: 700; letter-spacing: .03em; text-decoration: none; }
|
||||
.site-header nav, .nav-links { display: flex; align-items: center; gap: 27px; }
|
||||
.site-header nav a, .nav-links a { color: var(--navy); font-family: var(--mono); font-size: 8px; }
|
||||
|
||||
/* Homepage */
|
||||
.home-main { width: min(980px, calc(100% - 64px)); margin: 0 auto; padding-bottom: 80px; }
|
||||
.home-hero { padding: clamp(70px, 8vw, 112px) 0 clamp(64px, 8vw, 96px); }
|
||||
.home-copy h1 { margin: 0; font-family: var(--condensed); font-size: clamp(112px, 10vw, 136px); font-weight: 700; letter-spacing: -.055em; line-height: .72; }
|
||||
.home-copy > p { max-width: 860px; margin: 42px 0 0; font-family: var(--serif); font-size: clamp(28px, 2.2vw, 32px); line-height: 1.08; }
|
||||
.home-issues { scroll-margin-top: 24px; border-top: 3px solid var(--navy); }
|
||||
.home-issue-group { margin: 22px 0 54px; }
|
||||
.home-issue-group h3 { margin: 0; padding: 18px 0 14px; border-bottom: 1px solid var(--line); font-family: var(--condensed); font-size: 30px; line-height: 1; }
|
||||
.home-case-head, .home-case-row { display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: clamp(30px, 6vw, 90px); }
|
||||
.home-case-head { padding: 11px 0; border-bottom: 1px solid var(--line-soft); color: var(--muted); font-family: var(--mono); font-size: 8px; }
|
||||
.home-case-row { min-height: 72px; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--line-soft); text-decoration: none; }
|
||||
.home-case-row:hover .home-case-name { color: var(--blue); text-decoration: underline; text-underline-offset: 4px; }
|
||||
.home-case-name { font-family: var(--serif); font-size: 19px; line-height: 1.15; }
|
||||
.home-case-summary { color: var(--muted); font-size: 13px; line-height: 1.5; }
|
||||
.home-case-row:hover .home-case-summary { color: var(--navy); }
|
||||
|
||||
/* Catalog and fixture pages: deliberately restrained versions of the same
|
||||
system so the content and injected SEO issue stay central. */
|
||||
.main { width: min(920px, calc(100% - 48px)); margin: 0 auto; padding: 62px 0 72px; }
|
||||
.main h1 { margin: 0 0 18px; font-family: var(--condensed); font-size: clamp(52px, 8vw, 82px); font-weight: 700; letter-spacing: -.035em; line-height: .93; text-wrap: balance; }
|
||||
.main h2 { margin: 48px 0 13px; font-family: var(--condensed); font-size: 34px; line-height: 1; }
|
||||
.main h3 { margin: 30px 0 9px; font-family: var(--condensed); font-size: 24px; }
|
||||
.main h4 { margin: 24px 0 7px; font-family: var(--mono); font-size: 13px; text-transform: uppercase; }
|
||||
.main p { margin: 0 0 17px; color: #263b5e; }
|
||||
.main .lede { max-width: 800px; margin-bottom: 28px; color: var(--navy); font-family: var(--serif); font-size: 25px; line-height: 1.32; }
|
||||
.main a { color: var(--blue); }
|
||||
.main code { padding: 2px 5px; background: #d4deed; font-family: var(--mono); font-size: 13px; }
|
||||
.main ul, .main ol { padding-left: 22px; color: #263b5e; }
|
||||
.main li { margin: 5px 0; }
|
||||
.main img { display: block; max-width: 100%; margin: 26px 0; border: 1px solid var(--line); background: var(--surface); }
|
||||
.next { margin-top: 12px; }
|
||||
|
||||
.panel { width: min(920px, calc(100% - 48px)); margin: 38px auto 0; padding: 20px 22px; border: 1px solid var(--line); border-top: 3px solid var(--navy); background: rgb(247 249 252 / 70%); }
|
||||
.panel-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
||||
.panel-kicker { color: var(--navy); font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.panel-cat { margin-left: auto; color: var(--muted); font-family: var(--mono); font-size: 9px; }
|
||||
.panel-summary { margin: 12px 0 0; color: var(--navy); font-family: var(--serif); font-size: 20px; }
|
||||
.panel-lesson { margin: 8px 0 0; color: var(--muted); font-size: 14px; }
|
||||
.panel-chips { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; margin-top: 16px; }
|
||||
.chips-label { margin-right: 3px; color: var(--muted); font-family: var(--mono); font-size: 9px; text-transform: uppercase; }
|
||||
.chip { display: inline-flex; align-items: center; gap: 7px; padding: 4px 9px; border: 1px solid var(--line-soft); background: var(--paper); font-family: var(--mono); font-size: 10px; }
|
||||
.chip::before { width: 7px; height: 7px; border-radius: 50%; background: var(--info); content: ""; }
|
||||
.chip-critical::before { background: var(--critical); }.chip-warning::before { background: var(--warning); }.chip-info::before { background: var(--info); }.chip-clean::before { background: #24845a; }
|
||||
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; color: var(--muted); font-family: var(--mono); font-size: 10px; }
|
||||
.legend span { display: inline-flex; gap: 6px; align-items: center; }
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--info); }
|
||||
.dot-critical { background: var(--critical); }.dot-warning { background: var(--warning); }.dot-info { background: var(--info); }
|
||||
.index-group { margin: 52px 0; }
|
||||
.index-group > h2 { margin-bottom: 12px; }
|
||||
.index-list { border-top: 1px solid var(--line); }
|
||||
.index-row { display: grid; grid-template-columns: minmax(190px, .75fr) 1.4fr auto; gap: 24px; align-items: baseline; min-height: 76px; padding: 18px 12px; margin: 0 -12px; border-bottom: 1px solid var(--line); color: var(--navy) !important; text-decoration: none; transition: background 150ms ease, color 150ms ease; }
|
||||
.index-row:hover { background: var(--blue); color: #fff !important; }
|
||||
.row-name { font-family: var(--serif); font-size: 19px; line-height: 1.1; }
|
||||
.row-sum { color: var(--muted); font-size: 13px; }
|
||||
.index-row:hover .row-sum { color: rgb(255 255 255 / 75%); }
|
||||
.row-sev { display: inline-flex; gap: 5px; align-items: center; }
|
||||
|
||||
.foot { margin-top: 36px; border-top: 1px solid var(--line); background: #dce4f0; }
|
||||
.foot-inner { display: flex; width: min(1700px, 100%); min-height: 110px; align-items: center; justify-content: space-between; gap: 16px 32px; margin: 0 auto; padding: 30px 3.5vw; color: var(--muted); font-family: var(--mono); font-size: 9px; }
|
||||
.foot-brand { color: var(--navy); font-family: var(--condensed); font-size: 18px; font-weight: 700; letter-spacing: .03em; text-decoration: none; }
|
||||
.foot-links { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-left: auto; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.home-case-head, .home-case-row { grid-template-columns: minmax(190px, .75fr) minmax(280px, 1.25fr); gap: 32px; }
|
||||
}
|
||||
.foot-inner a:hover {
|
||||
color: var(--ink);
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.site-header, .nav { min-height: 78px; align-items: center; margin: 0 18px; padding: 18px 0; }
|
||||
.site-header nav, .nav-links { gap: 14px; }
|
||||
.home-main { width: calc(100% - 36px); padding-bottom: 48px; }
|
||||
.home-hero { padding: 64px 0 58px; }
|
||||
.home-copy h1 { font-size: 100px; }
|
||||
.home-copy > p { margin-top: 25px; font-size: 22px; line-height: 1.16; }
|
||||
.home-case-head { display: none; }
|
||||
.home-case-row { grid-template-columns: 1fr; gap: 7px; padding: 18px 0; }
|
||||
.home-case-name { font-size: 21px; }
|
||||
.main { width: calc(100% - 36px); padding-top: 42px; }
|
||||
.main h1 { font-size: 51px; }
|
||||
.main .lede { font-size: 22px; }
|
||||
.panel { width: calc(100% - 36px); margin-top: 28px; }
|
||||
.panel-cat { width: 100%; margin-left: 0; }
|
||||
.index-row { grid-template-columns: 1fr auto; gap: 7px 15px; }
|
||||
.row-sum { grid-row: 2; grid-column: 1 / -1; }
|
||||
.foot-inner { align-items: flex-start; flex-direction: column; }
|
||||
.foot-links { margin-left: 0; }
|
||||
}
|
||||
.foot-links {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 1ms !important; }
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink-muted);
|
||||
font: inherit;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.footer-button:hover {
|
||||
color: var(--ink);
|
||||
color: var(--navy);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
/* ── analytics consent ── */
|
||||
/* Consent-gated analytics controls added by /analytics.js. */
|
||||
.consent-banner {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
@ -419,12 +154,12 @@ a:hover {
|
||||
left: 24px;
|
||||
width: min(350px, calc(100vw - 48px));
|
||||
padding: 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface);
|
||||
box-shadow:
|
||||
0 20px 50px rgb(17 17 17 / 14%),
|
||||
0 2px 8px rgb(17 17 17 / 7%);
|
||||
0 20px 50px rgb(12 27 58 / 14%),
|
||||
0 2px 8px rgb(12 27 58 / 7%);
|
||||
animation: consent-in 180ms ease-out;
|
||||
}
|
||||
.consent-banner[hidden] {
|
||||
@ -439,20 +174,20 @@ a:hover {
|
||||
}
|
||||
.consent-copy p {
|
||||
margin: 0 0 7px;
|
||||
color: var(--ink-muted);
|
||||
color: var(--muted);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.consent-copy a {
|
||||
color: var(--ink-muted);
|
||||
color: var(--muted);
|
||||
font-size: 12.5px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--hairline);
|
||||
text-decoration-color: var(--line-soft);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.consent-copy a:hover {
|
||||
color: var(--ink);
|
||||
text-decoration-color: var(--ink);
|
||||
color: var(--navy);
|
||||
text-decoration-color: var(--navy);
|
||||
}
|
||||
.consent-actions {
|
||||
display: grid;
|
||||
@ -461,27 +196,26 @@ a:hover {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.consent-actions button {
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
min-height: 36px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
color: var(--navy);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.consent-actions button:hover {
|
||||
background: var(--surface-2);
|
||||
background: var(--paper-deep);
|
||||
}
|
||||
.consent-actions .consent-accept {
|
||||
border-color: var(--ink);
|
||||
background: var(--ink);
|
||||
color: #ffffff;
|
||||
border-color: var(--navy);
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
}
|
||||
.consent-actions .consent-accept:hover {
|
||||
background: #000000;
|
||||
background: #000;
|
||||
}
|
||||
@keyframes consent-in {
|
||||
from {
|
||||
@ -503,43 +237,3 @@ a:hover {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.openseo-badge {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 50;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--ink);
|
||||
color: #ffffff;
|
||||
border-radius: 999px;
|
||||
padding: 11px 17px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.openseo-badge:hover {
|
||||
background: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.openseo-badge strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.openseo-mark {
|
||||
width: 22px;
|
||||
height: 24px;
|
||||
flex: none;
|
||||
background: url("/openseo-logo.png") center / contain no-repeat;
|
||||
/* The source logo is a silver tree on transparent; render it white so it
|
||||
reads on the dark pill with no backing chip. */
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.openseo-badge .badge-label {
|
||||
display: none;
|
||||
}
|
||||
.openseo-badge {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ interface CrawlLink {
|
||||
|
||||
async function warmup(): Promise<void> {
|
||||
// Prime the dev server so healthy pages don't read as slow on a cold start.
|
||||
const paths = new Set<string>(["/", "/catalog", "/privacy"]);
|
||||
const paths = new Set<string>(["/", "/privacy"]);
|
||||
for (const f of allFixtures) for (const p of fixturePaths(f)) paths.add(p);
|
||||
await Promise.all(
|
||||
[...paths].map((p) =>
|
||||
@ -247,7 +247,7 @@ async function main() {
|
||||
|
||||
if (process.env.DEBUG_DEPTH) {
|
||||
for (const p of pages) {
|
||||
if (p.url.includes("/structure/deep/") || p.url.endsWith("/catalog")) {
|
||||
if (p.url.includes("/structure/deep/")) {
|
||||
console.log(` depth ${p.crawlDepth} ${p.url}`);
|
||||
}
|
||||
}
|
||||
@ -334,7 +334,6 @@ async function main() {
|
||||
|
||||
// Non-fixture content pages must be clean.
|
||||
check("Homepage", "/", [], false);
|
||||
check("Catalog", "/catalog", [], false);
|
||||
check("Privacy policy", "/privacy", [], false);
|
||||
|
||||
const byCategory = new Map<string, Fixture[]>();
|
||||
|
||||
@ -3,23 +3,23 @@ import type { ReactNode } from "react";
|
||||
export function SiteLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<nav className="nav">
|
||||
<nav className="nav" aria-label="BadSEO navigation">
|
||||
<a className="brand" href="/">
|
||||
badseo.dev
|
||||
</a>
|
||||
<a className="nav-link" href="/catalog">
|
||||
Catalog
|
||||
BADSEO
|
||||
</a>
|
||||
<span className="nav-links">
|
||||
<a href="https://openseo.so">OpenSEO</a>
|
||||
<a href="https://github.com/every-app/open-seo">GitHub</a>
|
||||
</span>
|
||||
</nav>
|
||||
{children}
|
||||
<footer className="foot">
|
||||
<div className="foot-inner">
|
||||
<span>
|
||||
badseo.dev is maintained by OpenSEO. Every page here is broken on
|
||||
purpose.
|
||||
</span>
|
||||
<a className="foot-brand" href="/">
|
||||
BADSEO
|
||||
</a>
|
||||
<span className="foot-links">
|
||||
<a href="/catalog">Catalog</a>
|
||||
<a href="/#issues">All issues</a>
|
||||
<a href="https://github.com/every-app/open-seo">GitHub</a>
|
||||
<a href="https://openseo.so">OpenSEO</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
@ -33,15 +33,6 @@ export function SiteLayout({ children }: { children: ReactNode }) {
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
<a
|
||||
className="openseo-badge"
|
||||
href="https://openseo.so"
|
||||
title="Audit a site with OpenSEO"
|
||||
>
|
||||
<span className="openseo-mark" aria-hidden="true" />
|
||||
<span className="badge-label">Maintained by </span>
|
||||
<strong>OpenSEO</strong>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ const kitchenSink: Fixture = {
|
||||
category: "Kitchen sink",
|
||||
name: "Kitchen sink (everything at once)",
|
||||
summary:
|
||||
"One page, six problems: a title that is too long, no meta description, two H1s, a skipped heading level, an image with no alt, and a slow response.",
|
||||
"A title that is too long, no meta description, two H1s, a skipped heading level, an image with no alt, and a slow response.",
|
||||
lesson:
|
||||
"Real broken pages usually have more than one problem. An audit should report every one of these, not stop at the first.",
|
||||
expectedIssues: [
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Rendering primitives for badseo.dev.
|
||||
//
|
||||
// Everything the shared chrome emits (nav, footer, the "what this page tests"
|
||||
// panel, the OpenSEO badge) is deliberately SEO-NEUTRAL: no <h1>–<h6> and no
|
||||
// Everything the shared chrome emits (nav, footer, and the "what this page
|
||||
// tests" panel) is deliberately SEO-NEUTRAL: no <h1>–<h6> and no
|
||||
// <img>. That way each fixture's headings and images are fully under the
|
||||
// fixture's own control, and the audit measures exactly the defect we injected
|
||||
// — not accidental noise from the layout.
|
||||
@ -68,7 +68,7 @@ export function renderDocument(opts: DocumentOptions): string {
|
||||
head.push(
|
||||
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
||||
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
|
||||
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap">',
|
||||
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Newsreader:opsz,wght@6..72,500;6..72,600&display=swap">',
|
||||
);
|
||||
head.push('<link rel="stylesheet" href="/styles.css">');
|
||||
if (opts.headExtra) head.push(opts.headExtra);
|
||||
@ -86,27 +86,16 @@ ${opts.bodyHtml}
|
||||
|
||||
function navHtml(): string {
|
||||
return `<nav class="nav">
|
||||
<a class="brand" href="/">badseo.dev</a>
|
||||
<a class="nav-link" href="/catalog">Catalog</a>
|
||||
<a class="brand" href="/">BADSEO</a>
|
||||
<span class="nav-links"><a href="https://openseo.so">OpenSEO</a><a href="https://github.com/every-app/open-seo">GitHub</a></span>
|
||||
</nav>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sitewide backlink to openseo.so, pinned bottom-right on every page. The
|
||||
* logo is a CSS background-image (not an <img>) so the shared chrome stays
|
||||
* image-free and never affects the images-missing-alt check.
|
||||
*/
|
||||
function openseoBadge(): string {
|
||||
return `<a class="openseo-badge" href="https://openseo.so" title="Audit a site with OpenSEO">
|
||||
<span class="openseo-mark" aria-hidden="true"></span><span class="badge-label">Maintained by </span><strong>OpenSEO</strong>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function footerHtml(): string {
|
||||
return `<footer class="foot"><div class="foot-inner">
|
||||
<span>badseo.dev is maintained by OpenSEO. Every page here is broken on purpose.</span>
|
||||
<a class="foot-brand" href="/">BADSEO</a>
|
||||
<span class="foot-links">
|
||||
<a href="/catalog">Catalog</a>
|
||||
<a href="/#issues">All issues</a>
|
||||
<a href="https://github.com/every-app/open-seo">GitHub</a>
|
||||
<a href="https://openseo.so">OpenSEO</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
@ -156,8 +145,7 @@ function testPanel(fixture: Fixture): string {
|
||||
function withChrome(inner: string): string {
|
||||
return `${navHtml()}
|
||||
${inner}
|
||||
${footerHtml()}
|
||||
${openseoBadge()}`;
|
||||
${footerHtml()}`;
|
||||
}
|
||||
|
||||
interface PageOptions extends DocumentOptions {
|
||||
|
||||
@ -24,7 +24,7 @@ export const Route = createRootRoute({
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap",
|
||||
href: "https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Newsreader:opsz,wght@6..72,500;6..72,600&display=swap",
|
||||
},
|
||||
{ rel: "stylesheet", href: "/styles.css" },
|
||||
],
|
||||
|
||||
@ -1,109 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AUDIT_ISSUE_TYPES } from "../../../src/shared/audit-issues";
|
||||
import { SiteLayout } from "../components/site-layout";
|
||||
import {
|
||||
catalogLinkedFixtures,
|
||||
categories,
|
||||
duplicateUrlLinks,
|
||||
} from "../fixtures/registry";
|
||||
import type { Fixture } from "../fixtures/types";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/catalog")({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: "Technical SEO issues checklist | badseo.dev" },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"A checklist of common technical SEO issues, each with a live example page: head tags, duplicate content, redirects, HTTP status, speed, and site structure.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: CatalogPage,
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: "/", hash: "issues" });
|
||||
},
|
||||
});
|
||||
|
||||
function IssueDots({ fixture }: { fixture: Fixture }) {
|
||||
return fixture.expectedIssues.map((issueId) => {
|
||||
const issue = AUDIT_ISSUE_TYPES[issueId];
|
||||
return (
|
||||
<span
|
||||
className={`dot dot-${issue.severity}`}
|
||||
title={issue.title}
|
||||
key={issueId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function CatalogPage() {
|
||||
return (
|
||||
<SiteLayout>
|
||||
<main className="main">
|
||||
<h1>Technical SEO issues, by category</h1>
|
||||
<p className="lede">
|
||||
Every page on the site, grouped by the kind of technical SEO issue it
|
||||
shows. The dots are the issues an audit should report for that page.
|
||||
</p>
|
||||
<p className="legend">
|
||||
<span>
|
||||
<span className="dot dot-critical" /> critical
|
||||
</span>
|
||||
<span>
|
||||
<span className="dot dot-warning" /> warning
|
||||
</span>
|
||||
<span>
|
||||
<span className="dot dot-info" /> info
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{categories.map((category) => (
|
||||
<section className="index-group" key={category}>
|
||||
<h2>{category}</h2>
|
||||
<div className="index-list">
|
||||
{catalogLinkedFixtures
|
||||
.filter((fixture) => fixture.category === category)
|
||||
.map((fixture) => (
|
||||
<a
|
||||
className="index-row"
|
||||
href={fixture.path}
|
||||
key={fixture.path}
|
||||
>
|
||||
<span className="row-name">{fixture.name}</span>
|
||||
<span className="row-sum">{fixture.summary}</span>
|
||||
<span className="row-sev">
|
||||
<IssueDots fixture={fixture} />
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className="index-group">
|
||||
<h2>Duplicate URLs</h2>
|
||||
<p>
|
||||
The same page served again at a second address. This is the raw
|
||||
material for the duplicate-content check. They are linked here so a
|
||||
crawler reaches them and does not mistake them for orphans.
|
||||
</p>
|
||||
<p>
|
||||
{duplicateUrlLinks.map((duplicate, index) => (
|
||||
<span key={duplicate.path}>
|
||||
{index > 0 ? " · " : null}
|
||||
<a href={duplicate.path}>
|
||||
<code>{duplicate.path}</code>
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</section>
|
||||
<p style={{ marginTop: 32 }}>
|
||||
A few pages are left off this list on purpose. An orphan page (
|
||||
<code>/structure/orphan</code>) is only in the sitemap, and some 404,
|
||||
500, and 403 URLs are only in the sitemap too, so a crawler has to
|
||||
find them on its own.
|
||||
</p>
|
||||
</main>
|
||||
</SiteLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SiteLayout } from "../components/site-layout";
|
||||
import { allFixtures, fixturePaths } from "../fixtures/registry";
|
||||
import { catalogLinkedFixtures, categories } from "../fixtures/registry";
|
||||
|
||||
const totalPaths = allFixtures.reduce(
|
||||
(count, fixture) => count + fixturePaths(fixture).length,
|
||||
0,
|
||||
);
|
||||
const distinctIssueTypes = new Set(
|
||||
allFixtures.flatMap((fixture) => fixture.expectedIssues),
|
||||
).size;
|
||||
const PRODUCT_DESCRIPTION =
|
||||
"A website with example technical SEO issues for you to learn or test your agents";
|
||||
|
||||
const homepageCategories = [
|
||||
"Kitchen sink",
|
||||
...categories.filter(
|
||||
(category) =>
|
||||
category !== "Kitchen sink" &&
|
||||
category !== "Head tags & headings" &&
|
||||
category !== "Content quality",
|
||||
),
|
||||
"Head tags & headings",
|
||||
"Content quality",
|
||||
];
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: "Technical SEO issues, by example | badseo.dev" },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Every page here has one common technical SEO issue, from a missing title to a redirect loop, so you can test what your SEO crawler catches.",
|
||||
},
|
||||
{ title: "BadSEO | Technical SEO test site" },
|
||||
{ name: "description", content: PRODUCT_DESCRIPTION },
|
||||
],
|
||||
}),
|
||||
component: HomePage,
|
||||
@ -27,65 +30,41 @@ export const Route = createFileRoute("/")({
|
||||
function HomePage() {
|
||||
return (
|
||||
<SiteLayout>
|
||||
<main className="main">
|
||||
<section className="hero">
|
||||
<h1>A website demonstrating common technical SEO problems</h1>
|
||||
<p className="lede">
|
||||
Every page on this site has one thing wrong with it, on purpose: a
|
||||
missing title, a redirect loop, a page nothing links to. Point an
|
||||
SEO crawler at the site and see which problems it catches.
|
||||
</p>
|
||||
<main className="home-main">
|
||||
<section className="home-hero">
|
||||
<div className="home-copy">
|
||||
<h1>BadSEO</h1>
|
||||
<p>{PRODUCT_DESCRIPTION}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="stat-row">
|
||||
<div className="stat">
|
||||
<b>{allFixtures.length}</b>
|
||||
<span>broken pages</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<b>{distinctIssueTypes}</b>
|
||||
<span>issue types</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<b>{totalPaths}</b>
|
||||
<span>crawlable URLs</span>
|
||||
</div>
|
||||
<div className="home-issues" id="issues">
|
||||
{homepageCategories.map((category) => (
|
||||
<section className="home-issue-group" key={category}>
|
||||
<h3>{category}</h3>
|
||||
<div className="home-case-head" aria-hidden="true">
|
||||
<span>Issue</span>
|
||||
<span>What it demonstrates</span>
|
||||
</div>
|
||||
<div className="home-case-list">
|
||||
{catalogLinkedFixtures
|
||||
.filter((fixture) => fixture.category === category)
|
||||
.map((fixture) => (
|
||||
<a
|
||||
className="home-case-row"
|
||||
href={fixture.path}
|
||||
key={fixture.path}
|
||||
>
|
||||
<span className="home-case-name">{fixture.name}</span>
|
||||
<span className="home-case-summary">
|
||||
{fixture.summary}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2>What's on it</h2>
|
||||
<p>
|
||||
Each page breaks one thing and is otherwise fine, so an audit sees a
|
||||
single problem instead of a pile of them. Every page shows what it is
|
||||
testing and which issue an audit should report. Start with the{" "}
|
||||
<a href="/catalog">catalog</a>, or open the{" "}
|
||||
<a href="/kitchen-sink">kitchen-sink page</a>, which breaks six ways
|
||||
at once.
|
||||
</p>
|
||||
|
||||
<h2>Why it exists</h2>
|
||||
<p>
|
||||
SEO tools all say they find broken titles, duplicate pages, redirect
|
||||
chains, and thin content. It is hard to check that they actually do.
|
||||
badseo.dev gives you a site that is broken in known ways, so you can
|
||||
run a crawler against it and compare what it finds to what is really
|
||||
there. We use it to test the OpenSEO audit.
|
||||
</p>
|
||||
|
||||
<h2>It's open source</h2>
|
||||
<p>
|
||||
The site is open source. If there is a common SEO mistake it does not
|
||||
cover yet, you can add it. Each page is one small file that lists the
|
||||
issues it should trigger, so a new page also works as a test. It is
|
||||
maintained by the team behind <a href="https://openseo.so">OpenSEO</a>
|
||||
, an open-source SEO tool.
|
||||
</p>
|
||||
|
||||
<h2>Run an audit</h2>
|
||||
<p>
|
||||
<a href="https://openseo.so">OpenSEO</a> can crawl badseo.dev and
|
||||
report the issues on each page, along with how to fix them. It is a
|
||||
straightforward way to see what a crawler catches.
|
||||
</p>
|
||||
</main>
|
||||
</SiteLayout>
|
||||
);
|
||||
|
||||
@ -50,7 +50,7 @@ export async function handleFixtureRequest(
|
||||
metaDescription: "That URL is not one of the pages on this site.",
|
||||
bodyHtml: `<h1>404, not found</h1>
|
||||
<p class="lede">This URL is not one of the broken pages on the site. It is just missing.</p>
|
||||
<p>Go back to the <a href="/catalog">catalog</a> to see the pages that break on purpose.</p>`,
|
||||
<p>Go back to the <a href="/#issues">examples</a> to see the pages that break on purpose.</p>`,
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user