Let's go through the steps needed to build a modern, performant blog in 2020!
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.
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
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.
Inside our custom document head we can preload the font and then use the CSS
@font-face rule to load it.
Most browsers support woff2 files nowadays.
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.
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.
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,
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.
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.
I've created a separate file called
variables.csswhich unsurprisingly stores all the variables that change between modes. Styles will be applied to the
<html>element based on the
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-themeattribute 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.useDarkMode.tsCopied 🎉
Right now this is a glorified wrapper over
useStateand it could very well be placed inside our
DarkModeTogglecomponent, 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
You may have noticed that the code won't run if
undefinedand 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.localStorageand retrieve it whenever he visits the website. The
setLocalStorageDarkModefunction 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:useDarkMode.tsCopied 🎉
And it can be used in a component like this:DarkModeToggle.tsxCopied 🎉
To avoid FOUC, a blocking script is placed in the document head which sets
hidden. This attribute will set
pointer-eventsfrom the website. Once the hook evaluates the correct setting it will style it accordingly.
With this approach I can keep
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
whitebackgrounds 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-weightbased 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: 300The quick brown fox jumpsThe quick brown fox jumps
font-weight 400The quick brown fox jumpsThe 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-weightdoes the job.
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
.tsx, creates a route based on the file name.
.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:
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.
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.
Typically, inside a Next.js blog with simple Markdown there exist a way of using dynamic routes with
getStaticPropsto 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.
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.
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 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
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
rehype-shiki from npm and edit the
withMDX plugin on
next.config.js with your theme and preferences.
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:
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.
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.
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.
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.