Rebuilding Our Jekyll Website with Next.js and Theme UI

Avatar for Brandon Weiss

Brandon Weiss

Senior Engineer

5 Apr 2021

Graphic for blog post Rebuilding Our Jekyll Website with Next.js and Theme UI

Recently we redesigned and rebuilt our promotional site that was built in Jekyll and I thought it might be interesting to explain why we did it, as well as talk about the choices we made like deciding between Gatsby and Next.js for static site generation, using React (JSX and MDX) as a templating language and for writing docs, colocating styles with CSS in JS, and building a design system with Theme UI.

Jekyll is getting old

Jekyll was one of the first Ruby-based static site generators and is an impressive twelve years old now. In general it’s held up pretty well for us but one area where we really started to feel some pain was in the asset pipeline.

Jekyll’s asset pipeline is built on Sprockets, another venerable Ruby project built back when asset pipelining was both simpler and less mature. It worked well then, but in the intervening years the JavaScript and CSS landscapes have changed dramatically. New tools like Babel have been created and older tools like autoprefixer have migrated to become plugins of other systems like postcss. Trying to use standard, modern tools with Jekyll is either incredibly difficult or just not even possible.

We knew trying to redesign the site and keep using Jekyll was going to be untenable, so we decided to rebuild it at the same time using a modern static site generator. Doing a rewrite is often not a good idea because they so frequently turn in to disasters, but sometimes there really is no small, iterative path forward and it might be worth starting over from scratch.

One side benefit is also that while Jekyll is written in Ruby and uses Liquid as a templating language, everything else we have is written in JavaScript. It’s not that Ruby and Liquid are difficult to use, but switching to a JavaScript-based static site generator lets us remove some small amount of complexity and simplify things, which is nice.

Modern static site generators

Years ago when we chose Jekyll there were perhaps too few static site generators to choose from, but now there are arguably too many. There’s even a website called StaticGen that lists them all!

That said, while there are a lot of options, when it comes to choosing a foundational tool like a framework that you’re going to sink a lot of time and effort into, it’s best to choose something with a big community. Gatsby and Next.js are the two most popular projects (by GitHub stars) so our choice was between them.

Comparing Gatsby and Next.js

There’s a lot to look at when evaluating a tool to see if it’s something you should use, but almost everything tends to fall under three areas: functionality, complexity, and stability.

Functionality

While Gatsby and Next.js are both similar in many ways, the way they differ the most is in how they render and generate pages.

Gatsby can be used to generate static pages at build time with all the data ready to be served to the browser, or if you’re building something with more dynamic data like an application, it can serve a JavaScript bundle to the browser that pulls in data dynamically from an API.

What Gatsby doesn’t have is any concept of a backend. If you need server-side logic you’ll have to build a separate app in some other framework and consume it from Gatsby, like any other API.

Next.js on the other hand can generate static pages and fetch data in the browser just like Gatsby, but it also has its own backend built-in. You can run it in a “server-full”, process-oriented mode on any standard host, or it can be deployed as serverless functions on Vercel, Netfliy, AWS Lambda, et cetera.

The advantage of Next.js having the backend built-in is it’s one less thing you have to configure and set up yourself, and if you’ve ever tried to bolt an Express app onto an app built with Create React App, you know how unbelievably difficult it is to get everything working together in a seamless way in development.

Right now our marketing site is entirely static and doesn’t need a backend, but it’s not entirely unreasonable to think it might one day, so in this case I feel like it’s better to have it and not need than to need it and not have it.

✅ Advantage Next.js

Complexity

Complexity is also something really worth considering when choosing between tools. A tool should solve your current problems and a small amount of likely future ones with the minimum amount of complexity. A tool that is too complicated to use, either because it does too much or it was designed poorly, will cost you more than it’s worth in the long-run.

Documentation is a great proxy for figuring out how complicated a tool is without having any experience with it.

For example, Next.js’s docs are surprisingly short considering what the tool is. They’re really well-written and easy to understand. It doesn’t take long to read them and at the end I feel like I have pretty good handle on things.

On the other hand Gatsby’s docs are really sprawling and expansive. There’s a lot to read and I can literally get lost in them. I’m left feeling like I really don’t understand Gatsby very well, which makes me think it is too complex.

Gatsby’s data layer is an example of potentially unnecessary complexity. I really like GraphQL and the idea of using it as unified interface to interact with everything from local files and data to remote APIs is clever, but in practice it feels like it can make simple tasks really complicated and difficult. It’s hard to make a case that everyone on the team should learn GraphQL just so we can paginate a blog.

Next.js has no data layer and lets you fetch and interact with data how you currently do it or however you think makes sense, with nothing new to to learn.

✅ Advantage Next.js

Stability

The last thing that’s important when choosing a tool is stability, which is a tough balancing act. You don’t want a tool that never evolves and gets better, but neither do you want a tool that changes or breaks things too frequently. You want something somewhere in the middle.

Gatsby practices “continuous releasing” and releases patch versions quite frequently, sometimes daily or even multiple times a day. There are some benefits to this but the downside is that bugs can more easily sneak out and I got burned a few times by obscure bugs because I updated to the latest patch version.

On the other hand, Next.js feels like it has a better cadence. Releases happen regularly but not too frequently to be problematic and bugs seem rare. Next.js also includes release notes for patch versions which gives me more confidence. Overall it feels more reliable and easier to maintain.

✅ Advantage Next.js

Choosing Next.js

It seems like Next.js is the best choice for us, but for the same reason we chose a JavaScript-based static site generator (simplifying and reducing complexity) it also makes sense to look forward and ensure we’re not unintentionally growing complexity in the future.

Our client-side application is currently written in an old version of Angular (1.0). We haven’t upgraded because unfortunately it’s so different from modern Angular that we’d basically have to rewrite our entire application, which wasn’t worth the effort at the time. But we can’t put it off forever and if we have to do a rewrite anyways, we’ll probably switch to React, at which point we have to make a similar choice again: do we use Gatsby, Next.js, or something else?

Gatsby has the same problems already mentioned. We have a backend for our Angular application and we’re not going to rewrite it, that means we either have to create a bridge so Gatby’s data layer can talk to our API and then use GraphQL, or work around the data layer entirely. Neither of those are great options.

We could choose something like Create React App which is just a client-side framework for React. It’s probably the closest thing to our existing Angular app. But it doesn’t render static pages so we can’t use that to build our marketing site, which means we’d have to keep using two different frameworks.

This is where Next.js really shines. It’s flexible enough that you can use it to build static sites, server-rendered applications, client-rendered applications, APIs, or something that is some combination of any of those. And impressively it does it while feeling like one simple framework and not four differenet frameworks bolted together.

Using React as a templating language

Building a static marketing site with a JavaScript framework like React might seem like overkill as it was really designed for building interactive application interfaces. But we didn’t choose React for its “reactivity”—we chose it for JSX, its templating language.

JSX

Most of the benefits that people attribute to React actually come from JSX. On the surface JSX seems like just another templating language, but it’s much more than that, and it would be more accurate to describe it as a “syntax extension” to JavaScript.

Instead of writing an HTML file with special template tags that run code in another language and interpolate the output into the HTML, JSX is a JavaScript file with a special syntax that allows you to write HTML in your JavaScript.

It’s the difference between something like this:

<% if user %>
  <div>
    Welcome, <%= formatName(user) %>
  </div>
<% else %>
  <%= render partial: 'button', value: 'Sign in', id: "sign-in" %>
<% end %>

And something like this:

import Button from 'components/Button'

const formatName = (user) => {
  return `${user.firstName} ${user.lastName}`
}

const signIn = async () => {
  await fetch('/signIn').then(() => {
    window.location = '/dashboard'
  })
}

export default () => {
  if (user) {
    return (
      <div>
        Welcome, {formatName(user)}
      </div>
    )
  } else {
    return <Button value="Sign in" onClick={signIn} />
  }
)

There’s a lot going on here and if you strongly believe in the separation of concerns of HTML, CSS, and JavaScript then your initial reaction might be pretty negative, however there are some subtle but big benefits here that are worth considering.

Native logic

With a templating language you wind up weaving your conditionals and loops in and out of the HTML, which is sort of difficult to read and can result in really confusing errors when you get some syntax wrong.

With JSX you can write logic natively with JavaScript and then return HTML from your expressions. It cleverly makes both the JavaScript and HTML read and write as naturally together as if they were read and written separately. And when there are errors you get a real JavaScript error with a stracktrace and line numbers, instead of some rough approximation or nothing at all like in some templating languages.

Colocation

With a templating language you’re necessarily forced to separate your concerns. You write HTML in one place, then complex presentation logic become “helpers” in another place, and interactive logic becomes JavaScript in a different place. Conceptually this seems like a good idea, but in practice it tends to make code brittle, difficult to navigate, and hard to understand.

With JSX it’s possible to colocate code in a way that wasn’t really possible before. JSX is just JavaScript, so any presentation and interactive logic can now be kept in the same file where it’s being used. Not having to jump between files to understand a template is a huge win, and a byproduct is it creates a clear delineation between logic that is only used in one place and logic that is intended to be shared.

Linting

With a templating language you generally get pretty mediocre linting. It’s not impossible, but it’s pretty difficult to track code across multiple template tags in a way that lets you statically analyze it well, so template linters tend to be pretty simple and naive, and syntax errors are often found at runtime.

With JSX you get much more accurate and useful linting. The vast majority of errors can be caught in your editor before you ever even hit save or refresh your browser. It’s hard to overstate how much time you save and how much more enjoyable it is when you have that instant feedback.

MDX

For a long time anyone that wrote content for the web like blog posts or documentation had to use a content management system. You didn’t technically have to, but if you didn’t you’d have to write your content in HTML, tediously wrapping everything in the correct tags, and no one really wants to do that.

Then Markdown came along and provided another option. Instead of writing HTML you could use this lightweight syntax for describing common HTML elements that also happened to be human-readable. Ever since then a lot of technical people have opted to write their content in Markdown instead of HTML.

But while Markdown is great, it does have some limitations. For example in the middle of a docs page you want to insert some complex HTML and JavaScript for either showing code snippets in multiple languages or even a code snippet that you can actually run in a sandbox, there’s no easy way to do that.

You either wind up duplicating a giant chunk of HTML in every Markdown document, iframing in another application, or writing a Markdown plugin to do what you want, all of which is just difficult enough that it’s often not worth it. But then came MDX.

MDX is a blend of JSX and Markdown. In the same way that JSX is JavaScript that is extended to support HTML, MDX is JavaScript that is extended to support Markdown.

import snowfallData from './snowfall.json'
import BarChart from './charts/BarChart'

# Recent snowfall trends

2019 has been a particularly snowy year when compared to the last decade.

<BarChart data={snowfallData} />

Being able to import and use React components in Markdown unlocks all sorts of possibilities. For example our API documentation had lots of content that looked like this:

<h2>List Tests</h3>

<p>Fetch an array of all the tests in your account.</p>

<div class="panel panel-default">
  <div class="panel-heading">Request</div>
  <div class="panel-body">
    <span class="endpoint">
      <span class="method">GET</span>
      <span class="path">https://api.ghostinspector.com/v1/tests/?apiKey=<apiKey></span>
    </span>
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">Parameters</div>
  <div class="panel-body">
    <dl class="dl-horizontal">
      <dt><code>apiKey</code></dt>
      <dd>Your API key provided in your account</dd>
    </dl>
  </div>
</div>

Writing and reading docs like this was really cumbersome and difficult, and this example is even abbreviated to make it easier to understand. With MDX we can do this:

## List Tests

Fetch an array of all the tests in your account.

<Request
  method="GET"
  url="https://api.ghostinspector.com/v1/tests/?apiKey=<apiKey>"
  authenticated
/>

That’s just scratching the surface. There’s far more we can do with MDX to make our docs easier for us to manage and more useful to people using them.

Colocating styles with CSS in JS

I think CSS is one of the most deceptively complex programming languages ever created. At first it seems trivially simple, but the more you write the more you come to realize how difficult and maddening it is.

It seems like no matter what you do, as a project grows and more people work on it, the CSS always eventually devolves into a mess—no code seems to succumb to entropy quite so fast as CSS.

The root problem seems to be in its design, both its inherent lack of structure and its core feature: the cascade.

Over the years different ideas have sprung up about how to solve these problems, and although they’ve had varying levels of success, they do seem to be getting progressively better.

Semantic names

Early attempts at structuring CSS focused on semantics and naming. The idea was that class names shouldn’t describe the styles themselves, they should instead describe the entities that they were styling.

So this:

<div class="border red">
  Danger!
</div>
.border {
  border: 1px solid black;
}

.red {
  color: red;
}

Turned into this:

<div class="alert warning">
  Danger!
</div>
.alert {
  border: 1px solid black;
}

.alert.warning {
  color: red;
}

This was a big improvement! Previously it wasn’t obvious what would be affected if you changed .border without looking at every usage in the HTML. With semantic names, now both the HTML and the CSS make sense on their own.

But there was still the problem with the cascade. What happens when there is another .warning class? What happens when some parent style cascades down and styles the .alert? You wind up adding more styles and playing with specificity, which works in the moment but ultimately makes the CSS more confusing and more brittle, and causes more problems later on.

Block Element Modifier (BEM)

The next evolution was to create naming schemes for the semantic class names. There are lots of methodologies for this but BEM is probably the most popular one. It looks like this:

<div class="alert alert__state--warning">
  Danger!
</div>
.alert {
  border: 1px solid black;
}

.alert__state--warning {
  color: red;
}

The delimiters in BEM make the class names easier to read, understand, and remember, but the real win is how they work around the cascade and specificity. If every selector is at the “top” level, then when a style cascades down in a way you don’t want, overriding it as simple as just applying the class you do want. You never need to increase the specificity of your selectors or use !important.

But this is where semantic naming hit a bit of a dead end, because it assumed that everything was an entity which could be named, and it turns out that’s not the case.

Whitespace is a core component of design and it is created in CSS using margin (or Flexbox and Grid). If you put margin on an entity you will eventually find a scenario where you need that margin to be different, and there’s not a great way to do that.

You can’t create near-infinite BEM modifiers for every entity to handle spacing. You can create “wrapper” elements to modify the entities but the naming gets confusing and now you’re playing with specificity again.

The root problem is that layout and spacing are contextual and they’re being specified at the wrong level. Entities shouldn’t have layout and spacing, their parents (context) should. But it’s very difficult to create a new CSS class for every use of margin, Flexbox, or Grid, let alone figure out appropriate names for them, so it wasn’t really done until a new idea came along.

Utility frameworks

The concept of utility classes had been around for a long time but utility frameworks like Tachyons and Tailwind took them to their logical conclusion.

Utility frameworks throw out the concept of semantic class names in favor of a bundle of utility classes. Each utility class does exactly one thing and has a name that specifically describes what that one things is.

<div class="border-1 border-black text-red">
  Watch out!
</div>
.border-1 {
  border: 1px;
}

.border-black {
  border-color: black;
}

.text-red {
  color: red;
}

If that seems suspiciously similar to how we originally used to write CSS, that’s because it basically is. The main difference is just that the CSS is now all pre-written for you and all you have to do is use it.

The huge but somewhat unobvious benefit of colocating styles in HTML is how much more productive you can be and how much more enjoyable it is. You no longer have to constantly switch back and forth between an HTML file and multiple CSS files, a behavior we’re so used to we don’t realize how much friction it created until it’s gone.

Utility frameworks were a huge step forward, but they did have a few downsides. First, you have to learn and memorize all the utility class names, which is a big hump to get over. It eventually pays off, but it’s difficult up front.

Then there’s the weight of the CSS. All the classes are pre-made so the CSS file the browser loads contains everything, even classes you will never actually use. On top of that, the modifiers like responsive breakpoints, and customizations like colors have a multiplicative effect on the number of utility classes. It’s fairly easy to get to a point where the CSS file can balloon to 20 MBs, which is completely unusable.

To solve this some of the frameworks have strategies for purging unused styles from the CSS file in production. It works, but it’s tough to do well and requires everyone being very intentional and careful with how they write their classes so the framework doesn’t mistakenly purge any classes that are in use.

The last problem is a bit subjective but utility frameworks can result in HTML that’s hard to read and write. There just isn’t a great way to format an HTML element with 10+ classes applied to it, and when every element in an entire file has that many classes it can make working with it really difficult.

Despite the downsides, utility frameworks solve a lot of the problems with CSS and we seriously considered using Tailwind, but we decided to use a different methodology instead.

CSS in JS

The most recent evolution in writing CSS is called CSS in JS and is closely tied to React. Once JavaScript and HTML were colocated in JSX people started to experiment with writing CSS in JavaScript in order to colocate everything.

Every library has a slightly different approach, but they all look something like this:

<div
  css={`
    border: 1px solid black;
    color: red;
  `}
>
  Warning!
</div>

It’s basically a modernized form of inline styling, but behind the scenes when the website is built each chunk of CSS is given its own random, unique class name that is applied to its HTML element, then all the CSS is bundled together into one CSS file that is linked to just like a normal stylesheet.

CSS in JS is somewhat controversial at the moment because it’s so radically different from any previous CSS methodology or framework, but once you get past that you can start to see just how well it solves all of the problems with CSS that we’ve been dealing with up to this point.

With CSS in JS there’s no problem with specificity because there is no specificity. There’s no problem with naming either because there is no naming. The styling is colocated, just like utility frameworks, so you get all the benefits of the markup and styling being in the same file, but you don’t need to memorize a bunch of class names or purge the CSS file to keep it small. It does make the HTML a little more difficult to read, but it’s more readable than utility frameworks and there are ways of improving the readability further.

I’ve been using CSS in JS for a few years now and while I don’t want to overhype it, it’s hard to overstate how nice it is to no longer have to deal with the same CSS problems I’ve been trying to work around for over two decades.

Building a design system with Theme UI

The last piece of the stack is a design system. We want our website to be easy to build, easy to maintain, and feel cohesive. We don’t want to waste time rebuilding UI elements over and over again and end up with eighteen different buttons that are all slightly different.

Components are the basic building blocks of design systems, and React is a component system, so if you sprinkle in some CSS in JS you can start to create a design system.

const variants = {
  info: {
    border: '1px solid black',
    color: 'blue',
  },
  warning: {
    border: '1px solid black',
    color: 'red',
  },
}

const Alert = ({ children, variant }) => (
  <div css={variants[variant]}>
    {children}
  </div>
)

It’s pretty easy to implement component variants, but that means we’ll be re-implementing the same pattern in every component. We also want fixed typographic and space scales for consistency, but that means we’ll have to import them everywhere they’re used. And we want it all to be responsive, but that means we’ll have to layer in loads of media queries. It’s doable but it gets pretty tedious and you very quickly get to a point where you feel like you’re reinventing the wheel.

What we want is a framework sort of like Bootstrap that gives us a set of basic components, but without any styling so we can easily implement our own designs. It should use CSS in JS but have conveniences for dealing with scales and responsiveness. And that is the idea behind Theme UI.

Theme file

The core of Theme UI is its Theme file. It’s a document that defines all of a design system’s components and variants:

export default {
  alerts: {
    info: {
      border: '1px solid black',
      color: 'blue',
    },
    warning: {
      border: '1px solid black',
      color: 'red',
    },
  },
}

Then you use the variants like this:

import { Alert } from 'theme-ui'

export default () => (
  <Alert variant="warning">
    Warning!
  </Alert>
)

You might notice that this seems like the opposite of colocation—now the styles are in a separate file again, just a JavaScript file instead of CSS file—but the reason for this will make sense in a bit.

The sx prop

Theme UI has one other way to style things and that’s with the sx prop.

import { Box, Image } from 'theme-ui'

export default () => (
  <Box>
    An image <Image sx={{ float: 'right' }} />
  </Box>
)

The sx prop is basically the same inline styling approach used in other CSS in JS libraries with a few extra features added on.

Why two ways of styling? This is, I think, one of the best parts of Theme UI. I consider it a sort of hybrid colocation where the styles are colocated in the place that is most appropriate for them depending on their type: component or layout/spacing.

Components are the building blocks you use to build user interfaces, so the styling for them generally should be done once, up front, and then only tweaked or changed rarely. For that reason putting the component styles in a separate, special file makes sense.

Layout and spacing is the styling that happens when using the components to build interfaces. It is the contextual styling that surrounds components and lays them out. For that reason keeping the layout/spacing styles in the markup makes sense.

To summarize more succinctly, if styles are shared they go in the theme file and if styles are contextual they go in the markup.

This distinction has some additional benefits, like giving you an escape hatch to override a component, either as a one-off special case or as an experiment before moving the new styles into a named variant intended to be used in other parts of the user interface.

Scales and responsiveness

Theme UI has one more trick up its sleeve, which is a special syntax for handling scales and responsiveness. There are scales for responsive breakpoints, font sizes, spacing, and a few other things.

{
  breakpoints: ['40em', '52em', '64em'],
  //           0,  1,  2,  3,  4,  5,  6,  7,  8,  9
  fontSizes: [12, 14, 16, 18, 20, 24, 30, 36, 48, 64],
  //      0, 1, 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12,  13,  14,  15,  16,  16
  space: [0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128, 160, 192, 224, 256],
}

You define them in your theme file and then when you pass a number to a property it will look up the value in the appropriate scale at that array index.

import { Text } from 'theme-ui'

export default () => (
  <Box sx={{ padding: 2 }}>
    <Text sx={{ fontSize: 2 }}>Some text</Text>
  </Box>
)

This will make a box with 8px of padding and text with a font size of 16px. It gets even more interesting when you add in the array syntax.

import { Text } from 'theme-ui'

export default () => (
  <Text sx={{ fontSize: [2, 3, 5, 8] }}>
    Some text
  </Text>
)

This will map the values to the scales, but change them depending on the breakpoints scale. The font size will start at 16px and then change to 18px, 24px, and 48px at larger breakpoints.

How it went

I’ve built a lot of interfaces over the years and building with this stack was probably the most enjoyable experience I’ve ever had. In every new project there’s that point where it starts to feel like it’s getting away from you as it gets bigger and more complex, and that never really happened with this one.

Now that the new marketing site is out we’re starting to do the same work as before (updating the changelog, docs, write new blog posts, et cetera) but with the new stack, and while there are lots of small improvements the biggest win seems to be the docs. Editing them in MDX instead of raw HTML has made them exponentially easier to change and review.

My hat is off to the people who make Next.js, React, MDX, and Theme UI—they are incredibly impressive tools and I’m excited to see how they evolve!

Share this post