How To Use MDX Stored In Sanity In A Next.js Website

Recently, my team took on a project to build an online, video-based learning platform. The project, called Jamstack Explorers, is a Jamstack app powered by Sanity and Next.js. We knew that the success of this project relied on making the editing experience easy for collaborators from different companies and roles, as well as retaining the flexibility to add custom components as needed.

To accomplish this, we decided to author content using MDX, which is Markdown with the option to include custom components. For our audience, Markdown is a standard approach to writing content: it’s how we format GitHub comments, Notion docs, Slack messages (kinda), and many other tools. The custom MDX components are optional and their usage is similar to shortcodes in WordPress and templating languages.

To make it possible to collaborate with contributors from anywhere, we decided to use Sanity as our content management system (CMS).

But how could we write MDX in Sanity? In this tutorial, we’ll break down how we set up MDX support in Sanity, and how to load and render that MDX in Next.js — powered website using a reduced example.


If you want to jump straight to the results, here are some helpful links:

How To Write Content Using MDX In Sanity

Our first step is to get our content management workflow set up. In this section, we’ll walk through setting up a new Sanity instance, adding support for writing MDX, and creating a public, read-only API that we can use to load our content into a website for display.

Create A New Sanity Instance

If you don’t already have a Sanity instance set up, let’s start with that. If you do already have a Sanity instance, skip ahead to the next section.

Our first step is to install the Sanity CLI globally, which allows us to install, configure, and run Sanity locally.

# install the Sanity CLI
npm i -g @sanity/cli

In your project folder, create a new directory called sanity, move into it, and run Sanity’s init command to create a new project.

# create a new directory to contain Sanity files
mkdir sanity
cd sanity/
sanity init

The init command will ask a series of questions. You can choose whatever makes sense for your project, but in this example we’ll use the following options:

  • Choose a project name: Sanity Next MDX Example.
  • Choose the default dataset configuration (“production”).
  • Use the default project output path (the current directory).
  • Choose “clean project” from the template options.

Install The Markdown Plugin For Sanity

By default, Sanity doesn’t have Markdown support. Fortunately, there’s a ready-made Sanity plugin for Markdown support that we can install and configure with a single command:

# add the Markdown plugin
sanity install markdown

This command will install the plugin and add the appropriate configuration to your Sanity instance to make it available for use.

Define A Custom Schema With A Markdown Input

In Sanity, we control every content type and input using schemas. This is one of my favorite features about Sanity, because it means that I have fine-grained control over what each content type stores, how that content is processed, and even how the content preview is built.

For this example, we’re going to create a simple page structure with a title, a slug to be used in the page URL, and a content area that expects Markdown.

Create this schema by adding a new file at sanity/schemas/page.js and adding the following code:

export default { name: 'page', title: 'Page', type: 'document', fields: [ { name: 'title', title: 'Page Title', type: 'string', validation: (Rule) => Rule.required(), }, { name: 'slug', title: 'Slug', type: 'slug', validation: (Rule) => Rule.required(), options: { source: 'title', maxLength: 96, }, }, { name: 'content', title: 'Content', type: 'markdown', }, ],

We start by giving the whole content type a name and title. The type of document tells Sanity that this should be displayed at the top level of the Sanity Studio as a content type someone can create.

Each field also needs a name, title, and type. We can optionally provide validation rules and other options, such as giving the slug a max length and allowing it to be generated from the title value.

Add A Custom Schema To Sanity’s Configuration

After our schema is defined, we need to tell Sanity to use it. We do this by importing the schema into sanity/schemas/schema.js, then adding it to the types array passed to createSchema.

 // First, we must import the schema creator import createSchema from 'part:@sanity/base/schema-creator'; // Then import schema types from any plugins that might expose them import schemaTypes from 'all:part:@sanity/base/schema-type'; + // Import custom schema types here
+ import page from './page'; // Then we give our schema to the builder and provide the result to Sanity export default createSchema({ // We name our schema name: 'default', // Then proceed to concatenate our document type // to the ones provided by any plugins that are installed types: schemaTypes.concat([
- / Your types here! /
+ page, ]), });

This puts our page schema into Sanity’s startup configuration, which means we’ll be able to create pages once we start Sanity up!

Run Sanity Studio Locally

Now that we have a schema defined and configured, we can start Sanity locally.

sanity start

Once it’s running, we can open Sanity Studio at http://localhost:3333 on our local machine.

When we visit that URL, we’ll need to log in the first time. Use your preferred account (e.g. GitHub) to authenticate. Once you get logged in, you’ll see the Studio dashboard, which looks pretty barebones.

To add a new page, click “Page”, then the pencil icon at the top-left.

Add a title and slug, then write some Markdown with MDX in the content area:

This is written in Markdown. But what’s this? <Callout> Oh dang! Is this a React component in the middle of our content? 😱 </Callout> Holy buckets! That’s amazing!

Heads up! The empty line between the MDX component and the Markdown it contains is required. Otherwise the Markdown won’t be parsed. This will be fixed in MDX v2.

Once you have the content in place, click “Publish” to make it available.

Deploy The Sanity Studio To A Production URL

In order to make edits to the site’s data without having to run the code locally, we need to deploy the Sanity Studio. The Sanity CLI makes this possible with a single command:

sanity deploy

Choose a hostname for the site, which will be used in the URL. After that, it will be deployed and reachable at your own custom link.

This provides a production URL for content editors to log in and make changes to the site content.

Make Sanity Content Available Via GraphQL

Sanity ships with support for GraphQL, which we’ll use to load our page data into our site’s front-end. To enable this, we need to deploy a GraphQL API, which is another one-liner:

sanity graphql deploy

We can choose to enable a GraphQL Playground, which gives us a browser-based data explorer. This is extremely handy for testing queries.

Store the GraphQL URL — you’ll need it to load the data into Next.js!

The GraphQL API is read-only for published content by default, so we don’t need to worry about keeping this secret — everything that this API returns is published, which means it’s what we want people to see.

Test Sanity GraphQL Queries In The Browser

By opening the URL of our GraphQL API, we’re able to test out GraphQL queries to make sure we’re getting the data we expect. These queries are copy-pasteable into our code.

To load our page data, we can build the following query using the “schema” tab at the right-hand side as a reference.

query AllPages { allPage { title slug { current } content }

This query loads all the pages published in Sanity, returning the title, current slug, and content for each. If we run this in the playground by pressing the play button, we can see our page returned.

Now that we’ve got page data with MDX in it coming back from Sanity, we’re ready to build a site using it!

In the next section, we’ll create an Next.js site that loads data from Sanity and renders our MDX content properly.

Display MDX In Next.js From Sanity

In an empty directory, start by initializing a new package.json, then install Next, React, and a package called next-mdx-remote.

# create a new package.json with the default options
npm init -y # install the packages we need for this project
npm i next react react-dom next-mdx-remote

Inside package.json, add a script to run next dev:

 { "name": "sanity-next-mdx", "version": "1.0.0", "scripts": {
+ "dev": "next dev" }, "author": "Jason Lengstorf <>", "license": "ISC", "dependencies": { "next": "^10.0.2", "next-mdx-remote": "^1.0.0", "react": "^17.0.1", "react-dom": "^17.0.1" }

Create React Components To Use In MDX Content

In our page content, we used the <Callout> component to wrap some of our Markdown. MDX works by combining React components with Markdown, which means our first step is to define the React component our MDX expects.

Create a Callout component at src/components/callout.js:

export default function Callout({ children }) { return ( <div style={{ padding: '0 1rem', background: 'lightblue', border: '1px solid blue', borderRadius: '0.5rem', }} > {children} </div> );

This component adds a blue box around content that we want to call out for extra attention.

Send GraphQL Queries Using The Fetch API

It may not be obvious, but you don’t need a special library to send GraphQL queries! It’s possible to send a query to a GraphQL API using the browser’s built-in Fetch API.

Since we’ll be sending a few GraphQL queries in our site, let’s add a utility function that handles this so we don’t have to duplicate this code in a bunch of places.

Add a utility function to fetch Sanity data using the Fetch API at src/utils/sanity.js:

export async function getSanityContent({ query, variables = {} }) { const { data } = await fetch( '', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables, }), }, ).then((response) => response.json()); return data;

The first argument is the Sanity GraphQL URL that Sanity returned when we deployed the GraphQL API.

GraphQL queries are always sent using the POST method and the application/json content type header.

The body of a GraphQL request is a stringified JSON object with two properties: query, which contains the query we want to execute as a string; and variables, which is an object containing any query variables we want to pass into the GraphQL query.

The response will be JSON, so we need to handle that in the .then for the query result, and then we can destructure the result to get to the data inside. In a production app, we’d want to check for errors in the result as well and display those errors in a helpful way, but this is a post about MDX, not GraphQL, so #yolo.

Heads up! The Fetch API is great for simple use cases, but as your app becomes more complex you’ll probably want to look into the benefits of using a GraphQL-specific tool like Apollo or urql.

Create A Listing Of All Pages From Sanity In Next.js

To start, let’s make a list of all the pages published in Sanity, as well as a link to their slug (which won’t work just yet).

Create a new file at src/pages/index.js and put the following code inside:

import Link from 'next/link';
import { getSanityContent } from '../utils/sanity'; export default function Index({ pages }) { return ( <div> <h1>This Site Loads MDX From</h1> <p>View any of these pages to see it in action:</p> <ul> {{ title, slug }) => ( <li key={slug}> <Link href={`/${slug}`}> <a>{title}</a> </Link> </li> ))} </ul> </div> );
} export async function getStaticProps() { const data = await getSanityContent({ query: ` query AllPages { allPage { title slug { current } } } `, }); const pages = => ({ title: page.title, slug: page.slug.current, })); return { props: { pages }, };

In getStaticProps we call the getSanityContent utility with a query that loads the title and slug of all pages in Sanity. We then map over the page data to create a simplified object with a title and slug property for each page and return that array as a pages prop.

The Index component to display this page receives that page’s prop, so we map over that to output an unordered list of links to the pages.

Start the site with npm run dev and open http://localhost:3000 to see the work in progress.

If we click a page link right now, we’ll get a 404 error. In the next section we’ll fix that!

Generate Pages Programatically In Next.js From CMS Data

Next.js supports dynamic routes, so let’s set up a new file to catch all pages except our home page at src/pages/[page].js.

In this file, we need to tell Next what the slugs are that it needs to generate using the getStaticPaths function.

To load the static content for these pages, we need to use getStaticProps, which will receive the current page slug in

To help visualize what’s happening, we’ll pass the slug through to our page and log the props out on screen for now.

import { getSanityContent } from '../utils/sanity'; export default function Page(props) { return <pre>{JSON.stringify(props, null, 2)}</pre>;
} export async function getStaticProps({ params }) { return { props: { slug:, }, };
} export async function getStaticPaths() { const data = await getSanityContent({ query: ` query AllPages { allPage { slug { current } } } `, }); const pages = data.allPage; return { paths: => `/${p.slug.current}`), fallback: false, };

If the server is already running this will reload automatically. If not, run npm run dev and click one of the page links on http://localhost:3000 to see the dynamic route in action.

Load Page Data From Sanity For The Current Page Slug In Next.js

Now that we have the page slug, we can send a request to Sanity to load the content for that page.

Using the getSanityContent utility function, send a query that loads the current page using its slug, then pull out just the page’s data and return that in the props.

 export async function getStaticProps({ params }) {
+ const data = await getSanityContent({
+ query: + query PageBySlug($slug: String!) {
+ allPage(where: { slug: { current: { eq: $slug } } }) {
+ title
+ content
+ }
+ }
+ variables: {
+ slug:,
+ },
+ });
+ const [pageData] = data.allPage; return { props: {
- slug:,
+ pageData, }, }; }

After reloading the page, we can see that the MDX content is loaded, but it hasn’t been processed yet.

Render MDX From A CMS In Next.js With Next-mdx-remote

To render the MDX, we need to perform two steps:

  1. For the build-time processing of MDX, we need to render the MDX to a string. This will turn the Markdown into HTML and ensure that the React components are executable. This is done by passing the content as a string into renderToString along with an object containing the React components we want to be available in MDX content.

  2. For the client-side rendering of MDX, we hydrate the MDX by passing in the rendered string and the React components. This makes the components available to the browser and unlocks interactivity and React features.

While this might feel like doing the work twice, these are two distinct processes that allow us to both create fully rendered HTML markup that works without JavaScript enabled and the dynamic, client-side functionality that JavaScript provides.

Make the following changes to src/pages/[page].js to render and hydrate MDX:

+ import hydrate from 'next-mdx-remote/hydrate';
+ import renderToString from 'next-mdx-remote/render-to-string'; import { getSanityContent } from '../utils/sanity';
+ import Callout from '../components/callout'; - export default function Page(props) {
- return <pre>{JSON.stringify(props, null, 2)}</pre>;
+ export default function Page({ title, content }) {
+ const renderedContent = hydrate(content, {
+ components: {
+ Callout,
+ },
+ });
+ return (
+ <div>
+ <h1>{title}</h1>
+ {renderedContent}
+ </div>
+ ); } export async function getStaticProps({ params }) { const data = await getSanityContent({ query: ` query PageBySlug($slug: String!) { allPage(where: { slug: { current: { eq: $slug } } }) { title content } } `, variables: { slug:, }, }); const [pageData] = data.allPage; + const content = await renderToString(pageData.content, {
+ components: { Callout },
+ }); return { props: {
- pageData,
+ title: pageData.title,
+ content, }, }; } export async function getStaticPaths() { const data = await getSanityContent({ query: ` query AllPages { allPage { slug { current } } } `, }); const pages = data.allPage; return { paths: => `/${p.slug.current}`), fallback: false, }; }

After saving these changes, reload the browser and we can see the page content being rendered properly, custom React components and all!

Use MDX With Sanity And Next.js For Flexible Content Workflows

Now that this code is set up, content editors can quickly write content using MDX to enable the speed of Markdown with the flexibility of custom React components, all from Sanity! The site is set up to generate all the pages published in Sanity, so unless we want to add new custom components we don’t need to touch the Next.js code at all to publish new pages.

What I love about this workflow is that it lets me keep my favorite parts of several tools: I really like writing content in Markdown, but my content also needs more flexibility than the standard Markdown syntax provides; I like building websites with React, but I don’t like managing content in Git.

Beyond this, I also have access to the huge amount of customization made available in both the Sanity and React ecosystems, which feels like having my cake and eating it, too.

If you’re looking for a new content management workflow, I hope you enjoy this one as much as I do!

What’s Next?

Now that you’ve got a Next site using MDX from Sanity, you may want to go further with these tutorials and resources:

What will you build with this workflow? Let me know on Twitter!

We Need You In The Smashing Family

At Smashing, we are looking for a friendly, reliable and passionate person to drive the sales and management of sponsorship and advertising. We work with small and big companies to help them get exposure and have their voice heard across a number of different media — from this very magazine to our online conferences, meet-ups and workshops. This includes:

We sincerely hope to find someone who knows and understands the web community we publish for. A person who is able to bring onboard advertisers and sponsors that will be helpful to our audience, and who will benefit from the exposure and visibility at Smashing. We are looking for a person with experience in nurturing long-term relationships with advertisers, while not being afraid to push for new sales.

We are a small family of 12, and we’ve all been working remotely for years now. By joining our team, you will have the opportunity to shape the role and work with the Magazine as well as the Events team to create sponsorship opportunities that truly benefit both sides of the arrangement. We also would be open to outsourcing this work to another company or working with someone on a freelance basis who provides these services to other companies.

What’s In It For You?

  • A small, friendly, inclusive and diverse team that is aligned and very committed to doing great work;
  • The ability to shape your work in a way that would work best for you;
  • No lengthy meetings or micro-management: we do everything to ensure you can do your best work.

Role And Responsibilities

  • You’ll be working with your existing contacts (those of which Smashing has already made) and find new contacts to sell advertising and sponsorship across the range of our products;
  • You’ll be managing sponsors and advertisers once they come on board, ensuring that expectations are managed and deadlines on both sides understood;
  • You’ll be exploring creative partnerships to ensure that sponsors get exposure they need while staying true to principles that Smashing stands for;
  • Work closely with the our team to ensure that our commitments to sponsors are possible to fulfill given time and team availability;
  • Being able to think creatively in terms of how we maximize sponsorship opportunities across our different outlets.

We’d Like You To:

  • Have good written English, and ability to communicate clearly with sponsors from around the world;
  • Be able to manage a flexible schedule in order to make calls to sponsors in timezones including the US West Coast;
  • Be happy working in an asynchronous way, mostly via writing (we use Slack and Notion), given the distributed nature of the team and sponsors;
  • Be conversant with web technologies to the extent of understanding who would be a good fit as a sponsor;
  • Ideally, have existing connections with web companies;
  • Fully remote, and probably fulltime. (Again, we also would be open to outsourcing this work to another company or working with someone on a freelance basis who provides these services to other companies.)

A Bit About Smashing

At Smashing, we focus on bringing quality content for web designers and developers, and support our community. The community around Smashing is indeed very important to us. They tell us when they like what we are doing, and also when they do not!

We are always looking for new ways to reach out to our community. Over the past year, we’ve taken conferences online and started running online workshops in response to the pandemic. Things will likely change over the coming year too, and we are keen to bring our existing sponsors along with us and continue to think creatively about how we can offer good value to them in a changing world.

Yet again, we are a very small team of dedicated people — fully distributed even before the pandemic. The majority of the team is in Europe, but we also have team members in the USA and Hong Kong. Therefore, location tends to be less important than an ability to work in a way that respects the time lag when dealing with multiple time zones.

Contact Details

If you are interested, please drop us an email at, tell us a bit about yourself and your experience, and why you’d like to be a part of the Smashing family. We can’t wait to hear from you!

My Top 6 Conversion Tactics for 2021

Despite the unprecedented changes in 2020, the standard ecommerce usability requirements remain true: simplify navigation, ensure mobile-friendliness, and streamline the checkout process.

But there are plenty more conversion tactics merchants should deploy heading into 2021. Here are my top six below.

6 Conversion Tactics for 2021

Targeted email campaigns. Sending the same email to your entire list isn’t the best practice. Targeting customers based on purchases and interests produces more conversions. There’s little reason for a sporting goods store to promote basketball to those interested only in golf. Segment subscribers to better target what each wants.

You can always include links to other products in the footer. And continue emailing the entire database for storewide sales and unique features.

An attractive rewards program. Loyalty programs reward customers based on the amount of money they spend. Take things up a notch by providing additional benefits, such as first dibs on limited and exclusive items, special pricing on select goods, and free shipping after a set period of buying.

Place what you stand for front and center. 2020’s events caused many consumers to scrutinize companies’ ideals. Make sure shoppers know of your key charities and causes. More people than ever want to patronize stores whose priorities align with their own.

Avoid politics and religion, however, unless they are the focus of your business.

Spotlight your staff. Consumers want to know who their money helps — from the cleaning crew to tech support. Take a cue from Crutchfield and Zappos. Both regularly feature employees and emphasize their importance to the company.

Photo of several Crutchfield employees

Crutchfield incorporates employee photos, videos, and personal stories.

Emphasize value. Give your shoppers more purchasing power. Consumers are looking for the best overall value, not necessarily the cheapest items. Subscriptions are helpful for replenishable products. Volume pricing can entice people to buy in quantities and allow families and friends to group their purchases, resulting in higher average order values for the business and lower prices for customers.

Offer many ways to pay. Roughly 70 percent of Digital Commerce 360’s top 1,000 stores accept PayPal. Loyal Apple users enjoy the convenience and enhanced security of Apple Pay. Other popular methods for mobile shoppers include Amazon Payments, Stripe, and Google Pay. Buy now, pay later is increasingly popular.

In other words, people have preferred ways to pay, and it’s not always a credit card. Offering multiple payment methods can help almost every demographic. Limited payment options can result in a lost sale.