next.js: Preview mode doesn't work for pages not specified by getStaticPaths

Bug report

Describe the bug

Trying to access dynamic/catch-all routes in preview mode that are not specified by getStaticPaths returns 404 Not Found in production. I have verified that preview mode cookies are being sent on the requests, so that’s not the issue.

In development, the behaviour works as expected, though this might be due to the fact that getStaticProps is called on every request when in development mode.

To Reproduce

Repository: https://github.com/nextjs-preview-issue/nextjs-preview-issue

Demo: https://nextjs-preview-issue-demo.vercel.app

  • Accessing any pages not marked with “preview only” should render a page that has an appropriate title.
  • Accessing any pages marked with “preview only” should render a not found page.

Enable preview mode by clicking the “PREVIEW MODE” button and enter token1 or token2 into the prompt.

  • Accessing pages not marked with “preview only” should render a page that has an appropriate title with “(preview)” appended to it.
  • Accessing pages marked with “preview only” should render a draft page in preview mode, but it returns a not found page.

See “Additional context” for notable code snippets from the demo repository.

Expected behaviour

From the blog post:

Preview Mode allows users to bypass the statically generated page to on-demand render (SSR) a draft page from for example a CMS.

When trying to access pages that match a dynamic or catch-all route, I would expect that it always renders the page when in preview mode, even if it is not specified by getStaticPaths, because preview mode is intended for on-demand render of draft pages.

I considered using fallback: true in getStaticPaths, but that slightly changes behaviour in production regardless of preview mode - I shouldn’t need the server to check and render pages when not in preview mode.

Running the demo on a local machine in development mode using yarn dev exhibits the behaviour I expected.

System information

  • OS: macOS
  • Browser: tested on Chrome, Safari, Firefox
  • Version of Next.js: 9.5.2
  • Version of Node.js: 10.15.3

Additional context

Notable code snippets/files

Frontend: src/components/Preview.tsx

This function is attached to a button’s onClick handler which enables preview mode.

const previewMode = () => {
  const token = prompt("Enter preview token.");
  if (token) push(`/api/preview?token=${token}`)
};

Backend: src/pages/api/preview.ts

This API route enables preview mode and redirects the user to a page that matches the specified pageId if given, otherwise it redirects them to the index page. In the demo, the pageId parameter is not used (see above) so the user will always redirected to the index page when enabling preview mode.

Backend: src/utils.ts

This file contains the getStaticPaths and getStaticProps implementations used by the dynamic route (blog/[id].tsx) and the catch all route ([...slug].tsx).

The getStaticPaths implementations only lists pages/posts that have published: true because I do not want draft pages rendered at build time. getStaticProps retrieves the page’s/post’s data using the slug in the route params regardless of preview mode or not.

When in preview mode, getStaticProps passes the preview data to the page as the preview prop. The page will check this prop and render “(preview)” in the title when this is a non-null value.

See: Index page Blog post page Catch-all page

Backend: src/data.ts

This file contains the mock data used in the demo. In a real project these values would be retrieved from a database.

My use case and how I came across this issue

I’m building a page builder interface in my project and I want to allow users to preview pages they’ve created in the CMS that have not been published yet. Pages in my CMS database have a publishState flag. My getStaticPaths function only lists pages that have publishState set to "published" as I do not want draft pages to be rendered at build time. When draft pages are created, they have a publishState of "draft".

I deploy my project to Vercel and allow the users to publish new content by using a deploy hook to rebuild the project. Before the deploy hook is called, pages that are to be published have the state set to "published" so that getStaticPaths returns their ids for rendering during build time.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 8
  • Comments: 26 (10 by maintainers)

Commits related to this issue

Most upvoted comments

There’s two ways to solve this:

  • Development should also 404 in preview mode for a fallback: false page and an unmatched slug
  • Production should match a path not returned from paths: [] even if fallback: false, making preview mode work like fallback: true

I’m not sure which is the most optimal fix, but we’ll think about this. Thanks for the issue!

Update: I was able to enable the desired preview behavior, with only two small changes!

In this case, for a [slug].tsx page:

  1. Change fallback: false to fallback: 'blocking' in getStaticPaths()
  2. Return { notFound: true } from getStaticProps() if querying with context.params.slug fails to find data

Since our site is small, we’re doing full SSG instead of ISR, and this code has no impact on the normal production site.

I can confirm @vajajak’s findings, which we just stumbled on today as well. This issue only appears to surface when deployed on Vercel. Running locally in production mode (with next build && next start), preview mode is working as expected (calling getStaticProps for all requests regardless of presence in getStaticPaths) with fallback: false.

If getStaticPaths would receive a context parameter similar to getStaticProps including the preview boolean, you could control the behaviour yourself as a dev, whether fallback should be enabled in preview mode or not. 🤷🏻‍♂️

Somewhat related, on a /[…slug].js route I have, getServerSideProps({ params, preview }), preview is filled in on development builds, but is always undefined in production builds, despite __prerender_bypass and __next_preview_data cookies being present.

@ijjk To close the loop, indeed we just needed to gracefully handle the 404 response when fetching the preview. Thanks!

Ah so the problem is with the status code only or are you seeing the 404 page rendered as well? That note was specifically meaning the status code will be a 404 in preview mode for fallback: false pages but the preview content should render correctly.

@ijjk Thanks a lot for the fix. Seems like it’s working fine now 👍

Hi, this has been updated, please give it a try by re-deploying your application!

Note: with fallback: false pages and previewing a non-prerendered path a 404 status will be shown on Vercel to prevent crawlers indexing the 404 page although this will be updated in the future to only have the 404 status when not in preview mode.

@joshuabaker Yes and No. Let’s see what happens when we enable revalidate (for testing purposes let’s say 1 second).

  • ISR uses a stale-while-revalidate technique to update the static content, meaning that the first visitor to access the site will still see the old version. If you return the notFound flag as @alexburner has suggested, then the revalidate function obeys this and deletes that particular dynamic page (url), and returns 404, but only for the 2nd and all subsequent users. You probably could write some kind of trigger that crawls that page after you exit the preview mode, but this is still tricky cause you could write this inside the endpreview.js, but preview mode can also timeout without accessing the endpreview.js endpoint.
  • During the time you’re in preview mode, visitors can still access the url.
  • This still enables ISG and ISR page-wide, which depending on your use case might and might not suit your needs (for our website, this is not desired).

There’s two ways to solve this:

  • Development should also 404 in preview mode for a fallback: false page and an unmatched slug
  • Production should match a path not returned from paths: [] even if fallback: false, making preview mode work like fallback: true

I’m not sure which is the most optimal fix, but we’ll think about this. Thanks for the issue!

I personally think the second option is more flexible and gives more control to what can be done in preview mode, so my vote would be for that one haha