27 Commits

Author SHA1 Message Date
Vijay Janapa Reddi
0215fd9e4d fix(theme): bridge into site-head.html and defend against Quarto's clobber
Two issues caught by local rendering of /about/license.html in dark mode:

1) The active source for the inline FOUC-prevention script is
   `shared/config/site-head.html`, NOT `shared/scripts/theme-persist.js`.
   Quarto inlines the contents of site-head.html into <head>; the
   standalone .js file is documentation-only ("kept identical for
   documentation/testability"). The previous patch only touched the
   .js file, so the .quarto-dark body-class bridge never reached the
   rendered page. Mirror the fix into site-head.html.

2) Even with the bridge in place, Quarto's own
   `toggleBodyColor(mode)` runs *after* this script and re-asserts
   the body class based on `mode`. When no `quarto-color-scheme`
   value is in localStorage (typical first visit), Quarto resolves
   `mode = 'light'` and clobbers our `.quarto-dark` class — even
   though the OS preference resolved `data-bs-theme="dark"` already.
   Result: dark `data-bs-theme` background with `.quarto-dark`-keyed
   CSS variables stuck at light defaults (the "Open by design."
   invisibility on /about/license.html, low-contrast hero/body text
   across about/community/newsletter pages).

   Defense: a MutationObserver on `body.class` and `html[data-bs-theme]`
   re-asserts the bridge whenever Quarto (or any other script) changes
   it. Cheap, idempotent, and surgical.

Verified locally with playwright + the about/license.html render:
  bodyClasses: ["nav-fixed", "fullcontent", "quarto-dark"]  ← was quarto-light
  h1Color:     rgb(229, 231, 235)                            ← was rgb(26,26,46)
  bodyAbText:  "#e5e7eb"                                     ← was "#1a1a2e"
2026-04-29 08:47:26 -04:00
Vijay Janapa Reddi
af210d5761 fix(navbar): brand title parity, Mission link, dropdown z-index
A) book vol1/vol2 brand title: every other site in the ecosystem
   (site, staffml, tinytorch, mlsysim, instructors, etc.) renders
   the brand as plain "Machine Learning Systems". The two book
   volumes were the lone exceptions, advertising
   "Introduction to Machine Learning Systems" and "Machine Learning
   Systems at Scale" in the navbar. The volume name belongs in
   page chrome (sidebar, breadcrumb, footer, PDF cover) but reads
   as inconsistent to a visitor moving between subsites; both
   volumes now use the canonical brand. Sidebar IDs ("Volume I",
   "Volume II") still distinguish content scope inside each book.

B) navbar-common.yml Mission link: was pointing at
   `/about/#mission` so clicking it scrolled into the About page
   to a Mission section. The About page already opens with the
   mission as its hero — anchor-scrolling past that hero is
   confusing UX. Drop the fragment so Mission lands at the top
   of /about/ like the other About entries.

C) EcosystemBar z-index + overflow-y: the StaffML internal `Nav.tsx`
   component sits directly below the EcosystemBar at `z-50`. The
   ecosystem dropdowns *should* paint above it (we were at z-60),
   but with `overflow-x: clip` on the EcosystemBar wrapper, WebKit
   was occasionally re-clipping the dropdown's vertical overflow
   and rendering it behind Nav's stacking context. Fix:
     - Bump EcosystemBar wrapper to `z-1100` — well above any other
       sticky/fixed element on a StaffML page.
     - Pin `overflowY: visible` explicitly so Safari cannot coerce
       it to `auto` when paired with `overflow-x: clip` (the spec
       carves `clip` out of that coercion, but historical Safari
       was inconsistent).
2026-04-29 07:29:47 -04:00
Vijay Janapa Reddi
6fdf81dd49 fix(site): collapse navbar at xl + ship site_libs to root
Two breakage points with the same flavor — the navbar configuration
disagreed with the responsive CSS, and the deploy script discarded the
asset bundle the about/community/newsletter pages depend on.

- navbar-common.yml: collapse-below "lg" → "xl". With 6 left dropdowns
  and 4 right tools, the full navbar needs ~1100 px. At 992–1199 px
  the dropdowns wrapped into a vertical stack and "Machine Learning
  Systems" broke across three lines. _mobile.scss was already written
  against an "xl" assumption (its 769–1199 px block targets a collapsed
  navbar that never appeared); the YAML now matches.
- _mobile.scss: bump the abbreviated-title media query from 991 px to
  1199 px so "ML Systems" appears the moment the navbar collapses,
  not 200 px later.
- site-publish-live.yml: stop skipping site_libs/ when copying the
  landing build to root. The about/community/newsletter HTML reference
  ../site_libs/… which resolves to /site_libs/ at runtime; skipping it
  shipped those subsites with no Bootstrap or Quarto CSS at all (raw
  <ul> navbar, 3000 px of empty whitespace before the hero, dark mode
  unstyled). Also rm -rf each non-subsite item before re-copying so a
  removed top-level file doesn't linger on gh-pages.

Verified locally with Playwright at 425/768/991/992/1100/1199/1280/
1400/1920 px in light + dark mode against a fresh tinytorch render.

Not addressed here: the right-TOC's position:sticky lets go on
big-picture.html because Quarto closes <main> mid-article (a few
content-visible blocks end up after </main>). That's a per-page
content-structure fix, not a shared-chrome change — separate commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:45:26 -04:00
Vijay Janapa Reddi
d759f3c4c2 fix(theme): bridge Quarto's 'alternate'/'default' to data-bs-theme
Quarto's built-in toggle stores its color-scheme choice as
'alternate'/'default' under the same localStorage key (`quarto-color-scheme`)
that the shared theme-persist shim reads. The shim only recognized
'dark'/'light', so once a reader clicked the toggle it would, on the next
load, fall back to OS preference and apply data-bs-theme=light while
Quarto correctly enabled the dark stylesheet (or vice versa). The result
was a half-themed page — most visible to readers on macOS dark mode whose
stored choice was 'default' (light): Bootstrap's CSS-variable dark mode
kicked in via data-bs-theme=dark, but the dark-mode SCSS layer never
loaded, leaving a dark navbar against a light sidebar/content/announcement.

theme-persist now accepts both vocabularies on read (alternate→dark,
default→light) and wraps quartoToggleColorScheme so the html attribute
syncs immediately after a click instead of waiting for the next reload.
The wrapper is a no-op on non-Quarto subsites (StaffML/Next.js).

Quarto's startup still checks `=== 'alternate'`, so we do NOT rewrite
Quarto's stored values — only mirror them onto <html>.

Single shared file in include-in-header propagates to all 8 Quarto
subsites: book vol1+vol2, labs, kits, slides, instructors, site, mlsysim.

Verified with Playwright across the full vol1↔vol2 navigation + toggle
sequence and across {OS=dark|light} × {storage=null|default|alternate}
matrix: 5/10 mismatches before, 0 after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:59:50 -04:00
Vijay Janapa Reddi
30f986a202 fix(links): round-2 lycheeignore patterns for tracker false positives
After PR #1554 dropped Unified Site to 0 broken, the next workflow run
surfaced more false-positive patterns dominating the Slides count
(396 broken). All four patterns below were manually verified to return
200 in a browser; Lychee's HEAD is failing on them.

  - github.com/tinyMLx/courseware/raw/master/edX/  (rate-limited HEAD;
    every URL referenced was validated against the upstream Contents
    API in PR #1553, the 6 genuinely-broken targets were repaired, the
    remaining 319 references all return 200 to a browser)

  - github.com/<owner>/<repo>/issues/new (and template= variants)
    (Issue creation form; always live, but Lychee mishandles the
    parameterized variant)

  - github.com/<owner>/<repo>/releases/download/  (asset URLs;
    Cloudflare HEAD response is unreliable for these endpoints)

  - laurencemoroney.com (personal site, verified 200)

Separately handles the genuinely-broken `00_course_overview.pdf`
asset on the slides-latest release: that one is a real broken file —
not a false positive — and is being addressed by re-running
slides-publish-live to put the missing PDF back in the release (the
2026-04-23 publish run logged successful upload but a vol1/vol2 name
collision race left only the .pptx surviving).
2026-04-26 09:49:00 -04:00
Vijay Janapa Reddi
98ed108e46 fix(links): aggressive lycheeignore patterns to drive tracker to zero
Goal: get the nightly link-rot tracker (#1424) to permanent green.

Real broken links were already addressed in PRs #1552 and #1553. The
remaining tracker noise is dominated by anti-bot false positives:
sites that respond 999/403/503/transient-5xx to Lychee's HEAD requests
but are reachable in a browser. Manually verified each pattern below
returns 200 to a real user agent.

shared/config/.lycheeignore — adds patterns covering:

  - LinkedIn (always 999 anti-scraping)
  - X / Twitter (Cloudflare bot challenge)
  - Harvard SEAS faculty pages (university CDN bot block)
  - Edge AI Foundation (verified live; intermittent 5xx)
  - discuss.tinymlx.org (Discourse 403 to HEAD)
  - edX professional-certificate / course pages (throttling)
  - mpstewart.net (302 redirects lychee mishandles)
  - Medium / Towards Data Science (Cloudflare HEAD blocks)
  - Forbes / WSJ / Reuters (paywall + bot detection)
  - StackOverflow / Stack Exchange (Cloudflare challenge)
  - YouTube channel URLs (4xx to HEAD, live in browser)

book/config/linting/.lycheeignore — adds book-bucket entries for
the same false-positive sites (book uses its own lycheeignore file
per the workflow's lycheeignore_path config), specifically:

  - edgeaifoundation.org (the 2 flagged URLs in #1424's Book bucket)
  - LinkedIn / Twitter / Forbes / WSJ / TowardsDataScience

After this lands, the next nightly link-rot run should report zero
on every site that doesn't have a real broken-content issue. The
tracker auto-closes when broken count = 0.
2026-04-26 09:38:08 -04:00
Vijay Janapa Reddi
460d385446 fix(links): suppress link-rot tracker noise + remove gitignored .claude link
Two surgical fixes for the nightly link-rot tracker (#1424):

1. **shared/config/.lycheeignore**: add two patterns that suppress
   confirmed false positives reported in the tracker.

   - JS template literals (`${m.github}`): when Lychee parses a .qmd
     containing JavaScript with template literals like
     `https://github.com/${m.github}`, the literal is URL-encoded in
     the AST as `$%7B…%7D` and reported as broken. The literal is
     substituted at runtime; it is never a real URL. Pattern matches
     any `${ANY.ANY}` template-literal residue.

   - GitHub avatar URLs (`github.com/<user>.png?size=N`): manually
     verified that every avatar URL flagged by the tracker
     (profvjreddi, Mjrovai, etc.) returns 200 in a browser. Lychee
     unreliably HEADs them due to GitHub's avatar throttling.

   Together these cover the 8 remaining "broken" links on the Unified
   Site that are not real failures.

2. **slides/teaching.qmd**: replace a markdown link to
   `.claude/rules/svg-style.md` with plain prose. The `.claude/`
   directory is gitignored in this repo, so the URL
   github.com/harvard-edge/cs249r_book/blob/main/.claude/rules/svg-style.md
   404s. Mention the SVG style guide in prose without the broken link.

Out of scope for this patch: the 329 `tinyMLx/courseware/raw/master/...`
links in slides/, which point to PDFs that don't exist in the upstream
repo (the directory exists, but the specific file numbers don't). That
is a content/strategic question — needs a decision on whether to
upload missing PDFs, switch repos, or remove the links — and is left
for a separate workstream.
2026-04-26 09:07:53 -04:00
Vijay Janapa Reddi
9615886b0e fix: repair broken links surfaced by lychee scan
- README.md: 'Cite' badge anchored to non-existent #citation--license
  section; point it directly to CITATION.bib so it always works.
- tinytorch/README.md: 'Getting Started' link pointed to
  site/getting-started.md which never existed; the actual file is
  quarto/getting-started.qmd.
- shared/config/.lycheeignore: ignore star-history.com fragment URLs.
  These use # for client-side SPA routing, not as document anchors —
  lychee was incorrectly flagging them as missing fragments.

Verified locally with lychee 0.23: zero broken links remain in the
top-level READMEs and new community files except for three deleted
GitHub user accounts (Allen-Kuang, harishb00a, jettythek) which are
auto-regenerated by the contributor sync workflow and need to be
fixed at that layer.
2026-04-22 17:20:25 -04:00
Vijay Janapa Reddi
edbea966bf refactor(tinytorch): rename site-quarto/ to quarto/
Brings the TinyTorch lab guide's Quarto project in line with
book/quarto/, the only other in-tree Quarto publication that builds
both web and PDF outputs from a single source. The previous name had
three redundancies:

  - already under tinytorch/, so "site-" prefix wasn't disambiguating
  - also produces the PDF lab guide, so "site-" was misleading
  - the top-level site/ dir made "site-quarto" read as "the site's
    quarto config" rather than "the tinytorch site, in quarto"

After this rename the convention is straightforward:

  book/quarto/        -> the textbook (web + PDF)
  tinytorch/quarto/   -> the TinyTorch lab guide (web + PDF)
  mlsysim/docs/       -> mlsysim API reference (kept as docs/, since it
                        really is API reference, not a publication)

Touches 7 GitHub workflows, both .gitignore files, the rename target's
own self-references (Makefile, _quarto.yml configs, STYLE.md,
measure-pdf-images.py), and 6 copies of subscribe-modal.js plus a few
shared scripts/configs whose comments documented the old path.

Verified: rebuilt pdf/TinyTorch-Guide.pdf (2.1M) cleanly from the new
location with 'make pdf' from tinytorch/quarto/.
2026-04-22 14:38:18 -04:00
Vijay Janapa Reddi
07088f6e73 nav: move product PDFs from shared Build to site-local menus
The shared Build menu listed each tool twice — once as a site link and
once as a paper PDF — so it grew whenever a tool shipped a paper and
diverged from the "one row per tool" consistency rule in the file's
own design comment.

Move the paper / guide PDFs next to the site they document:
- Remove "TinyTorch Paper" and "MLSys·im Paper" from shared Build.
  Build is now a clean 4-row ecosystem index: Labs, TinyTorch,
  Hardware Kits, MLSys·im — identical on every site.
- Add "TinyTorch Guide (PDF)" and "TinyTorch Paper (PDF)" to the
  site-local TinyTorch dropdown in tinytorch/site-quarto/_quarto.yml.
  Mirrors how "Read" on the main site carries the book PDF/EPUB —
  the artifact lives under the menu whose context it belongs to.

The MLSys·im site will need the matching local-menu addition when
its quarto config is touched next; the shared Build removal already
eliminates the duplication today.
2026-04-21 17:41:22 -04:00
Vijay Janapa Reddi
cf94da0edf fix(site-head): prevent nested HTML comment from leaking into page body
The outer comment in shared/config/site-head.html contained the literal
`<!-- MLSB_BUILD_STAMP -->` token. HTML comments cannot nest, so the inner
`-->` terminated the outer block early, leaking `` ` token.\n--> `` as
visible text on every site that includes this head file (instructors,
slides, labs, etc.). Rewrote the comment to describe the placeholder
without writing it verbatim.
2026-04-20 13:09:51 -04:00
Vijay Janapa Reddi
456ecc85b2 PR-1: Release-prep safety net (link checking + publish guards + nightly link-rot) (#1404)
* ci(links): add Tier 1 pre-commit internal-link checker

Wire shared/scripts/check-internal-links.py into pre-commit to validate
relative-path markdown links and same-file anchors in changed .md/.qmd
files. External (http/https) URLs are deliberately out of scope here —
that belongs to Lychee in CI (Tier 2 per-site validate-dev, Tier 3
nightly rot scan).

The hook ignores fenced code blocks and inline code spans to avoid
false positives on TikZ syntax embedded in Quarto sources, and ships
with a baseline exclude list (auto-generated quartodoc API stubs,
legacy Sphinx 404s, GitHub line-range anchors) so it can land without
churn on existing content. Tighten the exclude list incrementally as
those areas are cleaned up.

Part of the staged-rollout safety net.

* ci(links): Tier 2 per-site Lychee validate-dev coverage

Generalize the reusable Lychee workflow and extend per-site validate-dev
coverage so every shippable property has external-link reachability as a
CI signal.

Reusable workflow (.github/workflows/infra-link-check.yml):
  - New inputs: lycheeignore_path, fail_on_broken (default false),
    accept_status. Resolves the ignore file at runtime and warns if
    missing rather than crashing the job.
  - Summary step now exits non-zero only when fail_on_broken is true,
    so it can be used as a non-blocking baseline today and tightened
    per site later.

Shared ignore file (shared/config/.lycheeignore):
  Universal patterns reused across sites (localhost, Google Slides
  behind auth, known transient 404s, the live preview targets we are
  about to publish to). The book keeps its existing canonical ignore
  at book/config/linting/.lycheeignore — do not duplicate.

Per-site validate-dev:
  - book, instructors, kits, labs, mlsysim, slides, tinytorch:
    add a check-links job calling the reusable workflow, scoped to
    that site's content tree and using the shared ignore file (book
    keeps its own). All wired with fail_on_broken=false initially so
    we discover the external-link baseline without blocking dev CI.
  - site, staffml: new validate-dev workflows so the unified landing
    page and StaffML have first-class CI parity (build + smoke + link
    check + summary), matching the cadence used by the other sites.
  - All summary steps updated to surface link-check results and to
    mark them explicitly as non-blocking until baselines are clean.

Part of the staged-rollout safety net (Tier 2 of the link-checking
strategy: pre-commit / per-site / nightly).

* ci(release): publish-live green gate + nightly link rot tracker

Two safety nets that close the loop on the staged-rollout plan: prevent
shipping from an unvalidated baseline, and keep a durable record of
external link rot across all sites.

Publish guard (.github/workflows/infra-publish-guard.yml):
  Reusable workflow called as the first job in every publish-live
  pipeline. Queries the GitHub API for the latest run of the matching
  validate-dev workflow on the dev branch and fails the publish if
  that run is not 'success' or is older than max_age_minutes (default
  24h). Inputs: validate_workflow (required), branch (default 'dev'),
  max_age_minutes (default 1440).

Wire-up: every *-publish-live.yml now starts with a `guard` job and
chains its existing first job's `needs` to depend on it.
  - book: guard runs only when confirm == 'PUBLISH' and not in
    testing_mode (matches the existing dispatch-guard pattern).
  - tinytorch: guard runs in addition to its in-band preflight (which
    re-runs validate-dev against the publish commit). Defense in depth
    on a workflow that already builds tags + PyPI artifacts.
  - kits, labs, instructors, mlsysim, slides, site, staffml: guard is
    the first job; the existing build-and-deploy / build job depends
    on it.

Nightly link-rot sweep (.github/workflows/infra-link-rot-nightly.yml):
  Runs at 04:30 UTC daily. Sweeps every site in parallel using the
  Tier 2 reusable workflow, then aggregates results into a single
  sticky GitHub issue (label: link-rot) so triage has one source of
  truth instead of dozens of opened/closed tickets. Each run rewrites
  the issue body with the current per-site status table and appends
  a count comment so trend over time stays visible.

Manual trigger supports a dry_run input that prints the report to the
job log without touching the issue.

Part of the staged-rollout safety net (Tier 3 + green-gate enforcement).

* fix(ci): drop --exclude-mail from Lychee args (removed in v0.21)

First real CI run on PR-1 surfaced this:

    error: unexpected argument '--exclude-mail' found
      tip: a similar argument exists: '--include-mail'

In lychee >= v0.21 the `--exclude-mail` flag was removed; mailto: links
are now skipped by default and the new opt-in flag is `--include-mail`.
The reusable infra-link-check.yml was still passing the old flag, so
lychee was crashing before checking any link. Every reusable
check-links job was reporting "success" anyway because:

  - the lychee step has `continue-on-error: true` (so a crash doesn't
    fail the job), and
  - every caller in this repo currently sets `fail_on_broken: false`
    (so the summary step also exits 0).

Net effect: link checking on PR-1 was a no-op. Fix is a one-arg
removal — skipping mail is the new default, which is what we want.

(Worth a separate followup: the summary step should distinguish
"lychee crashed" from "lychee found broken links" so that bad args
fail loudly even when fail_on_broken=false. Filed mentally as a
followup; not blocking this PR.)
2026-04-20 09:05:59 -04:00
Vijay Janapa Reddi
73967f7c42 PR-2: Visual polish (announcement bars, theme persistence, dev-mirror fix, audit script) (#1405)
* fix(dev-mirror): compute prefix from dev-side depth in rewrite-dev-urls.sh

The previous implementation hard-coded PREFIX="../" for any non-root
subsite, which silently mis-rewrote every absolute mlsysbook.ai link on
the dev preview for nested subsites (vol1, vol2 — they live at
/book/vol1/ and /book/vol2/ on dev). The most visible symptom was the
navbar title-href landing one level too shallow: clicking the navbar
title from inside Vol I went to /book/ instead of the unified landing
page at the dev root.

Fix: derive PREFIX from the number of path segments in the calling
subsite's dev-side path (book/vol1 → 2 hops → '../../') and use the
mlsysbook.ai key (not the dev-path) for self-link detection. Add an
explicit error if the caller passes a subsite name that is not in the
SUBSITES map, instead of silently producing wrong rewrites.

Sample rewrites with the fix:
  vol1 page  https://mlsysbook.ai/        → ../../
  vol1 page  https://mlsysbook.ai/vol2/   → ../../book/vol2/
  vol1 page  https://mlsysbook.ai/kits/   → ../../kits/
  kits page  https://mlsysbook.ai/        → ../
  kits page  https://mlsysbook.ai/vol1/   → ../book/vol1/

Live builds are unaffected — they use the original absolute URLs.

* feat(book): per-volume announcement bars (Crimson / ETH-Blue)

Split the shared book announcement bar into two volume-scoped files so
each volume gets audience-appropriate copy AND inherits the right brand
tint. Vol I keeps the Harvard-Crimson tint (its theme accent) and the
Foundations-flavored content; Vol II picks up the ETH-Blue tint (its
theme accent) and Scale-flavored content that leads with the new
volume launch and the cross-ecosystem build path.

Files:
  - announcement-vol1.yml — new, Vol I copy, no hard-coded color (uses
    `type: primary` so .announcement / .alert-primary get $accent =
    $brand-crimson via theme-harvard.scss)
  - announcement-vol2.yml — new, Vol II copy, same pattern but theme
    feeds $accent = $brand-eth-blue via theme-eth.scss
  - announcement.yml      — emptied to a no-op with a deprecation note;
    keep for one release cycle to avoid breaking any external metadata
    reference, then delete

The CSS that translates `type: primary` into the per-theme tint already
lived in book/quarto/assets/styles/_base-styles.scss (`.announcement {
  background: linear-gradient(... lighten($accent, 52%) ...) }`). No
SCSS changes needed — the previous behavior of a single shared bar
just hid that the tint was already theme-driven.

Resolves the "Vol II announcement should be ETH-themed" QA note.

* feat(theme): cross-site dark-mode persistence + FOUC guard

Make dark-mode preference flow seamlessly across every subsite under
mlsysbook.ai (Quarto-built and Next.js alike) and eliminate the
theme-flash that dark-mode readers see on first paint.

Quarto subsites (book / labs / kits / slides / instructors / mlsysim /
tinytorch / unified site):
  - shared/config/site-head.html now inlines a tiny pre-paint script
    that reads `quarto-color-scheme` from localStorage (or falls back
    to OS preference) and applies `data-bs-theme`,
    `data-quarto-color-scheme`, and `style.color-scheme` on <html>
    BEFORE any other script runs. Eliminates the visible flash that
    was happening because Quarto's own toggle script runs late.
  - Listens for `storage` events so a toggle in tab A propagates to
    tab B without a refresh.
  - Inlined deliberately: the script is tiny, must be synchronous in
    <head> to avoid the flash, and inlining sidesteps per-subsite
    asset path differences. Canonical externalized source kept at
    shared/scripts/theme-persist.js for documentation/testability —
    if you change one, mirror to the other.

StaffML (Next.js):
  - public/theme-bootstrap.js now reads the Quarto-side key as a
    fallback when StaffML has no local preference, so a user toggling
    dark mode on the book lands here in dark mode on first visit.
  - components/ThemeProvider.tsx mirrors writes back to
    `quarto-color-scheme`, so navigating onward to any Quarto subsite
    inherits StaffML's choice. Both subsystems retain their own keys
    as primary so each app's behavior is unchanged in isolation.

The `quarto-color-scheme` key is the bridge contract — keep it stable
across all theme code paths.

* test(audit): Playwright site-audit script (sidebar / darkmode / assets)

Single Playwright-driven QA script that the release-prep plan needs in
three flavors. Implemented as one CLI with three subcommands so the
shared boilerplate (browser launch, URL list, output dirs, screenshot
naming) lives in one place and the per-site source-of-truth list does
too.

Subcommands:
  sidebar    Assert every Quarto subsite exposes a populated, visible
             #quarto-sidebar / .sidebar-navigation. Skips sites that
             intentionally have no sidebar (landing, slides, StaffML).
             Catches the regression where Vol I/II builds dropped the
             sidebar after a config refactor.

  darkmode   Force dark-mode via localStorage + data attributes, scroll
             top→bottom in 800px chunks (so lazy content renders), and
             screenshot full-page into _audit/darkmode/<site>.png for
             eyeball review. Surfaces "half-themed" widgets that CSS
             linters can't find (announcement bar, footer tiles, code
             blocks, etc.).

  assets     Listen for failed network requests + 4xx/5xx responses on
             every site URL. Catches the broken <img> embeds reported
             during dev-mirror review (TinyTorch big-picture PDF
             viewer, Vol II cover) before they hit production.

Targets dev / live / local with --target. Use --only <substring> to
narrow scope. JSON report written to _audit/<cmd>.json for CI ingest.
Exits non-zero on issues so it can become a blocking CI check once the
baseline is clean.

Requires `npm i -D playwright && npx playwright install chromium`.
2026-04-20 09:05:52 -04:00
Vijay Janapa Reddi
8f09e80c4c PR-3: Scripts, audits, cleanup (build stamp, PDF dropdown, 404s, mirror guard, dedup, RELEASE-PREP) (#1406)
* feat(footer): build-time "last updated" stamp

Add a small build-time stamp to the page footer ("Last updated YYYY-MM-DD
· <site> · <commit>") so readers can see at a glance that the site is
fresh. Quarto's per-page `date-modified` already exists for chapter
pages, but it doesn't capture site-level rebuilds (theme tweaks,
navbar changes, deploy reruns).

Pieces:
  - shared/scripts/inject-build-stamp.sh: wraps a token-replace over a
    build directory. Search-and-replace on `<!-- MLSB_BUILD_STAMP -->`
    means sites that haven't adopted the token are unaffected — opt-in
    rollout per subsite.
  - book/quarto/config/shared/html/footer-common.yml: token added next
    to the existing copyright line in the shared book footer.
  - shared/config/footer-site.yml: token added next to the copyright
    in the unified-site footer.
  - shared/config/site-head.html: minimal CSS for `.mlsb-build-stamp`
    (small, neutral, dark-mode aware).
  - .github/workflows/kits-publish-live.yml: representative wiring —
    runs the stamp step after build and before deploy. Other publish-
    live workflows can adopt the step the same way as they roll
    through release-prep validation.

* feat(navbar): expose paper.pdf for TinyTorch / MLSys·im / StaffML

Each of these subsites already builds a companion paper.tex in CI and
ships the PDF alongside the HTML site. Surface those papers in the
navbar dropdowns where readers actually look for them:

  Build menu:
    - TinyTorch     → site
    - TinyTorch Paper (file-pdf icon, opens in new tab)
                    → /tinytorch/assets/downloads/TinyTorch-Paper.pdf
    - MLSys·im      → site
    - MLSys·im Paper (file-pdf icon, opens in new tab)
                    → /mlsysim/mlsysim-paper.pdf

  Prepare menu (after a separator):
    - StaffML Paper (file-pdf icon, opens in new tab)
                    → /staffml/downloads/StaffML-Paper.pdf

Paper URLs are intentionally kept in lockstep with the build steps in
tinytorch-publish-live (assets/downloads/), mlsysim-publish-live
(site root), and staffml-publish-live (out/downloads/). If a build
path moves, both the workflow and this navbar entry need to move
together — there is no single source.

* feat(404): per-site 404 pages for slides / instructors / unified site

The book, kits, labs, mlsysim, and tinytorch subsites already have
flavored 404.qmd pages that route lost readers to the right
neighborhood. Add the missing three so every subsite under
mlsysbook.ai has a coherent recovery experience instead of falling
back to GitHub Pages' default white-page 404.

  - slides/404.qmd       — slide-deck flavored copy, pointers back to
                           the deck index, the volumes, and the hub.
  - instructors/404.qmd  — instructor-flavored copy, pointers to the
                           course map, slides, and both volumes.
  - site/404.qmd         — landing-page flavored copy, the most
                           ecosystem-wide nav (links to every subsite)
                           because this is the most common 404 source
                           for inbound links from the legacy single-
                           volume mlsysbook.ai.

StaffML already has its own React not-found.tsx so no work needed.
TinyTorch's legacy Sphinx 404.md is preserved for now (still wired on
the Sphinx site that hasn't migrated yet).

* ci(precommit): block subsite-mirror drift on shared assets

Add a pre-commit hook that runs `shared/scripts/sync-mirrors.sh --check`
on every commit. The hook fails if any of the per-subsite real-file
mirrors (subscribe-modal.js, theme SCSS partials, logo) has drifted
from its canonical source in `shared/`.

Why a guard, not just a sync: Quarto's resource-copy step preserves
symlinks instead of dereferencing them, so we have to keep real
copies. Without the guard, "I'll edit the canonical and forget to
re-sync" silently re-introduces the duplicate-divergence bug we just
spent effort fixing. `always_run: true` because a mirror can drift via
deletion of the canonical, not just by editing the canonical itself.

To re-sync after a deliberate change:
  bash shared/scripts/sync-mirrors.sh

* refactor(audit): duplicate-file finder + clean up obvious leftover

Add shared/scripts/find-duplicates.py as a periodic duplication
auditor. It SHA-1 hashes every source-y file across the ecosystem
roots, groups identical contents, subtracts the intentional groups
declared in shared/scripts/sync-mirrors.sh, and reports the rest as
unintended duplicates. JSON report written to .audit/duplicates.json
for CI ingest later; --strict makes it exit non-zero.

Defaults err on the side of being useful out of the box:
  - Skips symlinks (those are deliberate aliases, not duplicates).
  - Skips small files (<256B) — LICENSE stubs, .gitkeep, etc.
  - Skips _site / _build / node_modules / .next / out / .git.
  - Source-y suffix list (.js, .ts, .scss, .css, .html, .yml, .py, .sh).
    Binary assets (images, PDFs) are NOT scanned because their dup
    story is different (logos, icons are intentionally repeated).

Initial-cleanup pass:
  - Delete tinytorch/scripts/cleanup_repo_history.sh — byte-identical
    leftover; the canonical version lives at
    tinytorch/tools/maintenance/cleanup_history.sh and is the one
    referenced by tinytorch/tools/maintenance/README.md.

After this commit the only remaining unintended duplicate is
runHistoryProvider.ts in three vscode-ext packages (kits / labs /
tinytorch). Promoting that into a shared vscode-ext package is real
refactor work — out of scope for release-prep, captured for later.

Add .audit/ and _audit/ (the latter from the Playwright site-audit
script) to .gitignore.

* docs(release-prep): handoff notes covering all five PR groupings

Add a single document at the worktree root that walks through what
this branch contains, why each piece is there, the recommended PR
split (PR-1 safety-net, PR-2 visual polish, PR-3 scripts/audits/
cleanup, PR-4 TinyTorch prep, PR-5 cutover skeletons), what was
intentionally LEFT OUT (and why), and what verification was done
locally vs. what still needs the dev mirror to exercise.

Treat this as the cover memo for the staged-rollout foundation
work; once the five PRs are individually merged into dev, this file
will outlive the branch but the per-PR sections still document why
each piece exists for anyone debugging months from now.
2026-04-19 16:23:26 -04:00
Vijay Janapa Reddi
773e106c63 PR-5: Cutover skeletons (rollback-legacy + redirect map + sitemap aggregator) (#1409)
* feat(launch): rollback-legacy.sh — snapshot + restore the gh-pages root

Add the panic button for the mlsysbook.ai cutover. The staged-rollout
plan keeps the legacy single-volume site at the gh-pages root while
the new properties (Vol I, Vol II, TinyTorch, labs, kits, slides,
mlsysim, instructors, staffml, unified landing) get deployed into
subdirectories. Once everything is verified, the unified landing
page replaces the legacy root — and at exactly that moment we want a
one-command revert path that doesn't require remembering which gh-
pages SHA "the old root" lived at.

Three modes:
  snapshot          Take a timestamped backup of the legacy root files
                    (everything at gh-pages root that is NOT a known
                    subsite directory) and push to legacy-backup/<TS>/.
  restore <ID>      Copy a snapshot back to root, OVERWRITING current
                    root files but leaving subsite directories alone.
  list              List available snapshots.

Design choices worth flagging:

1. Subsite-aware. The script hard-codes the list of top-level
   subsite directories (book/, tinytorch/, kits/, labs/, mlsysim/,
   slides/, instructors/, interviews/, staffml/, about/, community/,
   newsletter/) and excludes them from BOTH snapshot capture AND
   restore overwrites. Rolling back the legacy landing page should
   never wipe out actively-deployed properties.

2. Dry-run by default. Every destructive mode requires --apply. The
   default behavior prints what would happen, including a diff
   preview for restore. This is the same posture the existing
   sync-mirrors.sh / link-checker / publish-guard scripts take.

3. Snapshots are kept, not moved. Restoring a snapshot is itself a
   reversible commit on gh-pages; the snapshot directory is preserved
   so a "rollback the rollback" is one more command away.

4. Doesn't touch the working tree. Operates against a fresh shallow
   clone in mktemp, so it can be run from any clone of the repo
   (developer machine or a GitHub Actions runner) without dirtying
   anything local.

Typical sequence on launch day is documented inline at the top of
the script. Two short commands wrap the whole rollout: snapshot
before deploy, restore-by-ID if anything looks wrong.

* feat(seo): redirect-map skeleton + HTML-stub generator

Add the cutover plumbing for legacy-URL → new-URL redirects so the
PageRank accumulated under the old single-volume mlsysbook.ai
structure flows into the new ecosystem URLs (`/book/vol1/`,
`/labs/`, `/about/`, etc.) as soon as the unified landing replaces
the legacy root.

Two artifacts:

1. `shared/config/redirect-map.json` — declarative source of truth.
   Schema:
     - `from`:   legacy path (must start with '/')
     - `to`:     destination URL or path (resolves against base_url)
     - `status`: 301 / 302 / 307 / 308 (default 301)
     - `note`:   optional human note
   A trailing-`*` wildcard is supported in `from` for whole-subtree
   moves like `/contents/labs/* → /labs/*`. The file ships
   intentionally small: just enough entries to demonstrate the
   patterns and seed the launch — populating the full inventory
   from the legacy mlsysbook.ai sitemap is a separate task.

2. `shared/scripts/build-redirects.py` — generator.
   For each entry it emits a tiny HTML stub at the legacy path
   containing:
     <meta http-equiv="refresh" content="0;url=<dest>">
     <link rel="canonical" href="<dest>">
     <meta name="robots" content="noindex,follow">
   That combo is the closest GitHub-Pages-friendly equivalent of a
   301: real users get redirected in <100ms; crawlers treat the
   canonical as authoritative and drop the legacy URL on recrawl;
   PageRank flows through. The script ALSO emits a Netlify-format
   `_redirects` file from the same map, so the day we move off
   GitHub Pages (Cloudflare Pages, Netlify, S3+CF) the same source
   of truth produces real 301s with no rewrite.

   `--check` mode validates the JSON without writing anything (CI
   hook). Wildcards skip stub emission (we'd have to walk the
   deployed tree to expand them) but are still emitted to the
   Netlify file where they work natively.

Wiring into a *-publish-live workflow is a one-liner step
(`python3 shared/scripts/build-redirects.py --map shared/config/
redirect-map.json --out gh-pages-staging/`) but is intentionally
NOT done in this commit — it should land alongside the actual
unified-landing deploy, when there is something for the legacy
URLs to redirect away from.

* feat(seo): aggregate per-subsite sitemaps into mlsysbook.ai/sitemap.xml

The new ecosystem has every subsite (Vol I, Vol II, TinyTorch, labs,
kits, slides, instructors, mlsysim, staffml, the unified landing)
emitting its own `<subsite>/sitemap.xml` because that's what Quarto
and Next produce automatically. Search engines, however, want a
single authoritative entry point per *domain*. Without an aggregated
index they end up either crawling the subsite sitemaps separately
(if they happen to discover them) or missing some entirely.

This commit adds the aggregator:

  shared/scripts/build-sitemap.py
    Walks a deployed gh-pages tree, discovers every sitemap.xml under
    it (skipping the root one, legacy-backup snapshots, _archive,
    _site, and the like), and writes a single sitemap-index.xml at
    `<root>/sitemap.xml` that points at each subsite's sitemap as a
    `<sitemap><loc>…</loc></sitemap>` entry. It also creates or
    appends to `<root>/robots.txt` so the index is surfaced to
    crawlers via the standard `Sitemap:` directive.

    Optional `--include-subsite` allowlist (repeatable) for staged
    rollouts where we want the index to advertise only the subsites
    that have been verified live, even if other ones happen to be
    deployed in the tree. Defaults to "everything found".

    `--check` does discovery without writing.

  .github/workflows/infra-build-sitemap.yml
    Reusable workflow (`workflow_call`) wrapping the script so any
    `*-publish-live` workflow can refresh the index as its final
    step. Also `workflow_dispatch`-able for manual rebuilds. Joins
    the existing `gh-pages-deploy` concurrency group so it never
    races a publish push.

    Uses sparse-checkout to grab just the script from `dev` (no need
    to clone the whole monorepo into the runner) and a full clone of
    `gh-pages` to do the work.

Wiring into per-subsite publish workflows happens in a follow-up
commit alongside the actual launch — this PR is "skeletons", and
the per-publish trigger is best landed when each subsite's launch
PR ships.
2026-04-19 16:22:11 -04:00
Vijay Janapa Reddi
d6029d2c35 feat(site): landing freshness badge, lower nav collapse, mobile QA doc
Landing page (site/index.qmd, site/landing-v3.css):
  Add a small freshness indicator to the hero — pulsing green dot,
  "Actively maintained · Last updated April 2026 · Release notes"
  link. Communicates to first-time visitors that the textbook is a
  living project, not a shipped-and-forgotten resource. Pulse uses
  reduced-motion-friendly animation timing.

Subscribe modal (site/_quarto.yml):
  Switch the script src from a cross-site /vol1/... URL to the local
  /assets/scripts/subscribe-modal.js — the marketing site now ships
  its own copy via the mirror sync (shared/scripts/sync-mirrors.sh),
  so it doesn't depend on the book bundle being reachable.

Navbar breakpoint (shared/config/navbar-common.yml):
  Lower navbar.collapse-below from "xl" (1200px) to "lg" (992px) so
  13-15in laptops keep the full nav visible instead of collapsing
  into the hamburger. Affects every subsite that includes the
  shared navbar config.

Mobile QA (book/docs/MOBILE_QA.md):
  Manual checklist for verifying margin sidenotes, citations,
  navbar, and TOC behaviour on iOS / Android at common breakpoints.
  Used as a release gate; not part of automated CI yet.
2026-04-19 10:31:41 -04:00
Vijay Janapa Reddi
1f18ae2249 Merge feat/instructors into dev (brand href + toggle position) 2026-04-18 18:07:24 -04:00
Vijay Janapa Reddi
e57fc922a0 fix(navbar): brand links to landing page + fix toggle position
Two fixes:
1. Clicking "Machine Learning Systems" title/logo now navigates to
   https://mlsysbook.ai/ (the main landing page) from any subsite.
   Set via shared navbar-common.yml href so all sites inherit it.

2. Fix dark mode toggle positioning when navbar collapses at half-screen
   width. The floating .top-right toggle is now fixed-positioned to
   align cleanly next to the search icon instead of floating awkwardly.
2026-04-18 18:07:24 -04:00
Vijay Janapa Reddi
3ba3858b74 MLSys·im 0.1.0 release-prep audit (#1397)
* docs(mlsysim): release-prep audit fixes for 0.1.0

Fixes the broken links, stale numerical claims, and naming inconsistencies
surfaced by the 0.1.0 release-prep review. Output of the docs site now matches
what the engine actually computes, internal navigation has no unresolved targets,
and the Hatch announcement banner uses an absolute URL so sub-pages render the
"Get started" link correctly.

Notable changes:
- Hero example on docs/index.qmd and getting-started.qmd now reflect the actual
  Engine.solve(ResNet50, A100, bs=1, fp16) output (Memory / 0.54 ms / 1843).
- Update Python version requirement (3.10+) and document the editable-install
  limitation (Hatch sources rewrite is not supported by editables).
- Standardize the typographic brand to "MLSys·im" in the navbar, OG/Twitter
  metadata, and the shared cross-site dropdown.
- Add the four solvers missing from the quartodoc list
  (BatchingOptimizer, ForwardModel, NetworkRooflineModel, PlacementOptimizer)
  and surface the orphan tutorials (01_pipeline_callbacks,
  02_differential_explainer, 12_design_space_exploration) in the sidebar.
- Rename every reference to the now-deleted hello_world / llm_serving /
  sustainability / 11_full_stack_audit tutorials to their current filenames.
- Add the missing @mlsysbook2024 entry to references.bib so whitepaper.qmd
  no longer logs a citeproc warning.
- Fix the CLI sample on the parent site/index.qmd card to use real model
  identifiers (Llama3_70B H100 --batch-size 1).
- Soften the Colab/Binder copy until launch buttons are wired in.
- Remove the duplicate "Differential Explainer" card on tutorials/index.qmd.

* release(mlsysim): add 0.1.0 release notes and runbook

- RELEASE_NOTES_0.1.0.md: GitHub-release-ready notes promoted from CHANGELOG
  with install/quickstart copy and a "known limitations & gotchas" section
  covering the editable-install issue, broken example scripts, and unpublished
  slide tag.
- RELEASE.md: copy-pasteable runbook for cutting a release (pre-flight check,
  tag, build, twine upload, docs deploy via workflow_dispatch, GitHub release,
  and post-release verification).
- CHANGELOG.md: corrected the test count from 334 to the actual 367 currently
  passing on dev.

* mlsysim: nest package layout, enable editable installs, clean lint

Restructure mlsysim into the standard nested layout (`mlsysim/mlsysim/...`)
so `pip install -e .` works out of the box. The previous flat layout used
a Hatch `sources = {"." = "mlsysim"}` prefix-add rewrite that the
`editables` backend cannot handle, breaking editable installs entirely.

Packaging
- pyproject.toml: drop `sources` rewrite, set `packages = ["mlsysim"]`,
  add explicit `[tool.hatch.build.targets.sdist]` include list.
- Wheel and sdist now contain only the package and project metadata
  (no `tests/`, `docs/`, `examples/`, `paper/`, `vscode-ext/` leakage).
- Update `pyright.exclude` for nested layout.
- Update GitHub source links in `docs/math.qmd` and
  `docs/models-and-solvers.qmd` to point to `mlsysim/mlsysim/...`.

Lint configuration
- Add `[tool.ruff]` to pyproject.toml with sensible per-file ignores:
  `__init__.py` re-export pattern (F401/F403/F405/F811),
  `core/constants.py` star import from unit registry,
  tests/examples idioms.
- `ruff check .` reports zero issues (down from 621).

Real bug fixes uncovered by lint cleanup
- `core/solver.py`: remove unused `from pydantic import BaseModel` that
  was being shadowed by the local `BaseModel = ForwardModel` alias.
- `sim/simulations.py`: remove redundant local `Fleet` import that was
  shadowing the module-level import and triggering F823 (referenced
  before assignment) on the earlier `isinstance(..., Fleet)` check.
- `cli/commands/audit.py`, `cli/commands/eval.py`: narrow three bare
  `except:` clauses to specific exception types.
- `tests/test_sota.py`: add the missing speculative-decoding ITL
  assertion (`res_opt.itl < res_base.itl`) — `res_base` was previously
  computed but never compared.
- `cli/commands/eval.py`: drop unused `is_json` local.
- `labs/components.py`: drop unused `energy` placeholder local.

Examples
- `examples/06_multi_objective_pareto.py`: rewrite around the actual
  `BatchingOptimizerResult` API (which has no `pareto_front` attribute);
  build the front explicitly by sweeping batch sizes through
  `ServingModel` + `TailLatencyModel`, then highlight the optimum
  returned by `BatchingOptimizer`.
- `examples/gemini_design_loop.py`: fix multi-line f-string syntax errors
  (`f"\n[…]"` instead of an embedded literal newline) so the file imports
  on every supported Python version.

Dev scripts
- `generate_appendix.py` and `paper/scripts/validate_anchors.py`: switch
  from package-relative imports to absolute `from mlsysim... import` so
  they run cleanly under the nested layout.

Docs / release notes
- `docs/getting-started.qmd`: replace the editable-install caveat with
  `pip install -e ".[dev]"` (now supported).
- `RELEASE_NOTES_0.1.0.md`: drop the three "known limitations" entries
  that this commit resolves (editable install, pareto example, gemini
  example).
- `CHANGELOG.md`: add a "Packaging & Tooling" section describing the
  layout change and the resolver bug fixes.

Verification
- `python -m pytest tests/` → 367 passed (was 367, no regressions).
- `ruff check .` → All checks passed.
- `pip install -e .` → succeeds; live source picked up.
- Fresh-venv wheel install + CLI smoke test → succeeds.
- `examples/06_multi_objective_pareto.py` and
  `examples/gemini_design_loop.py` → both exit 0.

* fix(mlsysim): repair docs build + lab test after nested-package restructure

The 0.1.0 release prep moved the package from `mlsysim/` to `mlsysim/mlsysim/`
to support `pip install -e .`. Two CI jobs still depended on the old layout:

1. **Docs build (`mlsysim-preview-dev`)** — every tutorial and zoo page used
   a hand-rolled `importlib.util.spec_from_file_location` block to load
   `<repo>/mlsysim/__init__.py` directly from source. After the restructure,
   that path no longer exists. Replaced the hack in 17 docs/.qmd files with
   a plain `import mlsysim` — the package is already pip-installed in the
   docs build environment via `pip install ".[docs]"`. Updated the matching
   guidance in `contributing.qmd`.

2. **Lab static tests** — `test_no_localstorage_import` hard-coded
   `mlsysim/labs/state.py`; updated to the new nested path
   `mlsysim/mlsysim/labs/state.py`.

Verified locally: `pytest labs/tests/test_static.py::TestStateImplementation`
passes, and `quarto render docs/zoo/models.qmd` succeeds end-to-end.
2026-04-18 13:11:13 -04:00
Vijay Janapa Reddi
58a6b4cf3d refactor(site): overhaul About & Community pages — AI engineering framing
Mission section: boxed mission statement, AI engineering definition,
Hennessy/Patterson & Andrew Ng acknowledgment, 1M learner goal with
log-scale star history chart, star-on-GitHub ask.

New "How We Teach" section: four pillars + expanded stats strip
(chapters, lecture decks, TinyTorch modules, universities).

People page: restructured as credits — featured founders (Vijay + Kari),
Global Outreach (Marco, Brian, Marcelo, Jeremy), Global Educators
compact grid, edX instructors. Initials fallback for missing avatars.

Events page: simplified from 250 lines to 3 cards + CTA linking to
tinyml.seas.harvard.edu as the operational hub.

Community page: renamed outreach section, added "Become a Member" CTA.
Partners page: updated to AI engineering framing.

New license page: multi-license structure (CC-BY-NC-SA for content,
Apache 2.0 for TinyTorch, AGPL v3 for StaffML).

Fixed root LICENSE.md from CC-BY-NC-ND to CC-BY-NC-SA to match intent.
Updated README badge to match.

Navbar: reordered About dropdown (Mission → Our Story → People →
Contributors → License). Stripped textbook accent-bar headers from
site pages via CSS overrides. Threaded "AI engineering" consistently
across all page heroes and taglines.
2026-04-03 08:49:41 -04:00
Vijay Janapa Reddi
c18e071221 fix(site): tighten dropdown item names — Labs, StaffML
- "Interactive Labs" → "Labs" (drop filler adjective)
- "StaffML Interview Vault" → "StaffML" (brand name only, parallel with TinyTorch and MLSys·IM)
2026-04-02 15:15:11 -04:00
Vijay Janapa Reddi
260b663edc feat(site): restructure navbar — 6 verb-based dropdowns with audience alignment
Read | Build | Teach | Prepare | Connect | About

- Add "Prepare" dropdown for StaffML (Vault, Study Plans, Gauntlet)
- Remove "Interview Prep" from Teach (wrong audience)
- Add "Course Map" to Teach (instructor adoption funnel)
- Rename "Community" → "Connect" for verb consistency
- Reorder Build by increasing commitment (Labs → TinyTorch → Kits → MLSys·IM)
- Remove License from About (available on page + footer)
- Bump collapse-below from lg to xl for 6-dropdown width budget
- Update tablet breakpoints and right-side icon-only range
- Expand dev landing page from 4 to 8 cards (all subsites)
- Add staffml path to rewrite-dev-urls.sh
2026-04-02 15:15:10 -04:00
Vijay Janapa Reddi
012367039d feat(site): add SEAS shield logo, fix navbar collapse at medium widths
- Replace favicon with SEAS shield as navbar logo
- Change collapse-below from xl to lg so hamburger kicks in earlier
- Hide right-side nav text labels between lg-xl breakpoints (icon only)
- Constrain logo to 28px height
2026-03-31 15:53:12 -04:00
Vijay Janapa Reddi
9b3a87e897 feat(site): split community into standalone pages, redesign contributors & people
- Split Partners & Sponsors into community/partners.qmd with hardcoded logos (fixes silent JS fetch failure)
- Split Workshops & Events into community/events.qmd with TinyML workshop data, courses, past events timeline
- Community hub now links to sub-pages via Explore grid, added Looker Studio readership embed
- Contributors page redesigned: circle avatar mosaic replaces long row list
- People page: compact circle grid for Working Group (11 new members) and Research groups
- Updated navbar to point to new standalone pages
- Added CSS for event cards, series grid, course grid, partner cards, sponsor CTA, compact avatars
2026-03-31 11:13:23 -04:00
Vijay Janapa Reddi
c8e799754b feat(site): split People & Contributors into standalone pages, data-drive community content
- Split People and Contributors out of about/index.qmd into dedicated pages
- People page loads from people.yml (27 people, 5 groups) — easy to edit
- Contributors page shows all 92 contributors with stats and "How to Contribute"
- About page now shows a compact teaser grid with links to full pages
- Community events and partners now loaded from YAML data files
- Updated navbar links to point to new standalone pages
- Added CSS for team bios, compact cards, and contributor table rows
2026-03-25 18:43:54 -04:00
Vijay Janapa Reddi
396506d29d refactor(site): unify 4 site subsites into single Quarto project
Architecture:
- Merge landing, about, community, newsletter into one site/ project
- Move navbar-common.yml to shared/config/ (used by 12 configs)
- Create shared/config/footer-site.yml for centralized footer
- Create shared/scripts/subscribe-modal.js as canonical copy
- Single _quarto.yml replaces 4 independent configs
- One site_libs/ copy replaces four

Features gained:
- Google Analytics on ALL hub pages (was only on book volumes)
- Subscribe modal on landing page (was missing)
- Centralized footer with consistent links

Workflows updated:
- site-preview-dev.yml: matrix strategy → single build job
- site-publish-live.yml: loop over subsites → single build + deploy
- sync-newsletter.yml: builds from unified site project
- publish-all-live.yml: removed stale subsite input
- rewrite-dev-urls.sh: added --shallow flag for unified builds

All 12 navbar-common.yml references updated:
  book vol1/vol2, site (unified), slides, instructors, interviews,
  kits, labs, mlsysim
2026-03-21 13:30:24 -04:00
Vijay Janapa Reddi
4b840ff957 refactor(site): shared styles system and fix subscribe modal for subsites
- Create shared/ directory with centralized SCSS partials and site-head.html
  so site subsites (about, community, newsletter) no longer duplicate
  inline header config or reference book-only styles
- Fix subscribe modal: change script src from broken relative path
  (../../book/quarto/assets/scripts/subscribe-modal.js) to absolute URL
  (https://mlsysbook.ai/vol1/assets/scripts/subscribe-modal.js) which
  the rewrite-dev-urls.sh script converts for dev preview automatically
2026-03-21 12:50:25 -04:00