How to Create an MDX Blog with Supabase and Next.js
We're going to build a small blog with Next.js, Supabase, and MDX. The goal is simple: write posts as MDX files, store article metadata in Supabase, and statically generate the pages.
What We Will Build
- MDX posts in the
pagesdirectory - a shared layout component
getStaticPropsinjected into each MDX page- Supabase-backed metadata for each article
Setting Up a Next.js Project
Create the app with TypeScript:
npx create-next-app --ts
cd next-mdx-supabase-blog
Install the packages we need:
npm install @supabase/supabase-js
npm install @next/mdx --save-dev
Wire up MDX in next.config.js:
/** @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
Replace pages/index.tsx with pages/index.mdx:
# Hello Blog
Welcome to my **awesome** blog!
Run the dev server and confirm the page renders:
npm run dev
Creating a Layout Component
Add a small layout that receives the article path and the article metadata:
import {ReactNode} from 'react';
export interface LayoutProps {
id: number;
path: string;
}
export function Layout({children, id, path}: LayoutProps & {children: ReactNode}) {
return (
<>
<header>{path}</header>
<main>{children}</main>
<footer>{id}</footer>
</>
);
}
The layout will get its props from getStaticProps.
Injecting getStaticProps
Create a helper that returns props for a given page path:
import {GetStaticPropsResult} from 'next';
import {createClient} from '@supabase/supabase-js';
import {LayoutProps} from '../components/Layout';
export async function getArticleProps(path: string): Promise<GetStaticPropsResult<LayoutProps>> {
return {
props: {id: 1, path},
};
}
For now this just proves the plumbing. The next step is to inject that helper into each MDX page.
Create a remark plugin:
module.exports = () => async (tree, file) => {
const getPath = (file) => file.history[0].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} />;`,
});
};
Then enable it in next.config.js:
options: {
remarkPlugins: [require('./scripts/remark-static-props')],
rehypePlugins: [],
},
Restart the dev server and you should see /index.mdx in the header.
Setting Up Supabase
Create a Supabase project and add your credentials to .env.local:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
In Supabase, create an article table with a path column:
| Key | Value |
|---|---|
| Name | path |
| Description | The name of the file relative to the pages directory |
| Type | text |
| Define as array | unchecked |
| Allow nullable | unchecked |
Insert a row for /index.mdx so the page has metadata to fetch.
Reading Article Data
Now swap the stubbed return value for a real Supabase lookup:
import {GetStaticPropsResult} from 'next';
import {createClient} from '@supabase/supabase-js';
import {LayoutProps} from '../components/Layout';
export async function getArticleProps(path: string): Promise<GetStaticPropsResult<LayoutProps>> {
const supabase = createClient(
<string>process.env.SUPABASE_URL,
<string>process.env.SUPABASE_ANON_KEY,
);
const {data, error} = await supabase
.from<LayoutProps>('article')
.select('*')
.eq('path', path)
.single();
if (error || !data) {
return {notFound: true};
}
return {props: data};
}
Refresh the page and you should see the article id in the footer.
Closing Remarks
This is the basic pattern:
- Write the content in MDX.
- Inject
getStaticPropsfor each page. - Fetch article metadata from Supabase.
- Render both the content and the metadata in a shared layout.
From here, you can extend the table with slugs, titles, authors, images, or view counts without changing the overall architecture.