A static site serves documents prepared before their requests arrive. The important question is whether visitors need different server-generated representations, rather than how many pages the site contains. Hundreds of articles can share one publishing model: edit sources, build HTML and assets, validate the output, then expose those files through a web server or CDN.

Put document generation at the right boundary

When an article means the same thing for every reader, assembling it on every request often adds little value. Templates, navigation, translations, and indexes can be resolved during a build. The serving path then needs the published files, not the authoring database or generator. Requests can still be logged, encrypted, cached, and access-controlled; static delivery does not remove these responsibilities.

Static HTML can include JavaScript and interactive controls. A client-side search index may suit a modest public collection, whereas private search or personalized results need a service that enforces access. Forms, comments, payments, and authenticated mutations likewise require appropriate processing. Identify those boundaries separately: making the article static does not make a browser authoritative for permissions or durable state.

Build-time generation moves work rather than eliminating it. Large collections can have expensive rebuilds, dependencies can fail, and content freshness is limited by the publishing pipeline. A new source edit is not automatically visible. For time-sensitive material, measure edit-to-publication delay, keep build failures visible, and verify the released document instead of assuming a successful editor save reached readers.

When it breaks

A common failure is serving the repository root. Source data, build scripts, or private drafts may then become downloadable even though no navigation links mention them. Use a dedicated output directory containing only intended artifacts. Excluding a document from an index is different from excluding its file from publication. Inspect the actual release inventory, including copied assets and generated search data.

Another failure is updating files individually while requests arrive. An HTML page can reference an asset not yet uploaded, or an old page can lose a dependency deleted during cleanup. Prepare complete releases, preserve necessary old assets, and use the hosting platform’s supported switch mechanism. Restoring an old source commit alone does not restore dependencies, artifacts, cache state, or external services.

Static output also carries injection risks. Untrusted text needs context-appropriate escaping; inserting it directly into HTML can turn an article value into active markup. Escaping text does not validate arbitrary URLs, sanitize intentionally allowed HTML, or protect JavaScript contexts. Review the actual template positions and source trust instead of treating static generation as automatic security.

Check it quickly

Save the following as static-build.py and run python3 static-build.py with Python 3. It creates a temporary source outside a public directory, builds one HTML file, and serves only that directory over loopback. The script exercises publication with actual HTTP requests and removes its temporary files after closing the server.

from functools import partial
from hashlib import sha256
from html import escape
from http.client import HTTPConnection
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Thread
import json
import os


class Handler(SimpleHTTPRequestHandler):
    def log_message(self, *args):
        pass


with TemporaryDirectory() as directory:
    root = Path(directory)
    public = root / 'public'
    public.mkdir()
    source = root / 'source.json'
    source.write_text(json.dumps({'title': 'Version one', 'text': 'Published content'}))

    def build():
        content = json.loads(source.read_text())
        document = f'<!doctype html><html lang="en"><head><meta charset="utf-8"><title>{escape(content["title"])}</title></head><body><h1>{escape(content["title"])}</h1><p>{escape(content["text"])}</p></body></html>'
        pending = root / 'next.html'
        pending.write_text(document)
        os.replace(pending, public / 'index.html')
        return document.encode()

    first = build()
    server = ThreadingHTTPServer(('127.0.0.1', 0), partial(Handler, directory=str(public)))
    thread = Thread(target=server.serve_forever)
    thread.start()
    try:
        def fetch(path='/'):
            connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
            try:
                connection.request('GET', path)
                response = connection.getresponse()
                return response.status, response.read()
            finally:
                connection.close()

        source.unlink()
        assert fetch() == (200, first)
        source.write_text(json.dumps({'title': 'Version two', 'text': '<b>literal text</b>'}))
        assert fetch() == (200, first)
        second = build()
        assert fetch() == (200, second) and second != first
        assert b'&lt;b&gt;literal text&lt;/b&gt;' in second
        assert fetch('/source.json')[0] == 404
        print(json.dumps({'serves_without_source': True, 'edit_requires_rebuild': True,
                          'published_sha256': [sha256(first).hexdigest(), sha256(second).hexdigest()],
                          'source_not_published': True, 'text_escaped': True}))
    finally:
        server.shutdown()
        thread.join()
        server.server_close()

First, the program deletes the editable source and still receives the previously published bytes. It recreates a changed source but confirms that requests continue returning version one until build runs again. The second build changes the document digest and returns version two. Its literal <b>literal text</b> is escaped in the HTML, and a request for /source.json returns 404. Expect all four behavior flags true and two different published hashes.

These checks passed with Python 3.14.4. The replacement uses os.replace on the same filesystem for one complete file. It does not demonstrate atomic publication of a multi-resource site, crash durability, automatic rollback, or a particular hosting provider’s deployment mechanism. The experiment isolates the source-to-artifact boundary. For a real release, inspect all generated files, test representative links and missing paths, and verify caching alongside artifact availability. Choose static delivery when this boundary matches the product, then operate the build and serving layers with their own observable checks.

Further reading

Sources