Building a blog in 2020

Use at own risk sign on a fence near some plants.

Photo by Janilson Furtado on Unsplash

Let's go through the steps needed to build a modern, performant blog in 2020!

Goals

Before going over the what I used to build this blog, I think it's helpful to list the features I planned to have.

  • It needed to be performant and bloat-free (as much as possible). Being a blog, most of the content is static so I wanted to have something that works well with caching and can be served statically.

  • Has to be accessible and respect web standards. This may seem obvious for such a relatively simple website, but often enough I've browsed through blogs with disabled focus or insufficient color contrast. On this note, if you find something inaccessible, by all means, let me know and I'll fix it!

  • I wanted to use a modern framework, preferably based on React since that's what I'm most comfortable using and Markdown for blog posts.

  • Ideally, I wanted to have a pipeline in which I don't have to manage anything besides coding the website and writing the blog posts.

Choosing a suitable tech stack

I chose to use React for this blog, which is usually used in single page applications or web apps, so why did I decide to use it on a website that screams build me with HTML, CSS and Javascript?

Being that some of the arguments I want to write about are Data Visualization, WebGL, and SVG I wanted to empower my blog posts with many kinds of interactive components and experience taught me that a website of this kind can get out of hand pretty fast if built on vanilla javascript. It seemed pretty likely that I would end up building a worse version of a framework trying to implement these features. I also wanted to keep the advantages of having a static website and this is where Next.js comes through.

I won't go super in-depth about Next.js as their documentation is top-notch, but what made me choose this framework was that under certain conditions it allows the developer to write React code which will get transformed to HTML pages at build time. This lets me keep all the advantages of working with React without having to worry about the user downloading a MB of javascript when visiting my website.

Next.js is not the only player in this field, you might have heard of Gatsby for React, Nuxt.js for Vue, Sapper for Svelte, and many others. I'm sure you could get equally good results using any of those frameworks, the reason I chose Next.js is because its API is a very thin layer on top of React. It boils down to 2 or 3 functions depending on your use case. Not only it's that simple, but the later versions also ship with some very nice components like an image component which handles all the tedious image optimization for you with just a starting image.

For my hosting, I decided to take advantage of the great deployment workflow provided by Vercel, which is both the easiest and recommended way of doing things when using Next.js. I keep my files inside a public Github Repository and every time I git-push it automatically rebuilds and deploys the new website. Magic.

Styling

I decided to use Sass as it's what I normally use and I like my CSS decoupled from my components as much as possible. Again this honestly does not matter as I could have achieved the same result with plain CSS, Tailwind, Less, CSS in JS, you name it.

After some research I decided to go with a single column layout, it's a very intuitive and popular layout for blogs. I've taken inspiration for the design from some of the blogs I follow, notably Josh W. Comeau or Amelia Wattenberger.

One of the advantages of using this layout is that it's intrinsically responsive, only a small handful of components needs to be styled differently for smaller devices.

I chose to use Inter by Rasmus Andersson for my website font, specifically its variable version.

Inside our custom document head we can preload the font and then use the CSS @font-face rule to load it.

TsxTsx codeblock / snippet / file
_document.tsx
Copied 🎉
<link
rel="preload"
href="/fonts/Inter-Var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
SassSass codeblock / snippet / file
main.scss
Copied 🎉
@font-face {
src: url("/fonts/Inter-Var.woff2") format("woff2");
font-family: "Inter";
font-weight: 100 900;
font-display: swap;
}

Most browsers support woff2 files nowadays.

Woff2 support chart

Woff2 support chart by Caniuse

You might have noticed I have font-display: swap enabled. This means that loading the font won't block the website's rendering. It's likely that the user will see text styled with his system font and whenever Inter loads it will swap between them. I personally don't mind this given that it happens only before the font gets saved in the cache.

Next.js by default doesn't add the appropriate header for our font, but we can set it up ourselves in the vercel.json file.

JsonJson codeblock / snippet / file
vercel.json
Copied 🎉
{
"headers": [
{
"source": "/fonts/Inter-Var.woff2",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}

Code snippets are styled use a different combination of fonts based on what's available on the user system. This is known as the system font stack.

Colors

Choosing the perfect color scheme is never an easy task. Initially, I tried using various palette generators but no matter which I used I was never satisfied.

Some gave me way too many colors and I was getting confused. I didn't need 9 shades of my primary color and 27 shades of white, black, and gray. Some didn't respect accessibility and others were just plain ugly.

At that point, I decided to go with the tried and tested 😉 way of choosing a starting color, #7646fd for light mode, #ffa7c4 for dark mode and fiddle with them until I get something I like. The ending result is a very small palette looks quite pretty. My initial idea was to use pastel colors but I was unable to get a satisfying result.

I didn't mess with the starting colors at random until I got the results I wanted. While I was doing it I was considering what I wanted to convey with colors.

The end goal is for users to have a pleasant reading experience so I decided to avoid bright colors and only use them to highlight emphasized content. At the same time, colors are only one way of doing that, being over-reliant on colors to show the UI state may become problematic.

Dark mode secrets

The blog was designed with dark mode support from the get-go. There are many good articles about implementing dark modes with the pros and cons of various approaches, notably this one by CSS Tricks.

When using React a popular approach is to have a top-level context to handle it but I'd rather do it with CSS variables and a custom hook.

Here's how I've done it.

  1. I've created a separate file called variables.css which unsurprisingly stores all the variables that change between modes. Styles will be applied to the <html> element based on the data-theme attribute value.

  2. Building the useDarkMode hook

    To manage dark mode I've decided to go with a custom hook that will determine whether we are in dark mode or not and modify the data-theme attribute accordingly. With CSS variables being intrinsically reactive a simple change of attribute is sufficient for our needs.

    First of all, we need a way to keep track of the user preference inside our code, so let's build a custom hook that does that.

    TypescriptTypescript codeblock / snippet / file
    useDarkMode.ts
    Copied 🎉
    1import { useState } from "react";
    3type ReturnProps = [
    4 boolean | undefined,
    5 React.Dispatch<React.SetStateAction<boolean | undefined>>
    6];
    8export const useDarkMode = (): ReturnProps => {
    9 const [darkMode, setDarkMode] = useState<boolean | undefined>(undefined);
    11 return [darkMode, setDarkMode];
    12};

    Right now this is a glorified wrapper over useState and it could very well be placed inside our DarkModeToggle component, it's a personal preference of mine to keep them in separate files.

    It still lacks two things, first let's add a way to edit the data-theme attribute.

    TypescriptTypescript codeblock / snippet / file
    useDarkMode.ts
    Copied 🎉
    1import { useState } from "react";
    3type ReturnProps = [
    4 boolean | undefined,
    5 React.Dispatch<React.SetStateAction<boolean | undefined>>
    6];
    8export const useDarkMode = (): ReturnProps => {
    9 const [darkMode, setDarkMode] = useState<boolean | undefined>(undefined);
    11 useEffect(() => {
    12 if (darkMode !== undefined) {
    13 if (darkMode) {
    14 document.documentElement.setAttribute("data-theme", "dark");
    15 } else {
    16 document.documentElement.setAttribute("data-theme", "light");
    17 }
    18 setLocalStorageDarkMode(darkMode);
    19 }
    20 }, [darkMode]);
    22 return [darkMode, setDarkMode];
    23};
    25const setLocalStorageDarkMode = (isDark: boolean): void => {
    26 if (isDark) {
    27 window.localStorage.setItem("dark-mode", "dark-mode-active");
    28 } else {
    29 window.localStorage.removeItem("dark-mode");
    30 }
    31};

    You may have noticed that the code won't run if darkMode is undefined and we initially set useState(undefined). Why? That's because on the first load we have no way of knowing whether the user wants dark mode or not. This is easily solved by storing a cookie containing the user preference inside window.localStorage and retrieve it whenever he visits the website. The setLocalStorageDarkMode function does half of this, it takes care of setting the cookie.

    But what if the user has never visited our website? Should we display light mode as the default option? Luckily, with the CSS media query prefers-color-scheme we have a way to know if the user requested his operating system to use a light or dark theme. While we are checking this it makes sense to retrieve the cookie if it was previously stored.

    This is the completed hook with our new addiction:

    TypescriptTypescript codeblock / snippet / file
    useDarkMode.ts
    Copied 🎉
    1import { useState, useEffect } from "react";
    3type ReturnProps = [
    4 boolean | undefined,
    5 React.Dispatch<React.SetStateAction<boolean | undefined>>
    6];
    8export const useDarkMode = (): ReturnProps => {
    9 const [darkMode, setDarkMode] = useState<boolean | undefined>(undefined);
    11 useEffect(() => {
    12 if (darkMode !== undefined) {
    13 if (darkMode) {
    14 document.documentElement.setAttribute("data-theme", "dark");
    15 } else {
    16 document.documentElement.setAttribute("data-theme", "light");
    17 }
    18 setLocalStorageDarkMode(darkMode);
    19 }
    20 }, [darkMode]);
    22 useEffect(() => {
    23 let darkModeVal = getLocalStorageDarkMode();
    24 if (
    25 window.matchMedia &&
    26 window.matchMedia("(prefers-color-scheme: dark)").matches
    27 ) {
    28 if (darkModeVal === null) {
    29 darkModeVal = true;
    30 }
    31 }
    33 setDarkMode(!!darkModeVal);
    34 }, []);
    36 return [darkMode, setDarkMode];
    37};
    39const getLocalStorageDarkMode = (): true | null => {
    40 const darkMode = window.localStorage.getItem("dark-mode");
    41 return darkMode ? true : null;
    42};
    44const setLocalStorageDarkMode = (isDark: boolean): void => {
    45 if (isDark) {
    46 window.localStorage.setItem("dark-mode", "dark-mode-active");
    47 } else {
    48 window.localStorage.removeItem("dark-mode");
    49 }
    50};

    And it can be used in a component like this:

    TsxTsx codeblock / snippet / file
    DarkModeToggle.tsx
    Copied 🎉
    1import { useDarkMode } from "../hooks/useDarkMode";
    3export const DarkModeToggle: React.FC = () => {
    4 const [darkMode, setDarkMode] = useDarkMode();
    6 return (
    7 <button
    8 onClick={() => setDarkMode(!darkMode)}
    9 role="switch"
    10 aria-checked={darkMode}
    11 aria-label={`dark mode is ${darkMode ? "active" : "not active"}`}
    12 >
    13 TOGGLE
    14 </button>
    15 );
    16};
  3. To avoid FOUC, a blocking script is placed in the document head which sets data-theme to hidden. This attribute will set visibility to hidden and remove pointer-events from the website. Once the hook evaluates the correct setting it will style it accordingly.

    With this approach I can keep data-theme=light as the default option so users with javascript disabled will still see the blog styled correctly.

  4. Dark mode is not just having two color palettes and swapping them accordingly. There are other settings you may want to change.

    It's very rare to see black or white backgrounds or text color, it's almost always a shade of very dark / very light gray.

    You may want to apply filters to images to reduce contrast or brightness. This varies from image to image so it's a bit tricky to get right without going over each case.

    Certain elements like shadows or trying to add depth to the UI are harder to do when using dark mode. Dark mode doesn't mean black, and it's important to not sacrifice clarity over a pretty palette, it's perfectly normal to use lighter shades of your color of choice for these features.

    Lastly, I wanted to go over choosing the right font-weight based on the background color. Thanks to a phenomenon called Irradiation Illusion identically sized elements may appear bigger if they are white on a black background. This is easily visible in the following example.

    font-weight: 300
    The quick brown fox jumps
    The quick brown fox jumps
    font-weight 400
    The quick brown fox jumps
    The quick brown fox jumps

    I didn't find any mathematical way of dealing with this so I've just eyeballed it and found that +- 100 font-weight does the job.

Writing blog posts

One of the key points for this blog was the ability to write posts in Markdown. I realized very quickly that Markdown was not going to cut it for what I wanted to do. Anything slightly complex made me miss the rigidity of HTML and the abstraction layer that components allow me to have, so I tried to write each blog post as a React component. This fixed the aforementioned issues but it was getting a bit tedious having to write blog post in HTML, it was less readable than Markdown and much more verbose.

At this point, I was a bit lost. I was looking for a sweet spot in the middle and after some research, I found this amazing project called MDX.

MDX is exactly what I was looking for, a format that lets you write JSX components inside Markdown documents. Luckily there is an official plugin to integrate MDX and Next.js. The documentation shows very clearly how to integrate them so I'll go over some key points that may be confusing.

Inside a Next.js project, there are a few "special" directories reserved for a particular use. One of those is the pages directory.

Next.js, at build time, looks into the pages directory and, for every file ending with .js, .jsx, .ts, .tsx, creates a route based on the file name. With the next/mdx plugin, .md and .mdx files will also be included in this list.

Now that we have blog posts working I wanted to show a list of available blog posts on the home page. Given the small size, I could have hardcoded the content but Next.js provides a function called getStaticProps that is going to solve this problem.

This function gets called at build time and allows us to fetch data and pass it to a component. MDX allows to export variables from inside the file so I decided that every blog post should have a metadata object containing various properties, here's the metadata from this page:

JavascriptJavascript codeblock / snippet / file
blog.mdx
Copied 🎉
export const metadata = {
tags: ["react", "next.js", "javascript", "mdx"],
timestamp: 1606877483 ,
title: "Building a blog in 2020",
href: "/build-a-blog-2020",
description: "⚡ Build a lightning fast blog with Next.js and MDX!",
imageSrc: `https://lorenzopepe.dev${images[0].src}`,
imageAlt: images[0].alt
};

I've created a simple function that loops over the files inside my blog folder and extracts the metadata for each of them. This function is called inside getStaticProps. The data returned from this function will be passed to a component that will render the post previews like in the example below.

TsxTsx codeblock / snippet / file
index.tsx
Copied 🎉
1import Head from "next/head";
2import { GetStaticProps } from "next";
3import { Fragment } from "react";
4import { PostPreview } from "../Components/Blog/PostPreview";
5import { extractMetadata } from "../utils/extractMetadata";
7export interface PostMetadata {
8 tags: string[];
9 timestamp: number;
10 href: string;
11 title: string;
12 description: string;
13 imageSrc: string;
14 imageAlt: string;
15}
17interface IndexProps {
18 postsMetadata: PostMetadata[];
19}
21const Index: React.FC<IndexProps> = ({ postsMetadata }) => {
22 return (
23 <Fragment>
24 <Head>
25 <title>Blog</title>
26 <meta name="description" content="Lorenzo Pepe Blog" />
27 </Head>
28 <ul className="blog-post-list-preview">
29 {postsMetadata
30 .sort((a, b) => {
31 if (a.timestamp < b.timestamp) {
32 return 1;
33 } else if (a.timestamp > b.timestamp) {
34 return -1;
35 } else {
36 return 0;
37 }
38 })
39 .map((d) => (
40 <PostPreview key={d.timestamp} metadata={d}>
41 {d.description}
42 </PostPreview>
43 ))}
44 </ul>
45 </Fragment>
46 );
47};
49export default Index;
51export const getStaticProps: GetStaticProps = async () => {
52 const postsMetadata = await extractMetadata();
53 return {
54 props: {
55 postsMetadata,
56 },
57 };
58};

With this, I managed to have a fully functioning blog. Now it's time to point out some of the issues I have with MDX.

  1. Typically, inside a Next.js blog with simple Markdown there exist a way of using dynamic routes with getStaticPaths and getStaticProps to fetch your content. I was not able to do this with MDX as I had trouble parsing it's output and rendering it as a component. I remember reading a blog post by the tailwind.css team and they did face similar problems.

  2. I'm not exactly sure if this is on VSCode or MDX but the extension which is recommended to use does not seem to work well. It has problems with autocomplete and importing components is kind of a pain. Same for components props or syntax highlighting which for some strange reason at the moment of writing this is completely broken.

  3. This blog post is not overly long and changes to the file take a very long time compiling, sometimes even as much as 20 seconds. I'm not coding on a supercomputer but it's still a decent machine, so you may wanna consider a different workflow such as writing section by section and combining them at a later time. This kinda sucks and it's by far the most annoying thing about working with MDX.

Syntax Highlighting

Syntax highlighting is a crucial piece of blog posts. I wanted to get it right and the most popular solutions didn't fit my needs.

The way I've seen it done most frequently in the wild is to use something like prism-react-renderer, connecting it to the output of a MDX code block and let it take care of tokenizing and highlighting based on the provided theme.

Unfortunately, the results weren't good enough. I wanted something that highlighted the code exactly like VSCode does. prism-react-renderer unfortunately was not doing this and I didn't want to load a library to do this at runtime. Ideally, this would be all done at build time since my code blocks are not editable.

After some research, I found Shiki. Shiki is a syntax highlighter that under the hood uses TextMate grammar to tokenize strings and color tokens. This is how popular editors like VSCode, Sublime and Atom handle syntax highlighting. It also does this at build time which works perfectly with this blog statically generated pages.

We need to connect MDX code blocks to Shiki. Since MDX is part of the unified collective we can use to remark and rehype plugins to transform its content. To connect them we can use the rehype-shiki plugin.

The usage is straightforward. First thing we need to do is to choose a theme. If you are using VSCode you can use Ctrl + Shift + P to open the Command Palette and choose the Developer: Generate Color Theme From Current Settings setting. This will create a json file that you can use inside Shiki.

Inside Shiki's github repo you can find some default themes pre-generated.

After selecting the theme, download shiki and rehype-shiki from npm and edit the withMDX plugin on next.config.js with your theme and preferences.

JavascriptJavascript codeblock / snippet / file
next.config.js
Copied 🎉
const rehypeShiki = require("rehype-shiki");
const withMDX = require("@next/mdx")({
extension: /\.mdx?$/,
options: {
rehypePlugins: [
[
rehypeShiki,
{
theme: "./utils/material-theme-palenight.json",
useBackground: true,
},
],
],
},
});

To use our code blocks we can take advantage of the MDXProvider component. To do that we can pass an object that follows this scheme:

JavascriptJavascript codeblock / snippet / file
mdxComponents
Copied 🎉
const mdxComponents = {
h1: (props) => <MyFancyHeading props={props}/>
}

The object key is the HTML element we are targeting and its value is the component we want to render when MDX encounters that element. Here you can find more info on MDX Provider here.

In my case, I use a custom <CodeBlock> component every time I encounter a <pre> element. This allows me to easily add other features such as line highlighting, the file name, the file extension and line numbers.

TsxTsx codeblock / snippet / file
_app.tsx
Copied 🎉
const mdxComponents = {
pre: (props: any) => (
<CodeBlock
background={props.style[0]}
language={props.children.props.className}
metastring={props.children.props.metastring}
>
{props.children.props.children}
</CodeBlock>
)
}

Interesting tidbits

I think I've covered pretty much everything I wanted but I'm left with a handful of interesting things I discovered.

Emojis aren't accessible, they need to be wrapped up in a <span> with a role of img and an aria-label with a description.

Copying text to the user clipboard is a real pain depending on the device he's using. I think I got it right from testing it on various devices but there might be some edge cases that I haven't detected yet. Of course, it has to be this way because of the security and privacy concerns regarding the user clipboard so I can't complain too much although the way I've set it up feels a bit hacky.

This website should be readable without javascript enabled. It didn't take much work and it only a few features such as copying code snippets and dark mode are missing.
This seems like a good compromise to me. I don't expect many people to have javascript disabled but it may happen. The only issue I have yet to fix is that mobile users with disabled javascript can't open the hamburger menu.

Writing in English has been harder than expected. I always thought I was quite good at writing and reading, with pronunciation being my Achille's heel. Turns out I need extra english practice. In the meantime hopefully Grammarly can save me 😁.

And last but not least accessibility is queen. At this point, I feel like I'm repeating myself so I also want to point out that there are almost always going to be edge cases where certain users may experience difficulties on your website. Just testing for color blindness alone is very hard. By using the console built-in Emulate vision deficiencies console feature you can see that even this same website, after how much I emphasized accessibility, maybe be difficult to read for some users.

Accessibility is a complex and broad topic which isn't only affected by the styling of the website and would need its own series of blog posts but there are also some easy to fix problems such as not breaking the user zoom, avoid breaking scroll and, respecting the WCAG color contrast scores. Browsers are also getting very good at telling you where the problems are and how to fix them.

Ending Thoughts

Coding this blog was slightly harder than I initially thought but not because of technical issues. What gave me the most trouble was overthinking stuff as if I expected thousands of people to browse it on launch day or that a single mistake would tarnish my reputation forever 😨.

At one point I added features such as a complex search system, dropdowns with fancy animations and, a lot of small, pretty (and frankly a bit useless) components without actually having written a single word let alone a single blog post. That finally made me realize that I was missing the point of writing a blog in the first place. Of course, it has to look good but what should matter the most is the content and I can worry about most of the features I want to add in the future.

Ultimately this was a good reminder that while thinking and planning about what you are going to do is a great skill to have, it can easily get out of hand. This, in my experience, will snowball into not seeing any tangible progress and will lead to losing interest in the project. Sometimes it's best to bite the bullet and ship things that may not be perfect yet.

On the technical side of things, it has been nothing but a pleasure. Next.js dev experience is fantastic, honestly, I cannot recommend it enough. I've found tons of resources about the problems I encountered and it has an extremely active and growing community.

Overall building this kind of website was a good exercise that allowed me to review many concepts that are key to modern web development. The source code is available and free to use at lorenzored98/lorenzopepe.