Series: templating

Why Websites Need Templates

By John Hardy

When I load a web page in a browser, what arrives is a single document. It might be long and include navigation plus footers and sidebars around a main column of text, but to the browser it is just one block of HTML.

When a site grows beyond a handful of pages, I am no longer writing one document but a whole set. Each page carries its own content, yet the structure repeats. The header and navigation stay consistent across the set. The footer and typography stay consistent, as does the layout.

If I were to copy that shared structure into every file, the site would be hard to maintain. A small change to the navigation or layout would require touching every page. Over time those copies drift, and the site becomes inconsistent. This is the practical problem templates exist to solve for me.

A template lets me write the shared structure once and reuse it. I define a common outer document with <html> and <head>, plus a shared header and navigation. The footer sits in the same frame, and I leave one or more places for page-specific content. Each article or page then supplies its own content, and the build places it into those slots.

In its simplest form, templating is just composition. A page is the result of taking some content and placing it inside a larger HTML frame. The content and the frame are separate files, but the output is a single document.

This separation matters because it lets me work on content and structure independently. I can write or revise an article without touching the site chrome. A change to the site chrome does not require me to rewrite the archive. The template system is what connects those two strands.

Once a site grows beyond a few pages, this quickly becomes essential. Without templates, I either duplicate markup everywhere or invent ad-hoc scripts to assemble pages. Templates are the conventional way to express that assembly.

This baseline matters because the rest of the series argues for a stricter, stamp-like approach to templates.

Where templates get more complicated is in how much responsibility I give them. Many systems ask templates to do more than place content into a frame. They ask templates to decide which pieces of content should appear and in what order, sometimes with conditions. That turns the template into a control layer with decisions baked in.

Whether that is a good idea depends on what I want from my publishing system. That is why I want templates to act as mechanical stamps that receive prepared content. I use the next piece to make that trade-off explicit.

In the next article I look at how that additional responsibility changes what templates are, and why it affects the clarity and predictability of the final HTML. If you want to continue, head to Templates as Pure HTML.

Templates as Pure HTML

By John Hardy

If you missed the earlier piece, start with Why Websites Need Templates. It lays out why templates exist and why the shared frame matters. It also gives the baseline this piece argues against.

Once a template starts making decisions, it stops behaving like a document and begins acting like a control layer. It gets access to collections of articles and loops through them. It filters results and hides sections based on conditions. That is convenient at first and hard to reason about later, because the output depends on hidden logic inside the layout.

Here is the type of template logic I am talking about. It looks like layout, but it carries behaviour.

{% if featured_posts.length > 0 %}
<h2>Featured</h2>
<ul>
  {% for post in featured_posts %}
    <li><a href="{{ post.url }}">{{ post.title }}</a></li>
  {% endfor %}
</ul>
{% endif %}
Posts I want to surface on the home page.

When templates carry that logic, I can no longer read the file and know what the HTML will be. A small edit to a condition can change the page structure, and a metadata tweak can move a heading or drop a list. That is the point where I stop trusting the output.

I avoid that by keeping templates from deciding what content exists. The build pipeline and the query layer take on the responsibility instead. That keeps selection in one place where I can inspect it.

Before the build touches any template, it walks the filesystem and reads frontmatter to construct an index of all available articles. Queries apply to that index to produce explicit, ordered lists of article records. Each query has a name, and that name refers to a specific deterministic result set.

By the time a template enters the pipeline, the interesting work is already complete. It receives the result of a query with a list of article records that already sit in the correct order, whether the list is empty or not. That list arrives as data with no instruction to compute.

That is what lets templates stay simple. It keeps the scope small when I need to debug. It also keeps the source legible when I return to it months later.

A template contains ordinary HTML and one or more <template> tags that act as render slots. Each slot names a query so the build has a precise target to stamp. At build time, the system takes the results of that query and stamps the corresponding content into the slot. If the query returned nothing, the system uses the fallback HTML inside the <template> tag instead. After stamping, the build removes the <template> tag itself, leaving only normal HTML behind.

Because of this, a template has no conditionals or loops. It does not handle counts or empty tags, and the build resolves ordering earlier. The template defines where the stamped output should appear and what should show up when there is nothing to stamp.

This turns templating into a mechanical operation. Given the same set of article records and the same queries, the same templates will always produce the same HTML. There is no hidden state and no branching behaviour inside the template files themselves.

It also makes the templates readable in a way that is hard to achieve in more conventional systems. When I open one, I see the page structure as it will exist in the final output. I see the header and navigation, then the main content with its fallback messages for empty sections. There is no embedded logic to mentally execute. The only dynamic pieces are clearly marked as slots.

That is what I mean by calling the system boring. I want the boring sections to stay readable, not clever. The templates stay simple and ignore metadata or sorting rules, while avoiding visibility flags as they provide a fixed shape that content slots into. Boring here means predictable and inspectable over time.

Here is a concrete example that matches the home page most readers expect. It is a normal page with a header and navigation plus a list of recent posts. The only special tag is the slot where the build will stamp the query results:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Semantic Scroll</title>
  </head>
  <body>
    <header>
      <h1>Semantic Scroll</h1>
      <nav>
        <a href="/archive/">Archive</a>
        <a href="/tags/">Tags</a>
      </nav>
    </header>

    <main>
      <template data-query="latest-posts">
        <p>No posts have been published yet.</p>
      </template>
    </main>

    <footer>
      <p>© John Hardy</p>
    </footer>
  </body>
</html>
The latest entries as they land in the archive.

Everything in that file is real HTML. If I open it in a browser, it renders as a page with an empty state because the <template> tag is inert by design. When the build runs, the system replaces that one tag with the output of the latest-posts query. Three returned articles become three summaries in that space, and the build then removes the <template> tag itself. If the query returns nothing, the fallback paragraph remains and the rest of the page stays the same.

That simplicity keeps the system predictable and inspectable, and it stays durable over time. The complexity lives in the build process and the query definitions, where I can validate it and reason about it directly. The templates remain what they look like: static HTML documents with a few clearly defined places where content will appear. If you can read the template, you can understand the page.

The aim is for a reader to open a template and feel the page is already there, with only the content missing.

Years