Skip to content

Repository files navigation

Wybthon Demo Template

A polished GitHub template for static Wybthon applications: Python components running in the browser through Pyodide, deployed straight from the main branch via GitHub Pages. No server, no bundler, no production build workflow.

PyPI Version License: MIT


What you get

  • A working Wybthon app (app/): run-once components, signals, memos, and event handlers, with a live counter and text-input demo.
  • A static bootstrap (bootstrap.js): loads a pinned Pyodide from CDN, installs a pinned Wybthon wheel via micropip, copies your Python modules into Pyodide's virtual filesystem, and mounts the app, reporting progress the whole way, capturing Python stdout/stderr, and showing a retryable error overlay if anything fails.
  • One configuration source (config.json): the Pyodide and Wybthon versions live in a single file read at boot.
  • Content-based cache busting: tools/build_manifest.py stamps every Python module, bootstrap.js, and assets/styles.css with a short SHA-256 of its content, so browsers can cache aggressively but never go stale.
  • Modern Python tooling via uv: .python-version, dependency groups, and a committed uv.lock.
  • CI (.github/workflows/ci.yml): Ruff, Black, MyPy, pytest, manifest-staleness validation, and an optional (non-blocking) Playwright smoke test that boots the real site in Chromium.

Project layout

demo-template/
├── index.html               # Page shell, metadata, loading overlay
├── bootstrap.js             # Pyodide + micropip + manifest-driven module loader
├── config.json              # Pinned Pyodide + Wybthon versions (single source of truth)
├── assets/
│   └── styles.css           # All styling; design tokens on :root
├── app/                     # Your Python application
│   ├── manifest.json        # Generated: files + content hashes (committed)
│   ├── main.py              # async main(), called by bootstrap.js
│   ├── app.py               # Root <App> component
│   └── components/          # Counter and Greeting demo components
├── tools/
│   └── build_manifest.py    # Regenerates manifest + index.html cache busters
├── tests/                   # Generator unit tests, config guardrails, e2e smoke
├── .github/workflows/ci.yml # Lint, types, tests, manifest check, browser smoke
├── .nojekyll                # Serve files as-is, skip Jekyll on GitHub Pages
├── pyproject.toml           # Tooling config + dependency groups
└── uv.lock                  # Locked dev dependencies (committed)

Quick start

  1. Click Use this template on GitHub (or clone this repo).
  2. Install the toolchain and run the site:
# Install Python (from .python-version) and all dependency groups
uv sync --all-groups

# Serve the static site
make serve            # → http://localhost:8000

That's it. The page itself needs nothing but a static file server; Pyodide and Wybthon are fetched at runtime from jsDelivr and PyPI using the versions pinned in config.json.

Prefer raw commands over make? Every target is a thin wrapper; e.g., make serve is uv run python -m http.server 8000.

Everyday development

Edit the app

The Python app lives in app/. Components use the @component decorator; bodies run once at mount, and zero-argument callables embedded in the tree become fine-grained reactive holes. Start with app/app.py (page structure) and app/components/ (the demos).

Regenerate the manifest

Whenever you add, rename, remove, or edit files under app/ (or touch bootstrap.js / assets/styles.css), regenerate the manifest and cache busters, and commit the result:

make manifest         # uv run python tools/build_manifest.py

bootstrap.js fetches exactly the files listed in app/manifest.json, using each entry's content hash as a ?v= query parameter. The same script keeps the ?v= hashes on the stylesheet and bootstrap references inside index.html current. Editing a file without regenerating serves the old cached copy to returning visitors; CI catches this (see below).

Validate

make ci               # everything CI runs, in one shot

Or piece by piece:

make lint             # ruff check + black --check
make fmt              # black + ruff --fix (auto-format)
make typecheck        # mypy
make test             # pytest (generator + config guardrail tests)
make check            # fail if manifest/cache busters are stale

Browser smoke test (optional)

Boots the real site in headless Chromium, waits for the Python app to mount, clicks the counter, and asserts the DOM reacted:

uv run playwright install chromium   # one-time browser download
make e2e

CI runs this as a non-blocking job because it exercises live CDNs; delete continue-on-error: true in ci.yml to make it required.

Customizing

Bump Pyodide or Wybthon

config.json is the single source of truth; bootstrap.js reads it at boot:

{
  "pyodideVersion": "314.0.6",
  "wybthonVersion": "0.28.0"
}

When bumping Wybthon, also update the wybthon==X.Y.Z pin in pyproject.toml (it exists only so Ruff/MyPy/your editor resolve the same version the browser installs) and run uv lock. tests/test_config.py fails if the two pins drift apart, and if a version gets hard-coded back into bootstrap.js.

A note on [tool.uv]: dependency resolution ignores packages published within the last 7 days (exclude-newer = "P7D"), a supply-chain guard against freshly compromised releases. Wybthon itself is exempt via exclude-newer-package, so you can pin a new Wybthon release the day it ships. Other tooling upgrades (Ruff, Black, and so on) become resolvable once they're a week old.

Styling

All CSS lives in assets/styles.css. The palette, radii, shadows, and fonts are CSS variables on :root; components reference the classes via class_= props. Remember make manifest after editing so the stylesheet's cache buster updates.

Page shell and metadata

index.html owns the <title>, description, favicon (an inline SVG; swap in your own), and the loading overlay. Open Graph and Twitter cards need absolute URLs, so add those after you know your deployed URL (there's a commented stub in the file).

Dependencies

  • Runtime (browser) dependencies are installed by micropip in bootstrap.js; add more micropip.install(...) calls there if your app needs them.
  • Local tooling lives in [dependency-groups] in pyproject.toml (dev for lint/type/test, e2e for Playwright). After changing them, run uv lock and commit uv.lock.

Deploying with GitHub Pages

This repository deploys directly from the main branch; merging is the deployment. There's no build workflow to maintain:

  1. Push the repository to GitHub.
  2. Go to Settings → Pages.
  3. Under Build and deployment, set Source to Deploy from a branch.
  4. Choose the main branch and the / (root) folder, then save.

Your site appears at https://<user>.github.io/<repo>/ within a minute or two.

Notes:

  • Repository subpaths just work. Every URL in index.html and bootstrap.js is relative (./app/..., ./assets/...), so the site behaves identically at a domain root and under /<repo>/.
  • .nojekyll is required. It tells Pages to serve files as-is instead of running Jekyll.
  • Custom domains are opt-in: this template deliberately ships no CNAME file. If you want one, add a CNAME file containing your domain and configure DNS per the GitHub Pages docs.
  • Caching: config.json and app/manifest.json are fetched with cache: "no-store", while every hashed file is safe to cache indefinitely; its URL changes when its content does.

How the boot works

  1. bootstrap.js fetches config.json and loads the pinned Pyodide from cdn.jsdelivr.net, wiring Python stdout/stderr into a capture buffer (window.__wybthonPyLog) and the devtools console.
  2. micropip installs the pinned wybthon wheel from PyPI.
  3. Every file in app/manifest.json is fetched with its content-hash query parameter and written into Pyodide's virtual filesystem under /app.
  4. app.main.main() is awaited, mounting the component tree into <div id="app">; the loading overlay (with live status and a real progress bar) fades out.

If any step fails, an overlay shows the error, the recent Python output, and a Retry button that reloads the page.

License

MIT © 2026 Owen Carey

About

Static Wybthon app template: Python in the browser, no build step.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages