tawasuyu · Making
Your WordPress, without WordPress: the same site to the pixel, with its store, without a single line of PHP
A WordPress site is a program that runs every time someone visits, with its database, its plugins and its admin doors open to the internet. qhatu takes it whole, identical to the pixel —and with its store, if it has one—, and leaves nothing to execute: the same site, faster, and without a single line of PHP.
12 min readBy Gioser Editorial
Every time someone opens a page of a WordPress site, a program starts on the server: it reads the database, loads the theme, wakes up every plugin, builds the HTML and sends it. The same thing, over and over, for text that changes once a month. And all the while, that program leaves the doors it is administered through open to the whole internet. This article tells how qhatu —tawasuyu's content manager— takes a real WordPress site whole, leaves it identical to the pixel, brings its WooCommerce store along, and strips away everything that could be attacked.
The site that is built on every visit
WordPress is the most common way to have a site, and for good reasons: it installs in an afternoon, there is a theme for everything, and a builder like Elementor lets you design without writing code. The price is paid later, in installments. Every plugin is someone else's code running with all of the site's permissions; every postponed update is a door; and the database, the login and the API stay listening around the clock, even if one person edits the site twice a month.
The site all this was tested on belongs to a real client: a retreat center built with WordPress, the WoodMart theme and Elementor Pro, with 64 pages, a gallery, a blog and two contact forms. An ordinary site, of the kind there are millions of. The request was simple: keep it exactly the same, and keep it easy to edit.
One command
What was done by hand on the first day —downloading the rendered site, cleaning what WordPress adds to every file name, re-pointing the forms— is now a verb:
qhatu absorb https://the-site.com/ folder
It reads the sitemaps WordPress publishes, downloads every page and everything it needs —stylesheets, scripts, images in all their sizes, fonts, icons—, and leaves every reference pointing to the local copy. It does not download what is not content: the cart, the admin panel, the feeds, the searches. If part of the site is only assembled by JavaScript in the browser, with --render each page first goes through a headless Chrome and is saved already assembled. On the client's site:
| what | measured |
|---|---|
| pages | 64 |
| resources (stylesheets, scripts, images, fonts) | 448, 45 MB |
| time, with a courtesy pause between requests | 6 minutes |
| broken references in the copy | 0 |
And it does not have to be WordPress: the same verb took a Joomla site, a Squarespace demo that builds everything with JavaScript, and the first website in history, CERN's, with zero broken references in all three.
Identical to the pixel, with a control arm
"Exactly the same" can be measured. The same page of the original and of the copy was opened in Chrome, at 1366 by 2400 pixels, and the two screenshots were compared point by point. On the home page and on a blog post, 0 different pixels out of 3,278,400. For that zero to mean something, the test has a control arm: each screenshot has 216,987 distinct colors, so it is not comparing two blank pages.
And honesty includes what did not come out zero. On a third page, 1,785 different pixels showed up in the menu strip. Before blaming the copy, the original was compared with itself, twice: it gave 1,642 and 0. It is the page itself, which does not paint the same way twice in a row.
No old link breaks
A site with years has links with years: on Facebook, in emails, on other blogs. WordPress answers addresses nobody typed by hand —/?p=123, /2023/05/a-post/, the categories, the feed— and a move that loses them leaves a trail of 404 errors. qhatu builds a table with all of them, and the server checks it before the files. Against the live site: 143 old addresses, none of them a 404 —55 that are still a page and 88 that the table takes to their new place—. And an invented one still gives a 404, which is what has to happen.
Nothing to hack
A WordPress scanner always starts the same way: it asks for the addresses every WordPress has. That same question was asked of both sites, in passive mode —only read requests to public addresses, without trying a single password—:
| address | what it is | WordPress | copy |
|---|---|---|---|
/wp-login.php | the login door | answers | 404 |
/wp-admin/ | the panel | answers | 404 |
/xmlrpc.php | the classic door for batch brute force | answers | 404 |
/wp-json/ | the API | answers | 404 |
/wp-json/wp/v2/users | the list of who can log in | answers, with 3 names | 404 |
/?author=1 | the first author's username | tells it | no |
/readme.html | the exact version | answers | 404 |
/wp-admin/install.php | the installer | answers | 404 |
/wp-cron.php | tasks anyone can trigger | answers | 404 |
Nine out of nine against zero out of nine. On top of that, the original announced in its own code the exact version of WordPress and of two of its plugins: exactly what an attacker looks for to cross against a list of known flaws. It is not that the copy has those doors better locked: it does not have them. There is no PHP to execute, no database to query, no users to guess.
Measuring this turned up a detail worth telling. The copy still said "WordPress" in an invisible tag of the HTML, inherited from the original: a scanner read it and announced a WordPress that no longer existed. The absorber now removes it.
And the contact forms, which in WordPress are one more plugin? On the copy they are handled by a WebAssembly function that runs on the server with declared capabilities: it can store what arrives, read the clock and send the notice by email, and nothing else. It cannot read the disk; what it did not ask for does not exist for it. The messages are read in the panel, and download as a spreadsheet. The search magnifier the copy can add to its menu is another function like that: it reads an index published along with the site, and there is no search server to attack.
And if it also sells
Many WordPress sites are also a store, with WooCommerce. A store is not a set of pages: it is a catalog with prices, variants and stock, and a checkout. So it is not copied as HTML: with --as-content, qhatu reads the catalog through the same public API WooCommerce's own cart uses, and every product goes into qhatu's own store with its name, its exact price, its currency, its images and one variant per combination —size, color—, with the sold-out one marked as sold out. The price never goes through a number with a decimal point: it is stored in cents, and at checkout the server recalculates it against the published catalog, so nobody pays whatever they like by sending another number.
It was tested against a real WooCommerce, with the sample products WooCommerce itself ships with: 16 products and 7 variants came in, and an instrument that compares the original store against the copy, product by product and variant by variant, came out identical. Its control arm: after absorbing, a price was changed and a variant sold out on the original, and the instrument named exactly those two differences. Each product's reviews come with it, at the foot of its page. And with a WordPress application password —the kind WordPress itself lets you create for this— comes how many units are left of each product and each variant too: the server counts down what is sold. Then a purchase was made: a hoodie, into the cart, paid with PayPal in its test environment, and the store came back saying the order was paid, with PayPal's capture recorded on the server side.
That store is online, to look at and to buy with play money: tienda.gioser.net.
And it does not look like two sites stuck together. What qhatu builds —the products, the list, the cart, the blog posts— goes inside the original site's header, menu, footer, colors and fonts. Measured: the header of a page that came from the original and that of a product built by qhatu give 0 different pixels. The phone menu opens on the copy even with the original WordPress already turned off, and it no longer offers "Checkout" or "My account", which do not exist on the copy.
Faster, and measured without cheating
Five pages, five cold loads on each side —a fresh browser per load, no cache—, alternating original and copy so that a bad streak on the network does not fall entirely on one side. The medians:
| page | first byte | largest paint | full load |
|---|---|---|---|
| home | 2.5 s → 0.9 s | 6.1 s → 4.9 s | 9.9 s → 5.9 s |
| service page | 1.9 s → 0.8 s | 6.5 s → 3.8 s | 14.9 s → 5.4 s |
| long page with gallery | 2.3 s → 0.9 s | 11.7 s → 6.3 s | 22.3 s → 7.9 s |
| blog listing | 2.4 s → 0.7 s | 4.5 s → 4.1 s | 5.1 s → 5.3 s |
| a post | 2.3 s → 0.8 s | 5.9 s → 2.5 s | 7.0 s → 5.2 s |
The bytes that travel are practically the same: it is the same page. What changes is the server. And there is a caveat an honest article has to make: the two sites are on different servers, in different places, and the copy was closer to where it was measured from. So the network was separated from the software. Opening the connection costs 392 milliseconds to the original and 179 to the copy; that is distance, not code. Subtracting one round trip on each side, what remains is the work of building the page:
And on a phone there is something more. When publishing, qhatu prepares every large image in several widths and the browser picks the one it needs: on the client's home page, seen at 390 pixels wide, 72% fewer image bytes are downloaded, without moving a pixel on a desktop screen.
And it is still edited, in place
A static copy that cannot be touched would be a museum. qhatu's is edited from a panel that lives at its own address —nothing on the public site links to it—, on the page itself, just as it looks:
- Click on a text and type. A bar offers bold, italics, links, lists, headings and quotes, and the colors are the site's own, not just any picker. Pasting from Word or Google Docs keeps the formatting and throws away the junk.
- Click on an image and replace it. Choosing several at once adds photos to a gallery: each extra one duplicates the item and puts its own in.
- Duplicate, move and delete any block: a card, a section, a list item. And a new page is a copy of another, at whatever address you want.
- The site's palette and fonts on the side. The colors the page uses, counted; change one and see it instantly, and a button applies it to the eighty-odd stylesheets Elementor left behind. Fonts change the same way. If a text stops being readable on its background, the panel says so with the contrast measurement in hand.
- Publishing is a signed act. Saving changes the folder; publishing turns the site into a new, signed version that the server serves whole or not at all. The history says what changed between any two versions, page by page. Going back to the previous one is a button, and it leaves the folder as it was too.
And what the original built with Elementor no longer stays as HTML that can only be touched in place: with --as-content it comes in as qhatu blocks —heroes, columns, carousels, FAQs— that are edited with the block editor and painted with the site's theme. On the client's site, two of its Elementor pages came in as 22 and 12 blocks, five heroes with their background photo and three carousels of ten to twelve photos, without a single widget left without an equivalent and without one image that failed to download.
All of that runs on saywa, tawasuyu's web server. On the machine where this site lives, saywa and the project's own forge have already replaced four programs: the web server, the git forge, the proxy and the PHP interpreter. Someone wrote both pieces from scratch, in Rust, with a rule that repeats across the whole project: what was not measured is not claimed.
What it does not do yet
- Charging real money has not been tested. The store charges with PayPal, and end to end it was tested in PayPal's test environment; with a real account, not yet.
- From WooCommerce the catalog and its reviews come, not the history. Past orders, customers, coupons and shipping and tax rules stay in WordPress.
- Rendering is slow. With
--renderthe client's site took about 30 minutes instead of 6: Chrome loads every page in full. - There are reader accounts, members-only pages and a newsletter, but no paid membership.
- The speed numbers are from one day and one place. That is why the network separation is there: it is the part that does not depend on where it is measured from.
How to try it
The store in this article is at tienda.gioser.net, and its panel is open: at tienda.gioser.net/_/ anyone can get in with «Try it as an author» and edit it at will; their changes are wiped after 24 hours. The same qhatu builds this journal and the tawasuyu manual, and with one more line in its configuration it becomes a wiki: links by title, each page's signed history, and two people editing the same page at once without overwriting each other. To have the same on your own machine, a freshly created VPS is enough: qhatu server install --domain the-site.com leaves the server, the panel and the certificate running —measured on Debian 13 and Fedora 44, with a test certificate authority: the home page answers over HTTPS seven seconds after having access to the machine— and qhatu absorb brings the WordPress over. qhatu lives in the tawasuyu repository —git.tawasuyu.net, on its own server— under 02_ruway/qhatu, and saywa under 03_ukupacha/saywa. The instruments of this article —exposure, speed, pixels and the store comparison— are in scripts/qhatu-careo/: anyone can run them against their own WordPress and against its copy, and compare.