Creating a blog platform with Astro and MDX
TLDR: source code can be found here
Why?
Ever since I started blogging, I wanted to do this on my personal website thomasledoux.be. Since it seemed rather difficult to set up Markdown/MDX support in Remix & Next.js (the 2 frameworks I use for my website before), I chose to write my blogs at dev.to instead. This worked really well for a while, but now I want to have more control over my blog’s layout, analytics and to have MDX support. So I started building my own blog platform, and chose to do this with Astro!
How?
Setting up Astro
Setting up Astro is very easy.
You can just run npm create astro@latest
in your terminal, follow the steps, and you’ll have your project up and running in seconds.
Once your Astro project’s set up, the next thing we’ll do is adding MDX support.
You can add MDX support through an Integration in Astro.
This is as easy as running npx astro add mdx
in your terminal.
Creating MDX blogs
Once the MDX integration is installed, you can start using MDX components and pages in your application.
You can get started by adding a folder /blog
in the /src/pages
folder.
Inside this folder you then create your .mdx
files, for example best-features-nextjs-conf-2021.mdx
.
At the top of your MDX files, you can add frontmatter
properties.
You can set up a layout
, which will make your blog’s content get rendered inside of the given layout
.
Inside the frontmatter
properties you can also add custom properties, like title
, date
, tags
, …
This will look like:
---
title: 'The 3 best features announced at Next.js Conf 2021';
layout: '../../layouts/BlogLayout.astro';
tags: ['nextjs', 'javascript'];
date: '2021-06-15T15:14:39.004Z';
---
These properties will be available to use when you import the MDX component/page in another component/page, or when you use Astro.glob()
to read MDX files from the filesystem.
Your can use this a blog overview page to display the title, creation date and tags from the blog.
---
let posts = await Astro.glob('./*.mdx');
/* output:
[
{
title: 'The 3 best features announced at Next.js Conf 2021',
layout: '../../layouts/BlogLayout.astro',
tags: ['nextjs', 'javascript'],
date: '2021-06-15T15:14:39.004Z'
}
]
*/
---
<section>
{
posts.map(post => (
<article>
{post.frontmatter.title} - {post.frontmatter.title}
<div class="flex gap-x-4">
{post.frontmatter.tags.map(tag => (
<span>{tag}</span>
))}
</div>
</article>
))
}
</section>
Adding reading time for blog posts
Astro makes it easy to add Remark or Rehype plugins to your markdown.
You can extend add a markdown
property to the Astro config file, an add a function/plugin to the remarkPlugins
property (the extendDefaultPlugins
property is added to make sure the default plugins aren’t overwritten by this config change):
import { remarkReadingTime } from './src/utils/calculate-reading-time.mjs';
export default defineConfig({
integrations: [mdx()],
markdown: {
remarkPlugins: [remarkReadingTime],
extendDefaultPlugins: true,
},
});
The ./src/utils/calculate-reading-time.mjs
file will look like this:
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';
export function remarkReadingTime() {
return function (tree, { data }) {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
// readingTime.text will give us minutes read as a friendly string,
// i.e. "3 min read"
data.astro.frontmatter.minutesRead = readingTime.text;
};
}
So you’d use 2 external libraries reading-time
and mdast-util-to-string
to make this work. Don’t forget to npm install
these!
Adding this to the Astro config, makes this data available in the frontmatter
of all your MDX files.
You can now start using this in our blog overview from earlier:
<section>
{
posts.map(post => (
<article>
<h2>
{post.frontmatter.title} - {post.frontmatter.date}
</h2>
<div class="flex gap-x-4">
{post.frontmatter.tags.map(tag => (
<span>{tag}</span>
))}
</div>
<p>{post.frontmatter.minutesRead}ing time</p>
</article>
))
}
</section>
Setting up SEO (meta tags, og:image)
Because we are defining a layout
to be used on our MDX pages, we can start using the frontmatter
properties of the MDX page inside the layout
because they are passed in the Astro.props
object.
These frontmatter
properties can then be used to add something like a <title>
and an og:image <meta>
tag:
---
const { frontmatter } = Astro.props;
---
<head>
<title>{frontmatter.title}</title>
<meta content={frontmatter.title} property="og:title" />
<meta content={frontmatter.title} property="twitter:title" />
<meta name="twitter:card" content="summary_large_image" />
<meta
content={`https://website-thomas.vercel.app/api/og?title=${frontmatter.title}`}
property="og:image"
/>
</head>
Note that in the example I’m using an API route of a different domain, this is because I’m using the @vercel/og
package to generate the og:image
based on the title of my blog post. You can read the docs here.
I also added some sparkles to the background, based on the example provided by Vercel.
The code for the og:image
generation looks like this (created inside a Next.js API route on my Next.js site):
I’m using the Inter
font, so if you want to use this too, make sure to download the font files and include them in your project.
import {ImageResponse} from '@vercel/og'
export const config = {
runtime: 'experimental-edge',
}
const font = fetch(new URL('../../assets/Inter.ttf', import.meta.url)).then(
res => res.arrayBuffer(),
)
export default function handler(req) {
const fontData = await font;
try {
const {searchParams} = new URL(req.url)
const hasTitle = searchParams.has('title')
const title = hasTitle
? searchParams.get('title')?.slice(0, 100)
: 'My default title'
return new ImageResponse(
(
<div
style={{
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
fontFamily: 'Inter',
backgroundImage:
'radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)',
backgroundSize: '100px 100px',
}}
>
<div
style={{
width: '80%',
display: 'flex',
flexDirection: 'column',
textAlign: 'center',
alignItems: 'center',
}}
>
<p style={{fontSize: 32}}>Lionel Tchami's blog</p>
<p style={{fontSize: 64}}>{title}</p>
</div>
</div>
),
{
width: 1200,
height: 600,
fonts: [
{
name: 'Inter',
data: fontData,
style: 'normal',
},
],
},
)
} catch (e) {
console.log(`${e.message}`)
return new Response(`Failed to generate the image`, {
status: 500,
})
}
}
And that’s it, my blog is now hosted on my own site! Go check it out at https://www.thomasledoux.be/blog. In a next blog article, I’ll dive deeper into how I added a system for comments on blogs, and analytics using Prisma and Planetscale for the DB stuff.