Sitehoster is how all of my static sites are hosted, this one included. Each site is a folder on my own computer, named after its domain. A small tool mirrors the folders to my own server, which serves each one over HTTPS. Its own description is the shortest: drop a folder, get an HTTPS site.
There are three small pieces: Caddy in front, serving the sites and looking after the certificates, an API on the server that only Caddy can reach, and the local tool that sends the changes up.
Folders are domains
The one rule is that a top-level folder named after a domain becomes that site:
sites/
config.json loose files aren't synced
lewpen.com/ https://lewpen.com/
index.html
articles/
stylesheet.org/ https://stylesheet.org/
blog.example.com/ a subdomain is just another folder
_example.com/ starts with _, so it's offline
- A subdomain is another folder, named for the whole host.
www.example.comredirects toexample.com, unless there's awww.example.comfolder of its own.- Plain HTTP redirects to HTTPS.
- A folder starting with
_isn't synced, so renaming a site's folder is a switch that takes it offline. Anything starting with.is skipped at any depth. - Files loose at the top level aren't synced either, which is why the config file can sit next to the sites.
A new site is a new folder, and it's live on the next sync. There's nothing to configure on the server and nothing to restart.
Automatic HTTPS
Caddy gets a certificate from Let's Encrypt the first time someone visits a domain, once its DNS points at the server, and renews it from then on. To stop anyone asking for certificates for domains you don't host, Caddy checks with the API first, and the API only says yes when there's a folder for that domain:
{
on_demand_tls {
ask http://127.0.0.1:8787/internal/allow
}
}
https:// {
tls {
on_demand
}
handle {
root * /opt/localhoster/sitehoster/sitehoster-sites/{host}
file_server
}
}
{host} is the name the visitor asked for, so the same two lines serve every site there is.
Stateless sync
The local tool and the server don't keep any record of what's been sent. Each sync compares the two sides afresh, using a tree of hashes built from file metadata, never the contents:
- Each file's hash is the SHA-256 of its size and modification time. Each folder's hash covers its children, in order.
- The tool asks for the server's root hash first. If it matches, nothing has changed anywhere, and the sync is over after one request.
- If not, it goes down only into the folders whose hashes differ, uploads the files that changed with their modification times, and deletes anything that's gone.
It's rsync's quick check, size and time, applied to a whole tree at once. Uploads are written to a temporary file and renamed into place, so a visitor never sees half a file, and either end can be restarted at any point without anything to repair. (The local tool does keep an index file, but only to save rebuilding the tree each time. Delete it and it's rebuilt.)
Because a sync with nothing to do is a single request, it's cheap enough to run all the time. I run it every 30 seconds, so a saved file is live within a minute.
The server
The server needs Linux with systemd, Caddy and Node.js 20 or later. One command installs it:
curl -fsSL https://sitehoster.org/install.sh | sudo bash
The installer prints a token, which goes in the local tool's config file along with the server's address and the folder the sites are in:
{
"sitehoster-config-serverAddress": "203.0.113.5",
"sitehoster-config-apiToken": "the token the installer printed",
"sitehoster-config-contentRoot": "/home/you/sitehoster/sites",
"sitehoster-config-tools": "/home/you/sitehoster/sites/_tools"
}
The API is plain Node.js with no dependencies. It only listens on the server itself, runs as an unprivileged user, and answers anything without the right token with the same "Not Found" page as a missing file, so from outside there's nothing to find. The token file is the only thing the server keeps for itself; everything else is the sites.
On your own computer, the local tool is one command:
node sitehoster-local.js --config /path/to/config.json --sync
Automation
Static sites repeat themselves: the same header on every page, a list of articles on the home page, the same list again on each topic page. Sitehoster fills these in from markers written into the pages as HTML comments. The local tool runs them before every sync, writing the results into your files, so what you see locally is exactly what goes live. The markers themselves are stripped out on the way up, so the live pages are plain HTML.
There are four kinds:
- Duplicates. A block marked as the master is copied into every block with the same name, so the header and footer are edited in one place.
- Navigation. A duplicated menu where each copy highlights its own page.
- Records and listings. A page can carry records, sets of named fields. A listing anywhere on the site gathers the records of a collection, filters, sorts and groups them, and writes each one out through a template.
- Counts. The number of records that match, in a sentence.
Every article on this site carries a record:
<!-- sitehoster:record collection="articles" -->
<!-- sitehoster:record-field name="title" type="text" value="A queue on any table in PHP and MySQL" -->
<!-- sitehoster:record-field name="url" type="text" value="articles/2025/10/a-queue-on-any-table-in-php-and-mysql/" -->
<!-- sitehoster:record-field name="date" type="text" value="2025-10-14" -->
<!-- sitehoster:record-field name="month" type="text" value="2025-10" -->
<!-- sitehoster:record-field name="tags" type="text" value=",php,databases," -->
<!-- sitehoster:record-field name="summary" type="html"
```
How to make any table into an atomic queue without adding extra columns.
``` -->
<!-- sitehoster:/record -->
And the PHP topic page lists every record tagged php, newest first:
<!-- sitehoster:listing collection="articles" -->
<!-- sitehoster:listing-filter field="tags" op="contains" value=",php," -->
<!-- sitehoster:listing-sort field="date" order="desc" -->
<!-- sitehoster:listing-item
```
<a class="article-row" href="{{url}}"><h3>{{title}}</h3><p>{{summary}}</p></a>
``` -->
<a class="article-row" href="articles/2026/10/maintaining-a-php-framework-for-25-years/">...</a>
...
<!-- sitehoster:/listing collection="articles" -->
The whole of the articles section is built this way:
- Topic pages filter on the tags, month pages on
month, and year pages onyear, with a heading each time the month changes. - The Previous and Next buttons on topic and year pages are listings too: the nearest one before or after, limited to one, so a button disappears by itself at either end.
- The line under each article's title is a listing of the article's own record, so the summary is written once and appears on the page and on every card.
- The count on the home page is "
{{count}} articles so far."
Adding an article is just writing its page. Every listing it belongs in picks it up on the next sync.
The markers are strict. A marker the tool doesn't recognise, or one that's badly formed, is an error, and that site's pages are left as they were until it's fixed, while every other site carries on.
Why static sites
Most of my sites don't need a server-side language or a database, and without them there's very little to go wrong: very little software on the server to keep patched, nothing to back up but the folders, and pages that are as fast as the network.
They also suit working with AI agents. A static site is just files, which an agent can read and change like any other code, and with the records in the pages themselves there's no database for it to query or keep in step. An agent edits a page, the next sync rebuilds the listings and publishes it, and I look at the result on the live site a minute later.
Sitehoster has a browser extension for that last part. You describe a change while you're looking at a page, and it hands a ready-made prompt, with the page's address in it, to an agent working in your folder of sites, which knows from the address which folder and file to change.
Where a site does need something dynamic, there are sibling services that work the same way, a folder mirrored to your own server: Datahoster for an API, Userhoster for accounts, and others listed on Localhoster. The stylesheet the sites share is a site on Sitehoster too.
Things to know
- The server has to be reachable on ports 80 and 443 for Let's Encrypt to issue certificates.
- The local tool needs Node.js 20 or later, and nothing else. It runs on a Mac here.
- Sites are synced as they are on disk, so a page saved half-edited goes live half-edited on the next run.
- Only the top level of the sites folder is special. Inside a site, the folders are just paths.
See also
- A globally standard stylesheet - the one stylesheet these sites share
- My AI workflow in 2026 - building stylesheet.org and the other sites with Sitehoster
- Clients bearing AI gifts - including what to think about when choosing hosting