{"slug": "agentic-flow-for-a-simple-spa-anyone-can-build", "title": "Agentic Flow for a Simple SPA Anyone Can Build", "summary": "A developer known as kolodkin released spa-template, a GitHub template for building single-page apps from plain HTML, CSS, and ES modules with no bundler, Node toolchain, or build step. The workflow pairs a Python static server and Playwright tests with a GitHub Actions pipeline that tests, copies files, and deploys to GitHub Pages, and the developer says an AI agent authored every line across 97 pull requests that were merged after screenshot review.", "body_md": "I've got three sites live that look nothing like each other, and I vibe-coded all of them. An agent wrote every line across 97 pull requests; I reviewed the screenshots and merged.\n\nUnder the hood they're the same thing: a folder of plain HTML, CSS and ES modules, a Python `http.server` for local dev, Playwright tests, and a GitHub Actions workflow that deploys to Pages on every push to `main`. No bundler, no Node toolchain, no build step.\n\nGetting your own is a two-minute job. [kolodkin/spa-template](https://github.com/kolodkin/spa-template) is a GitHub template, so hit **Use this template** — or say to your agent:\n\ncreate an SPA GitHub Pages repo based on [https://github.com/kolodkin/spa-template](https://github.com/kolodkin/spa-template)\n\nGetting each one to a first deployed page took minutes. Steering the UI to something I actually liked took weeks. This post is about the flow that made the second part survivable.\n\n``` python\nweb/\n  index.html      import map + one <script type=\"module\">\n  main.js         exports init(); replace with your app\n  styles.css\n  favicon.svg\nserve.py          static server with ES-module MIME types, no caching\nrun.sh            ./run.sh -> http://127.0.0.1:8000\ntests/            Playwright e2e\nconftest.py       starts the server on a free port for the tests\n.github/workflows/pages.yml\n```\n\n**No bundler.** Libraries are resolved by an import map in `index.html`, and that's the whole build system. Here's what PCL Viewer adds for three.js, for example:\n\n```\n<script type=\"importmap\">\n{\n  \"imports\": {\n    \"three\":         \"https://unpkg.com/three@0.160.0/build/three.module.js\",\n    \"three/addons/\": \"https://unpkg.com/three@0.160.0/examples/jsm/\"\n  }\n}\n</script>\n```\n\nAll three sites also use Preact with [htm](https://github.com/developit/htm), which gives you components without JSX, so there's nothing to transpile. That's my choice for these projects, not something the template cares about. Put whatever you like in the map.\n\n**The filesystem is the router.** What They Mean's six concept pages are six folders, each with its own `index.html`, `app.js` and `styles.css`, linked from the menu by a relative path. No JS router, no server rewrites, and no way for one demo's styles to leak into another. Adding a page is `cp -r web/db web/<name>` plus a card on the menu.\n\n**One flag keeps the tests fast.** Every app sets `window.__APP = { ready: true }` once it has rendered, and the tests wait on that instead of sleeping:\n\n``` python\ndef test_page_loads(server_url, page, shot):\n    page.goto(server_url + \"/\")\n    page.wait_for_function(\"() => window.__APP && window.__APP.ready === true\")\n    expect(page.locator(\"h1\")).to_have_text(\"Hello, world!\")\n    shot(\"home\")\n```\n\nThat `shot(\"home\")` is the other half of the trick. Tests take a screenshot whenever they reach a state worth seeing, numbered in order, so after every run `test-results/shots/` reads like a walkthrough of the app. Archer takes this further: its suite actually plays the game through `window.__ARCHER` hooks with a seeded RNG, so every run is reproducible and the screenshots show real arrows in flight.\n\n**Deploy: test, build, deploy.** Pull requests run the first two jobs as a check. Pushes to `main` run all three.\n\n```\njobs:\n  test:\n    steps:\n      - uses: actions/checkout@v4\n      - uses: astral-sh/setup-uv@v5\n      - run: uv sync\n      - run: uv run playwright install --with-deps chromium\n      - run: uv run pytest\n\n  build:\n    needs: test\n    steps:\n      - uses: actions/checkout@v4\n      - run: mkdir -p _site && cp -r web/. _site/\n      - uses: actions/upload-pages-artifact@v3\n        with: { path: _site }\n\n  deploy:\n    if: github.event_name != 'pull_request'\n    needs: build\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - id: deployment\n        uses: actions/deploy-pages@v4\n```\n\nYes, the \"build\" is a `cp`.\n\nThe one-time setup is **Settings → Pages → Source: GitHub Actions**, and the site shows up at `https://<owner>.github.io/<repo>/`. Pages only serves one artifact per repo, so the samples repo stages both apps into it (`_site/pcl-viewer` and `_site/archer`) with a root redirect.\n\n```\n./run.sh              # edit, refresh, repeat\nuv run pytest         # tests + fresh screenshots\ngit push              # CI tests and deploys\n```\n\nThere's nothing to install beyond `uv` and a browser. No `node_modules`, no watcher. You refresh the tab and your edit is there, because `serve.py` sends `Cache-Control: no-store`.\n\nWith an agent driving, one round is: I describe the change, it works on a branch, runs the suite, and opens a pull request. I look at the screenshots that run produced, and either merge or say what's still wrong. That's it. Every one of the 97 merged pull requests across these three repos came in on an agent branch, and I never once had to read a diff to find out what the page now looked like.\n\nThe screenshots are why this works. Reviewing an agent's UI change by reading its code is slow and unreliable, and spinning the app up myself defeats the point of delegating. A folder of numbered PNGs from the run I'm reviewing answers the only question I have, which is whether it looks right yet.\n\nFrom an empty folder to a tested, documented, deployable first version:\n\nThese aren't toy apps. The viewer decodes Draco-compressed LiDAR frames through a bounded worker queue. The game runs animated glTF characters with arrow physics, cover, and a monster radar. What makes them fast to start is that the structure has already answered every question that isn't about the app itself. Where do files go? `web/`. How do I run it? `./run.sh`. How do I ship it? Push.\n\nEvery one of these sites was live within a day. Then the UI ate weeks: 24 pull requests on PCL Viewer in 8 days, 48 on Archer in a month, 25 and counting on What They Mean.\n\nAn agent will build you the thing you asked for. It cannot guess what you'll think of it once you see it, and neither can you. So most of those pull requests are one round of me looking and saying \"no, more like this.\"\n\n**PCL Viewer — Where should the camera start?** Bird's-eye at first. Then low and forward-facing. Then closer to the sensor. Then aimed down the road. Then an elevated chase-cam. Six tries to settle a question no user ever consciously asks.\n\n**Archer — How should the aim cue look?** It started as a bullseye, became a dashed trajectory lane, then got a marker at the impact point, then a soft warmth on the hit zone, then a brighter one. Where it ended up is a small point light on the exact patch of ground or enemy the arrow would hit, with nothing drawn over the scene at all.\n\n**What They Mean — How tall should a panel be?** The database demo's context pane went from half the screen to \"size to content\" to \"cap at 50vh\", all in one day. The Play demo button moved three times before it settled next to the back link.\n\nThose are just a few examples. Most of the 97 were this kind of small correction, and I couldn't have specified any of them up front. You look at the page, something's off, you say so, you look again.\n\nGoing back and forth on a panel height a few times in one day might look like indecision. It's just what it takes to get a UI right. The agent makes each try cost minutes instead of an afternoon, so you can afford to keep going until it feels right — which is the whole trick, because taste is the one part you can't delegate.\n\nFeel free to reach out with any questions, in the comments or directly. Happy to go deeper on any part of this.", "url": "https://wpnews.pro/news/agentic-flow-for-a-simple-spa-anyone-can-build", "canonical_source": "https://dev.to/mark_kolodkin_4b39d571d41/agentic-flow-for-a-simple-spa-anyone-can-build-4ck0", "published_at": "2026-09-21 18:42:18+00:00", "updated_at": "2026-09-21 19:01:47.265506+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["GitHub", "GitHub Pages", "Playwright", "Preact", "htm", "three.js", "kolodkin"], "alternates": {"html": "https://wpnews.pro/news/agentic-flow-for-a-simple-spa-anyone-can-build", "markdown": "https://wpnews.pro/news/agentic-flow-for-a-simple-spa-anyone-can-build.md", "text": "https://wpnews.pro/news/agentic-flow-for-a-simple-spa-anyone-can-build.txt", "jsonld": "https://wpnews.pro/news/agentic-flow-for-a-simple-spa-anyone-can-build.jsonld"}}