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:
- create a new directory under
src/writing
- write in Markdown
- create a simple
index.pug
file - 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:
- split
result
into lines of HTML - count the total number of lines to determine the maximum number of decimals. This is necessary for properly left-padding.
- for each line and index
- generate a left-padded string displaying the line number
- wrap the line in a new
span
- add an HTML Data Tag on the
span
with the value of the generated line number string
- 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:
- better embeddings, i.e.
iframe
s with improved security and less tracking - an image gallery / carousel with custom styles
- cards that turn into the same HTML as my Pug cards
Caveats
- Pug will escape text by default, even in
src
andhref
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 thea.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…