Production grade Next.js

Preview content

Try before you buy

You ever accidently pushed some content changes live to your site that wasn't ready? A blog post, tagline on the marketing page, wrong price on the pricing page? Well, Next.js team wants to help you not do that anymore, so they created previews. Previews, not be be confused with the deploy previews that vercel offers, allow you to preview a page in your Next.js app with data that is not yet ready. This allows you to see what some draft content / data would look like on a page before you actually make those changes.

You probably do this now locally because, well, its local. But the preview feature allows you to preview pages no matter the environment of your app as its not dependent on that (but it can be if you want). Previews are what I call a raw feature, because it was created to be used by a headless CMS and then offered to you that way. Previews really shine when there is a GUI to initiate the preview request (the CMS). Our CMS in this project does not have a GUI, so we'll setup and initiate previews manually.

Lets modify our landing page and blog to handle previews so we can see changes before we go live.

Setup

To enable preview, we have to make an API route that sets a cookie that tells our pages that the incoming request is a preview request.

./pages/api/preview.ts
import { NextApiResponse } from 'next'

export default function handler(req, res: NextApiResponse) {
  // sets the preview cookie
  res.setPreviewData({})
  // redirects to the page you want to preview
  res.redirect(req.query.route)
}

Next.js makes it simple to set the preview cookie with a built in method on the response object. Beautiful. To initiate a preview we fire a GET request to /api/preview?route={appRouteToPreview}

Hold on, we have an issue now. As long as that cookie is set, we'll be in preview mode, looking at draft data. This can be confusing and may cause you to freak out thinking your draft content is live. Fear not, Next.js thought of this and has a few solutions. We can set a custom ttl on the cookie when we set it, but a better solution is to just clear the cookie. We can do that with another API route.

./pages/api/clear-preview.ts
export default function handler(req, res) {
  // clears the preview cookie
  res.clearPreviewData()
  res.end('preview disabled')
}

Doing a GET request to /api/clear-preview will now disabled the cookie. All is good now. Next we need to use the preview cookie to fetch draft content in our server side functions.

Fetching draft content

We'll head back to our index page and update our function to respect the preview cookie and fetch draft data when its present.

./pages/index.tsx
export function getStaticProps(ctx) {
  const content = ctx.preview ? home.draft : home.published
  return { props: { content } }
}

We can access a Boolean on the context object. ctx.preview will be true if the preview cookie is set. If it is, then we get the draft home page instead of the published. If this were a real API call to a CMS, you'd just change a parameter to draft: true or whatever that CMS says when querying for draft content. Now, lets look at our landing page with preview content:

  1. initiate the preivew by going to: /api/preview?route=/ in your browser
  2. you will be redirected back to the landing page but, with preview content
  3. when you want to see the pubslished content, go to /api/clear-preview in your browser. You won't be redirected this time. So just go to / to see the landing page with published content.

Lets do the same for our blog. This one is a little more involved because we have n + 1 pages. 1 being the blog index page, and n being every page for every blog post that we can make. Same concept applies though.

./pages/blog/index.tsx
export async function getStaticProps(ctx) {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = fs.readdirSync(postsDirectory)
  // check that preview boolean
  const cmsPosts = ctx.preview ? postsFromCMS.draft : postsFromCMS.published
  const filePosts = filenames.map((filename) => {
    const filePath = path.join(postsDirectory, filename)
    return fs.readFileSync(filePath, 'utf8')
  })
  
  const posts = orderby(
    [...cmsPosts, ...filePosts].map((content) => {
      const { data } = matter(content)
      return data
    }),
    ['publishedOn'],
    ['desc'],
  )

  return { props: { posts } }
}

All we did here was check the preview boolean on the context object to determine if we get the draft posts or published posts. Lets do the same for the blog post page.

./pages/blog/[slug].tsx
// bottom of the file

export async function getStaticProps({ params, preview }) {
  let postFile
  // is the slug for a file system post or cms post
  try {
    const postPath = path.join(process.cwd(), 'posts', `${params.slug}.mdx`)
    postFile = fs.readFileSync(postPath, 'utf-8')
  } catch {
    // check that cookie
    const collection = preview ? postsFromCMS.draft : postsFromCMS.published
    postFile = collection.find((p) => {
      const { data } = matter(p)
      return data.slug === params.slug
    })
  }

  if (!postFile) {
    throw new Error('no post')
  }

  const { content, data } = matter(postFile)
  const mdxSource = await renderToString(content, { scope: data })

  return { props: { source: mdxSource, frontMatter: data }, revalidate: 30 }
}

As before, you get trigger a preview with a call the preview api, just change the route query param to either /blog to preview the blog index page or /blog/some-slug to preview a blog post. You can see the slugs for the CMS posts in ./content.ts.