How To: Use RedwoodJS with Sanity.io

RedwoodJS with Sanity.io

This past weekend, I built a tiny application with RedwoodJS using Sanity.io as a CMS. I videostreamed myself building it for those of you who are interested.

What I Built

Sanity.io comes with a movie dataset out of the box. I kept it simple and built a MovieList page and a MovieDetail page. I was mostly focused on how I can get RedwoodJS to work with Sanity.io as a data source.

How to use Sanity.io with RedwoodJS

Create GraphQL Movie Object Type

After standing up my Sanity server (instructions on how to do that in the project README), I used Sanity Vision to get a feel for the movie data object. Since yarn rw g scaffold movie needs a Prisma model in schema.prisma (which I didn’t use for this small project), I kind of cheated. I made a temporary movie model in schema.prisma to leverage yarn rw g scaffold movie . Then, I went into movies.sdl.js and edited it a bit. I added the types I would render in the UI to movies.sdl.js so there are some data in the Sanity.io movie data object that went unaccounted for.

Create Movie Service

I had to edit the movie service to query from Sanity.io rather then the Prisma database. First, I created a Sanity client hooked up to my Sanity project:

import sanityClient from '@sanity/client'

export const sanity = sanityClient({
  projectId: process.env.SENTRY_PROJECT_ID,
  dataset: 'production',
  useCdn: true,
})

Then, I used this client in my movies service to fetch all movies and a movie by its slug:

import { sanity } from '../../lib/sanity'

const moviesQuery = /* groq */ `*[_type == "movie"]`
export const movies = () => {
  return sanity.fetch(moviesQuery)
}

const movieBySlugQuery = /* groq */ `*[_type == "movie" && slug.current == $slug][0]`
export const movie = ({ slug }) => {
  return sanity.fetch(movieBySlugQuery, { slug })
}

Update the Movie Cells

Next, I updated the MoviesCell and the MovieCell with updated GraphQL queries and each movie in the MoviesCell linking to the MovieDetailsPage:

// MoviesCell.js
import { Link, routes } from '@redwoodjs/router'
import { urlFor } from 'src/lib/sanity'

export const QUERY = gql`
  query {
    movies {
      poster {
        asset {
          _ref
        }
      }
      slug {
        current
      }
      title
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }) => <div>Error: {error.message}</div>

export const Success = ({ movies }) => {
  return movies.map((movie) => {
    return (
      <Link
        key={movie.slug.current}
        to={routes.movieDetail({ slug: movie.slug.current })}
      >
        <img src={urlFor(movie.poster.asset).width(200).url()} />
      </Link>
    )
  })
}

// MovieCell.js
import { urlFor } from 'src/lib/sanity'

export const QUERY = gql`
  query($slug: String!) {
    movie(slug: $slug) {
      poster {
        asset {
          _ref
        }
      }
      slug {
        current
      }
      title
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }) => <div>Error: {error.message}</div>

export const Success = ({ movie }) => {
  return (
    <div>
      <h1>{movie.title}</h1>
      <img src={urlFor(movie.poster.asset).width(500).url()} />
    </div>
  )
}

From there, it was smooth sailing. I rendered MoviesCell and MovieCell in my MovieListPage and MovieDetailPage , respectively.

Notes on My Experience

  1. Unfortunately, my project does not build. I documented this here and I’d love some help in getting it deployed!

  2. At first, I mentally prepared myself to not use the API side of RedwoodJS at all. I expected to be able to use Sanity.io directly from the cells. However, much to my disappointment, cells are tightly coupled with the API side (at least that’s my understanding). The exported QUERY is run against the API side with the data being injected into the cell as props. I’m a bit worried that makes it impossible to leverage everything RedwoodJS has to offer without the API side (though, at the same time, maybe that’s the point of using an opinionated framework? :thinking:).

    • What I secretly wish: What if, instead of a GraphQL query that is exported and run against the API side, there was an exported function that returns an object that is then injected to props? That way, instead of:

       // MoviesCell.js
       import { Link, routes } from '@redwoodjs/router'
       import { urlFor } from 'src/lib/sanity'
      
       export const QUERY = gql`
         query {
           movies {
             poster {
               asset {
                 _ref
               }
             }
             slug {
               current
             }
             title
           }
         }
       `
       // Loading, Error and Empty removed for brevity
      
       export const Success = ({ movies }) => {
         return movies.map((movie) => {
           return (
             <Link
               key={movie.slug.current}
               to={routes.movieDetail({ slug: movie.slug.current })}
             >
               <img src={urlFor(movie.poster.asset).width(200).url()} />
             </Link>
           )
         })
       }
      

      we have:

       import { Link, routes } from '@redwoodjs/router'
       import { request } from 'graphql-request'
       import { urlFor } from 'src/lib/sanity'
      
       const QUERY = gql`
         query {
           movies {
             poster {
               asset {
                 _ref
               }
             }
             slug {
               current
             }
             title
           }
         }
       `
      
       export const getter = () => {
         const data = request('/api', QUERY)
         return data
       }
      
       // Loading, Error and Empty removed for brevity
      
       export const Success = ({ movies }) => {
         return movies.map((movie) => {
           return (
             <Link
               key={movie.slug.current}
               to={routes.movieDetail({ slug: movie.slug.current })}
             >
               <img src={urlFor(movie.poster.asset).width(200).url()} />
             </Link>
           )
         })
       }
      

      We’re able to run the same query we did before AND use data stemming from a different source instead of being tightly coupled to the API side.

  3. I ran into a problem where I needed my Sanity client on both the web and API sides. I wasn’t easily able to share code so I had to write the same code in both the /api and /web directories. :face_vomiting: I’m excited to see what goes into the cookbook to solve this.

    • I remember using nx a bit for monorepos and they had a nx generate @nrwl/node:library <name> command that was quite nifty.
  4. I funnily found out the Redwood Router supports page redirects. There was nothing about it in the docs but I tried it and it just worked:
    <Route path="/" redirect="/movies" />

8 Likes

Hey Aryan, thanks for this write up! I know it’s been a few months but I’m currently doing a little research into CMS’s and this is a very helpful resource.

I’m curious if you’ve worked with any others like Strapi, Prismic, Netlify CMS, etc, and have any insight into the pros and cons of different options. The space is really daunting and I’m not seeing a lot of great content that makes direct comparisons between these.