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.
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.
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.
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:
- initiate the preivew by going to:
/api/preview?route=/
in your browser - you will be redirected back to the landing page but, with preview content
- 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.
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.
// 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
.