Alex Gaffen

Alex Gaffen — personal site

Source for alexgaffen.com: React + TypeScript + Tailwind CSS, prerendered to plain static HTML and hosted on GitHub Pages.

Every page is rendered to HTML at build time, so the content is on screen in the first frame with no JavaScript. Scripts then add behaviour: React only where there is real state (the terminal, photo gallery and lightbox, résumé widgets, blog reader), plain TypeScript everywhere else.

Commands

npm install
npm run dev        # dev server: every page, live-reloading (same prerender as production)
npm run build      # type-check, bundle, prerender -> dist/
npm run preview    # serve dist/ locally
npm run typecheck
npm run icons      # re-subset the icon fonts after using a new fa-* / devicon-* class (needs Python + fonttools)

Needs Node 22.18 or newer.

How it is put together

public/                  copied to the site root as-is
  assets/, photography/  images, video, self-hosted fonts (URLs are stable)
  CNAME, robots.txt, sitemap.xml, site.webmanifest
src/
  pages/                 one file per page: its <head> metadata + body (React)
    index.tsx  resume.tsx  photography.tsx  notes.tsx  note-defending-llms.tsx  game.tsx  blog.tsx
    resume/  photography/  blog/    the content and components of the bigger pages
  components/            TabBar, Document (the <html> shell), Island, shared class bundles
  console/               the AG-OS terminal (React; loaded only when used)
  client/                browser TypeScript: theme, prerender hints, blur-up, home fitter, game ...
  entries/               one tiny client entry per page
  styles/site.css        Tailwind setup, design tokens, fonts, theme-switch transitions
scripts/build.ts         client bundle -> server bundle -> prerender every page
.github/workflows/       build + deploy to Pages

Common edits

Design notes

Bundle size: the React runtime

Pages with islands (résumé, photos, blog) load React to hydrate them: about 70 KB gzipped. Home, notes and the game run no framework at load; the terminal’s files download in the background but only run when it is used.

If that cost matters, the whole site can run on Preact’s React-compatible runtime (~7 KB) with no source changes – the pages are still rendered to HTML by React at build time, and only the browser bundle is swapped:

npm i -D preact
// vite.config.ts: wrap the config as defineConfig(({ isSsrBuild }) => ({ ... })) and add
resolve: isSsrBuild ? {} : { alias: {
  react: 'preact/compat', 'react-dom/client': 'preact/compat/client',
  'react-dom': 'preact/compat', 'react/jsx-runtime': 'preact/jsx-runtime',
} },

This was tried against the same visual and behavioural comparisons as the React build and matched.

Deploying

Pushes to main build and publish through .github/workflows/deploy.yml. In the repository’s Settings → Pages, the source must be GitHub Actions.

Contact

LinkedIn · GitHub · alexgaffen@gmail.com

© 2026 Alex Gaffen