2-10 / Series
2-10 № 10 · 2026

Content in AsciiDoc,
the frame only HTML and CSS.

Content in AsciiDoc and Mermaid, the frame in minimal HTML and CSS

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.

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.

  1. 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)
  2. Build the new site with text manuscripts + minimal HTML/CSS + Python
  3. Preserve the URL structure (to carry the search equity over)
  4. Verify behavior in a staging environment and prepare the reindex requests
  5. Pick the DNS cutover date
  6. After cutover, run WordPress read-only for one month
  7. 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.

flowchart LR subgraph Content["content (reusable at other exits)"] MD["article *.adoc
(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:

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.

Write the frame minimally

The frame — header, navigation, footer, layout, color — is written directly in HTML and CSS. Minimal is enough.

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

What you stop needing

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.

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.

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.

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:

And that FastAPI is kept minimal too.

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.

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.

  1. One article sits there as an AsciiDoc file, with no HTML tag inside it
  2. One command runs the build, pages appear under html/, and it finishes in seconds
  3. Opening the produced HTML in a browser shows the frame (header, navigation, footer) and the body, with the Mermaid diagram drawn
  4. The dependency list fits in a few lines, and node_modules is nowhere
  5. 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

Actions the AI states before performing

Versions checked, and when

Summary

Split the web-building tools into two layers.

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