andy pai's tils

Setting up Parcel + Pug + Marked

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.

Technologies Used

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.

Setting Up Marked

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.

Supporting metadata

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
}

Supporting Syntax Highlighting & KaTeX

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)
    },
  },
}

Adding a Table of Contents

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}`
          }
      }
    }
)

Usage within Pug

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