How to Create an MDX Blog with Supabase and Next.js
We're going to look at how you can create a dynamic blog using Next.js, Supabase, and MDX.
I'm going to assume you have some familiarity about these technologies. Here's all I'll say about them:
- Next.js is framework for building React web apps.
- Supabase is an "open source Firebase alternative".
- MDX lets you embed React/JSX components inside of markdown.
Here's the GitHub repo for the blog we'll be building.
What We Will Build
- We'll use MDX to author our blogs in markdown.
- We'll initialize Supabase's PostgreSQL database so that we can store metadata about our articles.
- We'll use the Next.js framework to statically generate the pages, optimizing our site for production.
Setting Up a Next.js Project
First, let's set up our Next.js app with TypeScript.
npx create-next-app --ts
For this tutorial, we'll name our project next-mdx-supabase-blog
.
Once Create Next App has done its thing, we'll cd
into our project.
cd next-mdx-supabase-blog
Installing Project Dependencies
To power MDX, we'll use Vercel's @next/mdx
library.
This library allows us to write our articles directly
in the pages
directory.
npm install @supabase/supabase-js
npm install @next/mdx --save-dev
Let's now initialize our next.config.js
to create pages with MDX files.
/** @type {import('next').NextConfig} */
const withMDX = require('@next/mdx')({
extension: /\.(md|mdx)$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
module.exports = withMDX({
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
Creating Pages with MDX
Let's try creating a page with MDX.
Delete pages/index.tsx
and create a new file called pages/index.mdx
.
We'll put in some placeholder content to get us started.
# Hello Blog
Welcome to my **awesome** blog!
Now let's run the dev server.
npm run dev
Navigate to http://localhost:3000
and we should see our rendered page.
Creating a Layout Component
Next, we're going to create a bare bones Layout
component.
import {ReactNode} from 'react';
export interface LayoutProps {
path: string;
}
export function Layout(props: LayoutProps & {children: ReactNode}) {
return (
<>
<header>{props.path}</header>
<main>{props.children}</main>
<footer />
</>
);
}
The MDX content will come from props.children
.
We'll show the relative path of the MDX file in the header.
In fact, props.path
will be piped through getStaticProps
as we'll
see in the next section.
Leveraging getStaticProps
with MDX Pages
We're going to create a remark plugin to
inject an export
of getStaticProps
.
getStaticProps
will let us query the Supabase Postgres database
on the server before rendering the page.
Let's create a file where our getStaticProps
implementation will live.
Since we're creating a blog site, we'll creatively call this function getArticleProps
.
import {GetStaticProps} from 'next';
import {createClient} from '@supabase/supabase-js';
import {LayoutProps} from '../components/layout';
export const getArticleProps = async (
path: string,
): Promise<ReturnType<GetStaticProps<LayoutProps>>> => {
return {
props: {path},
};
};
For now, this function is just piping the file path through as a prop.
Custom Remark Plugin to Inject getStaticProps
into MDX Pages
Now we'll create the remark plugin to inject our getStaticProps
into the MDX AST.
Create a file called scripts/remark-static-props.js
.
module.exports = () => async (tree, file) => {
const getPath = (file) => {
let filepath = file.history[0];
return filepath.substring(`${process.cwd()}/pages`.length);
};
tree.children.unshift(
{
type: 'import',
value: `import {Layout} from '../components/layout';`,
},
{
type: 'import',
value: `import {getArticleProps} from '../lib/ssg';`,
},
{
type: 'export',
value: `const filepath = '${getPath(
file,
)}'; export const getStaticProps = async () => getArticleProps(filepath);`,
},
);
tree.children.push({
type: 'export',
default: true,
value: `export default (props) => <Layout {...props} />;`,
});
};
The getPath
helper function gets the file path relative to the pages
directory
in the project.
For example, /foo/bar/next-mdx-supabase-blog/pages/index.mdx
becomes
/index.mdx
.
As you can see also we mutate the MDX AST:
- We import the
Layout
andgetArticleProps
functions. - We export
getStaticProps
. - We default export the
Layout
, spreading the props through.
We'll return to next.config.js
and add our remark plugin.
/** @type {import('next').NextConfig} */
const withMDX = require('@next/mdx')({
extension: /\.(md|mdx)$/,
options: {
remarkPlugins: [require('./scripts/remark-static-props')],
rehypePlugins: [],
},
});
module.exports = withMDX({
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
Now restart your dev server.
npm run dev
You should see /index.mdx
printed in the header.
Setting Up Supabase to Store Blog Metadata
Now we're ready to set up a new Supabase project.
Go to https://app.supabase.io, create an account, and click New project.
Create a name for your project, and choose a strong password for your database.
Once the project is set up, now is a great time to create a file called .env.local
.
We're going to put our Supabase credentials here.
Note, .env.local
is ignored by Git. Check .gitignore
to verify.
You'll want to replace the values below with values generated by your project.
SUPABASE_URL=https://kxboqzytxrrjmqvfecel.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYyOTY4MzA1MywiZXhwIjoxOTQ1MjU5MDUzfQ.H7RmOoVEJ8k0-Z6wt47bwRCr6rylxzOWsiAt6NmTkz0
Creating a Table in Supabase for Article Metadata
In the Supabase console, go to the table editor from the left nav menu.
Click create new table.
We'll name the table article
.
Keep all the default settings for the primary key.
Click save once you're ready.
While we're still in the table editor, let's click new column.
Here let's add a new column named path
.
This is how we'll index into our database from getStaticProps
.
Here's the configuration for path
:
Key | Value |
---|---|
Name | path |
Description | The name of the file relative to the pages directory. |
Type | text |
Define as array | unchecked |
Allow nullable | unchecked |
Great. Now we can create a row for our home page.
Click insert row.
The primary key is auto-generated. All we need to do is add the path for our home page: /index.mdx
.
Sourcing Data from Supabase in getStaticProps
Update Layout
so that the footer
shows the id
of the article generated
by the database.
import {ReactNode} from 'react';
export interface LayoutProps {
id: number;
path: string;
}
export function Layout(props: LayoutProps & {children: ReactNode}) {
return (
<>
<header>{props.path}</header>
<main>{props.children}</main>
<footer>{props.id}</footer>
</>
);
}
Now in getArticleProps
, we'll use the Supabase client
to access our database.
import {GetStaticProps} from 'next';
import {createClient} from '@supabase/supabase-js';
import {LayoutProps} from '../components/layout';
export const getArticleProps = async (
path: string,
): Promise<ReturnType<GetStaticProps>> => {
// First we create the Supabase client.
const supabase = createClient(
<string>process.env.SUPABASE_URL,
<string>process.env.SUPABASE_ANON_KEY,
);
// Then we access the article where the path matches.
// We expect a singular piece of data, so we use `single()`.
const {data, error} = await supabase
.from<LayoutProps>('article')
.select('*')
.eq('path', path)
.single();
// If there's an error or no data, then handle the error.
// You may want to handle the error cases differently, but this
// is just to give you an idea.
if (error || !data) {
return {
notFound: true,
};
}
// Finally, let's return the props to the layout.
return {
props: data,
};
};
Now when we look at our page, we should see the pages ID 1
in the footer.
Closing Remarks
As you can see we are just scratching the surface of what we can do with a Postgres database backing our blog.
For example, we could add a column to track the number of views, number of likes, author information, image URL, and so on. The possibilities are endless.
When you want to add a new blog:
- Create a new MDX file in your
pages
directory. - Create a corresponding row in your Supabase database.