Skip to content

fix: every page claimed to be the homepage, and two WCAG Level A failures - #121

Merged
fas89 merged 1 commit into
mainfrom
docs/polish
Sep 14, 2026
Merged

fas89 merged 1 commit into
mainfrom
docs/polish

Conversation

@fas89

@fas89 fas89 commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

211 of 213 pages emitted an identical og:url pointing at the homepage, and none carried rel="canonical". hostname reached the standalone sitemapPlugin but never defaultTheme() — and the theme gates both its bundled SEO and sitemap plugins on exactly that value, so the SEO plugin never registered and the only OG tags were a hand-written site-wide block.

before after
pages with rel="canonical" 0 213
distinct og:url values 1 213
robots.txt sitemap 404 resolves
sitemap <loc> entries 212 212 (identical)

Two things the audit had wrong

Found by reading the installed packages rather than trusting the finding:

  • hostname alone gives per-page og:* but zero canonicals. canonical is a separate option — getCanonicalLink returns null without it.
  • hostname must be the bare origin. All three call sites concatenate hostname + base, which is what produced /forge_docs/forge_docs/sitemap.xml. The <loc> entries were never affected, because they're written root-relative and the URL resolver discards the hostname's path — verified by diffing all 212 before and after.

The manual sitemapPlugin registration is dropped: the theme registers it with the same single option, and running both meant two competing onGenerated hooks rewriting robots.txt.

Skip link — WCAG 2.4.1, Level A

324 focusable stops preceded <main>, because the entire sidebar renders on every interior page. The pill is hidden by translation rather than display:none so it stays in the tab order, and the handler focuses the target explicitly so it works after router hydration, not only on a cold fragment navigation.

Brand link accessible name — WCAG 4.1.2

The theme hides .vp-site-name whenever logoAlt resolves equal to the title — which is what logoAlt ?? title gives when no logo is configured, and this site deliberately configures none (the PNG has a baked-in white background that reads as a bright box in dark mode). The anchor therefore had no unhidden child and an empty accessible name on every page. An explicit logoAlt breaks that equality.

Worth noting how this nearly shipped: the accessibility work correctly identified it as a config.ts change and reported the exact diff, the config owner didn't apply it, and both reported success. Caught by checking the built HTML.

Contrast, favicon, weight

  • --vp-c-text-subtle on --vp-c-bg-alt was 4.25:1 in light mode, below the 4.50 AA floor and the only sub-AA pair on the site. Now 4.77:1.
  • Favicon: 69,734 bytes at 256×139 → 2,181 at 32×32. The first regeneration preserved the logo's aspect ratio and produced a 32×17 icon; square now, caught by checking the file rather than the byte count.
  • Search index: main JS chunk 128K → 79K gzipped.

Deliberately not fixed

Monaco's CSS. The mechanism is worth recording: @vuepress/bundler-vite hard-sets cssCodeSplit: false, so Vite merges styles reachable only through an async-only chunk into the single render-blocking stylesheet. Monaco is ~162 KB of it — 65.9%, not the 58.6% first reported. The fix is either overriding that Vite option (which VuePress disables deliberately for SSG style ordering) or loading Monaco's CSS at runtime. Both are larger than this change should carry.

**211 of 213 pages emitted an identical og:url pointing at the homepage, and
none carried rel="canonical".** `hostname` was passed to the standalone
sitemapPlugin but never to `defaultTheme()`, and the theme gates BOTH its
bundled SEO and sitemap plugins on exactly that value — so the SEO plugin never
registered and the only OG tags were a hand-written site-wide block.

Two things the audit had wrong, found by reading the installed packages:

- `hostname` alone gives per-page `og:*` but **zero** canonicals. `canonical` is
  a separate option (`getCanonicalLink` returns null without it).
- `hostname` must be the bare ORIGIN. All three call sites concatenate
  `hostname + base`, which is what produced the doubled
  `/forge_docs/forge_docs/sitemap.xml` in robots.txt. The `<loc>` entries were
  never affected, because they are written root-relative and the URL resolver
  discards the hostname's path — verified by diffing all 212 before and after.

The manual sitemapPlugin registration is dropped: the theme registers it with
the same single option, and running both meant two competing `onGenerated` hooks
rewriting robots.txt.

| | before | after |
|---|---|---|
| pages with `rel="canonical"` | 0 | 213 |
| distinct `og:url` values | 1 | 213 |
| robots.txt sitemap | 404 | resolves |

**Skip link**, WCAG 2.4.1 Level A: 324 focusable stops preceded `<main>`,
because the whole sidebar renders on every interior page. The pill is hidden by
translation rather than `display:none` so it stays in the tab order, and the
handler focuses the target explicitly so it works after router hydration and not
only on a cold fragment navigation.

**Brand link accessible name**, WCAG 4.1.2. The theme hides the `.vp-site-name`
span whenever `logoAlt` resolves equal to the title — which is what
`logoAlt ?? title` gives when no logo is configured, and this site deliberately
configures none. The anchor therefore had no unhidden child and an empty
accessible name on every page. An explicit `logoAlt` breaks that equality.

This one fell between two agents' ownership: the accessibility work correctly
identified it as a config.ts change and reported the exact diff, and the config
owner never applied it. Caught by checking the built HTML rather than the
reports.

**Contrast**: `--vp-c-text-subtle` on `--vp-c-bg-alt` was 4.25:1 in light mode,
below the 4.50 AA floor and the only sub-AA pair on the site. Now 4.77:1.

**Favicon**: 69,734 bytes at 256x139, fetched on every page load, now 2,181
bytes at 32x32. The first regeneration preserved the logo's aspect ratio and
produced a 32x17 icon — square now, caught by checking the file rather than the
byte count.

**Search index**: the main JS chunk drops from 128K to 79K gzipped.

Monaco's CSS is NOT addressed here and the mechanism is worth recording:
`@vuepress/bundler-vite` hard-sets `cssCodeSplit: false`, so Vite merges styles
reachable only through an async-only chunk into the one render-blocking
stylesheet. Monaco is ~162 KB of it, 65.9%. The fix is either overriding that
Vite option — which VuePress disables deliberately for SSG style ordering — or
loading Monaco's CSS at runtime. Both are larger than this change should carry.
@fas89
fas89 merged commit 922a075 into main Sep 14, 2026
8 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 8e366719 Deployed Sep 14, 2026 by fas89 via deploy #149
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant