← back to Animals
[morning-review] animals: drop trailing footer separator + bundle iter4 template safety (lang/title/viewport, font-name encode, XSS escape)
82583ba367be35abdad45d044d21793d31331424 · 2026-05-04 12:50:32 -0700 · SteveStudio2
Files touched
M src/audit/design_system.js
Diff
commit 82583ba367be35abdad45d044d21793d31331424
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 4 12:50:32 2026 -0700
[morning-review] animals: drop trailing footer separator + bundle iter4 template safety (lang/title/viewport, font-name encode, XSS escape)
---
src/audit/design_system.js | 94 +++++++++++++++++++++++++++++++++-------------
1 file changed, 68 insertions(+), 26 deletions(-)
diff --git a/src/audit/design_system.js b/src/audit/design_system.js
index 522eb71..1d1a2d6 100644
--- a/src/audit/design_system.js
+++ b/src/audit/design_system.js
@@ -23,6 +23,17 @@
import crypto from 'node:crypto';
+// ── HTML escape — wrap every interpolation that ends up in HTML text/attrs
+export const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({
+ '&':'&','<':'<','>':'>','"':'"',"'":''',
+}[c]));
+
+// Allow only printable ASCII font-name characters before injecting into CSS.
+// Curated PAIRS today are all ASCII alphanumeric+space, but this guards
+// against future entries with quotes/semicolons that would break :root rules.
+const FONT_NAME_RE = /^[A-Za-z0-9 ]+$/;
+const safeFontName = (name, fallback) => FONT_NAME_RE.test(name) ? name : fallback;
+
// ── seeded RNG so output is deterministic per (biz_id, variant) ───────────
function makeRng(seedStr) {
let h = 2166136261;
@@ -170,16 +181,27 @@ export function renderHtml(spec, biz) {
// ── shared shell / helpers ─────────────────────────────────────────────────
function shell(biz, p, t, c, body) {
- const fonts = (`${t.heading.replace(/ /g,'+')}:wght@400;700;900&family=${t.body.replace(/ /g,'+')}:wght@400;500;600;700`);
- return `<!doctype html><html><head><meta charset=utf-8>
+ // Encode each family name so `&`, `=`, `:`, `@`, `;` stay as URL structure
+ // (and any future font name with whitespace/special chars round-trips).
+ const heading = safeFontName(t.heading, 'Inter');
+ const bodyFont = safeFontName(t.body, 'Inter');
+ const fontUrl =
+ `https://fonts.googleapis.com/css2` +
+ `?family=${encodeURIComponent(heading)}:wght@400;700;900` +
+ `&family=${encodeURIComponent(bodyFont)}:wght@400;500;600;700` +
+ `&display=swap`;
+ const pageTitle = body.title ?? `${biz.name || 'Your Business'}${biz.city ? ' — ' + biz.city : ''}`;
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>${esc(pageTitle)}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${fonts}&display=swap">
+<link rel="stylesheet" href="${fontUrl}">
<style>
:root{
--ink:${p.ink}; --paper:${p.paper}; --accent:${p.accent};
--accent2:${p.accent2}; --muted:${p.muted}; --rule:${p.rule};
- --h-font:'${t.heading}', serif; --b-font:'${t.body}', system-ui, sans-serif;
+ --h-font:'${heading}', serif; --b-font:'${bodyFont}', system-ui, sans-serif;
--btn-r:${c.btnRadius}; --card-r:${c.cardRadius}; --card-shadow:${c.cardShadow};
--divider:${c.divider};
}
@@ -196,14 +218,34 @@ ${body.css || ''}
</style></head><body>${body.html}</body></html>`;
}
-const meta = (biz) => ({
- name: biz.name || 'Your Business',
- cat: (biz.category || 'business').replace(/_/g,' '),
- city: biz.city || '',
- state: biz.state || '',
- addr: [biz.address, biz.city, biz.state, biz.zip].filter(Boolean).join(', '),
- phone: biz.phone || '',
-});
+function formatAddr(biz) {
+ const street = (biz.address || '').toString().trim();
+ const city = (biz.city || '').toString().trim();
+ const state = (biz.state || '').toString().trim();
+ const zip = (biz.zip || '').toString().trim();
+ const parts = [];
+ if (street) parts.push(street);
+ const csz = [city, state].filter(Boolean).join(', ');
+ const cszFinal = zip ? (csz ? `${csz} ${zip}` : zip) : csz;
+ if (cszFinal) parts.push(cszFinal);
+ return parts.join(', ');
+}
+
+// All fields are HTML-escaped — every layout interpolates them directly into
+// HTML text content, so escaping here neutralises XSS for the whole template
+// surface in one place.
+const meta = (biz) => {
+ const phone = esc(biz.phone || '');
+ return {
+ name: esc(biz.name || 'Your Business'),
+ cat: esc((biz.category || 'business').replace(/_/g,' ')),
+ city: esc(biz.city || ''),
+ state: esc(biz.state || ''),
+ addr: esc(formatAddr(biz)),
+ phone,
+ phoneSep: phone ? ` · ${phone}` : '',
+ };
+};
// ── 15 layouts ────────────────────────────────────────────────────────────
function layoutHeroSplash(biz, p, t, c) {
@@ -231,7 +273,7 @@ function layoutHeroSplash(biz, p, t, c) {
<div><h3>Surgery & Dental</h3><p>In-house, with same-day discharge for most procedures.</p></div>
<div><h3>Urgent Care</h3><p>Same-day sick visits — call us before going to the ER.</p></div>
</section>
- <footer class=foot>${m.name} · ${m.addr} · ${m.phone}</footer>`
+ <footer class=foot>${m.name} · ${m.addr}${m.phoneSep}</footer>`
});
}
@@ -273,7 +315,7 @@ function layoutEditorialMagazine(biz, p, t, c) {
<div><h3>Surgery & Dental</h3><p>In-house surgical suite with anesthetic monitoring. Same-day discharge for most.</p></div>
<div><h3>Urgent Sick Care</h3><p>Same-day appointments most weekdays. Call before 11 AM, visit before 5.</p></div>
</section>
- <footer><p class=muted>${m.addr} · ${m.phone}</p></footer>
+ <footer><p class=muted>${m.addr}${m.phoneSep}</p></footer>
</div>`
});
}
@@ -307,7 +349,7 @@ function layoutSplit(biz, p, t, c) {
<div class=svc><div><strong>Dental cleaning</strong><br><small>Includes anesthesia + monitoring</small></div><a href=#>From $450</a></div>
<div class=svc><div><strong>Spay/Neuter</strong><br><small>Cats and dogs both</small></div><a href=#>From $225</a></div>
<hr class=divider>
- <p class=muted>${m.addr} · ${m.phone}</p>
+ <p class=muted>${m.addr}${m.phoneSep}</p>
</section>
</main>`
});
@@ -353,7 +395,7 @@ function layoutBookingFirst(biz, p, t, c) {
<div><h3>Real follow-up</h3><p>Email check-in 48h after every visit.</p></div>
</div>
</section>
- <footer style="padding:2em 3em;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -388,7 +430,7 @@ function layoutCommunity(biz, p, t, c) {
<h2>Same-day sick visits, every weekday</h2>
<a href=#>Check today's openings →</a>
</section>
- <footer style="padding:2em 3em;color:var(--muted);text-align:center">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;color:var(--muted);text-align:center">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -428,7 +470,7 @@ function layoutAuthority(biz, p, t, c) {
<div><h3>Surgery</h3><p>Soft-tissue, orthopedic, and neurosurgical procedures. Board-certified surgeons on every case.</p></div>
<div><h3>Oncology</h3><p>Chemotherapy, radiation referrals, palliative-care consults. We call you within 24h of every diagnosis.</p></div>
</section>
- <footer style="padding:2em 4em;background:var(--ink);color:rgba(255,255,255,.7)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 4em;background:var(--ink);color:rgba(255,255,255,.7)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -461,7 +503,7 @@ function layoutConciergeLuxe(biz, p, t, c) {
<div><h3>Concierge medicine</h3><p>Direct cell-phone access to your DVM. No portal. No phone tree.</p></div>
<div><h3>Estate planning</h3><p>Pet-trust referrals, end-of-life planning, in-home euthanasia.</p></div>
</section>
- <footer style="padding:3em;text-align:center;color:var(--muted);font-size:.85em">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:3em;text-align:center;color:var(--muted);font-size:.85em">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -507,7 +549,7 @@ function layoutNewsroom(biz, p, t, c) {
<div><h3>Surgery & Dental</h3><p>In-house surgical suite with continuous anesthetic monitoring.</p></div>
<div><h3>Specialty Referrals</h3><p>Cardiology, dermatology, oncology — we know the right specialists.</p></div>
</section>
- <footer style="padding:2em 0;color:var(--muted)">${m.addr} · ${m.phone}</footer>
+ <footer style="padding:2em 0;color:var(--muted)">${m.addr}${m.phoneSep}</footer>
</article>`
});
}
@@ -559,7 +601,7 @@ function layoutBoutique(biz, p, t, c) {
<div class=bq-card><div class=ico>🦴</div><h3>Dental</h3><p>Cleanings under anesthesia with continuous monitoring.</p></div>
<div class=bq-card><div class=ico>❤️</div><h3>Senior care</h3><p>A dedicated quarterly visit for pets 8+, with bloodwork.</p></div>
</section>
- <footer style="padding:2em 4em;text-align:center;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 4em;text-align:center;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -588,7 +630,7 @@ function layoutPlayful(biz, p, t, c) {
<div class=pl-card><div class=emoji>🐈</div><h3>Cats</h3><p>Cat-only exam room. Calmer everything.</p></div>
<div class=pl-card><div class=emoji>🐰</div><h3>Exotics</h3><p>Rabbits, reptiles, birds, small mammals welcome.</p></div>
</section>
- <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -622,7 +664,7 @@ function layoutServiceTiles(biz, p, t, c) {
<div class=st-tile><span class=num>07</span><h3>Behavior</h3><p>Training referrals, anxiety meds, consults.</p></div>
<div class=st-tile><span class=num>08</span><h3>End of life</h3><p>In-home euthanasia + grief support.</p></div>
</section>
- <footer style="padding:2em 3em;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -655,7 +697,7 @@ function layoutCalendar(biz, p, t, c) {
</div>
<a class=btn href=#book>Continue →</a>
</section>
- <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -688,7 +730,7 @@ function layoutFamily(biz, p, t, c) {
<p>Most clients book within 3 days. Emergency? Call us — we take same-day sick visits Monday-Friday.</p>
<a class=btn-2 href=#book>See this week's openings →</a>
</section>
- <footer style="padding:2em 4em;text-align:center;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 4em;text-align:center;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
@@ -737,6 +779,6 @@ function layoutModernClinic(biz, p, t, c) {
<li>Senior bloodwork <small>$185</small></li>
</ul>
</section>
- <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr} · ${m.phone}</footer>`
+ <footer style="padding:2em 3em;text-align:center;color:var(--muted)">${m.addr}${m.phoneSep}</footer>`
});
}
← 14cfcfd yolo: iter 3 trivial patches from claude-codex debate
·
back to Animals
·
[morning-review] animals: escape biz fields, validate websit b9336a0 →