mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-16 14:42:29 -05:00
The release pill assigned a data-link attribute straight to a.href, which CodeQL flags as DOM XSS (js/xss-through-dom). Add a scheme allowlist so only anchor, relative, and http(s) links pass; anything else such as javascript: or data: falls back to #release.
134 lines
4.8 KiB
HTML
134 lines
4.8 KiB
HTML
<!--
|
|
MLSysBook release-pill — shared footer version chip for Quarto + static sites.
|
|
|
|
Renders a small "v0.1.0 · Apr 26, 2026" chip from a deployed
|
|
release-manifest.json. Best-effort chrome: silent on any fetch failure
|
|
(manifest missing, offline, CORS). Use at any site that emits a manifest
|
|
via scripts/version/release.py emit-manifest.
|
|
|
|
── Standardized placement (all MLSysBook sites) ──
|
|
|
|
The chip lives in the page-footer, right slot, so it reads as an
|
|
intentional part of the footer bar (not dangling below the page). Two
|
|
pieces per site:
|
|
|
|
1. This file loads the style + populator script site-wide:
|
|
|
|
format:
|
|
html:
|
|
include-in-header:
|
|
- text: |
|
|
<meta name="release-manifest" content="/<project-base>/release-manifest.json">
|
|
include-after-body:
|
|
- file: ../shared/release/release-pill.html
|
|
|
|
2. Each page-footer places the chip span where it should render:
|
|
|
|
website:
|
|
page-footer:
|
|
right:
|
|
- text: |
|
|
<span class="release-pill" data-release-pill></span>
|
|
- icon: github
|
|
href: ...
|
|
|
|
The meta tag tells the script where to find the manifest. The script
|
|
populates every span carrying data-release-pill, so the chip can sit in
|
|
the footer (canonical), an about page, or anywhere else.
|
|
|
|
── Manifest contract ──
|
|
|
|
The manifest is the JSON written by scripts/version/release.py
|
|
emit-manifest. Required keys: releaseId, releaseHash. Optional but
|
|
recommended: buildDate, project. Anything else is ignored.
|
|
|
|
── Why runtime fetch instead of build-time templating ──
|
|
|
|
Build-time templating would be deterministic but requires per-project
|
|
Lua/template machinery. Runtime fetch is project-agnostic and works
|
|
identically across Quarto, Next.js, and plain HTML. The one fetch per
|
|
page-load is cheap and the chip degrades to nothing on failure.
|
|
-->
|
|
|
|
<style>
|
|
/* Fixed muted gray on every site — the chip must look identical regardless of
|
|
each Quarto theme's accent colors (do NOT inherit --bs-secondary / link color). */
|
|
.release-pill {
|
|
display: inline-flex;
|
|
align-items: baseline;
|
|
gap: 0.25em;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
font-size: 0.75rem;
|
|
line-height: 1;
|
|
color: var(--release-pill-color, #6c757d);
|
|
}
|
|
.release-pill a,
|
|
.release-pill a:visited,
|
|
.release-pill-version,
|
|
.release-pill-date {
|
|
color: var(--release-pill-color, #6c757d);
|
|
text-decoration: none;
|
|
}
|
|
.release-pill a:hover {
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.release-pill-date {
|
|
opacity: 0.75;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
var pills = document.querySelectorAll('[data-release-pill]');
|
|
if (!pills.length) return;
|
|
|
|
// Manifest URL resolution order:
|
|
// 1. data-manifest attribute on the pill (per-page override)
|
|
// 2. <meta name="release-manifest" content="..."> in <head>
|
|
// 3. './release-manifest.json' relative to current page (fallback)
|
|
var meta = document.querySelector('meta[name="release-manifest"]');
|
|
var metaPath = meta && meta.getAttribute('content');
|
|
|
|
fetch(metaPath || './release-manifest.json', { cache: 'no-store' })
|
|
.then(function (res) { return res.ok ? res.json() : null; })
|
|
.then(function (m) {
|
|
if (!m || !m.releaseId || !m.releaseHash) return;
|
|
var date = m.buildDate
|
|
? new Date(m.buildDate).toLocaleDateString('en-US',
|
|
{ year: 'numeric', month: 'short', day: 'numeric' })
|
|
: '';
|
|
var shortHash = String(m.releaseHash).slice(0, 7);
|
|
var title =
|
|
(m.project ? m.project + ' ' : '') +
|
|
'v' + m.releaseId +
|
|
(date ? ' · built ' + date : '') +
|
|
' · hash ' + shortHash;
|
|
|
|
pills.forEach(function (el) {
|
|
var rawLink = el.getAttribute('data-link') || '#release';
|
|
// XSS hardening: allow only anchor, relative, or http(s) links; reject javascript:/data: URLs.
|
|
var linkHref = /^(?:#|\/|\.{1,2}\/|https?:\/\/)/i.test(rawLink) ? rawLink : '#release';
|
|
var a = document.createElement('a');
|
|
a.href = linkHref;
|
|
a.title = title;
|
|
a.setAttribute('aria-label',
|
|
'Release v' + m.releaseId + (date ? ', built ' + date : ''));
|
|
var v = document.createElement('span');
|
|
v.className = 'release-pill-version';
|
|
v.textContent = 'v' + m.releaseId;
|
|
a.appendChild(v);
|
|
if (date) {
|
|
var d = document.createElement('span');
|
|
d.className = 'release-pill-date';
|
|
d.textContent = ' · ' + date;
|
|
a.appendChild(d);
|
|
}
|
|
el.innerHTML = '';
|
|
el.appendChild(a);
|
|
});
|
|
})
|
|
.catch(function () { /* silent: best-effort chrome */ });
|
|
})();
|
|
</script>
|