Build a website in two layers.
2-09: Meetings and Calendars on Your Own Side — Jitsi and CalDAV put gatherings and calendars on your own side. This chapter assembles, in your own hands, the web you show to the outside. Content in AsciiDoc and Mermaid, the frame in HTML, CSS, and minimal JavaScript, with Python connecting the two. The content format is the one from 2-07; Markdown does the same job. Where the baked HTML then goes is the next chapter's job.
React fatigue comes from the number of tools
For the past ten years, web development has been an arms race of frameworks.
jQuery, Backbone, Angular, React, Vue, Svelte. Build tools: Grunt, Gulp, Webpack, Rollup, Vite, Turbopack. CSS: Sass, Less, PostCSS, Tailwind, CSS-in-JS. Server-side rendering: Next.js, Nuxt, Remix, SvelteKit, Astro.
Each of them solved some problem. But line up the problems they solved against the problems they created, and it stops being worth it more and more often.
- The build configuration takes time to learn
- The number of dependencies is beyond what a person can follow
- Security updates never stop arriving
- Given time, the build stops working
- Even a small site needs a large folder
This is not the complexity of the technology. It is complexity we added ourselves. Come back to raw HTML, CSS, and JavaScript and all five clear at once. The browser reads those three directly, so no conversion layer sits in between. And those three plus text manuscripts are exactly what AI writes most reliably.
WordPress has four problems
"I'm not using React — we're on WordPress." Many people are thinking exactly that.
That matches the facts. 40.1% of the world's websites run on WordPress (W3Techs, 2026-10-05). In Japan too — corporate sites, personal blogs, news media, government sites — it is everywhere.
WordPress has its own problems, though. They differ in nature from React's, and are at least as serious, often more.
Problem 1: the content sits in MySQL
WordPress posts are not text files. They are records inside MySQL, mixed with HTML. Convert the same post to PDF, move it to another site, hand it to AI for analysis — every path starts with an export.
And the export format is .xml (WordPress's own WXR), with HTML tags and
proprietary shortcodes (like [gallery]) mixed in. The content stays on the
WordPress side. That is the same shape as the "data still inside Excel" seen in
2-03: Lay the Foundation — SQLite, PostgreSQL, pgvector, DuckDB, Polars.
Problem 2: plugins widen the attack surface
WordPress extends through plugins built by volunteers around the world. Dozens of plugins on one site is not unusual. Each surfaces vulnerabilities periodically. The whole site becomes a collection of attack surfaces. That four tenths of the world's web runs on the same machinery means, from the attacker's side, that four tenths of the targets are the same machinery.
Problem 3: updates stop each other
WordPress core, themes, plugins — all of them depend on each other. Update one and another stops working. "We applied an update and the site went down" is a common event in WordPress operations.
Problem 4: Python gives steadier AI output than PHP
WordPress themes and plugins are written in PHP. The AI can write PHP, but output is steadier in Python. If you are building tools alongside AI, leaning to the Python side is faster.
The urgent case is the online shop. It handles payments and customer personal data, which makes it a high-value target — card skimming, data leaks. The damage lands directly on money and trust. A WordPress shop should go static first of all. Hand payment to an external checkout page such as Stripe and never hold card data yourself. Let go of the running PHP, the database, and the admin login, and the attack surface goes with them.
Escaping WordPress takes seven steps
If you are on WordPress, the way out is settled. You move in the same *parallel operation* shape covered in 2-12: Build an API — Expose Core Logic with FastAPI.
- Keep the existing WordPress running, and export the content to text (Markdown or AsciiDoc; plugins or CLI tools automate it, and the AI writes the converter)
- Build the new site with text manuscripts + minimal HTML/CSS + Python
- Preserve the URL structure (to carry the search equity over)
- Verify behavior in a staging environment and prepare the reindex requests
- Pick the DNS cutover date
- After cutover, run WordPress read-only for one month
- If nothing surfaces, stop WordPress and cancel the hosting contract
The cost changes too. WordPress hosting carries a monthly bill; baked HTML placed on the 2-02 machine costs nothing extra (where it goes is 2-11).
WordPress, too, is left through parallel operation. Export to text, write only the frame in HTML and CSS, generate with Python, serve it static. That is how you stop WordPress.
If there is no time to rebuild, there is a faster move — *mirror the existing site
whole into static files*. A headless browser (Playwright) opens each page, saves the
HTML and every file it loads (CSS, JS, images, fonts), and rewrites references to
relative paths. By default it does not execute JavaScript; it saves the raw HTML the
server returns — light HTML, free of the huge DOM that ad and analytics widgets add
at render time. Turn rendering on only for sites that assemble their body with JS.
Either way the browser follows and downloads every file the page actually pulls in,
so nothing slips through the way it does with requests or wget. Put that on
static hosting and the running WordPress stops, attack surface and all. What
SiteSucker (Mac, paid) does, AI can write for you in Python (this site's repository
ships tools/mirror_site.py).
Split content from frame, in two layers
What is in a website splits into two.
| Kind | What you write | Tool |
|---|---|---|
| Content | Prose, tables, quotes, code, diagrams | AsciiDoc + Mermaid (Markdown works the same) |
| Frame | Header, navigation, footer, layout, color | HTML + CSS + minimal JavaScript |
| Connection | Pour content into the frame and emit HTML | Python |
Writing these mixed together has been the problem in web production so far. A React component holds prose, formatting, and logic in one place. A WordPress post holds HTML tags and prose in one place. The mixture makes itself felt later, when something has to be rewritten.
The AI-native split keeps content and frame fully apart. The content is written in AsciiDoc and Mermaid only, with no HTML tags. The frame is written in HTML and CSS and never touches the content of any individual article. Python connects them mechanically.
(AsciiDoc)"] MMD["diagram *.mmd
(Mermaid)"] end subgraph Frame["frame"] HTML["template.html"] CSS["style.css"] JS["main.js (minimal)"] end Py(("Python
build")) MD --> Py MMD --> Py HTML --> Py CSS --> Py JS --> Py Py --> Site["static HTML site"] MD -.->|as is| PDF["PDF / print"] MD -.->|as is| AI[("AI analysis")] MD -.->|as is| Book["e-book"] classDef good fill:#e8f5e9,stroke:#7a9a6d,color:#3a4d34 classDef bad fill:#fef3e7,stroke:#c89559,color:#5a3f1a class MD,MMD good class HTML,CSS,JS bad
Holding content as text adds exits
The biggest reason to write content in AsciiDoc and Mermaid is that the same data travels to exits beyond the web.
From the same manuscript file:
- A web page (Python converts it to HTML)
- A PDF (
pandocor the AI does the conversion) - A print-ready manuscript
- An e-book (EPUB)
- Input to AI for summary, translation, and questions
- Material to paste into other media
- Diffs and history in Git
- Co-editing with other people
The content does not stay on the web side. Content written in WordPress or Wix ends when the service ends. Content in Notion lives inside Notion's format. A text manuscript belongs nowhere in particular.
This is the web version of the "hold the content as plain text" idea from 2-07: Take Documents Back — Prose in AsciiDoc, Working Tables in a Grid, Printed Pages from Templates. Separate entrance, content, and exit.
- Entrance — the AI turns images, PDFs, audio, and Word into text
- Content — held in AsciiDoc and Mermaid, versioned in Git
- Exit — web, PDF, print, AI analysis, e-book. Python converts per use
Write the frame minimally
The frame — header, navigation, footer, layout, color — is written directly in HTML and CSS. Minimal is enough.
- HTML templates: one to a few files
- CSS: one file
- JavaScript: only where it is genuinely needed (a mobile menu toggle, say), tens of lines
You touch these a few times a year. Do not spend too much on the frame. The skeleton stays a skeleton, and the time goes into the content.
What you keep in hand
- HTML (structure) — one template file
- CSS (appearance) — one file
- JavaScript (only the moving parts) — tens of lines
- AsciiDoc + Mermaid (content) — one file per article
What you stop needing
- JavaScript frameworks (React, Vue, Angular, Svelte)
- Build tools (Webpack, Vite, Turbopack, Parcel)
- TypeScript (plain JS is enough)
- CSS frameworks (Tailwind, Bootstrap)
- Package managers (npm, yarn, pnpm) and
node_modules
What remains is raw web standards and text manuscripts. HTML / CSS / JS have no conversion layer, and AsciiDoc and Markdown are notations the AI handles well. Both can be placed exactly as the AI returns them.
The other reason to leave npm is the supply chain
Slow builds and heavy dependencies are not the whole of it. The other reason to leave npm is the structure itself: *other people's code runs on your machine, through the registry*.
Supply-chain attacks on the npm registry happen repeatedly. Typosquatting that lures a mistaken install with a similar package name, code injected through a dependency, package takeover through a compromised maintainer account, secrets exfiltrated in a postinstall script. The verifiable landmarks alone line up like this.
- 2018: the
event-streamincident (aimed at cryptocurrency wallets) - 2021: the
ua-parser-jsincident (mining and info-stealing malware injected) - 2022: the
colorsandfakerincidents (destructive commits by the author) - 2024: the
xz-utilsbackdoor (not npm, but the classic case of a long-running maintainer takeover)
- September 2025: the takeover of
chalk,debug, and more than 18 other packages (the maintainer's credentials were phished with a mail posing as npm)
Nearly every year, a large incident is reported. For any individual incident, go to the primary sources yourself with the procedure in 3-02: Checking the Story — Verifying Vendor Narratives Against Primary Sources.
The structure comes from npm's design
The issue lies in the design of the npm ecosystem itself.
- One project depends transitively on hundreds to thousands of packages
- Those packages are maintained by individuals all over the world
- Updates arrive automatically (
npm installpulls the latest) - The package manager executes dependency code without inspecting it
- Arbitrary code runs in build steps and postinstall hooks
Take over any one of them and it reaches every project that pulled it in. One package's takeover reaching everyone who uses it without knowing is a shape that comes along with npm. How that shape plays out over time meshes with 3-05: The Lock-In Problem.
Against that, this chapter's stack carries few dependencies. AsciiDoc + HTML + CSS + JS has zero external dependencies (browser standards only), and the Python build has a single-digit count. With a single digit, reading all of the source together with the AI is within reach, and odd behavior is noticeable. Hundreds or thousands are beyond what a person's eyes can follow. This chapter's decision is to keep the count to a single digit. The surface that can be compromised is that much smaller.
What it means to put AI into dependencies you cannot follow
There is one more weight to this in the age of AI.
An AI agent reads files and runs commands behind the editor. If a malicious package
is sitting somewhere in node_modules on the development machine, *its execution
can be triggered through the AI*.
Putting AI into a project whose dependencies you cannot follow is the same as inviting that many strangers you cannot vouch for into your own workspace.
A stack with no dependencies carries none of that worry. HTML, CSS, and text are just text. The only code that runs is code you and the AI wrote. Not merely "light" and "fast," but "no entrance to be compromised" — that is the other reason to hold dependencies down.
Python connects content and frame
Turning AsciiDoc into HTML, Mermaid diagrams into SVG or images, and pouring them into the frame template — that is the job of a Python script.
articles/foo.adoc ──→ Python ──→ html/foo/index.html
↑
tools/templates/article.html (frame)
You do not need to write that script yourself. Ask the AI to write it. Convert
AsciiDoc to HTML with pyasciidoc, pour it into the template with Jinja2. A
short script does it.
A single-digit count of dependencies covers it.
pyasciidoc— an AsciiDoc parser (built onmarkdown-it-py, so Markdown reads as-is too)Jinja2— an HTML template enginePillow— image processing (OG image generation, only when needed)- Mermaid diagrams render in the browser, or go to SVG with
mermaid-cli
Type uv add pyasciidoc jinja2 pillow once, and from there git clone && uv sync
brings it up on another machine. No node_modules appears.
This site's build script (tools/build_article.py) was mostly written by the AI.
The build tool can be held in your own hands now. Instead of following a framework's
conventions, write under your own. Diagrams and documents continue in
2-13: Make Diagrams and Documents — Mermaid, Marp, and the Other Tools.
For the moving parts, pick one FastAPI
"You can't build dynamic sites that way," you might think. The moving parts go on the server side. Python's FastAPI, that one alone.
Flask, Django, Go, Rust, Ruby — there are mountains of choices. But each added choice divides the organization by that much. "We write in FastAPI" — once decided, the argument past that point ends. Pick, then forget you picked. That is how tool choice works here.
Why FastAPI:
- It is Python, so it lines up with the rest of this series' stack
- Type hints and Pydantic work naturally
- async is standard
- OpenAPI documentation generates itself
- The AI writes it well (open source, with rich training data)
- Little boilerplate
And that FastAPI is kept minimal too.
- No ORM. Write SQL directly with a PostgreSQL driver (asyncpg or psycopg)
- No stacked layers. No repository layer, service layer, or domain layer — receive the request, run the SQL, return the HTML
- No growing dependencies. FastAPI, the PostgreSQL driver, and Jinja2 (when needed) are enough
- No growing config files. Environment variables run it
- One process
One handler takes this shape.
@app.get("/items", response_class=HTMLResponse)
async def items():
rows = await conn.fetch("SELECT name, price FROM items")
return render("items.html", rows=rows)
Receive the request, run the SQL, return the HTML. From there, keep asking what more is actually needed. Since the AI writes SQL directly, it reaches without an ORM's abstraction in between. The AI already knows the syntax; what the human hands over is the decisions.
Client-side JavaScript stays minimal as well. <a> handles link navigation,
<form> handles submission. If partial updates are needed, HTMX (a few KB library,
not a framework). Add WebSocket when it becomes genuinely necessary. Do not start
with an SPA.
How to write FastAPI, and how to expose core logic as an API, belong to 2-12: Build an API — Expose Core Logic with FastAPI. Here, the reason for the choice is enough.
What this shape builds
"Content in text, frame in HTML and CSS, Python in between" reads faster in cases.
- A sole proprietor's portfolio — one
.adocper work plus images, one template file and onestyle.css - A small business's corporate site — about, services, contact, and blog as manuscripts. The contact form is FastAPI over the 2-08 mail; the job list is SQLite + FastAPI
- A non-profit's event announcements — one manuscript per event, like
events/2026-04-foo.adoc. Python generates the yearly index; sign-ups go to the booking page of 2-12 - A school's class newsletter (parents only) — weekly manuscripts and photos, one
style.cssin the school colors. Basic auth (htpasswd), or behind the gate from 2-05: Stand Up the Gate — One Login with PocketBase - A research group's papers and data — the paper manuscripts plus dataset metadata, with the data downloaded directly as Parquet
What they share: a single-digit dependency count, short builds, almost no extra cost, and AI writing most of the code. Both building and maintaining are light. This site (aiseed.dev) itself runs in this shape.
This combination still runs in ten years
When a framework's major version moves, the way of writing changes and the old way stops building. Build-tool configuration needs rewriting to match.
The core specs of HTML, CSS, and JavaScript have held backward compatibility. A file written in HTML 4.01 in 1999 opens in today's browser. Markdown has barely changed since its first edition in 2004, nor AsciiDoc since its first in 2002. The same files will read in the years ahead.
Frameworks depend on their era. Web standards and text manuscripts cross eras.
In numbers
The number that matters when deciding is the dependency count. Counted on this
site (aiseed.dev): 9 Python dependencies in requirements.txt, 443 baked pages,
and 7,928 lines of build scripts (2026-10-05, countable in the public repository
aiseed-dev/website). Zero React, Next.js, or TypeScript. AsciiDoc, Mermaid, and
minimal HTML/CSS only. Nine dependencies can be read to the last one.
Hand the decisions over as they are
Here are this chapter's decisions in one place. When you have AI build the site, handing this over is enough. The AI already knows the syntax; what the human hands over is the decisions.
| Decision point | What is decided |
|---|---|
| Content format | AsciiDoc + Mermaid (Markdown works the same). No HTML tags inside the content |
| Frame | One to a few HTML templates, one CSS file, tens of lines of JavaScript |
| The connection | Python. pyasciidoc + Jinja2 (Pillow only when images are needed) |
| Where things live | Content in articles/, frame in tools/templates/, output in html/ — all inside Git |
| What is not placed | JS frameworks, build tools, TypeScript, CSS frameworks, npm and node_modules |
| Moving parts | FastAPI. No ORM; SQL written directly. Jinja2 for templates, HTML as the response |
| Partial updates | HTMX only when needed. No SPA |
| Publishing | Serve the baked static files, from your own machine or Cloudflare Pages (2-11) |
How to check you are done
This chapter is done when these five hold.
- One article sits there as an AsciiDoc file, with no HTML tag inside it
- One command runs the build, pages appear under
html/, and it finishes in seconds - Opening the produced HTML in a browser shows the frame (header, navigation, footer) and the body, with the Mermaid diagram drawn
- The dependency list fits in a few lines, and
node_modulesis nowhere - Making a PDF from the same manuscript gives you the content as it is
uv add pyasciidoc jinja2 pillow
uv run python -c "import pyasciidoc, jinja2; print('ok')"
What the human holds
Values the human supplies
- The site's structure — which pages exist, and in what order
- Colors, logo, typeface
- How URLs are formed (when moving from an existing site, its URL structure)
- The range of prose and images that may be published
- Which packages may enter the dependencies
Actions the AI states before performing
- Deleting the output folder wholesale, or overwriting an existing site
- Stopping the existing WordPress, or cancelling the hosting contract
- Changing a published URL structure
- Pointing a crawl at a site that is not yours (where
tools/mirror_site.pyis aimed) - Adding a new dependency package
Versions checked, and when
pyasciidoc0.5 (PyPI),markdown-it-py,Jinja2,Pillow— no version pinned- Playwright,
mermaid-cli— no version pinned - FastAPI, asyncpg / psycopg, HTMX — no version pinned
- WordPress's share is W3Techs; the npm incidents are the vendors' reports; checked 2026-10-05
- The procedure was written on 2026-09-21 and reviewed on 2026-10-05
- If a version has moved, have the AI confirm the official procedure before proceeding
Summary
Split the web-building tools into two layers.
- Two layers — content in AsciiDoc and Mermaid, the frame in HTML, CSS, and minimal JavaScript, with Python connecting them
- Leaving WordPress — seven steps move the content to text. With no time to rebuild, Playwright mirrors the whole site into static files
- Exits for the content — the same manuscript becomes the web, a PDF, print, input to AI, and an e-book
- A single-digit dependency count — npm supply-chain incidents land nearly every year. A single digit can all be read through. Putting AI into dependencies you cannot follow invites that many strangers in
- The moving parts — pick one FastAPI, with no ORM and no stacked layers
- Durable in time — web standards that have held backward compatibility, and text notations unchanged since their first editions
At this point the HTML that can be published exists in your hands. The next chapter
publishes that html/. There are two homes for it: your own machine from 2-02
(Caddy), or Cloudflare Pages.
Related articles
- 2-11: Publish the Web — Your Own Machine, or Cloudflare Pages
- 2-07: Take Documents Back — Prose in AsciiDoc, Working Tables in a Grid, Printed Pages from Templates
- 2-12: Build an API — Expose Core Logic with FastAPI
- 2-13: Make Diagrams and Documents — Mermaid, Marp, and the Other Tools
- 3-05: The Lock-In Problem