Pete Naish
Pete Naish

Pete Naish · Senior Developer

4 min read

Building a blog in the year 2021

Building a blog in the year 2021 cover photo.

Sure we could've used one of the thousands of online publishing platforms available, but where's the fun in that?

We wanted to own our content and customise it for our needs. We also love playing around with new technology at CreateTOTALLY so this was a great excuse.

There were a few prerequisites:

  1. It had to support MDX - we wanted to give authors the ability to drop components into their articles
  2. It had to be databaseless (DBless) - we wanted to save articles directly in the codebase
  3. It had to be lightening fast and accessible
  4. It had to be optimised for SEO

Checkout our lighthouse scores:

Lighthouse scores

Calling on old friends

We absolutely love Next.js and Tailwind CSS here at CreateTOTALLY. So much so we built our client facing platform using it.

Next.js

Next.js is a React Framework which gives you server-side rendering, dynamic routing and lots more out of the box.

Tailwind CSS

Tailwind is a utility-first CSS framework. In our case we didn't write a single line of custom CSS. You simply update tailwind.config.js (or don't and use the defaults) and use the class names provided out of the box.

Here's our config file:

const colors = require('tailwindcss/colors')

module.exports = {
  mode: 'jit',
  purge: ['./pages/**/*.tsx', './components/**/*.tsx'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    colors: {
      ...colors,
      primary: '#596bff',
    },
    extend: {
      zIndex: {
        '-10': '-10',
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [require('@tailwindcss/typography')],
}

All we had to add is a custom z-index for the canvas animation and our primary branding colour. Simple.

We also added the Tailwind typography plugin which adds a set of prose classes to our markdown content.

MDX

MDX allows you to embed React components directly in your Markdown files. Check this out:

This is a JSX embedded directly into Markdown.

And here's what it looks like in our MDX file:

### MDX

MDX allows you to embed React components directly in your Markdown files. Check this out:

<div className="bg-gray-600 text-pink-400 rounded p-4">
  This is a JSX embedded directly into Markdown.
</div>

Combined with Next.js's static generation, we created a simple build step which loops through all of our local .mdx files and generates dynamic links, just like the one you're reading now.

Static post generation

We used the official blog starter as a starting point.

This allows us to have a directory structure which looks like this:

   _posts
     another-blog-post.mdx
     building-a-blog-in-2021.mdx

We then have a simple API to loop through each of the .mdx files in this directory and create the static paths Next.js needs using the filenames as slugs.

Draft articles

Draft articles are simply kept outside of this directory and copied to _posts when we're ready to publish.

Metadata

As described in the blog-starter, you can store metadata directly in the MDX files and parse them using a library called gray-matter.

Here's what the metadata looks like in the MDX files:

---
title: 'Building a blog in the year 2021'
excerpt: 'How we built this blog using our some of our favourite tools including Next.js, TailwindCSS and MDX.'
date: '2021-10-11T11:45:07.322Z'
author:
  slug: pete-naish
coverImageUrl: '/images/covers/tim-mossholder-6gY2MGFecpk-unsplash.jpg'
ogImageUrl: 'tim-mossholder-6gY2MGFecpk-unsplash.jpg'
---

Rendering React in Markdown

Finally, in order to fully support MDX we used the guide over at MDX - Do it yourself to create our renderWithReact method.

import { ReactChildren } from 'react'
const babel = require('@babel/core')
const React = require('react')
const { renderToStaticMarkup } = require('react-dom/server')
const mdx = require('@mdx-js/mdx')
const { MDXProvider, mdx: createElement } = require('@mdx-js/react')
const transform = (code: string) =>
  babel.transform(code, {
    plugins: [
      '@babel/plugin-transform-react-jsx',
      '@babel/plugin-proposal-object-rest-spread',
    ],
  }).code

const renderWithReact = (mdxCode: string) => {
  const jsx = mdx.sync(mdxCode, { skipExport: true })
  const code = transform(jsx)
  const scope = { mdx: createElement }
  const fn = new Function(
    'React',
    ...Object.keys(scope),
    `${code}; return React.createElement(MDXContent)`,
  )
  const element = fn(React, ...Object.values(scope))
  const components = {
    h1: ({ children }: { children: ReactChildren }) =>
      React.createElement('h1', children),
    div: ({ children, ...rest }: { children: ReactChildren }) => {
      return React.createElement('div', { ...rest }, children)
    },
  }
  const elementWithProvider = React.createElement(
    MDXProvider,
    { components },
    element,
  )
  return renderToStaticMarkup(elementWithProvider)
}

export default renderWithReact

This is then called in our getPostBySlug method to convert our MDX content to React.

export function getPostBySlug(slug: string, fields: Fields) {
  const realSlug = slug.replace(/\.mdx$/, '')
  const fullPath = join(postsDirectory, `${realSlug}.mdx`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)

  const items: IPost = {}

  // Ensure only the minimal needed data is exposed
  fields.forEach((field) => {
    if (field === 'slug') {
      items[field] = realSlug
    }

    if (field === 'content') {
      // here we convert our MDX content
      items[field] = renderWithReact(content)
    }

    if (data[field]) {
      items[field] = data[field]
    }
  })

  return items
}

We'll be open sourcing this repository shortly, so you can see exactly how it's built.

CreateTOTALLY  is a next-generation marketing execution platform, uniquely connecting media planning to campaign automation.