fix: maintain Services dropdown header from one canonical source

The site header / Services mega-dropdown was duplicated across two render
systems (Astro pages via Base.astro->nav.html, and ~80 pre-rendered static
public/**/index.html pages each embedding their own copy). They had drifted
into 5 different variants (missing 'New Carrier Setup', misplaced Healthcare
column, NEW vs FREE badges, em-dash encoding differences), so
dev.performancewest.net, the order pages, and the rest of the site disagreed.

- Make site/src/partials/nav.html the single source of truth (adopts the most
  complete variant).
- Add scripts/sync_nav.py to rewrite every static page's <nav> block from
  nav.html (idempotent; --check guards against drift in CI/deploy).
- Run the sync automatically in deploy.sh and scripts/deploy-dev.sh.
- Deprecate scripts/inject_healthcare_nav.py (now delegates to sync_nav.py).
- Neutralize the broken no-op SiteNav.astro component.

All 80 headers + the Astro-built order pages now render the identical dropdown.
This commit is contained in:
justin 2026-06-05 14:27:24 -05:00
parent 695ace207c
commit bd9a70607f
86 changed files with 250 additions and 645 deletions

127
scripts/sync_nav.py Normal file
View file

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Single source of truth for the site header / Services mega-dropdown.
Background
----------
The marketing site is served from two rendering systems that historically each
carried their OWN copy of the header markup, which is why the Services dropdown
drifted out of sync (dev.performancewest.net vs the order pages vs everything
else):
1. Astro pages (site/src/pages/**/*.astro) -> layouts/Base.astro reads
src/partials/nav.html and injects it.
2. Pre-rendered static pages (site/public/**/index.html, 404.html) each
embedded a hard-coded <nav>...</nav> block.
This script makes ``site/src/partials/nav.html`` the ONE canonical source and
rewrites the <nav>...</nav> block of every static page to match it byte-for-byte.
It is idempotent and safe to run on every build/deploy.
Canonical block
---------------
``nav.html`` is a single line that begins with the document <body> tag and the
"<!-- Navigation -->" comment, followed by exactly one ``<nav>...</nav>`` block.
The <nav>...</nav> portion is what gets synced into the static pages (their own
<body ...> attributes are preserved).
Usage
-----
python3 scripts/sync_nav.py # rewrite static pages from nav.html
python3 scripts/sync_nav.py --check # exit 1 if any page is out of sync
Run with --check in CI / pre-commit to guarantee the header never drifts again.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PUBLIC = ROOT / "site" / "public"
NAV_PARTIAL = ROOT / "site" / "src" / "partials" / "nav.html"
NAV_RE = re.compile(r"<nav\b.*?</nav>", re.S)
def canonical_nav() -> str:
"""Extract the canonical <nav>...</nav> block from the partial."""
partial = NAV_PARTIAL.read_text()
m = NAV_RE.search(partial)
if not m:
sys.exit(f"ERROR: no <nav>...</nav> block found in {NAV_PARTIAL}")
if "services-menu" not in m.group(0):
sys.exit(f"ERROR: canonical nav in {NAV_PARTIAL} is missing the Services dropdown")
return m.group(0)
def static_pages() -> list[Path]:
files = sorted(PUBLIC.rglob("index.html"))
fof = PUBLIC / "404.html"
if fof.exists():
files.append(fof)
return files
def sync(check_only: bool) -> int:
canon = canonical_nav()
pages = static_pages()
out_of_sync: list[Path] = []
changed = 0
skipped_no_nav = 0
for f in pages:
html = f.read_text()
if "services-menu" not in html:
skipped_no_nav += 1
continue
m = NAV_RE.search(html)
if not m:
# has services-menu text but no <nav> wrapper -> structural anomaly
print(f"WARN: {f.relative_to(ROOT)} has a Services dropdown but no <nav> wrapper; skipping")
skipped_no_nav += 1
continue
current = m.group(0)
if current == canon:
continue # already in sync
out_of_sync.append(f)
if not check_only:
new_html = html[: m.start()] + canon + html[m.end():]
f.write_text(new_html)
changed += 1
if check_only:
if out_of_sync:
print(f"OUT OF SYNC: {len(out_of_sync)} static page(s) differ from nav.html:")
for f in out_of_sync:
print(f" - {f.relative_to(ROOT)}")
print("\nRun `python3 scripts/sync_nav.py` to fix.")
return 1
print(f"OK: all {len(pages) - skipped_no_nav} header(s) match nav.html")
return 0
print(
f"synced: {changed} already-in-sync: {len(pages) - skipped_no_nav - changed}"
f" no-dropdown: {skipped_no_nav}"
)
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--check",
action="store_true",
help="report drift and exit non-zero instead of rewriting",
)
args = ap.parse_args()
return sync(check_only=args.check)
if __name__ == "__main__":
raise SystemExit(main())