Skip to main content

PHP has a Static Site Generator

For a long time there was no Static Site Generator for PHP that really won me over.

Since Stenope came along, that has changed. So let me use this happy occasion to talk about the world of Static Site Generators.

At the end of this article I'll give you a short technical look at how I built this very blog post — the one you are reading right now — with Stenope.

What is a Static Site Generator?

Glad you asked! A Static Site Generator produces static HTML. Nothing more, nothing less.

Usually the data — blog posts, for instance — is kept in a text-based format like Markdown or JSON and turned into pages on demand.

The HTML that comes out of it is always static, which is to say unchangeable. Every single person in the world ends up seeing exactly the same page.

The idea is anything but new. HTML was made for precisely this purpose. In its early days the web was a collection of static HTML pages that people everywhere could reach.

Well-known Static Site Generators like Hugo or Gatsby have been around for a long time, and they are mostly used by tech-savvy people who are willing to keep their content in Markdown and version it with Git.

By now there are Content Management Systems (CMS) that at least offer this approach (Statamic), or go all in on it (TinaCMS). The idea is to combine the strengths of static HTML with those of a Content Management System. Static Site Generator is usually shortened to SSG.

What are the advantages of a Static Site Generator?

A Static Site Generator saves time and money. It makes your website faster, because the static HTML can go straight to your visitors' browsers with nothing in between. And the static HTML brings plenty of other advantages with it:

  • Static HTML can live on a Content Delivery Network (CDN), so it needs no server of its own.
  • Serving static HTML costs you very little, often nothing at all.
  • Static HTML is very secure — security holes like CVE-2026-109618 and CVE-2026-109619 need a server that runs code, and that is exactly what your site no longer has.
  • Static HTML is blazing fast — because it caches beautifully. In the browser as well as on a CDN.
  • Static HTML itself knows nothing of system updates. It's nothing but documents — not a complex application like a WordPress system.
  • Static HTML is long-lived. It just sits there on the web, and it will still be sitting there in ten years — without anyone lifting a finger.

Do I really have to generate my own HTML?

Of course, you don't need a Static Site Generator just to produce static HTML. You can get your own website online in 10 minutes:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Seeblick Training Institute</title>
    </head>
    <body>
        <h1>Seeblick Training Institute</h1>
        <p>
            First Aid, Fire Safety, Forklift Certification. New dates every
            month.
        </p>
    </body>
</html>

If you upload this manually to a CDN (Content Delivery Network), your free, blazing fast website will be available worldwide immediately.

Okay, it's not exactly pretty. But we can fix that with a little inline CSS. You simply develop locally on your laptop, and as soon as you're satisfied with the result, you upload everything again.

But what if you add more pages? Every website needs at least a legal notice and a privacy policy.

Now things are already starting to get more complicated. If you don't want every page to look different, you'll have to duplicate HTML structures on every single page from now on to keep your layout consistent.

Then it hits you — a menu would be a good idea.

If you continue to write your HTML yourself, you'll have to copy your menu onto every single page.

Making changes to your menu now forces you to go into every single HTML file and manually make the same change over and over. Sure, these days AI can help you with that, but even then, this method quickly becomes a breeding ground for inconsistency and wasted effort.

Let's just say you're particularly persistent and actually enjoy maintaining your HTML yourself. That enjoyment will fade the day you want to offer multiple versions of your content, such as JSON-LD metadata or Markdown versions of your pages.

Now you not only have duplicates of your HTML structures across all your HTML files, but also duplicates of the content itself. By this point, writing simple HTML has likely become uninteresting for most people.

And this is exactly where Static Site Generators come into play.

Take a look at this:

{# @var section App\Content\Section #}
{# @var article App\Content\BlogPost #}
{% trans_default_domain 'website' %}
{% extends "website/base/article.html.twig" %}

{% block title -%}
    {{ 'blog_post.meta_title'|trans({ '%title%': article.title }) }}
{%- endblock %}

{% block meta -%}
    {% include 'website/partials/_meta.html.twig' with {
        page_title: block('title')|trim,
        page_description: article.description,
        og_type: 'article',
        markdown_url: markdown_url,
    } only %}

    {% include 'website/blog/_article_meta.html.twig' with {
        article: article,
        section: section.title,
    } only %}
{%- endblock %}

{% block main_foot -%}
    {% include 'website/blog/_citations.html.twig' with {
        citations: article.citations,
        references: article.citationReferences,
    } only %}
{%- endblock %}

What you see here is the Twig template for this blog post you're currently reading. This concise template is also sufficient to generate all blog posts in all languages — not just this one in English.

Individual parts (known as “partials”) are also reusable. As you can see, there's an inheritance structure with the ability to override individual blocks of the parent template. Other sections are simply included as needed.

This keeps everything easy to read, consistent, and reusable. It improves maintainability and significantly reduces the cost of making changes — so it improves extensibility.

Is a Static Site Generator right for my website?

Let's say you run a company website on WordPress. You were handed the credentials to your admin area, and from there you can maintain your own content.

There's just one catch. Can you guess it? A problem you may not even be aware of. Do you really maintain your content? You could do it any time — but you hardly ever do, or you stopped doing it altogether.

I'm not blaming you. Your website is finished, after all. The world has kept turning since.

The problem now is that you're paying to run a complex application on the internet whose main purpose is to let you maintain content without having to hire anyone. And that, paradoxically, is exactly what landed you in an expensive dependency on your agency. Does this sound familiar?

There are studies showing you're not alone in this.

SIINDA, the European industry association for local search, examined the online presence of more than 350,000 small businesses in eight European countries for its 2019 Landscape Study. Of the websites analyzed, 46 percent had not been updated within a year.

Even pages that rank on the first page of Google are updated on average only every two years, according to an analysis.

So let's imagine your company wants to change something once a year — upload a new team photo, say, or publish a blog post about an award the company won. Then a Static Site Generator is a perfect fit. Your content is static — so static HTML is the most honest and most efficient answer for you and your business.

The following signs tell you that a static HTML website suits your company better than a Content Management System.

  • You barely ever log into your system, and you keep forgetting your credentials.
  • Your CMS is too complicated for you, so you call your agency even for changes the CMS was supposed to let you make yourself.
  • Your system costs you too much, and you wonder why a simple company website runs up that much every year.
  • Your system is too slow for you. You may already have moved servers and installed a performance plugin, and your site is still slow.
  • You want no more system updates. Every few years your CMS runs out of support, and the migration costs nearly as much as building it again — for what is essentially static HTML.
  • You want to feel good about it. Maybe you've been the target of an attack once, and you never want to fear again that your company website ends up a billboard for somebody else's junk.

Why does no one ever recommend a Static Site Generator?

If you have ever worked with an agency that relies on a Content Management System, you may already suspect why a different tune is sung there. They would rather not hear about Static Site Generators. Instead they talk up how easily you can maintain your website with the CMS. They show you shiny dashboards with endless possibilities and wonderful plugins.

That it also sells expensive maintenance contracts, upgrades to newer versions, relaunches and optimizations is no coincidence — it's the core of the business model.

There is also the wish for an all-purpose tool. Agencies want something that covers simple websites and complex systems alike. So the CMS quickly gets used for anything and everything and is declared the holy grail.

It also makes a handy selling point, and something that builds trust. The CMS is essentially the core of the agency, and a CMS lives off the many agencies that sell it.

But would you buy yourself a truck, service it, insure it and pay the running costs — just so you can pull it out of the shed you built specially for it once every year or two?

True, you can transport anything with it. From a tiny shoulder bag to a truckload of potting soil. Wouldn't that be absurd? Yes, it would.

Running a web application on the internet, dropping in every year or two to change a small piece of your static content — is just as wasteful.

How much can static HTML save?

Without static HTML you need a web server running a language like PHP, plus a database. Every minute that system loses power, your website is gone. Poof. Vanished. So it has to stand ready around the clock. Even at three in the morning.

I wanted to know what that actually costs — in electricity and in hosting.

A Hetzner EX44 — their entry model — draws 25 watts idle and 118 under full load.1 Running a WordPress site with modest traffic, that comes to roughly 350 kilowatt-hours a year. It rents for 44 euros a month, so 528 euros a year (as of October 2026).

There are cheaper ways to host a company website, and there are far more expensive ones. I'd say this entry model from Hetzner strikes a fair middle ground for this example.

You're probably wondering how much of those 528 euros you could save. The answer: just about all of it!

This very website costs me nothing to host. Cloudflare, a worldwide network of data centers, serves static files without limits2 — no matter how much data, no matter how often someone opens the page. Even a huge project with 100,000 pages runs about five euros a month, busy or not.

Of course Cloudflare runs servers too. They just don't run for your website alone. And a single request costs so little that it would take billions of them to match a year of your Hetzner EX44 sitting idle.13

That's because serving a finished file takes almost no computing power. No PHP starts up, no database is queried, no HTML is assembled. The finished file just sits there and gets passed straight through.

The machines in those data centers are anything but small. Each one serves tens of thousands of requests a second — precisely because a single request costs so little.

Server with WordPress Static HTML
Electricity 350 kWh a year the same amount covers billions of requests
Hosting 528 euros a year 0 euros a year
More visitors need a bigger server change nothing

Netcraft counts around 216 million active websites.4 As we saw earlier, 46 percent of the websites surveyed went a whole year without a single update.

Let's say, very conservatively, that just eight million websites worldwide could be static but are run dynamically. The potential yearly savings are enormous.

one website eight million websites
Electricity 350 kWh 2,800 GWh
Hosting 528 euros 4.2 billion euros

2,800 gigawatt-hours is almost what Malta uses in a year.5

And yet hosting is usually the smallest cost of all. The real cost drivers of a CMS are maintenance contracts, security patches, time-consuming migrations with every major update — and above all the ever-slowing pace of development, until the next relaunch becomes inevitable.

Does static HTML make you dependent on an agency?

If you have no wish to edit your content in Markdown yourself, and no wish to get involved with Git versioning, you'll most likely need a technician's help for your changes.

In that case, though, you're paying purely for the changes — and not for the running costs of a complex system. In other words, you're paying only for honest work and real value.

My clients now get an MCP connection to their website if they want one. That lets your AI chatbot manage the content of your site — including preview links with optional password protection. If that sounds interesting. Just get in touch. Then I'll show you what I mean.

The limits of static HTML

Static HTML is wonderfully suited to millions of company and portfolio sites, but of course it has a limited and clearly defined area of use.

Dynamic content

As the name suggests, static HTML isn't dynamic by nature. Every page has to be rendered in advance.

That becomes obvious as soon as many different combinations of the same page are possible — as with this course catalog.

Nobody can really know which combination a visitor will filter and sort by — categories, venues or prices.

You can solve this with JavaScript and tricks like hidden data embedded in invisible HTML tags. It's still ugly and a little hacky, though. And it isn't great for SEO either.

So for things like that, a simple web application is the right choice. And I deliberately don't mean a CMS — just because your website has one central dynamic element doesn't mean it needs WordPress.

Content with an expiry date

A year like the one in “© 2026 Braunstetter” will still read 2026 in static HTML long after 2046 has come around.

The same problem applies to the course catalog mentioned above. Pure static HTML would be showing hopelessly outdated courses twenty years from now.

This problem can be solved too. You can set up a scheduled pipeline, for instance. It renders your static website again at regular intervals and so keeps your data current.

You might be thinking: once my website depends on a regular rebuild, I'm dependent again on a system that has to still be running years from now.

That's true, of course. But this system is much smaller than a CMS, and you pay the price on purpose, for something you actually need. A good developer will pick a Static Site Generator that will still render your pages reliably years from now.

On top of that, HTML already sitting on the web is untouched by any of this. In the worst case your content would simply stop being updated. A CMS website whose server has a problem would suddenly start throwing ugly crashes, getting slower, or worse.

Protected areas

Static HTML is a real win for a lot of websites. If you want an area where you can look at all your courses, though, like this one, static HTML is unfortunately not an option. For an admin area like that you need a web application.

That said, you can run your static website through an MCP connection without any admin area at all. You can simply tell your AI agent: “Show me my last 5 blog posts.” From there you can work on a post together with your AI and publish it once you are happy with it.

Curious? Then get in touch and let's talk about it.

Forms

Without a running system, your website simply cannot take form submissions. Showing a form is perfectly possible, but static HTML cannot process any requests. There is nothing there to answer — just documents sitting on the web.

The good news: most websites don't need forms at all. The classic contact form mostly just puts people off. Or do you enjoy filling out forms like that?

At worst, a badly built contact form turns into a magnet for all kinds of annoying spam.

A mailto link is far more pleasant. It even lets you draft the mail for your readers. Click here — then you'll see it live. I'd love to hear from you.

Should your static website really need a form one day, serverless functions will do the job. Cloudflare offers Cloudflare Pages Functions for exactly that. Condolence messages, inquiries or registrations are straightforward to build that way.

A website built around forms should be a web application, though, not a static site.

Stenope

At the start I promised to show you how I rendered this blog post with Stenope. But let me put a few important things in perspective first.

What is Stenope?

Strictly speaking Stenope is not even a Static Site Generator — it is a Symfony bundle. Which makes Stenope nothing more than an extension for the popular Symfony framework.

Why Stenope?

For many developers Hugo, Gatsby or Next.js is surely the better choice. For me as a Symfony developer, though, Stenope has one huge advantage — the upgrade path.

Imagine a static company website is all you need at first. Over time, though, your demands grow. Maybe you want customer management, an employee platform or a product catalog.

If I started projects like this with Hugo, I would have to migrate them from Hugo/Go to Symfony/PHP the hard way, and I would spend days or even weeks on nothing but restoring the status quo.

With Stenope the step from static website to web application turns out to be comparatively easy. Essentially everything can stay the way it is. All that comes on top is the new infrastructure, web server and database — and of course the logic of your new business software.

Stenope in practice

Software only ever speaks one native language: code.

Think of machine code and assembly as sounds, syllables and single words. High-level languages like Java, PHP or Ruby finally let us form whole sentences — and with them, texts that say something.

I like to picture a project like this website as a book. Today I want to show you the short chapter that renders the very post in front of you.

// src/Controller/BlogPostController.php
final class BlogPostController extends AbstractController
{
    public function __construct(
        private readonly SectionRepository $sectionRepository,
        private readonly PageGraphBuilder $pageGraphBuilder,
        private readonly Trail $trail,
        private readonly MarkdownDocumentBuilder $markdownDocuments,
    ) {
    }

    #[ContentCollectionRoute('/{_locale}/blog/{slug}/')]
    public function __invoke(BlogPost $entry, Request $request): Response
    {
        if ($request->getRequestFormat() === 'md') {
            return $this->markdownDocuments->forBlogPost($entry);
        }

        return $this->render('website/blog/show.html.twig', [
            'section' => $this->sectionRepository->find(SiteSection::Blog),
            'article' => $entry,
            'parents' => $this->trail->to($entry),
            'markdown_url' => $this->generateUrl(
                route: ContentRouteName::forMarkdownRoute($request->attributes->getString('_route')),
                referenceType: UrlGeneratorInterface::ABSOLUTE_URL,
            ),
            'json_ld_graph' => $this->pageGraphBuilder->forBlogPost($entry),
        ]);
    }
}

One file, one page

Let's walk through it.

The whole BlogPostController is an ordinary Symfony controller with a single controller action.

What makes it special is the PHP attribute ContentCollectionRoute. It registers a route of its own for every blog post that sits as a Markdown file under content/blog/en/ — and Stenope renders that route as static HTML.

Whenever I want to add a new post, I simply create a file and start writing. Symfony and Stenope pick up the new post automatically.

Isn't that neat?

Markdown and JSON-LD

As you can see, there is even a Markdown version of this blog post. AI agents can pull the important parts out of it much faster and with far fewer tokens.

JSON-LD is in there too, of course. That's structured data, which lets you classify content on the web with a well-known vocabulary.

This post, for instance, declares itself a WebPage whose main entity is a BlogPosting. The page also carries a BreadcrumbList that ends with the page itself.

Static HTML comes out

The controller action BlogPostController::__invoke renders a template at the end.

This does not happen by magic, of course — but it does happen “at the push of a button,” in a way. A single command in the terminal is all it takes:

bin/console stenope:build ./static

Et voilà — the static website is finished. It now sits in the static folder of your project.

If you want to know more — how to get the whole thing onto the internet, for instance — I'm happy to show you. Just write me on whichever channel you prefer. I won't hold anything back.

I hope you enjoyed this article. Maybe I even won you over to static HTML.

If you have any questions or thoughts, let me know. You know your way to my contact page by now.

Are you already wondering whether your website should go static?

Sources

  1. Hetzner EX44 – power draw and price of the server a b
  2. Cloudflare Pages Limits – limits of the free plan ↑
  3. Cloudflare Emissions Inventory 2024 – emissions of the network ↑
  4. Netcraft Web Server Survey, July 2026 – number of active websites ↑
  5. National Statistics Office Malta, Electricity Supply 2024 – Malta's electricity consumption in 2024 ↑