Building a website with Sanity & NextJS

By Derek Nguyen

In this tutorial, we will build a landing page website with Sanity & NextJS, extend it to add a blog section, and deploy it on Netlify.

A brief overview

We've created a suite of excellent products, but people haven't yet heard of them. Let's create a site to share it with the world!

But first, a quick primer on how this stack works:

We will manage our data in Sanity Studio, an open-source, flexible, highly customizable React app.

For the frontend, we will use NextJS, another open-source project that is flexible, easy to maintain, and performant out of the box.

Finally, we'll deploy our site on Netlify, a deployment platform known for developer experience & speed!

Protip

Two ways to make a site

NextJS supports Static Site Generation (SSG), Server-Side Rendering (SSR), or both. In this guide, we will deploy our site with the SSG export mode, but the template is designed to easily deploy it as a full NextJS app, taking advantage of its flexibility.

Setup the local environment

Let's get started with this NextJS template. It will give us a good set of basic building blocks & integrate with Netlify out-of-the-box.

First, head to create.sanity.io and follow the instructions there to create the Next.js landing page.

Once completed, we should have the project code in our GitHub, typically at https://github.com/<username>/sanity-nextjs-landing-pages.

Let's clone the project:

git clone https://github.com/<username>/sanity-nextjs-landing-pages
cd sanity-nextjs-landing-pages
npm install

Now, let's examine the project structure:

root
  |--studio # our sanity studio
  |--web # our nextjs frontend
  `--package.json

To verify that everything is correct, run the development command from the root directory:

npm run dev

After the script finishes running, we'll find the frontend at localhost:3000 and the Studio at localhost:3333. Both Sanity Studio and Nextjs support hot-reloading, so we can modify the code and see the change immediately without refreshing.

Protip

The project is a Lerna monorepo. When we run `npm run dev`, Lerna runs the `dev` script of both `web` and `studio` projects.

Sanity Studio

Sanity Studio Dashboard screenshot

After logging in, we should see a dashboard similar to the screenshot above. There are two items we care about:

- Pages: Static pages.

- Site Config: Global configs such as logo, URL, navigation, and SEO settings.

Note: Sanity Studio's beauty is its customizability — we can have two Sanity projects that look entirely different from each other. Learn how to customize the Studio here.

Add a New Page

This template provides us a good base, but it's missing a Product page — let's make one right now.

Let's create a new page & fill its content to our heart's desire. At the very least, we'll need a slug and a title. The template has already provided us with a few basic page sections to choose from. Once the content is ready, let's publish it.

Now that we got a Product page let's add it to the navigation bar. Since it's a significant page, let's put it as the first item.

Sanity Studio > Site Config

Now, navigate to localhost:3000 & see the newly created product page show up in the navigation bar.

Deploy the site

At this point, we can deploy the site along with our new page. The template comes with a set of handy deploy buttons we can use to publish our site:

Sanity Studio > Netlify Widget

Alternatively, we may also configure Netlify to build & deploy automatically on every content edit via webhooks. Go to the web project dashboard on Netlify, find Settings -> Build hooks and create a new one.

Netlify > Site Settings > Build Hooks

Once we got the hook URL (https://api.netlify.com/build_hooks/...), we can either connect it to Sanity from the project UI (typically manage.sanity.io/projects/<id>/settings/API) or, better, from the terminal:

# make sure we're in the `studio` directory.
cd studio
npx sanity hook create
? Hook name: Netlify
? Select dataset hook should apply to production
? Hook URL: https://api.netlify.com/build_hooks/<...>
Hook created successfully

Add a blog section

We've got a basic site going, but we still haven't got a place to share helpful tips on using our products. It's time to add a blog section.

Add a blog schema

First, let's add a new 'blog' schema to our Studio. Navigate to studio/schemas/documents and add a new file called post.js.

cd studio
touch ./schemas/documents/post.js

It might seem a little daunting at first, but configuring a Sanity schema is nothing other than a couple of objects. To begin, there are three sections we care about:

export default {
  /* 1. Basic metadata. We want this schema type to be 'document'. */
  name: 'blog',
  type: 'document',
  title: 'Blog',
  /* 2. A list of field */

  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Title',
    },
    {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      validation: (Rule) => Rule.required(),
      options: {
        source: 'title',
        maxLength: 96,
      },
    },
    {
      name: 'mainImage',
      title: 'Main image',
      type: 'image',
      options: {
        hotspot: true,
      },
    },
    {
      name: 'body',
      title: 'Body',
      type: 'portableText',
    },
  ],

  /* 3. Content for the card preview in Sanity Studio */
  preview: {
    title: 'title',
    subtitle: 'slug.current',
  },
}

A document is a schema type that may represent a blog post, a page, or a site config; we may use other schema types for a reusable piece of schema. For example, we may create an object schema that contains the SEO fields for both blogs and pages.

Example

If you'd like to follow the code, check out the repo here.

Protip

Find inspiration and build your schema quickly from this community-contributed schema repository.

For brevity's sake, we'll use a straightforward blog schema. Feel free to customize as needed. See here for a list of basic building types. Once we're done, let's plug the schema into Sanity Studio. Navigate to `studio/schemas/schema.js` and import our blog schema:

  // studio/schemas/schema.js

  // Document types
  import page from './documents/page';
+ import post from './documents/post';
  import siteConfig from './documents/siteConfig';

  /* ... */
  export default createSchema({
    name: 'default',
    types: schemaTypes.concat([
    /* long list of schema types */
+     post,
      ])
  })

After the studio restarts, we can now see the new Blog type in the dashboard. Let's quickly add a simple post!

Sanity Studio with Blog type

Add a blog to the NextJS frontend

Time to get our hands dirty(er). Let's dive into the NextJS code in `/we.

Create a new folder inside /web/pages/ called blog, and in there, create a new file called [slug].js.

In Next.JS, wrapping brackets around a page's name will allow us to create dynamic routes. In our case, Next.JS will use the component in web/blog/[slug].js to process requests to URLs with the subpath /blog, such as example.com/blog/a-new-title. The matched path (a-new-title) will then be available on the page as a param under the name slug. Learn more about dynamic routes in Next.JS docs.

Our structure will now look like this:

web
 `--pages
      |--index.js
      |--[slug].js
      `--blog
           `--[slug].js

Let's start with something very simple, just to test the water:

// web/pages/blog/[slug].js

function Post() {
  return (
    <div>Hello</div>
  )
}
export default Post

Navigating to localhost:3000/blog/hello, we should see that the page is working.

Now let's get some real data to show. We will fetch the data from Sanity via a query language called GROQ. Let's say we've created a post with the slug hello-world, we can fetch the data from Sanity like this:

import client from '../../client'

client.fetch(`
  *[_type == 'blog' && slug.current == 'hello-world'][0] { ... }
`)

This query is translated to “get me one of those Blog documents with the slug hello-world”.

Protip

We can try any queries in the Vision tab in Sanity Studio. Learn more about writing GROQ queries here.

When it comes to fetching data in Next.js, there are a few options to choose from, but for a marketing site, the best option is getStaticProps. Since we'll be exporting the app statically, this method is practically the same as getInitialProps. However, it'll allow us the flexibility of switching to a full Next.JS app in the future (see below) and benefit from Incremental Static Generation, where Next.JS can pre-render pages individually on-the-go, and decrease the build time.

// web/pages/blog/[slug].js

export async function getStaticProps(context) {
  const data = await client.fetch(`*[_type == 'blog' && slug.current == 'hello-world'][0] {...}`)
  return {
    props: {
      data,
    }
  }
}

Let's give this a try at localhost:3000/blog/hello-world.

Server Error
Error: getStaticPaths is required for dynamic SSG pages and is missing for /blog/[slug].
Read more: https://err.sh/next.js/invalid-getstaticpaths-value

Oops, that doesn't work. We're missing a piece: Next.js wants to know in advance the list of possible slugs, so it can pre-render the page and give us that magic static-site performance.

The good news is we have all this information in Sanity. Let's give it to Next.js in another query:

*[_type == "blog"] { slug }

"Hey, give me all the slug of the type blog!"

This query will return a list of slugs:

[
  { slug: { current: 'hello-world' } },
	{ slug: { current: 'another-blog-title' } },
]

...which we can then pass to Next.js in a new getStaticPaths:

// web/pages/blog/[slug].js

export async function getStaticProps(context) {
  const slug = context.params.slug || ''
  const data = await client.fetch(`*[_type == 'blog' && slug.current == $slug][0] {...}`, { slug })
  return {
    props: {
      data,
    },
  }
}

export const getStaticPaths = async () => {
  const slugs = await client.fetch(groq`*[_type == "blog"] { slug }`)

  // mould the data into the shape Nextjs demands
  const paths = slugs.map((slug) => ({ params: { slug: slug.current } }))

  return {
    paths,
    fallback: false,
  }
}

Note that instead of hard-coding the slug hello-world, we're now using $slug, and giving the client the { slug } object in the second params.

Protip

GROQ is such a cool language that it'll handle the data moulding part for you! Check this out:

*[_type == "blog"]{ "params": { "slug": slug.current } }

We can now test the data returned by Next.js in our component:

function Post({ data }) {
  return (
	 <article>
		 <h1>{data.title}</h1>
		 {/* TODO: Insert an awesome blog design here */}
	 </article>
  )
}

And with that, our content is live!

Add a blog index page

We can now share our blogs on Slack and Twitter, but our loyal visitors can't find our blogs on the site — we haven't got an index page for our blog! Fortunately, this will be a breeze.

First, create an index page at web/pages/blog/index.js. This file will be in charge when we navigate to http://localhost:3000/blog.

Let's keep thing simple with this basic layout:

// web/pages/blog/index.js

import Link from 'next/link'
import client from '../../client'

export default function BlogIndexPage({ data = [] }) {
  return (
    <ul>
	 {/* TODO: Insert an awesome blog index design here */}
      {data.map(post => (
        <li key={post._id}>
          <Link href={`/blog/${post.slug}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
	)
}

And get all the blog entries with the following query:

export async function getStaticProps() {
  const data = await client.fetch(`
    *[_type == 'blog'] {
      _id,
      'slug': slug.current,
      title,
      mainImage {
        asset ->
      }
    }
  `)
  return {
    revalidate: 60,
    props: {
      data,
    }
  }
}

Protip

What's that arrow? Well, `asset` does not actually contain the image - it is only a reference. With the arrow, we're telling Sanity to give us the full data instead of just the reference name.

This time, we won't need a getStaticPaths since pages/blog/index.js is just a page and its path is not dynamic.

Now visit http://localhost:3000/blog and see the new page! Try adding pages in Sanity Studio and see them showing up there.

View the site in production mode, locally

So far, things have been great. But remember, we'll be exporting these pages statically. Let's build the site locally.

# make sure we're in the web directory
cd web
npm run build

# serve the static site locally at port 5000
npx serve out

Navigate to http://localhost:5000 to review the site. Once everything is right, we can deploy the site again.

Bonus: Deploy a full NextJS site on Netlify

Great news! It is now possible to deploy a full NextJS app on Netlify. With a full NextJS app, we can have static-generated pages and server-rendered pages in the same app, have static pages re-rendered incrementally, and take advantage of the live preview feature.

First, we need a new build script. Go to /web/package.json and change the build script to below:

// /web/package.json

{
  "scripts": {
	 "build": "next build"
	}
}

Make sure all of our getStaticProps return a revalidate property, which is an amount of time, in seconds. This number determines whether the page content could be out-dated or not. For example, if we set this property to 15 for a page, Next.JS will attempt to re-generate the page if someone requests it and it has been over 15 second since that page was last generated. That visitor will receive an older version of the page, but future visitors will get the re-generated page.

export async function getStaticProps() {
  const data = await client.fetch(`...`)
  return {
    revalidate: 60,
    props: {
      data,
    }
  }
}

Go to the web project's Netlify Dashboard -> Plugins -> Plugin Directory and search for `Next on Netlify. Install it & follow instructions here.

Finally, if you have set up any trigger hooks previously on Netlify, you should consider removing them to let Next.JS handle creating & updating pages.

And that should be it. Thanks for reading!

Example

We've omitted the part where we add blog posts to the sitemap for the brevity of this tutorial. See the code here for an example of statically generating sitemap, and here for a dynamic sitemap, generated via Next.JS api.