Building a blog in the year 2021
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:
- It had to support MDX - we wanted to give authors the ability to drop components into their articles
- It had to be databaseless (DBless) - we wanted to save articles directly in the codebase
- It had to be lightening fast and accessible
- It had to be optimised for SEO
Checkout our 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:
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.