D3 Part 1: shared website assets + site gates (pages untouched)

- website/assets/site.css: ONE stylesheet for all seven pages — vendored
  @font-face (4 faces, latin+latin-ext, /assets/fonts/ paths), the
  design-system v2 :root token block verbatim, a hand-written shared
  base (reset, nav, two-tone heading as solid blue-bright, section/page
  headers, buttons, card, footer, hamburger + mobile menu, icon-tile,
  reduced-motion), and per-page sections mechanically converted from
  the seven embedded style blocks (tokens renamed, radii → 2px,
  box-shadows/text-gradients/hover-lifts removed, greens → blue per
  exception-color) scoped under .page-<name> body classes.
- website/assets/fonts/: the 4 woff2 files copied byte-identical from
  felhom-controller (self-hosted — removes the Google Fonts CDN / GDPR
  exposure once the pages switch over).
- website/assets/icons.svg: 70-symbol Lucide sprite (the D0 30 + 40
  marketing icons) for <use href="/assets/icons.svg?v=1#i-name">.
- scripts/site_gates.py: 8 gates (BOM bytes, Python-codepoint emoji,
  nav/footer consistency after active-marker normalization, analytics
  presence, no-CDN, banned legacy tokens, zero <style> blocks,
  ?v= cache-busting). Baseline against the unconverted pages: 84
  problems, 182 emoji — goes green with the page conversion commits.
- Live site unaffected: nothing references the new assets yet.
This commit is contained in:
2026-07-02 22:00:01 +02:00
parent a336fe3123
commit dd54e4cdf6
7 changed files with 2222 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""TASK-D3 site gates — mechanical checks against the static site's known failure mode:
silent per-page drift. Run from the repo root: python scripts/site_gates.py
Gates (all must pass; non-zero exit on any failure):
1. BOM — every website/*.html begins with EF BB BF (byte-checked)
2. emoji — zero emoji/pictographs in website/*.html (codepoint ranges; NEVER grep —
Windows grep false-negatives multibyte emoji, proven in D0)
3. nav — the <nav>…</nav> and <footer>…</footer> blocks of all pages are identical
after stripping the active-link marker
4. analytics — the umami snippet is present on every public page (nonpublic draft exempt)
5. no-CDN — zero fonts.googleapis.com / fonts.gstatic.com references
6. banned — zero legacy hexes / 999px radius / box-shadow in pages + site.css
7. style — zero embedded <style> blocks in pages (everything lives in site.css)
8. cachebust — every site.css / icons.svg reference carries ?v=<N>
"""
import io, os, re, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
W = os.path.join(ROOT, "website")
PAGES = ["index.html", "kapcsolat.html", "alkalmazasok.html", "technologiak.html",
"biztonsagimentes.html", "gyik.html", "szolgaltatasok-nonpublic.html"]
ANALYTICS_EXEMPT = {"szolgaltatasok-nonpublic.html"}
ANALYTICS_MARK = "https://stats.felhom.eu/script.js"
BANNED = ["fonts.googleapis.com", "fonts.gstatic.com",
"#0d1117", "#161b22", "#1c2128", "#30363d", "#238636", "#da3633", "#d29922",
"border-radius: 999px", "box-shadow"]
fails = []
def fail(msg):
fails.append(msg)
print("FAIL:", msg)
def is_emoji(ch):
o = ord(ch)
return any(a <= o <= b for a, b in [
(0x1F000, 0x1FAFF), # pictographs, emoticons, transport, symbols-ext, cards
(0x2600, 0x27BF), # misc symbols + dingbats (incl. ✓ ✗ ★ ⚠ — sprite icons instead)
(0x2300, 0x23FF), # misc technical (⏱ ⏳ ⌛ …)
(0x2B00, 0x2BFF), # ⭐ etc.
(0xFE00, 0xFE0F), # variation selectors
(0x1F1E6, 0x1F1FF), # regional indicators
])
def norm_block(b):
b = b.replace(' class="active"', "")
b = re.sub(r'\s*aria-current="[^"]*"', "", b)
return b
pages = {}
for p in PAGES:
path = os.path.join(W, p)
raw = io.open(path, "rb").read()
# gate 1: BOM
if raw[:3] != b"\xef\xbb\xbf":
fail("%s: missing UTF-8 BOM (first bytes: %s)" % (p, raw[:3].hex()))
pages[p] = raw.decode("utf-8-sig")
# gate 2: emoji
total_emoji = 0
for p, s in pages.items():
hits = [(i, ch) for i, ch in enumerate(s) if is_emoji(ch)]
if hits:
total_emoji += len(hits)
sample = " ".join(ch for _, ch in hits[:10])
fail("%s: %d emoji (%s ...)" % (p, len(hits), sample))
if total_emoji:
print(" emoji total: %d" % total_emoji)
# gate 3: nav + footer consistency
ref_nav = ref_footer = None
for p, s in pages.items():
nm = re.search(r"<nav>.*?</nav>", s, re.DOTALL)
fm = re.search(r"<footer>.*?</footer>", s, re.DOTALL)
if not nm or not fm:
fail("%s: missing <nav> or <footer>" % p)
continue
nav, foot = norm_block(nm.group(0)), norm_block(fm.group(0))
if ref_nav is None:
ref_nav, ref_footer, ref_page = nav, foot, p
else:
if nav != ref_nav:
fail("%s: <nav> differs from %s (after active-marker normalization)" % (p, ref_page))
if foot != ref_footer:
fail("%s: <footer> differs from %s" % (p, ref_page))
# gate 4: analytics presence
for p, s in pages.items():
if p in ANALYTICS_EXEMPT:
continue
if ANALYTICS_MARK not in s:
fail("%s: analytics snippet missing (%s)" % (p, ANALYTICS_MARK))
# gate 5+6: banned strings in pages + site.css
targets = dict(pages)
css_path = os.path.join(W, "assets", "site.css")
if os.path.exists(css_path):
targets["assets/site.css"] = io.open(css_path, encoding="utf-8").read()
for name, s in targets.items():
low = s.lower()
for pat in BANNED:
c = low.count(pat.lower())
if c:
fail("%s: banned %r ×%d" % (name, pat, c))
# gate 7: no embedded style blocks
for p, s in pages.items():
c = len(re.findall(r"<style[\s>]", s))
if c:
fail("%s: %d embedded <style> block(s)" % (p, c))
# gate 8: cache-busting on shared assets
for p, s in pages.items():
for m in re.finditer(r"/assets/(site\.css|icons\.svg)([^\"'#\s>]*)", s):
if not m.group(2).startswith("?v="):
fail("%s: %s referenced without ?v= (got %r)" % (p, m.group(1), m.group(0)))
if fails:
print("\nSITE GATES FAILED: %d problem(s)" % len(fails))
sys.exit(1)
print("site gates OK — BOM, emoji=0, nav/footer consistent, analytics present, no CDN, no legacy tokens, no <style>, cache-busted assets")