skies.dev

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

7 min read

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.

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

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.

pages/index.mdx
# 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.

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

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

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 and getArticleProps 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.

next.config.js
/** @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.

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

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

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.

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

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

  1. Create a new MDX file in your pages directory.
  2. Create a corresponding row in your Supabase database.

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