Using Parcel with Go Templates
Released On: 2023-08-28 00:00Z
A 8 minute read.
When I built my blog engine (of because I build my own blog engine, because where is the fun in using a premade one) I really wanted to try TailwindCSS for the first time.
You can actually use tailwind just by itself, and it works, which is what I did at the start. I later wanted to manage fonts better than just statically serving
${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono[wght].ttf
, so a different solution was needed.
In the JavaScript SPA world, the use of bundlers like webpack/vite/parcel is ubiquitous, so this is the story about how I adapted parcel to work with my blog engine.
How my blog engine works
My blog engine is build around a SQLite database storing posts, tags (that aren’t used yet) and metadata (that also isn’t used yet). A go server reads the database and renders the markdown to HTML/RSS feeds.
The rendered markdown then gets templated into a quicktemplate to generate the HTML sent to the reader.
writer.Write("string")
, so it’s really fast.
I wanted parcel to take the templates and modify them to include CSS, fonts and, should I ever decide to integrate frontend JS, that as well.
What is a bundler and why do I want to use it
A bundler takes all your JS/Fonts/CSS files and combines them to a minimum of files. This reduces the amount of requests your browser has to make, and it makes caching easier. It also allows the bundler to merge your code and its dependencies, so for example if you do
@import 'npm:@fontsource-variable/jetbrains-mono/wght-italic.css';
in an included CSS file, your bundler can automagically also output the font into your application.
Parcel by default outputs files with hashes in their file name, so you can just tell your web server to set its cache policy to forever and bam, easy caching.
Getting parcel to play nice with templates
How parcel works
To understand how parcel works, reading its Plugin System Overview is probably the best resource.
But I’ll try to give a short overview to describe where I needed to hook in to make it work with qtpl
.
Parcel has Resolvers and Transformers to figure out which assets make up your project
Resolvers:
Resolvers turn dependency requests into absolute paths. So it’ll and convert our npm:@fontsource-variable/jetbrains-mono/wght-italic.css
import to <project_dir>/node_modules/@fontsource-variable/jetbrains-mono/jetbrains-mono.css
Transformers:
Transformers take a file and convert it somehow. So if you had for example a SCSS file, a transformer would convert it to CSS. Another Transformer might minimize an HTML file. They also add dependencies to the asset graph for the resolvers to resolve. So our JetBrains-Mono gets added to the asset graph by a CSS transformer.
The Assets then get bundled (by Bundler plugins) to combine files where possible, named (by Namer plugins) to figure out file paths, and then they’re written to the output directory.
The Parcel Plugins I needed to write
Parcel plugins are their own JS Projects with their own package.json, etc. Using yarn workspaces this wasn’t even as painful as I had thought.
The main JS file of the Plugin just has to default export the Plugin class itself.
A resolver to ignore most kind of imports in .qtpl
files
In the blog engine, links to posts/etc. get templated into the page at runtime. Parcel tries to import anything, even links to template strings.
So I needed to build a resolver that just ignores imports from .qtpl
files if the import isn’t CSS or JS
html/template
, just change the file endings.
That’s the resolver:
// packages/parcel-resolver-qtpl/src/index.js
const { Resolver } = require('@parcel/plugin'); // cjs is ugly but it just worked and I'm lazy
exports.default = new Resolver({
async resolve(x) {
if (!x.dependency | !x.dependency.sourcePath) return null; // dependency can be undefined
// make sure only css and js files are included from qtpl files
if (x.dependency.sourcePath.endsWith(".qtpl") &&
// this will be confusing pain should I ever use scss or typescript
!(x.specifier.endsWith(".css") || x.specifier.endsWith(".js"))) {
return { isExcluded: true };
}
return null;
}
});
simple, isn’t it
A Namer to place assets into a different directory
The default Namer just puts all your assets into the same directory. But as the output consists of both files to be read by the templating engine and assets, the files needed to be split into different directories.
- Templates go to
/templates
(I set this as the primary output path) - anything else goes to
/statics/dist
Writing Namers is also surprisingly simple. You just need to return the file path you want the file to have in the end (relative to the primary output path).
// packages/parcel-namer-split/src/index.js
const { Namer } = require('@parcel/plugin');
const path = require('node:path');
exports.default = new Namer({
name({ bundle }) {
if (bundle.type != "qtpl") {
let filePath = bundle.getMainEntry().filePath;
let bn = path.basename(filePath).split(".")
let hr = bundle.needsStableName ? "." : `${bundle.hashReference}.`
return `../statics/dist/${bn[0]}.${hr}${bn.slice(1).join("")}`;
}
return null; // when the namer returns null, the next namer will be tried
}
});
Combining plugins to have a working parcel configuration
That are all the needed plugins.
Now we just have to write a parcel configuration that combines our custom plugins with the defaults. A parcel configuration is just a JSON5 file describing what plugins to use.
If you don’t have any .parcelrc
it’ll just use @parcel/config-default
as its configuration.
We’ll just extend @parcel/config-default
because it does all the CSS transforming/… for us
{
"extends": "@parcel/config-default",
"resolvers": ["parcel-resolver-qtpl", "..."],
"transformers": {
"*.qtpl": [
"@parcel/transformer-posthtml", // the default html transformers
"@parcel/transformer-html"
],
"*.jsonld": ["@parcel/transformer-raw", "@parcel/transformer-inline-string"]
},
"packagers": {
"*.qtpl": "@parcel/packager-html" // the default html packager
},
"namers": ["parcel-namer-split", "..." ],
}
"..."
just includes the defaults
@parcel/transformer-raw
just takes the input and returns it as an output file.
@parcel/transformer-inline-string
takes an input and returns it as an inline string. The HTML transformer doesn’t like to write files into itself.
I needed to explicitly handle jsonld
and tell parcel to do nothing with it, as Parcel will - by default - transform JSON-LD meta tags to resolve listed dependencies, etc.
It allows specifying and linking together data, so you could for example define a blog posting and their authors in JSON-LD.
And that’s exactly what I use it for, I define a BlogPosting for every blog post of mine, because it’s an easy thing to do for some search engine optimization. (you can see it at the end of the HTML head
of this post)
I inject my JSON-LD at runtime, so it tried to parse the template string as JSON, without much success.
Including Tailwind CSS was as easy as just following Tailwind’s tutorial for PostCSS, without installing autoprefixer
, as Parcel already does that for us.
Parcel needs to also know from which files to start building the asset graph.
You can put an array of paths in your package.json
under the key source
.
Mine looks like this:
// package.json (excerpt)
{
"source": [
"./tmplsrc/basepage.qtpl",
"./tmplsrc/error.qtpl",
"./tmplsrc/index.qtpl",
"./tmplsrc/post.qtpl",
"./tmplsrc/posts.qtpl",
"./tmplsrc/simpleMdPage.qtpl"
]
}
Specifying a blob pattern should also work, but it broke the nix build somehow. Speaking of it:
Building the whole thing with nix
My build process consists out of three parts:
- Build the templates with parcel
- Convert the templates to go code with quicktemplate
- Build the go project
Building the templates with parcel in nix
I’m using yarn right now, so I just tried using yarn2nix
(included in nixpkgs) to build the yarn project with nix.
The parcel plugins are part of yarn workspaces, which we need to include manually with the yarn.lock
of the root package.
# flake.nix (excerpt)
xynoblog_tmpl = pkgs.mkYarnPackage rec {
pname = "xynoblog_tmpl";
version = "0.0.1";
src = ./.;
workspaceDependencies =
(map
(x:
pkgs.mkYarnPackage { # generate a yarn package for everything
src = "${./packages}/${x}";
yarnLock = src + "/yarn.lock"; # use root lock file
fixupPhase = "true";
inherit version offlineCache; # inherit the parents version and cache
}
)
(builtins.attrNames (builtins.readDir ./packages))); # import all packages in the packages directory
offlineCache = pkgs.fetchYarnDeps { # this fetches yarn dependencies into nix
yarnLock = src + "/yarn.lock";
# sha256 = pkgs.lib.fakeSha256;
sha256 = "sha256-ImagineARealSHA256Here/ItGetsGeneratedByNix="; # reproducible ✨
};
src = ./.;
distPhase = "true"; # we do everything in the buildPhase
installPhase = "true";
fixupPhase = "true";
buildPhase = ''
export HOME=$(mktemp -d) # yarn needs $HOME to be set
mkdir -p $out/templates # create output directory
yarn --offline parcel build --dist-dir $out/templates # run parcel
'';
};
Now all the build templates/assets are built into a nix derivation.
pkgs.fetchYarnDeps
, you get the right sha256 just like you do with pkgs.buildGoModule
.
Setting it to pkgs.lib.fakeSha256
and seeing onto which sha256 it mismatches.
Converting templates and building the application
I just put template copying/building into the derivation of the application itself.
# flake.nix (excerpt)
xynoblog = pkgs.buildGoModule rec {
pname = "xynoblog";
version = "0.0.1";
src = ./.;
nativeBuildInputs = [ pkgs.quicktemplate ... ];
preConfigure = ''
cp -r ${self.packages.${pkgs.system}.xynoblog_tmpl}/{statics,templates} . # copy the templates into application sources
chmod +w -R ./{statics,templates} # we need to write to them
qtc -dir=templates # run the code generator
'';
# in the buildPhase it'll turn into a normal go application
...
};
That’s how I use Parcel with a template engine. If you want to read my blogs source code, it’s open source and on GitHub.
But please don’t base your blog engine on it, and just learn a new language, and write your own
Thank you for reading, and a big thanks to Arson for their input and help in writing this post.