Preview does not work as intended with Contentful (localhost)
See original GitHub issueBug report
disclaimer: I have only tried running it locally, not sure if it would make a difference running on a live server
Describe the bug
I’ve implemented the preview API per instructions both from the NextJS documentation and Contentful Documentation. The issue I’m experiencing is that no preview cookies are set and the context.preview & preview.previewData is undefined. When I click on the preview button in contentful it hits the preview endpoint and redirects the user to the post correctly, so no errors occur.
However, if a simply refresh the page or if I go directly to preview API endpoint with the correct params then the cookies are set and the preview is working as expected.
To Reproduce
- Setup a contentful space (I’ve used the example that can be used to fill the space with some dummy content)
- Setup the preview API in contentful (Link provided further down below)
- Implement the code I’ve used (see Code Snippets section)
- Try the “Open Preview” button
Expected behavior
When I click the “Open Preview” button I should be able to see the content with the unpublished changes.
Code Snippets
// pages/api/preview.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { Entry } from 'contentful';
// Services
import { initContentful } from '../../services/contentful'
// Interfaces
import { BlogPost } from '../../interfaces/BlogPost';
const getEntryUrlPrefix = (entry: Entry<BlogPost>) => {
switch(entry.sys.contentType.sys.id) {
case 'blogPost':
return '/blog/'
default:
return '';
}
}
const getEntryPath = (entry: Entry<BlogPost>) => {
if ( typeof entry.fields.slug !== 'undefined' ) {
return entry.fields.slug
}
return entry.sys.id
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Check the secret - This secret should only be known to this API route and the CMS
// The secret should not be the same as your preview access token
if (process.env.CONTENTFUL_PREVIEW_SECRET !== req.query.secret) {
return res.status(401).json({ message: 'Invalid secret' })
}
// Check params
if (!req.query.id) {
return res.status(404).json({ message: 'You must pass an id' })
}
// Fetch the headless CMS to check if an entry with the provided `id` exists
const id = Array.isArray(req.query.id) ? req.query.id[0] : req.query.id;
try {
const cf = await initContentful({ preview: true })
const entry = await cf.getEntry<BlogPost>(id);
// If the slug doesn't exist prevent preview mode from being enabled
if (!entry) {
return res.status(404).json({ message: `No entry was found with id: ${id}` })
}
const prefix = getEntryUrlPrefix(entry)
const path = getEntryPath(entry)
// Enable Preview Mode by setting the cookies
res
.setPreviewData({})
.writeHead(307, { Location: `${prefix}${path}` })
.end('Preview mode enabled')
} catch (error) {
return res.status(500).json({ message: error.message })
}
}
// pages/blog/[slug].tsx
// This function gets called at build time
export const getStaticPaths: GetStaticPaths = async () => {
const cf = await initContentful()
const blogPosts = await cf.getEntries<BlogPost>({
content_type: ContentfulContentTypes.blogPost
})
// Get the paths we want to pre-render based on posts
const paths = blogPosts.items.map(blogPost => ({
params: { slug: blogPost.fields.slug },
}))
// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: true }
}
export const getStaticProps: GetStaticProps = async (context) => {
if (!context.params) {
return { props: { errors: 'Ops no parameters was given' } }
}
console.log('getStaticProps', context) // <-- This returns an object with the params but not with preview or previewData in the case I've described
try {
const { slug } = context.params
const slugPath = Array.isArray(slug) ? slug[0] : slug
const cf = await initContentful({ preview: context.preview ? true : false })
const blogPost = await cf.getEntries<BlogPost>({
content_type: ContentfulContentTypes.blogPost,
'fields.slug': slugPath
})
if ( blogPost.total > 0) {
return {
props: {
blogPost: blogPost.items[0],
preview: context.preview ? true : false
}
}
}
return { props: { errors: `No blog post was found with slug ${slugPath}` }}
} catch (err) {
return { props: { errors: err.message } }
}
}
// services/contentful.ts
import { createClient, CreateClientParams } from 'contentful'
interface Options {
preview?: boolean
}
export async function initContentful({ preview }: Options = { preview: false }) {
try {
const params: CreateClientParams = {
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: preview ? process.env.CONTENTFUL_PREVIEW_TOKEN : process.env.CONTENTFUL_ACCESS_TOKEN,
host: preview ? 'preview.contentful.com' : 'cdn.contentful.com',
}
const client = createClient(params)
return client;
} catch (error) {
throw error
}
}
Preview Url settings in contentful
http://localhost:3000/api/preview?id={entry.sys.id}&secret=MY_SECRET
System information
- OS: macOS
- Browser: Chrome
- Next.js: 9.3.1
- Node: v13.10.1
Additonal Context
{
"name": "with-typescript",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"type-check": "tsc"
},
"dependencies": {
"@contentful/rich-text-react-renderer": "^13.4.0",
"@contentful/rich-text-types": "^14.1.0",
"contentful": "^7.14.0",
"isomorphic-unfetch": "3.0.0",
"next": "^9.3.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"sass": "^1.26.3"
},
"devDependencies": {
"@types/node": "^13.9.2",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"dotenv": "^8.2.0",
"typescript": "^3.8.3"
},
"license": "ISC"
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:11 (4 by maintainers)

Top Related StackOverflow Question
@patrickedqvist I think I have the exact same error. In development mode (localhost) preview and previewData is empty