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.
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/.
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.
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.
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:preparescripts/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. Keep full-resolution copies in your own photo library.
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:renderscripts/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.
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 |
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; those hashes describe the fetched content rather than pinning its version.
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. 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. 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.
The GitHub Actions workflow refreshes remote assets on pushes and pull requests to main, manual workflow_dispatch runs, weekly scheduled builds, and repository_dispatch events named remote-assets-updated. The weekly build picks up upstream changes; the dispatch hook allows a source repository to request a rebuild immediately after changing an asset.
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.
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. Playwright and Axe check navigation, responsive layouts, gallery behavior, and accessibility on selected pages. Browser test artifacts are retained for seven days.
GitHub Pages receives the static dist/ artifact, with the custom domain recorded in public/CNAME.
| 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 and motion controls |
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 |