Making a boring static website with modern frontend tooling is fun!
First, you install React & React DOM… actually no, you probably won’t do that. You likely wind up choosing between create-react-app, parcel, vite, or a full fledged framework like NextJS and Gatsby. Oh and if you need Typescript you’ll have to install and configure a bunch of more stuff. Then there’s styling, you can install a component library like styled-components, or use a CSS system like Tachyons. You still need to ingest content from somewhere, so you can also make a Contentful account. Since you’re doing server side rendering, you’ll probably want a host like Netlify or Vercel that supports serverless functions. I think we’re done configuring our new project now? There isn’t anything on the page yet but the size of the build bundle you just created would probably indicate otherwise.
Don't get me confused, I'm not a hater of the modern frontend stack. If you’re building a full fledged application, all of this makes sense. But I’m just trying to refresh my personal website, which has 2 templates, 5 pages, and 2 interactive buttons. This site probably won’t get much bigger or more complicated than that in the next 5-10 years (or, ever…). So I decided to go old school and render some html templates using content stored in the file system. This blog is a quick run through of how I did it, and I’m sorry in advance that this project has serious “Gen-Z rediscovers what record players are” vibes.
First, I setup mustache (npm package) as the templating language (the npm package hasn’t been updated in two years, so that’s great). Mustache is really simple, it provides a few helper utilities, like replacing variables, or iterating over data,
<ul>
{{#links}}
<li><a href="{{href}}">{{label}}</a></li>
{{/links}}
</ul>
{
"links": [
{ "href": "itsjoekent.substack.com", "label": "Coder Brain" }
]
}
You can also define custom functions (“lambdas”) in your Javascript code that are then accessible in your Mustache template. For example, I created a lambda to inject the proper url prefix for all of my media assets,
function mediaLambda() {
function _mediaLambda(mediaFile, render) {
return `https://cdn.joekent.nyc/${render(mediaFile)}`;
}
return _mediaLambda;
}
Next, I setup a markdown parser, markdown-it. I then created another Mustache lambda for converting markdown to HTML and injecting it in the template output. I also ran the Markdown string through the Mustache parser, so I could use my “media” lambda inside markdown. This is where things got gross in the code.
Unfortunately, the mustache package I’m using seems bugged, because when I call a Lambda with a parameter that is a variable, and the variable value calls more Lambda’s (eg: the media lambda being used in my markdown text), it doesn’t do recursive replacement.
<div class="markdown"
{{#markdown}}{{blogContent}}{{/markdown}}
</div>
{
"blogContent": "The media URL is {{#media}}example.png{{/media}}"
}
// The markdown function outputs, "The media URL is {{#media}}example.png{{/media}}"
Oh and to make everything worse, I decided to use TOML (npm package, last published 4 years ago) to store my content because TOML is prettier than JSON and YAML (big brain decision making), and the TOML package I’m using is replacing forward slashes in multi-line strings with their HTML entity hex code.
# TOML
myMarkdownVariable = """
{{#media}}example.png{{/media}} // input
"""
# TOML node package output
{{#media}}example.png{{/media}} // output
This obviously does not work with the Mustache parser. So I had to uninstall another package to decode the HTML entities, and then deal with the original recursive replacement problem. Here’s what the final code looks like:
// 'markup' is the markdown string from the TOML file
// 'render' is the mustache renderer
markdown.render(render(he.decode(render(he.decode(markup)))), {
html: true,
linkify: true,
});
Thankfully that is the only really gross code in this entire project.
From here, everything was really straight forward. In local development, I run an express server that handles serving the frontend bundle (generated by webpack, intentionally boring choice on my part), and matching the requested path name to a TOML file in the content directory. Once the file is matched, it can pull the HTML template required for the content type and run the HTML through the Mustache engine with the content data, and return the HTML output to the client.
For deploying to production, I just repeat the above process for every content file and instead write the resulting HTML to the build folder.
// Development
app.use('/dist', express.static('www/dist'));
app.get('/*', async (request, response) => {
try {
const content = await readAllContent();
const match = content.find(
(compare) => compare.path === trimLastSlash(request.path)
);
if (!match) {
return response.status(404).send('not found');
}
const templateHtml = await renderTemplate(match);
const html = await renderTemplate({
...match,
template: 'index',
templateHtml,
});
response.set('content-type', 'text/html').send(html);
} catch (error) {
console.error(error);
response.status(500).send('server error');
}
});
// Production
const allContent = await readAllContent();
for (const content of allContent) {
const templateHtml = await renderTemplate(content);
const html = await renderTemplate({
...content,
template: 'index',
templateHtml,
});
const indexFolderPath = path.join(process.cwd(), 'www', content.path);
await fs.mkdir(indexFolderPath, { recursive: true });
await fs.writeFile(path.join(indexFolderPath, 'index.html'), html);
}
You can find the full source code for this here.
Going into this project, I wrote down 3 goals,
- Write HTML templates, CSS files, and JavaScript that uses native DOM API's.
- Store my content in the repo, media assets in S3 compatible storage.
- Deploy static files to a CDN.
And I think I succeeded on all fronts (for #3, I am using Cloudflare Pages).
But here’s the million dollar question… Do I regret rolling this custom file system based CMS & template engine, instead of just using React & some framework like NextJS, attached to a real CMS like Contentful?
…Not really. This was more fun than googling some opaque NextJS error for an hour. And this tiny website doesn’t need anything that fancy anyway. In hindsight, the only things I would do differently,
- Use a templating engine that doesn't require my ugly hack (or just make my own).
- Ditch TOML in favor of YAML or JSON 5.
Oh and you find my updated website here. Hope you enjoyed this journey back in time!
Top comments (0)