Jan 27 2025

Go Templates with Parcel and Live Reloading

Parcel and Go Templates

Sometimes I spend more time on my tools than than the actual thing I'm building. I should start a side-project counter...

Its 2025 and the web seems to be moving to heavier server-side workloads and thinner clients. Server-side Rendering is popular, with frameworks such as Next and SveltKit enabling SSR by default. Angular, React/Preact, Vue support SSR. Newer systems, such as Hotwire are built on sending HTML partials instead of JSON data, thus supporting a mixed strategy of SSR and Client-side Rendering. This blog post covers my work to make Go's http/template package a first-class citizen in the Parcel build tool for web development. The ultimate goal being a simple way to build sites that are HTML rendered from templates on the server.

Background

I have opinions when I build websites - I prefer low-to-zero JavaScript and making things as simple and fast for my users. One of my projects, WGSL Toywork in progress, is a site to host, showcase, and share WGSL shader applications. It requires a backend to store user code, allow registration, and support some social features. The frontend needs a code editor and will use the WebGPU APIs to draw to the screen. When I started, I chose to write the backend in Go as I wanted to learn Go and the language is well suited to the problem space.

Now, I have a few options to glue the backend and frontend together and ultimately Go's html/template package along with HTTP server routes for accessing user shader code and allowing dynamic interactions made the most sense. Additionally, static assets, such as the CSS and JavaScript, will be served using a CDN.

high-level architecture

The Problem

The frontend is built with Parcel, a popular build tool for JavaScript. Parcel will take source code, transform it based on the filetype, bundle it together, and package it for production use. This means the following HTML snippet will have the href attribute modified and the Less file will be compiled to CSS.

<!-- Before -->
<html>
  <head>
    <link rel="stylesheet" href="./style.less" />
  </head>
</html>

<!-- After -->
<html>
  <head>
    <link rel="stylesheet" href="style.afc9ee41.css" />
  </head>
</html>

Processing Go Templates with Parcel

The problem is that Parcel does not work out of the box with Go's http/template. Following a blog post by Lucy Hochkamp / xyno a few things can be done. First, Go templates can be transformed and packaged using the same default settings as HTML code. Second, the processed templates will be stored in a unique directory so that it can be consumed by the Go server-side code more easily. The xyno blog post also covers packaging things witth Nix (which I use) so that's a plus.

But there's one big flaw - live reloading. All the benefits of Parcel are lost when developing and the workflow is as follows:

  1. start with everything in a single HTML file
  2. convert the HTML file into Go template files
  3. process the template files with Parcel
    • inject the output templates into the Go server
    • serve the static assets from a CDN
  4. repeat when something new needs to be developed

Better Development with Go Templates and Parcel

To improve the dev cycle Parcel must be able to render templates as well as simply processing them as HTML per the xyno blog. Then, when building, the templates will simply be stored in a separate directory as before.

Following the @parcel/transformer-pug code a new Transformer must be written to execute a Go template, rendering HTML.

HTPP

Rendering a Go template must be done in... Go. To this end, I wrote htpp (or html/template plus plus). Naming is hard.

htpp is a simple executable that accepts JSON on stdin and a path to a template, executes the template, and prints the resulting HTML to stdout. The JSON is the values of the template variables. Additionally, htpp extends html/template to support an extends syntax for hierarchical templates, i.e. separating a single template across multiple files.

Now, a Transformer plugin for Parcel, parcel-transformer-htpp, can be developed. This works just like the Pug transformer except it uses child_process#execFile to execute the htpp CLI. Similarly to the Pug transformer, it checks for a config file in the directory .httprc which defines template variables, piping the variables as the JSON via stdout/stdin.

The Transformer code is still a work in progress and the pull request wgsltoy#1 tracks the effort. The code is showed in the next sections.

Template Hierarchies in Parcel

Now this is where things get tricky...

Templates have heirarchies, e.g.

{{/* base.htpp */}}
<html>
  <head>
    <title>My Site</title>
    
    {{ block "head" . }}
    {{ end }}
  </head>
  <body>
    {{ block "body" . }}
    {{ end }}
  </body>
</html>


{{/* index.htpp */}}
extends ./base.htpp

{{ block "body" }}
<h1>This is the body</h1>
<p>testing some text</p>
{{ end }}

And Parcel needs to know that index.htpp depends on base.htpp so the latter must be processed as well. htpp supports an additional CLI flag to help resolve these hierarchies by traversing from the leaf template to the root template. In the example above, http --print-dependencies index.htpp will simply print out base.htpp. These are then plugged into the asset.addURLDependency method in the Transformer plugin as follows:

// Execute `htpp --print-dependencies` for the supplied file
async function resolveDependencies(filename: string): string[] { ... }

// Transformer plugin contract
async transform({asset, config, logger, options}) {

  // A template may extend other templates. Those should be processed by a different pipeline to skip rendering.
  const dependencies = await resolveDependencies(asset.filePath);
  for (let filePath of dependencies) {
    const pathToDependency = path.relative(path.dirname(asset.filePath), filePath);

    asset.invalidateOnFileChange(filePath);
    asset.addURLDependency(pathToDependency, { 
      // Partials (i.e. templates that are dependencies but not rendered themselves) do not have this
      //  Transformer applied to them.
        pipeline: "htpp-partial",
        needsStableName: true,
      });
  }

  let assets = [asset];

  // when we are building for the development server, render the HTML too
  if (options.mode !== 'production') {
    const templateData = config ?? {};
    const html = await render(asset.filePath, templateData);
    let uniqueKey = `${asset.id}-html`;

    assets.push({
      type: 'html',
      content: html,
      uniqueKey,
      bundleBehavior: "isolated",
    });

    asset.addURLDependency(uniqueKey, {
      needsStableName: true,
    })
  }

  return assets;
},
...

Note in the above the pipeline: "http-partial" setting for addURLDependency. This is important so as to skip rendering the parent templates. The pipeline is defined in the globabl project .parcelrc configuration file. That looks like the following:

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.htpp": [
      "./parcel/parcel-transformer-htpp.js",
      "@parcel/transformer-posthtml",
      "@parcel/transformer-html"
    ],
    "htpp-partial:*": [
      "@parcel/transformer-posthtml",
      "@parcel/transformer-html"
    ]
  },
  "packagers": {
    "*.htpp": "@parcel/packager-html",
  },
  "namers": ["./parcel/parcel-namer-template.js", "..." ],
}

In the above, parcel-namer-template.js is similar to the xyno plugin.

All Together

With all of the above completed, I can now work on my frontend and Go templates at the same time. I can reference static pages from the template files, import CSS and JavaScript relatively, while the dev server is live-reloading and leveraging HMR. It's just as easy as the built-in Pug support. But, there are still a few rough edges - like the inability to place template files and static assets in two separately defined distribution directories. Overall, this workflow rocks, and I'm quite happy.

If you have thoughts / questions I would feedback! Find me on Bluesky or email me directly!