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
|
## 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: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-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: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-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-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-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.
|
- [ ] `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 |
|
| **Site structure** | orphan page, deep click-path |
|
||||||
| **Kitchen sink** | one page that breaks six ways at once |
|
| **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
|
## How it's built
|
||||||
|
|
||||||
badseo.dev is a TanStack Start app deployed to a Cloudflare Worker, following
|
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.
|
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
|
A TanStack catch-all server route keeps the deliberate fixtures as raw
|
||||||
responses with byte-level control over status codes, redirects, headers
|
responses with byte-level control over status codes, redirects, headers
|
||||||
(`X-Robots-Tag`, `Link: …; rel=canonical`), timing, and the malformed `<head>`
|
(`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
|
The harness drives the **real** OpenSEO crawl + issue-detection functions
|
||||||
(imported straight from `../src`) against a running badseo.dev, then asserts every
|
(imported straight from `../src`) against a running badseo.dev, then asserts every
|
||||||
fixture triggers exactly the issues it declares — and that the homepage,
|
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
|
```bash
|
||||||
# with `npm run dev` running in another terminal:
|
# with `npm run dev` running in another terminal:
|
||||||
|
|||||||
@ -1,417 +1,152 @@
|
|||||||
:root {
|
:root {
|
||||||
--canvas: #f5f1ec;
|
color-scheme: light;
|
||||||
--surface: #ffffff;
|
--navy: #0c1b3a;
|
||||||
--surface-2: #ebe7e1;
|
--blue: #1745d1;
|
||||||
--ink: #111111;
|
--paper: #e7ecf4;
|
||||||
--ink-muted: #626260;
|
--paper-deep: #d9e1ef;
|
||||||
--ink-subtle: #7b7b78;
|
--surface: #f7f9fc;
|
||||||
--hairline: #d8d1c8;
|
--line: #93a5c8;
|
||||||
--hairline-soft: #ebe7e1;
|
--line-soft: #bdc8da;
|
||||||
--orange: #ff5600;
|
--muted: #526584;
|
||||||
--footer-bg: #eee8de;
|
--critical: #d94040;
|
||||||
--critical: #d23b1f;
|
--warning: #cf7d18;
|
||||||
--warning: #b26a00;
|
--info: #6f82a5;
|
||||||
--info: #6b7280;
|
--mono: "IBM Plex Mono", monospace;
|
||||||
--mono:
|
--sans: "IBM Plex Sans", sans-serif;
|
||||||
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
--serif: "Newsreader", Georgia, serif;
|
||||||
--sans:
|
--condensed: "Archivo Narrow", sans-serif;
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
||||||
"Segoe UI", sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* { box-sizing: border-box; }
|
||||||
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; }
|
||||||
html {
|
button, input { font: inherit; }
|
||||||
-webkit-text-size-adjust: 100%;
|
a { color: inherit; text-underline-offset: 4px; }
|
||||||
}
|
a:focus-visible, button:focus-visible { outline: 3px solid #ff6a57; outline-offset: 3px; }
|
||||||
|
|
||||||
body {
|
/* Shared site chrome. No headings or images are introduced here, keeping the
|
||||||
margin: 0;
|
fixture pages' audit signals entirely under fixture control. */
|
||||||
background: var(--canvas);
|
.site-header, .nav {
|
||||||
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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-height: 92px;
|
||||||
|
align-items: end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px 24px;
|
gap: 28px;
|
||||||
flex-wrap: wrap;
|
padding: 31px 3.5vw 19px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.foot-inner a {
|
.site-brand, .brand { color: var(--navy); font-family: var(--condensed); font-size: 18px; font-weight: 700; letter-spacing: .03em; text-decoration: none; }
|
||||||
color: var(--ink-muted);
|
.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;
|
@media (prefers-reduced-motion: reduce) {
|
||||||
gap: 18px;
|
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 1ms !important; }
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-button {
|
.footer-button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--ink-muted);
|
color: var(--muted);
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
}
|
}
|
||||||
.footer-button:hover {
|
.footer-button:hover {
|
||||||
color: var(--ink);
|
color: var(--navy);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── analytics consent ── */
|
/* Consent-gated analytics controls added by /analytics.js. */
|
||||||
.consent-banner {
|
.consent-banner {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@ -419,12 +154,12 @@ a:hover {
|
|||||||
left: 24px;
|
left: 24px;
|
||||||
width: min(350px, calc(100vw - 48px));
|
width: min(350px, calc(100vw - 48px));
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--surface);
|
border: 1px solid var(--line);
|
||||||
border: 1px solid var(--hairline);
|
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
background: var(--surface);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 50px rgb(17 17 17 / 14%),
|
0 20px 50px rgb(12 27 58 / 14%),
|
||||||
0 2px 8px rgb(17 17 17 / 7%);
|
0 2px 8px rgb(12 27 58 / 7%);
|
||||||
animation: consent-in 180ms ease-out;
|
animation: consent-in 180ms ease-out;
|
||||||
}
|
}
|
||||||
.consent-banner[hidden] {
|
.consent-banner[hidden] {
|
||||||
@ -439,20 +174,20 @@ a:hover {
|
|||||||
}
|
}
|
||||||
.consent-copy p {
|
.consent-copy p {
|
||||||
margin: 0 0 7px;
|
margin: 0 0 7px;
|
||||||
color: var(--ink-muted);
|
color: var(--muted);
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
.consent-copy a {
|
.consent-copy a {
|
||||||
color: var(--ink-muted);
|
color: var(--muted);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: var(--hairline);
|
text-decoration-color: var(--line-soft);
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
.consent-copy a:hover {
|
.consent-copy a:hover {
|
||||||
color: var(--ink);
|
color: var(--navy);
|
||||||
text-decoration-color: var(--ink);
|
text-decoration-color: var(--navy);
|
||||||
}
|
}
|
||||||
.consent-actions {
|
.consent-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -461,27 +196,26 @@ a:hover {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.consent-actions button {
|
.consent-actions button {
|
||||||
border: 1px solid var(--hairline);
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--ink);
|
color: var(--navy);
|
||||||
font: inherit;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
.consent-actions button:hover {
|
.consent-actions button:hover {
|
||||||
background: var(--surface-2);
|
background: var(--paper-deep);
|
||||||
}
|
}
|
||||||
.consent-actions .consent-accept {
|
.consent-actions .consent-accept {
|
||||||
border-color: var(--ink);
|
border-color: var(--navy);
|
||||||
background: var(--ink);
|
background: var(--navy);
|
||||||
color: #ffffff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.consent-actions .consent-accept:hover {
|
.consent-actions .consent-accept:hover {
|
||||||
background: #000000;
|
background: #000;
|
||||||
}
|
}
|
||||||
@keyframes consent-in {
|
@keyframes consent-in {
|
||||||
from {
|
from {
|
||||||
@ -503,43 +237,3 @@ a:hover {
|
|||||||
padding: 15px;
|
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> {
|
async function warmup(): Promise<void> {
|
||||||
// Prime the dev server so healthy pages don't read as slow on a cold start.
|
// 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);
|
for (const f of allFixtures) for (const p of fixturePaths(f)) paths.add(p);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...paths].map((p) =>
|
[...paths].map((p) =>
|
||||||
@ -247,7 +247,7 @@ async function main() {
|
|||||||
|
|
||||||
if (process.env.DEBUG_DEPTH) {
|
if (process.env.DEBUG_DEPTH) {
|
||||||
for (const p of pages) {
|
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}`);
|
console.log(` depth ${p.crawlDepth} ${p.url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,7 +334,6 @@ async function main() {
|
|||||||
|
|
||||||
// Non-fixture content pages must be clean.
|
// Non-fixture content pages must be clean.
|
||||||
check("Homepage", "/", [], false);
|
check("Homepage", "/", [], false);
|
||||||
check("Catalog", "/catalog", [], false);
|
|
||||||
check("Privacy policy", "/privacy", [], false);
|
check("Privacy policy", "/privacy", [], false);
|
||||||
|
|
||||||
const byCategory = new Map<string, Fixture[]>();
|
const byCategory = new Map<string, Fixture[]>();
|
||||||
|
|||||||
@ -3,23 +3,23 @@ import type { ReactNode } from "react";
|
|||||||
export function SiteLayout({ children }: { children: ReactNode }) {
|
export function SiteLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="nav">
|
<nav className="nav" aria-label="BadSEO navigation">
|
||||||
<a className="brand" href="/">
|
<a className="brand" href="/">
|
||||||
badseo.dev
|
BADSEO
|
||||||
</a>
|
|
||||||
<a className="nav-link" href="/catalog">
|
|
||||||
Catalog
|
|
||||||
</a>
|
</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>
|
</nav>
|
||||||
{children}
|
{children}
|
||||||
<footer className="foot">
|
<footer className="foot">
|
||||||
<div className="foot-inner">
|
<div className="foot-inner">
|
||||||
<span>
|
<a className="foot-brand" href="/">
|
||||||
badseo.dev is maintained by OpenSEO. Every page here is broken on
|
BADSEO
|
||||||
purpose.
|
</a>
|
||||||
</span>
|
|
||||||
<span className="foot-links">
|
<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://github.com/every-app/open-seo">GitHub</a>
|
||||||
<a href="https://openseo.so">OpenSEO</a>
|
<a href="https://openseo.so">OpenSEO</a>
|
||||||
<a href="/privacy">Privacy</a>
|
<a href="/privacy">Privacy</a>
|
||||||
@ -33,15 +33,6 @@ export function SiteLayout({ children }: { children: ReactNode }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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",
|
category: "Kitchen sink",
|
||||||
name: "Kitchen sink (everything at once)",
|
name: "Kitchen sink (everything at once)",
|
||||||
summary:
|
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:
|
lesson:
|
||||||
"Real broken pages usually have more than one problem. An audit should report every one of these, not stop at the first.",
|
"Real broken pages usually have more than one problem. An audit should report every one of these, not stop at the first.",
|
||||||
expectedIssues: [
|
expectedIssues: [
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Rendering primitives for badseo.dev.
|
// Rendering primitives for badseo.dev.
|
||||||
//
|
//
|
||||||
// Everything the shared chrome emits (nav, footer, the "what this page tests"
|
// Everything the shared chrome emits (nav, footer, and the "what this page
|
||||||
// panel, the OpenSEO badge) is deliberately SEO-NEUTRAL: no <h1>–<h6> and no
|
// tests" panel) is deliberately SEO-NEUTRAL: no <h1>–<h6> and no
|
||||||
// <img>. That way each fixture's headings and images are fully under the
|
// <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
|
// fixture's own control, and the audit measures exactly the defect we injected
|
||||||
// — not accidental noise from the layout.
|
// — not accidental noise from the layout.
|
||||||
@ -68,7 +68,7 @@ export function renderDocument(opts: DocumentOptions): string {
|
|||||||
head.push(
|
head.push(
|
||||||
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
||||||
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
|
'<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">');
|
head.push('<link rel="stylesheet" href="/styles.css">');
|
||||||
if (opts.headExtra) head.push(opts.headExtra);
|
if (opts.headExtra) head.push(opts.headExtra);
|
||||||
@ -86,27 +86,16 @@ ${opts.bodyHtml}
|
|||||||
|
|
||||||
function navHtml(): string {
|
function navHtml(): string {
|
||||||
return `<nav class="nav">
|
return `<nav class="nav">
|
||||||
<a class="brand" href="/">badseo.dev</a>
|
<a class="brand" href="/">BADSEO</a>
|
||||||
<a class="nav-link" href="/catalog">Catalog</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>`;
|
</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 {
|
function footerHtml(): string {
|
||||||
return `<footer class="foot"><div class="foot-inner">
|
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">
|
<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://github.com/every-app/open-seo">GitHub</a>
|
||||||
<a href="https://openseo.so">OpenSEO</a>
|
<a href="https://openseo.so">OpenSEO</a>
|
||||||
<a href="/privacy">Privacy</a>
|
<a href="/privacy">Privacy</a>
|
||||||
@ -156,8 +145,7 @@ function testPanel(fixture: Fixture): string {
|
|||||||
function withChrome(inner: string): string {
|
function withChrome(inner: string): string {
|
||||||
return `${navHtml()}
|
return `${navHtml()}
|
||||||
${inner}
|
${inner}
|
||||||
${footerHtml()}
|
${footerHtml()}`;
|
||||||
${openseoBadge()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageOptions extends DocumentOptions {
|
interface PageOptions extends DocumentOptions {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const Route = createRootRoute({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "stylesheet",
|
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" },
|
{ rel: "stylesheet", href: "/styles.css" },
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,109 +1,7 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, redirect } 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";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/catalog")({
|
export const Route = createFileRoute("/catalog")({
|
||||||
head: () => ({
|
beforeLoad: () => {
|
||||||
meta: [
|
throw redirect({ to: "/", hash: "issues" });
|
||||||
{ 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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 { createFileRoute } from "@tanstack/react-router";
|
||||||
import { SiteLayout } from "../components/site-layout";
|
import { SiteLayout } from "../components/site-layout";
|
||||||
import { allFixtures, fixturePaths } from "../fixtures/registry";
|
import { catalogLinkedFixtures, categories } from "../fixtures/registry";
|
||||||
|
|
||||||
const totalPaths = allFixtures.reduce(
|
const PRODUCT_DESCRIPTION =
|
||||||
(count, fixture) => count + fixturePaths(fixture).length,
|
"A website with example technical SEO issues for you to learn or test your agents";
|
||||||
0,
|
|
||||||
);
|
const homepageCategories = [
|
||||||
const distinctIssueTypes = new Set(
|
"Kitchen sink",
|
||||||
allFixtures.flatMap((fixture) => fixture.expectedIssues),
|
...categories.filter(
|
||||||
).size;
|
(category) =>
|
||||||
|
category !== "Kitchen sink" &&
|
||||||
|
category !== "Head tags & headings" &&
|
||||||
|
category !== "Content quality",
|
||||||
|
),
|
||||||
|
"Head tags & headings",
|
||||||
|
"Content quality",
|
||||||
|
];
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
{ title: "Technical SEO issues, by example | badseo.dev" },
|
{ title: "BadSEO | Technical SEO test site" },
|
||||||
{
|
{ name: "description", content: PRODUCT_DESCRIPTION },
|
||||||
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.",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
component: HomePage,
|
component: HomePage,
|
||||||
@ -27,65 +30,41 @@ export const Route = createFileRoute("/")({
|
|||||||
function HomePage() {
|
function HomePage() {
|
||||||
return (
|
return (
|
||||||
<SiteLayout>
|
<SiteLayout>
|
||||||
<main className="main">
|
<main className="home-main">
|
||||||
<section className="hero">
|
<section className="home-hero">
|
||||||
<h1>A website demonstrating common technical SEO problems</h1>
|
<div className="home-copy">
|
||||||
<p className="lede">
|
<h1>BadSEO</h1>
|
||||||
Every page on this site has one thing wrong with it, on purpose: a
|
<p>{PRODUCT_DESCRIPTION}</p>
|
||||||
missing title, a redirect loop, a page nothing links to. Point an
|
</div>
|
||||||
SEO crawler at the site and see which problems it catches.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="stat-row">
|
<div className="home-issues" id="issues">
|
||||||
<div className="stat">
|
{homepageCategories.map((category) => (
|
||||||
<b>{allFixtures.length}</b>
|
<section className="home-issue-group" key={category}>
|
||||||
<span>broken pages</span>
|
<h3>{category}</h3>
|
||||||
|
<div className="home-case-head" aria-hidden="true">
|
||||||
|
<span>Issue</span>
|
||||||
|
<span>What it demonstrates</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat">
|
<div className="home-case-list">
|
||||||
<b>{distinctIssueTypes}</b>
|
{catalogLinkedFixtures
|
||||||
<span>issue types</span>
|
.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>
|
</div>
|
||||||
<div className="stat">
|
</section>
|
||||||
<b>{totalPaths}</b>
|
))}
|
||||||
<span>crawlable URLs</span>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</main>
|
||||||
</SiteLayout>
|
</SiteLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export async function handleFixtureRequest(
|
|||||||
metaDescription: "That URL is not one of the pages on this site.",
|
metaDescription: "That URL is not one of the pages on this site.",
|
||||||
bodyHtml: `<h1>404, not found</h1>
|
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 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,
|
status: 404,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user