May 04 2024

Rebuilding my Website - Part 2

This is part two of a series documenting the rework of my personal website. In this post I’ll cover the structure of the codebase, how the assets are created, and my development workflow.

The site is developed using Parcel, a build tool for web apps. It’s very productive for development/prototyping and ships many nice features out of the box, though it’s not perfect - there are some thorny bits.

Project Layout

I borrowed heavily from static site generators such as Zola and Hugo. The code is structured the same as it is served, seen below.

src
├── base.pug
├── site.css
├── index.css
├── index.pug
├── about.css
├── about.pug
├── 404.pug
├── assets
│   ├── resume.pdf
│   ├── preview-genuary-2024.png
│   ├── preview-genuary-2023.png
│   ├── headshot.jpg
│   └── nft-art-berlin.jpg
├── js
│   ├── highlight.js
│   └── hljs-number.js
├── visual
│   ├── 2018-01-01-gallery-a
│   │   ├── 01.jpg
│   │   ├── ...
│   │   ├── 07.jpg
│   │   └── index.pug
│   ├── 2019-02-25-assorted-artworks/...
│   ├── 2020-05-11-travel-2020/...
│   ├── index.css
│   ├── index.pug
│   ├── post.css
│   └── post.pug
└── writings
    ├── 2020-11-26-my-sketching-setup
    │   ├── index.pug
    │   ├── post.md
    │   └── preview.jpg
    ├── 2021-06-12-noise-orbit/...
    ├── 2022-03-01-reverse-zoology/...
    ├── 2024-01-24-recording-p5js/...
    ├── index.css
    ├── index.pug
    ├── post.css
    └── post.pug

HTML / Pug

Instead of writing raw HTML, I use the templating language Pug, which is supported by Parcel with zero configuration. It allows me to write static HTML content using a much simpler, more terse syntax - e.g. tags don’t need to be closed, classes may be applied using a shorthand. But Pug is not perfect and using it means letting my tools make some decisions for me.

The Layout

Base Template

The overall layout of the site is defined in the template file base.pug. This is the minimum viable webpage - things like setting the DOCTYPE, character set, navbar, footer, etc. This is all constant across each and every page of the site. The file looks something like this:

doctype html
html
  head
    title Stevan Dedovic

    meta(charset="utf-8")
    meta(name="viewport" content="width=device-width, initial-scale=1")
	 
	...

    block head
  body
    nav
      ...
    block content
    footer
      a.hidden(href="/src/404.pug")
      ...

This file is truly a template, i.e. it is never rendered on its own and instead extended by other pages and templates. It is worth noting an oddity in the footer - the reference to a 404 page which is never visible - exists because Parcel only builds files that are in use.

Static Pages

Each static, concrete page extends the base.pug file. Pages like about.pug and index.pug look like the following:

extends ./base.pug

block head
  link(rel="stylesheet", href="./about.css")

block content
  ...

These pages typically add some custom CSS or Javascript and implement the content block. Parcel handles resolving references in HTML and Javascript, as seen in the above example with the relative path to the about.css file. Parcel can find it, process (e.g. if it was SASS or LESS instead of CSS), and build the site.

Index Pages

While informational pages like about.pug and index.pug are pretty straightforward, I also use the website to showcase serial content such as blog posts, art galleries, and my yearly Genuary creations. The structure of my serial content is closer to that imposed by a static site generator (SSG).

Looking at either the writing/index.pug and visual/index.pug files,

extends ../base.pug

block head
  link(rel="stylesheet", href="./index.css")

mixin post(options)
  - 
    const { title, date, ref, image, text } = options;
    const dateObj = new Date(date);
    const day = dateObj.getDate();
    const month = dateObj.getMonth() + 1;
    const year = dateObj.getFullYear().toString().slice(2, 4);
    const displayDate = `${month}/${day}/${year}`;
  div.card
    a.card-image.card-image-small(href!=ref)
      img(src!=image + "?as=webp&width=420" alt!=title)
    div.card-content.card-content-top
      h4(class="display-date")= displayDate
      h2: a(href!=ref) #{title}
      p= text
      a(href!=ref) Read More

block content
  main
    +post({
      title: "Recording and Exporting p5.js Animations",
      date:  "2024-01-24",
      ref:   "./2024-01-24-recording-p5js/index.pug",
      image: "./2024-01-24-recording-p5js/preview.png",
      text: "A quick writeup on how I record and export my p5.js sketches."
    })
    +post({
      title: "Warping - A Refraction Shader",
      date:  "2022-03-20",
      ref:   "./2022-03-20-warping-refraction-shader/index.pug",
      image: "./2022-03-20-warping-refraction-shader/preview.gif",
      text: "This post is part of a series on the Reverse Zoology project. Check out the previous post if you haven’t yet!"
    })
    +post({
      title: "Reverse Zoology",
      date:  "2022-03-01",
      ref:   "./2022-03-01-reverse-zoology/index.pug",
      image: "./2022-03-01-reverse-zoology/preview.gif",
      text: "A few months ago I was introduced to Ellie Pritts, an outstanding digital artist whose goals and personality lined up with mine. We decided to partner on what has now become the Reverse Zoology project."
    })
	...

This leverages a feature of Pug called a_mixin_. A mixin is a custom defined function that takes in arguments and generates Pug code. I think of them as language macros. In the above example, I define the post macro which accepts an options object, a pattern that should feel familiar to Javascript developers. I turn the input into a “card”. A “card” is a general term I use for a self-contained piece of UI that displays an image, title, and some descriptions in a style similar to a photo id or license. The index pages for my serial content is just a series of cards which may be clicked to open the individual post. Using a mixin to template out cards ensure consistency and prevents bugs, such as setting the href tag on each of three a elements, consistently. Or making sure each card has the same CSS styles applied properly.

There’s another feature of Parcel, used above, that I want to cover. In the post mixin, notice the following line:

img(src!=image + "?as=webp&width=420" alt!=title)

The image, which is a directly referenced file in the directory (e.g. ./images/foo.png), is suffixed with a query string so that Parcel can re-encode and resize the source image. This is a massively convenient feature - I can store all my images raw in the git repository along side the code and optimize for serving at build time.

Blog Posts

I won’t cover my gallery pages as they are not very interesting nor novel. Instead, I want to talk about my process and how I do my writing. There are two parts to this but first I need to call attention to the project layout presented earlier. The writing directory contains a post.pug file and a folder for each blog post. The post.pug file has the following contents:

extends ../base.pug

block head
  link(rel="stylesheet", href="../post.css")
  script(type="module").
    import { highlight } from '../../js/highlight.js';
    highlight();

mixin post(title, date)
  - 
    const dateObj = new Date("2020-11-26");
    const pieces = dateObj.toString().split(" ");
    const month = pieces[1];
    const day = pieces[2];
    const year = dateObj.getFullYear()
    const displayDate = `${month} ${day} ${year}`;
  div.post
    div.post-heading
      h2= displayDate
      h1= title
    div.post-body
      if block
        block

Like the base.pug, this file is more of a template than content. It extends base.pug, includes some custom CSS, and adds a script for syntax highlighting in the head block. I’ll cover the syntax highlighting in a later section. Finally, the file defines a different post mixin.

Something to note is the relative paths to files such as post.css andhighlight.js are done with an extra upwards directory. On the other hand, extending base.pug is pathed to the direct parent directory. This is because the former is resolved by Parcel after the template is generated and the latter is resolved by Pug at template compile time. The two may not be the same and it has caused me minor headache.

Now back to the post mixin. It does something new by ending the mixing with a block if present. This allows me to use the mixin and define child content to inline. The utility becomes apparent when looking at the a specific post, e.g. writing/2020-01-01-example/index.pug:

extends ../post.pug

block content
  main
    +post("Recording and Exporting P5.js Animations", "2024-01-24")
      - 
      include:markdown-it(linkify html) ./post.md

This file, a single blog post, extends post.pug yet fills out the content block from base.pug. Then the file uses the post mixin discussed previously with some extra content from a Markdown file. I’ll discuss Markdown rendering and markdown-it, the renderer, in a later section.

Putting it altogether, my publishing workflow is as follows:

  1. create a new directory under src/writing
  2. write in Markdown
  3. create a simple index.pug file
  4. and add an entry to the writing/index.pug file While not perfect, this is pretty streamlined. In the (near) future I plan to create a simple CLI to automate creating the new files and adding an entry to the index page.

CSS

I am by no means a frontend developer. My use of CSS is quite limited so this section will be terse and an explanation of what , not why, decisions were made.

The codebase has a file, site.css, that contains all shared “baseline” CSS. This file starts with importing a reset.css I found online, defining a CSS Variables for site colors, and styling HTML elements. Then I have a section for the navbar, footer, and a stylized button used throughout.

Each concrete page on the site has a companion CSS file. For example, about.html pairs about.css, index.pug pairs index.css. All of these files start by importing site.css and then go to define styles that are specific to each page. The about.css file, for example, adds styles to display my socials (Github, Twitch, LinkedIn, etc.) in a nice grid.

Overall, I try to use more modern CSS features such as Flexbox, Grid, HSV colors, and more. These tend to make my life easier and I really don’t care to support styling on older browsers.

One change I would like to make in the (near) future is to switch to CSS modules. I don’t fully understand the differences or repercussions but I believe that Parcel is able to remove unused styles automatically, shrinking my website size down.

Highlight.js

For syntax highlighting I chose to use highlight.js. This library is quite small and very simple to use.

Unfortunately, highlight.js is not very feature rich - it simply performs syntax highlighting and nothing else. I need to add line numbering and the ability to copy code with a single click. These things are not impossible, but they require a bit of code.

Line Numbering

Highlight.js has a very straightforward plugin interface - a plugin is a map from events to functions which the library will invoke at different parts of its lifecycle. My simple plugin, defined in hljs-number.js is as follows:

export const number = () => {
  return {
    'after:highlightElement': ({ el, result }) => {
      const lines = result.value.split('\n')
      
      // result from highlight.js has a trailing newline. This removes it.
      lines.pop();

      const numDecimalPlaces = lines.length.toString().length;
      const html = lines
        .map((line, idx) => {
          const num = idx + 1;
          const displayNum = num.toString().padStart(numDecimalPlaces);
          return `<span class="hljs-number-line" data-hljs-number-linenumber="${displayNum}">${line}</span>`
          })
        .join('\n');

      el.innerHTML = html;
    }
  };
};

I export a function of no args, number. By making the export a function instead of the plugin object directly, my API is a factory and I can easily add configuration options in the future. Line numbering is registered on the event after:highlighElement, which seemed the most appropriate. The data inresult.value is raw newline-separated HTML and should be inserted into the supplied element el. To generate line numbers the process is as follows:

  1. split result into lines of HTML
  2. count the total number of lines to determine the maximum number of decimals. This is necessary for properly left-padding.
  3. for each line and index
    1. generate a left-padded string displaying the line number
    2. wrap the line in a new span
    3. add an HTML Data Tag on the span with the value of the generated line number string
  4. join it into a newline deliminated raw HTML string and replace the existing HTML of the target element

The final piece to make it all work is a bit of custom CSS that targets the HTML Data Tag by setting the content property to the value of the HTML Data Tag from earlier.

Markdown-It

Markdown-it is a Markdown renderer that plays well with Pug. It has a (fairly) rich plugin system and supports expanding the Markdown spec with custom syntax and utilities such as automatically converting text that looks like a URL to a hyperlink.

I have not explored it enough, but its on my roadmap to create custom plugins for:

Caveats

  • Pug will escape text by default, even in src and href tags, causing bugs with Parcel. E.g. src!="./path/to/image.png?as=webp&width=420"
  • Parcel only builds the things that are referenced. E.g. a reference to the 404.pug file must exist somewhere from the Parcel entry point, index.pug in my case. That explains the a.hidden(href="/src/404.pug) oddity
  • Parcel resolves relative links using a different syntax and context than Pug which becomes confusing when extending a Pug template in a different directory than relative referenced assets
  • The Parcel dev server and production build do not yield the same results often enough to causes bugs
    • SVG optimization fail when SVGs have metadata
    • Inlining source code as text will yield minified source code. This is pretty egregious because of misleading documentation of the Node.js fs module polyfill and a Github Issue thread claiming there is no difference in usage

Overall, what plagues Parcel is something inherent in the whole Javascript world - vapid changes causing constant small breakage that requires too much work to maintain and stabilize. It’s often easier to fail forward or remove support over maintaining a stable API. But I’m still here, using it for current and new projects. Given enough time and energy, I would love to replace this whole system with my own blogging engine. Maybe this is a step in that direction…