skies.dev

How to Create an MDX Blog with Supabase and Next.js

4 min read

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 pages directory
  • a shared layout component
  • getStaticProps injected 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:

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:

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:

components/Layout.tsx
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:

lib/ssg.ts
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:

scripts/remark-static-props.js
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:

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:

.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:

KeyValue
Namepath
DescriptionThe name of the file relative to the pages directory
Typetext
Define as arrayunchecked
Allow nullableunchecked

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:

lib/ssg.ts
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:

  1. Write the content in MDX.
  2. Inject getStaticProps for each page.
  3. Fetch article metadata from Supabase.
  4. 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.

Hey, you! 🫵

Did you know I created a YouTube channel? I'll be putting out a lot of new content on web development and software engineering so make sure to subscribe.

(clap if you liked the article)

You might also like