I wanted a simple setup for this TIL blog that could handle plain HTML/CSS/JS but also provide flexibility and control over everything, starting from the raw request when required. I tried various options, including GatsbyJS + Netlify, Nextjs, and even a bunch of frameworks supported by Cloudflare Pages.
Ultimately, I settled on using Pug for a cleaner template, Parcel for basic bundling, and deployed the project via Cloudflare Pages.
Parcel Parcel: A self-described "zero configuration build tool."
Pug Pug: Self-described as a "simple templating language with a strong focus on performance and powerful features". Pug supports using Markdown via filters
Marked Markdown: Via Marked, a low-level compiler for parsing Markdown.
Cloudflare Pages Cloudflare Pages: Provides lightening quick deployments to the Cloudflare global network.
While most of the setup was straightforward, getting Markdown to work nicely, especially with support for metadata, syntax highlighting and KaTeX, required some additional configuration.
At the top of my Markdown posts, I include a header that looks like this:
---
title: Setting up Parcel + Pug + Marked
brief: Notes on setting up Parcel, Pug, and Markdown
date: 2024-01-26
tags: web-dev
---
To extract this metadata from the Markdown file before parsing it, I use the following function:
# metadata.js
export default (fileContent) => {
// Regex to capture YAML front matter between "---" lines
const frontMatterRegex = /^---\n([\s\S]*?)\n---/
const match = frontMatterRegex.exec(fileContent)
if (!match) {
throw new Error('Could not find front matter')
}
// Extract the YAML part and convert it into a key-value structure
const yaml = match[1]
const yamlLines = yaml.split('\n')
const metadata = {}
yamlLines.forEach((line) => {
const [key, ...rest] = line.split(':')
const value = rest.join(':').trim()
// If it's a list of tags, split by commas and trim each tag
if (key.trim() === 'tags') {
metadata[key.trim()] = value.split(',').map((tag) => tag.trim())
} else {
metadata[key.trim()] = value
}
})
const content = fileContent.replace(frontMatterRegex, '').trim()
metadata.content = content
return metadata
}
For the Markdown setup, I use the following libraries:
Here's how I configure them:
import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import markedKatex from "marked-katex-extension"
import hljs from 'highlight.js'
import parseMeta from './metadata.js'
const marked = new Marked(
markedHighlight({
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
return hljs.highlight(code, { language }).value
},
}),
)
marked.use(markedKatex({
throwOnError: false
}))
export default {
filters: {
markdown: (text) => {
const { content } = parseMeta(text)
return marked.parse(content)
},
},
}
To generate a table of contents for the Markdown posts, I use the marked-gfm-heading-id extension. This library automatically adds IDs to the headings in the generated HTML, making it easy to create a table of contents.
import { gfmHeadingId, getHeadingList } from "marked-gfm-heading-id"
marked.use(gfmHeadingId({ prefix: "til-" }),
{
hooks: {
postprocess(html) {
const headings = getHeadingList()
const toc = ({ id, text, level }) => `
<li class="h${level}">
<a href="#${id}">${text}</a>
</li>`
return `<ul class="toc">${headings.map(toc).join('')}</ul> ${html}`
}
}
}
)
Within Pug templates, I can simply include the Markdown file for a post:
body
.markdown-body
include:markdown ./posts/setting-up-parcel-pug-marked.md