Skip to content

Latest commit

 

History

History
146 lines (96 loc) · 22.6 KB

README.md

File metadata and controls

146 lines (96 loc) · 22.6 KB

Jackson Ferguson — Personal Website

The source for jacksonferguson.me: my technical portfolio, photo journals, and creative archive. It brings together projects across software, infrastructure, hardware, and applied physics, alongside camping trips, live event sound, Blender scenes, and Python generative art.

How it's made

The site is built with Astro, TypeScript, and CSS, with static HTML generated at build time. GitHub Actions builds and checks the site for GitHub Pages, using jacksonferguson.me as the canonical domain. There is no application server or database to run.

Astro components handle the shared layout, project cards, navigation, and galleries. Trip journals and creative collections live in Markdown with schema-validated metadata. Project descriptions, site metadata, and contact links are centralized in TypeScript so the pages and structured data share the same source.

JavaScript supports specific interactions: a WebGL hero animation, an Asciinema terminal recording, photo viewers, and copy buttons. Astro generates responsive image variants, while fonts and selected upstream project assets are served from the site itself.

The New Computer Modern regular and italic WOFF2 files in public/fonts/ are Latin and common-symbol subsets of the 7.1.1 OpenType fonts distributed with TeX Live 2026. They were generated with FontTools pyftsubset using --unicodes='U+0000-024F,U+1E00-1EFF,U+2000-206F,U+20AC,U+2122,U+2200-22FF' --layout-features='*' --name-IDs='*' --name-languages='*' --flavor=woff2. The original full fonts fail Chromium's font parser; the subsets retain the characters used by this site and load in the browser. The bundled GUST font license remains in public/fonts/.

Visual system

The whole site uses one type scale. Each tier has a single size, defined as a custom property on :root in house-style's tokens.css (the journal tokens below stay in global.css), and the tokens shrink together at the 560px breakpoint. Components refer to the tokens rather than setting their own sizes.

The palette, the fonts, the section markers and buttons, the terminal window, and the install box are shared with the project sites on subdomains through house-style, which package.json pins to a release tag. Change them there, tag a release, and move this site to the new tag.

Tier Token Desktop / mobile Used for
Display — Hero only The name in the hero
H1 --fs-h1 40–72px Subpage and 404 titles
Label --fs-label 11px, mono Section markers, card eyebrows, tags, badges, figure labels
H2 --fs-h2 32–45px Section headline
H3 --fs-h3 32px / 28px Card title (every project and Beyond card, Protostar included), metric values
Lead --fs-lead 19px / 17px The one-line summary under a card title, the About lead, index page intros
H4 --fs-h4 18px / 17px Items inside a card or list: principles, thumbnails, resume cards, minor projects
Body --fs-body 16px All paragraphs
UI --fs-ui 14px / 13px Buttons, text links, metric captions, photo captions

Each major section opens with a short numbered marker (01 / ABOUT, 02 / PROJECTS, 03 / BEYOND, 04 / CONTACT), followed by an H2 headline that says something the marker doesn't. Only sections are numbered. Card eyebrows are unnumbered labels such as FEATURED PROJECT, since card order already shows importance.

Content inside a section is grouped into cards with the same anatomy: eyebrow, H3 title, optional lead, body, supporting content (demo, metrics, figures, or a thumbnail grid), then actions. Thumbnails inside a card have no panel of their own, so cards are never nested inside cards.

Project tags leave a consistent 20px gap above the actions below them. In paired secondary cards, the tag row and action link sit together at the bottom, with flexible space above the tags so both cards align regardless of description length.

Subpages follow the same system. Index pages (trips, creative) open with a PageIntro, and detail pages (a trip, a creative collection) open with an ArticleHeader that adds a breadcrumb; both use the H1 token. Collection links on the homepage and on the index pages are the same ThumbnailCard component. Blender and Python entries are .project-card cards with H3 titles. Subpages are not numbered, and neither are entry categories.

Journal text uses a second voice: New Computer Modern serif, for detail-page descriptions (--fs-journal-lead, 24px / 22px) and long-form .prose writing (--fs-journal-body, 20px / 19px). The serif runs smaller than DM Sans at the same size, so these tokens are larger than their sans equivalents. Headings inside prose stay in DM Sans on the H3 and journal-lead sizes.

Photo originals

Photos in src/assets/ are build inputs, not an archive: Astro generates every served size from them, and the largest is the 2400px photo viewer image. Before committing new photos, run:

npm run images:prepare

scripts/prepare-images.mjs caps the long edge at 3000px, applies the camera orientation, re-encodes JPEGs at quality 85, and removes all metadata, including GPS locations. PNGs stay lossless. Files that are already prepared are skipped, so running it again never re-compresses them. Pass paths to prepare only some files. The pre-commit hook rejects newly added files over 4 MB, and the site tests fail if an original still carries metadata or is over 3000px, or if any served image carries metadata or is over 3.5 MB. Keep full-resolution copies in your own photo library.

Social share cards

Links shared from the site show a branded card, not a gallery photo: public/images/og-card.png for the homepage, and one card per trip and creative page in public/images/og/. The PNGs are committed. After changing a page's title, description, location, or date, or the homepage name or tagline, re-render them:

npm run og:render

scripts/render-og-card.mjs reads the title, description, location, and date from each page's frontmatter and uses the first sentence of the description, so a long description never overflows the card. Re-rendering is byte-stable. Adding a trip needs no code change: render again, then commit the new trips-<slug>.png.

The card HTML is built in scripts/og-cards.mjs. Rendering records a hash of each card's HTML, including its embedded fonts, in config/og-cards.lock.json, and the site tests fail when a card's current HTML no longer matches, so a stale card cannot ship.

Remote project assets

config/remote-assets.json is the central manifest for assets sourced from other project repositories. scripts/fetch-remote-assets.mjs downloads and validates them, then writes the copies into public/ for Astro to include in the static site. These generated copies are git-ignored; this repository tracks the fetching process rather than snapshots that can go stale.

Asset ID Source project Local target Validation
protostar-demo Protostar interactive init demo public/protostar-demo.cast Asciinema v2 header and minimum size
protostar-icon Protostar public/images/protostar.svg SVG markup and minimum size
protostar-mutation-score Protostar metrics public/data/protostar-mutation-score.json JSON object with typed keys
protostar-rollback-faults Protostar metrics public/data/protostar-rollback-faults.json JSON object with typed keys
systems-audio-analysis Systems Audio Lab public/images/audio-analysis.svg SVG markup and minimum size
data-science-redshift-distribution Data Science Portfolio public/images/gmm-redshift-distribution.svg SVG markup and minimum size
systems-audio-report Systems Audio Lab technical report public/reports/systems-audio-lab-technical-report.pdf PDF signature and minimum size
data-science-aco-2670-report ACO 2670 dark matter analysis report public/reports/aco-2670-dark-matter-analysis-report.pdf PDF signature and minimum size

Why fetch at build time?

Embedding these files directly from upstream would allow a live page to change without this repository's build, tests, or accessibility checks running. Fetching them first keeps the project repositories authoritative while making the deployed site a checked snapshot. Visitors load these assets, including the full technical reports, from the site's own origin, where they open in the browser instead of downloading as raw.githubusercontent.com files do. Links to source repositories remain external.

The current sources follow upstream main branches, so rebuilding a portfolio commit can pick up newer assets. The fetcher logs SHA-256 hashes for the downloaded files and writes public/remote-assets-state.json, which Astro includes in the deployed site. This generated, git-ignored snapshot records the commit that built it (GITHUB_SHA in Actions, null locally) and each asset's ID, source, target, and content hash; it describes the fetched content rather than pinning its version.

Fetching and validation

npm run assets:fetch runs the fetcher directly. npm run dev, npm run check, and npm run build invoke it automatically through npm pre-scripts, since the project catalog imports the fetched mutation score and rollback count. Assets download in parallel. In CI, each failed or invalid download is retried with backoff (2, 5, 15, 30, 60, and 120 seconds, about four minutes in all) to ride out short GitHub outages; locally the fetcher retries twice within 1.5 seconds. If a download remains unavailable after retries, or fails validation, the command fails before Astro starts. Outside CI, the fetcher instead keeps an existing local copy that still passes validation and prints a warning, so offline development keeps working. CI never has a local copy, so there a failed download always fails the build.

Each manifest entry supplies a unique ID, a description, an HTTPS source, a unique target inside public/, a supported validation type, and a positive minBytes threshold. The type matches the asset format, and the size threshold helps catch empty or unexpectedly small responses. SVG checks look for opening and closing SVG markup; terminal recordings require a JSON Asciinema v2 header with numeric dimensions; PDFs must start with the %PDF- signature. A json entry also declares a shape mapping each key the site reads to string, number, or boolean, and the fetched document must be an object whose keys have those types. It may also declare a match regular expression for a string key; Protostar's metrics use it so a placeholder such as unknown fails the fetch instead of appearing on the homepage. These are format checks, not a full content audit.

Pages reference the generated local paths. The project catalog reads Protostar's mutation score and rollback count files while the site builds, and the homepage shows both linked to Protostar's metrics dashboard, so a new value appears on the next build. The asset pipeline is checked through the fetch command, production build, and site tests.

Refreshes from source repositories

The GitHub Actions workflow builds on pushes and pull requests to main, manual workflow_dispatch runs, and repository_dispatch events named remote-assets-updated. Pull requests run the checks without deploying. One job builds the site and runs the static checks, then Chromium, Firefox, and WebKit each run the browser tests against that build in their own parallel job; a deployment waits for all three. The dispatch hook allows a source repository to request a rebuild immediately after changing an asset.

Every day at 11:17 UTC, a scheduled job runs npm run assets:check without installing dependencies. It downloads and validates all remote assets, then compares their hashes, sources, and destinations, and the current main commit, with the live remote-assets-state.json on the domain in public/CNAME. If they match, the workflow skips the build, tests, and deployment. A changed, added, or removed asset triggers the normal build and deployment, as does a main commit that never deployed because its own build failed or its queued run was replaced by a newer one. Scheduled Actions runs may start later than their requested time.

The live snapshot is the baseline, so it advances only when a deployment succeeds. Failed builds remain eligible for retry the following day. A missing snapshot (HTTP 404) triggers a build to bootstrap this feature. Other HTTP errors and network failures are retried with the same backoff as downloads; if they persist, or the snapshot is invalid, the check fails instead of reporting the assets unchanged. A failure therefore means GitHub was unreachable for several minutes, and the next day's run retries regardless. Requests bypass caches and include a unique query parameter. The build fetches assets again and publishes the snapshot for the files it actually includes, even if upstream changes between the daily check and the build. Website pushes, manual runs, and dispatch events always build regardless of the daily comparison.

The source-side integration uses a GitHub Actions step like this:

- name: Refresh portfolio remote assets
  env:
    GH_TOKEN: ${{ secrets.PORTFOLIO_DISPATCH_TOKEN }}
  run: |
    gh api repos/JacksonFergusonDev/JacksonFergusonDev.github.io/dispatches \
      --method POST \
      --field event_type=remote-assets-updated \
      --field 'client_payload[asset_source]=${{ github.repository }}' \
      --field 'client_payload[ref]=${{ github.ref }}'

PORTFOLIO_DISPATCH_TOKEN is a fine-grained token authorized to send repository dispatch events to this portfolio, stored as an Actions secret in the source repository. The hook rebuilds the site; it does not bypass its validation or deployment conditions.

Build checks and hosting

zizmor audits the GitHub Actions workflows in pre-commit and CI. Run just zizmor locally with uv installed; pre-commit and just ci also run offline. CI enables online audits using the workflow's GitHub token. Configuration lives in .github/zizmor.yml, with a single exception for the Playwright image whose digest the build job resolves and validates. CI reports findings as annotations and fails on findings.

The workflow uses Node.js 24 and installs dependencies from the npm lockfile. It runs a dependency audit, Prettier, ESLint, Stylelint, Astro's type checks, the production build, and tests against the generated site, which check every internal link, srcset candidate, and cross-page anchor. Playwright and Axe check every built page for accessibility, broken images, failed requests, console errors, and layout breakage that throws no error: text covered by another element or pushed off-screen, overlapping cards, stretched images, text in a fallback font, and grids collapsed to one column. They also check responsive widths, gallery behavior, and that the hero canvas draws and pauses while off-screen; every check runs in Chromium, Firefox, and WebKit (Safari's engine), at desktop size and as a phone (a Pixel 7, an iPhone 15, and a narrow touch window in Firefox, which has no mobile emulation). Accessibility runs in Chromium only, since its results do not depend on the engine, as do two timing-sensitive reveal tests. Firefox on Linux creates a WebGL context only when an X display exists, so CI runs the browser tests under xvfb-run, and the hero renders in software through Mesa's llvmpipe in every engine. A separate test turns WebGL off to check the static hero that browsers without it get. The specs live in tests/browser/, and playwright.config.mjs serves the build for them. Browser test screenshots and failure traces are retained for seven days.

The browser jobs use the official Playwright container with browsers and Linux dependencies preinstalled, avoiding per-run browser downloads and package installation. The build job reads the Playwright version from package-lock.json, resolves the matching official image digest, and passes that same image to every browser job. Updating the npm lockfile also updates the selected image; there is no separate version pin to maintain. Each engine still runs in its own parallel job against the same build, with the existing npm cache.

GitHub Pages receives the static dist/ artifact, with the custom domain recorded in public/CNAME.

A finished deployment is not yet what visitors see: GitHub Pages serves the site through a CDN that caches each page for up to ten minutes (cache-control: max-age=600), and a hard refresh only bypasses the browser's cache, not the CDN's. CI builds stamp every page with <meta name="build-commit">, and after each deployment the verify-live job runs scripts/verify-live.mjs. It polls the live homepage, then every page in the sitemap, every 30 seconds until each serves the deployed commit, and fails if any page still serves an earlier build after 15 minutes. It then checks that the HTTPS certificate is valid for at least 14 more days, that HTTP redirects to HTTPS, that every page and every file those pages reference returns successfully (stylesheets, scripts, and PDFs with the right content type), and that a missing URL gets the site's own 404 page. Scheduled runs that deploy nothing still run these checks without waiting for a commit, so certificate, DNS, or Pages configuration problems surface within a day. Run it locally with node scripts/verify-live.mjs; set EXPECTED_COMMIT to wait for a build.

Links leaving the site, to GitHub repositories, release downloads, and project sites, can break without any change here. The links workflow runs lychee against every live page daily, alongside the remote asset check, and keeps a single broken-links issue: it opens one listing the broken links, updates it with a comment only when that list changes, and closes it once every link passes. Pull requests run the same check on the build as a non-blocking step. Settings and exclusions are in lychee.toml; LinkedIn is excluded because it answers automated requests with HTTP 999, and a GitHub rate limit counts as unchecked rather than broken. A broken link never blocks a deployment.

Repository map

Path Purpose
src/pages/ Homepage, archive routes, sitemap, and llms.txt
src/content/ Trip and creative Markdown collections and their schemas
src/components/ Shared interface components and interactive media
src/layouts/ Shared page structure, navigation, and metadata
src/lib/ Project descriptions, site metadata, and structured data
src/scripts/data-field.ts WebGL hero animation, run by house-style's runField
src/styles/global.css Typography, colour, spacing, and responsive layout
src/assets/ Source photography, artwork, and creative code
public/ Static downloads, fonts, favicon, and custom-domain file
config/ and scripts/ Remote asset manifest and build utilities
tests/ Generated-site, browser, and accessibility checks