Skip to content

fix(pages): consistency fixes — titles, structured data, dark mode, copy, email capture#34

Merged
SoapyRED merged 1 commit into
mainfrom
fix/page-consistency-r1
May 15, 2026
Merged

fix(pages): consistency fixes — titles, structured data, dark mode, copy, email capture#34
SoapyRED merged 1 commit into
mainfrom
fix/page-consistency-r1

Conversation

@SoapyRED
Copy link
Copy Markdown
Owner

Summary

Sprint page-consistency-r1 — five small fixes for documented R10/R11 consistency findings. The other four findings verified already-resolved by intervening PRs (#28, #30, #33) and required no action.

Phase 0 verification table (pre-fix, against working tree of origin/main @ 6f3d1b0)

# Finding Status Evidence
1 6 pages render with double-branded title (... | FreightUtils.com) YES app/layout.tsx:18 template was '%s | FreightUtils.com'; 6 affected pages use plain-string title:
2 WebApplication JSON-LD missing on /adr/lq-eq-checker NO — already present app/adr/lq-eq-checker/page.tsx:29
3 BreadcrumbList JSON-LD missing on ADR detail NO — already present app/adr/un/[unNumber]/page.tsx:356
4 BreadcrumbList JSON-LD missing on HS detail (code + heading) NO — already present app/hs/code/[subheadingCode]/page.tsx:44, app/hs/heading/[headingCode]/page.tsx:47
5 20 hardcoded #fff on /api-docs mostly resolved — only 1 remains app/api-docs/page.tsx:1816 — intentional white-on-red error-code badge; left alone
6 Dark-mode cross-tab sync missing YES app/components/ThemeToggle.tsx had no storage listener
7 Rate-limit copy inconsistent on /api-docs YES (mixed) 3 phrasings: 25 requests per day per IP (×2), 25 calls/day (×1)
8 /adr/lq-eq-checker layout: email after API CTA, source badge, FAQ, 200-word min NO — already correct Page already has ApiCtaBannerDataTimestampNewsletterCapture order, FAQPage JSON-LD with 3 questions, ~500 words of educational content
9 Email capture on ADR detail + HS detail PARTIAL ADR detail has it; HS code/heading templates did not

Fixes applied

  • app/layout.tsx: root title template '%s | FreightUtils.com''%s | FreightUtils'. Fixes 5 of the 6 documented pages in one component-level change.
  • app/changelog/page.tsx: switched to title.absolute so the template doesn't duplicate "FreightUtils" in a title that already contains it.
  • app/components/ThemeToggle.tsx: added a storage event listener so a theme flip in one tab mirrors into other open tabs.
  • app/api-docs/page.tsx: normalised the lone "25 calls/day" mention to "25 requests per day"; all three rate-limit references now match.
  • app/hs/code/[subheadingCode]/page.tsx + app/hs/heading/[headingCode]/page.tsx: added <NewsletterCapture /> below the "Found an error?" footer to match the ADR-detail / LQ-EQ pattern.
  • lib/seo/page-metadata.ts: docstring updated to reflect new template.
  • CHANGELOG.md + lib/changelog-data.ts: 2026-05-15 Bug Fix entry.

Verification

Static-HTML output from next build:

/about     → "About — Free Freight Tools & API Platform | FreightUtils"
/changelog → "Changelog — FreightUtils Updates & Releases"  (absolute, single brand)
/consignment-calculator → "Multi-Item Consignment Calculator | FreightUtils"
/duty      → "UK Import Duty & VAT Calculator | FreightUtils"
/unlocode  → "UN/LOCODE Lookup | FreightUtils"
/vehicles  → "Road Freight Vehicle & Trailer Types | FreightUtils"
/          → "FreightUtils — Free Freight Calculators & APIs"  (default title, unchanged)
/hs        → unchanged (uses title.absolute via lib/seo builders)
/changelog renders the new May 15 entry.

tsc --noEmit clean. ESLint on touched files: only the pre-existing react-hooks/set-state-in-effect warning on ThemeToggle.tsx:12 (the original setDark(isDark)) — unrelated to this change.

FAULT 5 checklist

  • siteStats.ts — N/A (no displayed-number change)
  • app/sitemap.ts — N/A (no new URLs)
  • public/openapi.json — N/A (no API contract change)
  • /api-docs — touched (copy normalisation only, no contract)
  • nav dropdown — N/A
  • homepage tool grid — N/A
  • CHANGELOG.md — YES (2026-05-15 entry)
  • lib/changelog-data.ts — YES (mirrored, newest-first)
  • /changelog page renders new entry — YES (verified in next build output)
  • MCP server tool registration — N/A
  • footer — N/A
  • GitHub README in freightutils-mcp — N/A
  • npm package version — N/A
  • Postman collection — N/A
  • 200-word page minimum — N/A
  • withAuditRest on new API routes — N/A
  • generateMetadata on new public pages — N/A
  • IndexNow ping — N/A (no new URLs)

Test plan

  • npx tsc --noEmit clean
  • npx next build succeeds; static HTML titles verified
  • ESLint on touched files: no new findings
  • /changelog static HTML contains the new May 15 entry
  • Vercel preview SUCCESS (auto)
  • Smoke test against preview
  • Post-merge production verification: curl 3 previously-broken titles, /adr/un/3480 BreadcrumbList, /hs/code/843691 BreadcrumbList, /adr/lq-eq-checker WebApplication, /api-docs rate-limit text
  • Sentry-quiet 5xx sweep on 10 paths

🤖 Generated with Claude Code


Generated by Claude Code

…ture

Five small fixes for documented R10/R11 consistency findings; the other four
findings (WebApplication/BreadcrumbList JSON-LD, LQ-EQ page layout, ADR-detail
email capture) verified already-fixed by intervening PRs.

Changes:
- app/layout.tsx: root title template `'%s | FreightUtils.com'` → `'%s | FreightUtils'`.
  Fixes 5 of the 6 documented double-branded pages in one place.
- app/changelog/page.tsx: switched to `title.absolute` so the template doesn't
  duplicate "FreightUtils" in a title that already contains it.
- app/components/ThemeToggle.tsx: added `storage` event listener so a theme
  flip in one tab mirrors into other open tabs.
- app/api-docs/page.tsx: normalised the lone "25 calls/day" mention to
  "25 requests per day" so all three rate-limit references match.
- app/hs/code/[subheadingCode]/page.tsx + app/hs/heading/[headingCode]/page.tsx:
  added <NewsletterCapture /> below the "Found an error?" footer to match the
  ADR-detail / LQ-EQ-checker pattern.
- lib/seo/page-metadata.ts: docstring updated to reflect new template.
- CHANGELOG.md + lib/changelog-data.ts: 2026-05-15 Bug Fix entry.

Verified via `next build` static HTML:
  /about     → "About — Free Freight Tools & API Platform | FreightUtils"
  /changelog → "Changelog — FreightUtils Updates & Releases"   (absolute)
  /consignment-calculator → "Multi-Item Consignment Calculator | FreightUtils"
  /duty      → "UK Import Duty & VAT Calculator | FreightUtils"
  /unlocode  → "UN/LOCODE Lookup | FreightUtils"
  /vehicles  → "Road Freight Vehicle & Trailer Types | FreightUtils"
  /          → "FreightUtils — Free Freight Calculators & APIs" (default title)
  /hs        → unchanged (uses title.absolute via lib/seo builders)
  /changelog renders the new May 15 entry.

`tsc --noEmit` clean. Lint on touched files: only pre-existing
react-hooks/set-state-in-effect on ThemeToggle (unrelated to this change).
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

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

Project Deployment Actions Updated (UTC)
freighttools Ready Ready Preview, Comment May 15, 2026 4:03am

Request Review

@SoapyRED SoapyRED marked this pull request as ready for review May 15, 2026 04:04
@SoapyRED SoapyRED merged commit 7d220f6 into main May 15, 2026
2 checks passed
SoapyRED added a commit that referenced this pull request May 16, 2026
…LT 15) (#44)

* audit(seo): diagnose title-template state (Phase 1)

Diagnosis-first per sprint hard rule. Findings:

1. The literal 'FreightUtils | FreightUtils.com' double-branding is NOT
   currently in prod. PR #34 fixed it at the root template (changed
   '%s | FreightUtils.com' to '%s | FreightUtils' in app/layout.tsx).
   Curl of 11 representative URLs shows single brand or no brand.
   Sprint context (R8/R9/R10 rounds) was written against pre-PR-#34 prod.

2. Two real issues remain:
   - ADR detail titles lost 'ADR 2025' context segment. Builder doesn't
     emit it; year/edition signal lives only in meta description.
   - Prevention is by convention. lint-seo-titles.mjs checks length /
     keyword / description rules but does not assert 'FreightUtils
     appears at most once per rendered title'. Three rounds of
     regression returned because the discipline isn't enforced.

3. Fix direction (Phase 2):
   - Extend lint-seo-titles.mjs with Rule A (no double-brand) covering
     ALL static metadata exports under app/, and Rule B (ADR titles
     contain 'ADR 2025') run against ADR builder fixtures.
   - Update buildAdrUnMetadata to emit 'ADR 2025' in titles using the
     same SITE_YEAR -> 2025 mapping already used for descriptions.

4. Contract test fails on pre-fix main (Rule B fires on every ADR
   fixture); passes after builder edit. PR body documents both states.

Companion follow-ups noted but out of scope:
- /changelog and /containers use title.absolute without brand — brand
  recall lost on those, debatable tradeoff.
- /pallet title is 78 chars — SERPs cut the brand. Separate length
  sprint.

Doc: docs/audit/title-template-2026-05-16.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* audit(seo): amend Phase 1 — wider grep finds 12+ double-branded pages

Initial spot-check covered only the 8 pages flagged by R8/R9/R10
consensus rounds and concluded the literal double-brand was gone.
Wider grep over app/**/page.tsx for title:.*FreightUtils finds 12+
pages where title is hardcoded as 'X — FreightUtils' and the root
template appends '| FreightUtils' — rendered as 'X — FreightUtils
| FreightUtils'. Prod curl confirms:

  /account            → Account — FreightUtils | FreightUtils
  /for-it             → For IT Departments — FreightUtils | FreightUtils
  /roadmap            → Roadmap — FreightUtils | FreightUtils
  /contact            → Contact — FreightUtils | FreightUtils
  /refund-policy      → Refund Policy — FreightUtils | FreightUtils
  /dpa                → Data Processing Agreement — FreightUtils | FreightUtils
  /docs/deprecation   → Deprecation Policy — FreightUtils | FreightUtils
  /docs/versioning    → Versioning Policy — FreightUtils | FreightUtils
  /guides             → Guides — FreightUtils | FreightUtils
  /status             → Status — FreightUtils | FreightUtils
  /signin             → Sign in — FreightUtils | FreightUtils
  /guides/[slug]      → LHR Shed Codes: ... — FreightUtils Guide | FreightUtils

The symptom mutated from the pre-PR-#34 form ('| FreightUtils.com |
FreightUtils.com') to the new form ('— FreightUtils | FreightUtils').
Same root cause: no enforced contract test for 'brand appears at most
once per rendered title'. Same architecture-level fix from Phase 2.

Updated Phase 1 'TL;DR' + 'Per-page static metadata exports' table
in docs/audit/title-template-2026-05-16.md to reflect the broader
scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(seo): single-brand titles + restore ADR 2025 (Phase 2)

Root cause (per docs/audit/title-template-2026-05-16.md): no enforced
contract test for 'FreightUtils appears at most once per rendered
title'. 12 pages drifted to 'X — FreightUtils' as the per-page title,
which then received '| FreightUtils' from the root template at
app/layout.tsx:18 — rendered double-branded on prod across /account,
/for-it, /roadmap, /contact, /refund-policy, /dpa, /docs/deprecation,
/docs/versioning, /guides, /guides/[slug], /status, /signin.

Three changes, all component-level:

1. scripts/lint-seo-titles.mjs — extended with two new rules:
   - Rule A: walks every app/**/page.tsx, extracts title literals
     from static metadata exports and generateMetadata bodies,
     composes the rendered title with the root template suffix where
     applicable, asserts 'FreightUtils' appears at most once. Catches
     the regression class going forward.
   - Rule B: asserts ADR builder fixtures produce titles containing
     'ADR 2025' (the dataset edition signal). Restores the year/edition
     context that was lost in a prior fix attempt.
   - Pre-fix run failed with 17 violations (12 Rule A + 5 Rule B).
   - Post-fix run: 103 title literals scanned across 54 page files,
     no double-brand; all 5 ADR fixtures contain 'ADR 2025'.

2. lib/seo/page-metadata.ts — buildAdrUnMetadata now emits 'ADR 2025'
   in the title suffix. Example: 'UN 1203 Petrol — ADR 2025 Class 3 PG II'.
   Adds 5 chars to the suffix; truncation logic for properShippingName
   absorbs the difference. Pinned via SITE_YEAR mapping ('2026' → '2025'),
   consistent with the same pattern already used in the meta description.

3. 12 page.tsx files — removed '— FreightUtils' from the per-page title
   string. Root template adds '| FreightUtils' once. /guides/[slug]
   changed from '${title} — FreightUtils Guide' to '${title} — Guide';
   template adds '| FreightUtils' → 'Title — Guide | FreightUtils'
   (single brand, retains Guide content signal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(seo): FAULT 15 + STATE baseline + CI gate + changelog (Phase 3-4)

Phase 3:
- STATE.md gains a 'Search performance baseline' subsection capturing
  the Bing Webmaster baseline (60 clicks / 4.7K imp / 1.29% CTR
  overall, 8 top zero-click queries with positions). Documents the
  Bing 3-7d / Google 7-21d re-crawl windows so the CTR uplift can be
  assessed against this baseline at 2026-05-23–05-30 (Bing) and
  2026-05-30–06-06 (Google). GSC pull noted as Soap-manual pending
  (no GSC connector configured).
- STATE.md Sprint cadence count bumped 18 -> 19 with PR #44 entry.
- /changelog roadmap 'GSC CTR improvement' card: not present in
  app/roadmap/page.tsx — N/A.

Phase 4:
- FAULT 15 added to docs/FAULT-HISTORY-AND-PREVENTION.md as a new
  category. Includes root cause, symptom, detection mechanism (lint
  Rule A + Rule B), and prevention via CI gate. Plus a 2026-05-16
  fault-log row referencing PR #44.
- package.json: new 'prebuild' script runs lint:seo-titles before
  'next build'. Vercel build now blocks any PR that introduces a
  double-brand title or drops the ADR edition signal.

Release hygiene:
- CHANGELOG.md gets a 2026-05-16 'SEO' entry at the top.
- lib/changelog-data.ts gets a 2026-05-16 'Bug Fix' entry; verified
  Rule A still passes after the edit (new entry doesn't contain
  'FreightUtils', the only safe form for a plain-string title).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: SoapyRED <soapyred@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants