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.
127 lines
4 KiB
Python
127 lines
4 KiB
Python
#!/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())
|