Skip to content

Fix analytics integrity, canonical URLs, redirects and edge caching - #26

Open
omermorad wants to merge 7 commits into
masterfrom
chore/site-remediation
Open

omermorad wants to merge 7 commits into
masterfrom
chore/site-remediation

Conversation

@omermorad

Copy link
Copy Markdown
Contributor

Follow-up to an audit of GA4, Cloudflare, Netlify and npm data covering April through September. Each commit stands alone and can be reviewed or reverted independently.

Why

Four problems, found by cross-checking the analytics property against edge logs and the built artifact.

1. Analytics could not be trusted. gtag was configured unconditionally, so deploy previews, branch deploys and local development all reported into the production GA4 property. Over Apr-Sep, 55% of recorded sessions were not real traffic; a single developer machine produced 6,834 pageviews, half of every pageview in the property. No decision resting on these numbers was safe.

2. Every leaf page contradicted its own canonical URL. The canonical tag and the sitemap used the slashless form, while Netlify Pretty URLs 301s that form to the slash form. Google crawled a sitemap URL, was redirected away from it, and landed on a page whose canonical pointed back at the URL it had just left. The four category indexes were already consistent; the 16 leaves were not.

3. Legacy URLs were soft redirects. Netlify skips a non-forced redirect when a real file exists at the path, and plugin-client-redirects emitted a real index.html at all 37 of its sources. The plugin silently won, so every legacy URL served 200 with a meta refresh instead of a 301 — on exactly the high-traffic paths the v4 restructure moved.

4. The edge cached nothing. Netlify sends max-age=0, must-revalidate on content-hashed immutable assets. Cloudflare obeys it: 140,800 requests over 30 days produced 39 cache hits (0.028%) and 775 MB of origin transfer.

What changed

Commit Change
ci Build every PR. onBrokenLinks: "throw" means a bad link fails the build, and that previously only surfaced in Netlify after merge
fix(analytics) gtag only when CONTEXT=production, removing the tag from the artifact rather than hiding it at runtime
fix(redirects) Delete the plugin, consolidate on netlify.toml, add five legacy paths seen 404ing at the edge
fix(seo) trailingSlash: true, Pretty URLs pinned in netlify.toml, sitemap excludes /404 and /search
perf(cache) static/_headers with immutable caching for fingerprinted assets
chore Remove unreferenced config/api-sidebars.js, fix a sidebar_position collision
docs Installation selects a test runner first; Automock migration path surfaced

trailingSlash: true adopts the form Netlify already serves, so no live URL changes and no redirect reverses direction. The alternative would have flipped a 301 on every leaf page.

The content change follows the download data: @suites/doubles.vitest grew 46.8% last month while @suites/doubles.jest fell 7.3%, yet installation hardcoded the Jest adapter in all three package-manager tabs and treated Vitest as a caveat. Separately, @automock/jest still pulls ~257k downloads a month while its migration guide drew 69 pageviews in six months, so the homepage and installation page now point at it.

Verification

Every redirect source was diffed mechanically before deletion: of 37 plugin sources against 46 netlify.toml rules, ten had no identical entry and all ten are covered by existing splats. No path becomes a 404. All 11 distinct doc destinations resolve to real routes in the build.

Checked against a production build:

  • gtag present in production build, absent with CONTEXT=deploy-preview (0 files)
  • Canonical is https://suites.dev/docs/guides/solitary/ and appears in the sitemap
  • All 21 sitemap URLs slash-terminated; /404 and /search excluded
  • 0 meta-refresh stubs, down from 37
  • Cache-Control set on specific paths only, never on /*

One thing the new CI caught immediately: trailingSlash: true broke six relative links, because with a trailing slash on the page URL ./solitary resolves as a child of the current page rather than a sibling. They are now absolute paths, which are immune to the setting.

Not included, needs dashboard access

  • Netlify: confirm whether docs.suites.dev is a domain alias. /.well-known/acme-challenge/__resolve-check is the single highest-traffic path on the site at 1,436 requests/day, doubled in the last two weeks, and a stale alias is the leading hypothesis
  • Cloudflare: WAF rule for referral spam; confirm SSL mode is Full (strict)
  • GA4: internal traffic filter, and an annotation dated this deploy so the step change in the data is not misread later
  • Search Console: verify the property, otherwise the SEO change has no feedback loop
  • npm deprecate @automock/jest pointing at the migration guide, which reaches ~257k monthly installs at the moment of friction

Review notes

Expect per-page pageviews to appear to jump after this deploys, because the two URL forms merge into one row. That is not growth.

onBrokenLinks is set to throw, so a broken internal link fails the build.
Without CI that failure only surfaced in Netlify after the merge had landed.

The Algolia reindex is not run here: it is a no-op outside a production
deploy and CI has no crawler credentials.
The measurement ID was configured unconditionally, so deploy previews and
branch deploys reported into the production GA4 property alongside local
development. Measured over Apr-Sep, 55% of recorded sessions were not real
traffic, and a single developer machine accounted for 50% of all pageviews.

Guarding on Netlify's CONTEXT removes the tag from the artifact entirely
rather than hiding it at runtime. CONTEXT is unset locally and in CI, so
yarn start and yarn build both stop reporting.
Two redirect systems were configured for overlapping paths. Netlify skips a
non-forced redirect when a real file exists at that path, and the client
redirects plugin emitted a real index.html at all 37 of its sources, so the
plugin silently won: legacy URLs served 200 with a meta refresh rather than a
301. Meta refresh is a soft signal search engines consolidate slowly, and it
covered every high-traffic URL moved by the v4 restructure.

All 37 plugin sources were diffed against the 46 netlify.toml rules before
deletion. Ten had no identical entry; all ten are covered by existing splats,
so no path becomes a 404.

Also adds five legacy paths observed 404ing at the edge, and drops four
explicit trailing-slash duplicate rules Netlify already matches.
Every leaf doc page contradicted itself. The canonical tag and sitemap used
the slashless form while Netlify Pretty URLs 301s that form to the slash
form, so each canonical URL redirected away from itself. Google crawled a
sitemap URL, was redirected, and landed on a page pointing back at the URL
it had just left. The four category indexes were already consistent; the
16 leaves were not.

trailingSlash: true adopts the form Netlify already serves, so no live URL
changes and no redirect reverses direction. Pretty URLs is pinned in
netlify.toml rather than left to the dashboard, since file configuration
overrides UI configuration and the two settings must agree.

Relative sibling links are rewritten to absolute paths: with a trailing
slash on the page URL, ./solitary resolved as a child of the current page
rather than a sibling. The build's broken-link check caught this.

Also excludes /404 and /search from the sitemap, and points the header's
documentation links at their destination instead of through a redirect.
Netlify defaults every response to max-age=0, must-revalidate. suites.dev
sits behind Cloudflare, which obeys it, so every request revalidated against
the origin: 140,800 requests over 30 days produced 39 cache hits (0.028%)
and 775 MB of origin transfer.

Docusaurus content-hashes its build output, so those files are safe to cache
permanently. HTML deliberately keeps Netlify's default so deploys go live at
once.

Cache-Control is set on specific paths only and never on /*. Netlify merges
all matching rules, and its behaviour on a duplicated header name is not
reliably most-specific-wins; a merged value would silently reinstate
max-age=0 and make the file a no-op.

Also stops publishing a committed .DS_Store.
config/api-sidebars.js is referenced by nothing; docusaurus.config.ts points
at docs-sidebars.js. unit-reference.md shared sidebar_position 5 with
mock.md, leaving the order to an alphabetical tiebreak.
Installation hardcoded the Jest adapter in all three package-manager tabs
and treated Vitest as a caveat section further down the page. Downloads say
the opposite: @suites/doubles.vitest grew 46.8% last month while
@suites/doubles.jest fell 7.3%.

Installation now selects a runner first and a package manager second.
Docusaurus persists both choices independently, so picking Vitest once
carries across the site. The Vitest section keeps its configuration content
but is retitled, since it documents decorator metadata setup rather than a
caveat about Suites.

@automock/jest still pulls ~257k downloads a month against 69 pageviews on
its migration guide in six months, so the homepage and the installation page
now point at it directly.
@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs.suites.dev Error Error Oct 7, 2026 8:01pm UTC

@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for suites-dev ready!

Name Link
🔨 Latest commit 279b850
🔍 Latest deploy log https://app.netlify.com/projects/suites-dev/deploys/6ac6a4b589fc4100089c1a5f
😎 Deploy Preview https://deploy-preview-26--suites-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

This branch had an error being deployed

1 failed deployment
Preview — 279b850d Deployed Oct 7, 2026 by vercel[bot]
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