My personal site — cjameshawkins.com — running entirely
in the browser as a Python WebAssembly app via PyScript
(Pyodide) and the PuePy framework. Blog posts are authored as
markdown and rendered client-side with Python's markdown package.
- PuePy — reactive, Vue-inspired Python frontend framework
- PyScript / Pyodide — CPython compiled to WASM, running in the browser
- Tailwind CSS (CDN) for styling
- Hatch — environment management and local/CI workflows
index.html PyScript scaffold (loads main.py + pyscript.toml)
main.py builds the app, restores theme, mounts to #app
app/ blog data layer, components, and pages
posts/*.md blog posts (front matter + markdown)
build.py regenerates posts/index.json + pyscript.toml, assembles dist/
pyproject.toml Hatch config (environments + scripts)
tests/ pytest suite for the data layer
This project uses Hatch.
hatch run build # regenerate the manifest/config and assemble dist/
hatch run serve # build, then serve dist/ at http://localhost:8000
hatch test # run the data-layer test suite
hatch run clean # remove dist/Before committing, run the checks:
hatch check code # lint (Ruff)
hatch check fmt # format check — add --fix to apply formatting
hatch check types # type check (Pyrefly)Type checking treats js/pyodide (browser-injected) and the untyped, highly
dynamic puepy framework as Any, so the checked surface is our own logic in
app/blog.py and build.py. Lint/format use the project's Ruff config in
pyproject.toml ([tool.ruff]).
PyScript must be served over HTTP (not
file://).hatch run servehandles that. Note: deep-linking to/blog/<slug>only works in production — GitHub Pages serves404.html(a copy ofindex.html) as the SPA fallback, which a plain local HTTP server does not.
Drop a markdown file in posts/ with front matter, then rebuild:
---
title: My Post
author: C. James
date: 2026-01-15T12:00:00Z
tags: [python, wasm]
---
Markdown body…hatch run buildPushing to main builds with Hatch and publishes dist/ to GitHub Pages
(see .github/workflows/continuous_deployment.yml).