Unpublished previews not working

Hi, im trying to setup previews on our site using the v4 plugin, with the help of the official docs I managed to get normal previews working fine, but I can't seem to get unpublished previews working.

preview.tsx
import * as React from 'react';

import { withPrismicPreviewResolver } from 'gatsby-plugin-prismic-previews';

// Update the path to your Link Resolver

import { linkResolver } from '../utils/linkResolver';

const PreviewPage = () => {
  return (
    <div>
      <h1>Loading preview…</h1>
    </div>
  );
};

export default withPrismicPreviewResolver(PreviewPage, [
  {
    repositoryName: process.env.PRISMIC_REPOSITORY_NAME || 'boxinc',
    linkResolver,
  },
]);
404.tsx
import React, { FC } from 'react';

import { graphql, PageProps } from 'gatsby';
import {
  componentResolverFromMap,
  withPrismicUnpublishedPreview,
} from 'gatsby-plugin-prismic-previews';
import { useTranslation, Trans } from 'react-i18next';
import styled from 'styled-components';

import { mediaMaxMobile } from '../components/global/breakpoints';
import { StyledA } from '../components/global/button';
import { LocalizedLink } from '../components/global/link';
import SEO from '../components/global/seo';
import {
  Heading1Css,
  Heading2Css,
  Heading3Css,
  Text1Css,
  Text2Css,
} from '../components/global/typography';
import BlogPostTemplate from '../templates/blogPost';
import InfoPageTtemplate from '../templates/infoPage';
import PageTemplate from '../templates/page';
import PageCoreTemplate from '../templates/pageCore';
import { Theme } from '../types/global';
import { linkResolver } from '../utils/linkResolver';
import { wrapPage } from '../utils/wrapPage';

const CenteredContent = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin: 52px 0 6rem 0;
`;

const TitleWrapper = styled.div`
  padding: 6.25rem 5rem;
  text-align: center;
  width: 100%;
  background-color: ${({ theme }) => theme.colors.background};
`;

const InfoTitle = styled.span`
  ${Heading1Css};
  display: block;
  max-width: 800px;
  margin: 0 auto;

  color: ${({ theme }) => theme.colors.lightText};

  ${mediaMaxMobile`
    ${Text2Css};
    color: ${({ theme }: Theme) => theme.colors.lightText};
  `};
`;

const ContentWrapper = styled.div`
  max-width: 690px;
  padding: 5rem 1rem;
  margin: 0 auto;
  text-align: center;
`;

const Message = styled.p`
  ${Heading2Css};
  color: ${({ theme }) => theme.colors.lightText};
  margin-bottom: 0;

  ${mediaMaxMobile`
    ${Heading3Css};
    color: ${({ theme }: Theme) => theme.colors.lightText};
  `};
`;
const Note = styled.div`
  ${Text1Css};
  max-width: 500px;
  color: ${({ theme }) => theme.colors.lightText};
  padding: 2.5rem 0;
  margin: 0 auto;

  ${mediaMaxMobile`
    ${Text2Css};
  `};
`;

const NotFoundPage: FC<PageProps> = () => {
  const { t } = useTranslation();

  return (
    <CenteredContent>
      <SEO title={t('errors.page_not_found_title')} />
      <TitleWrapper>
        <InfoTitle>{t('errors.page_not_found_title')}</InfoTitle>
      </TitleWrapper>

      <ContentWrapper>
        <Message>{t('errors.page_not_found_text')}</Message>
        <Note>
          <Trans i18nKey="errors.general_note">
            Pretext
            <LocalizedLink to={'/'}>Marketing</LocalizedLink>
            or
            <StyledA href={`mailto:${t('customer_service_email')}`}>
              Contact
            </StyledA>
            postText
          </Trans>
        </Note>
      </ContentWrapper>
    </CenteredContent>
  );
};

export const query = graphql`
  query NotFoundPageQuery($lang: String!) {
    allPrismicNavigation(filter: { lang: { eq: $lang } }) {
      ...LayoutNavigationFragment
    }
    allPrismicFooter(filter: { lang: { eq: $lang } }) {
      ...LayoutFooterFragment
    }
  }
`;

// @ts-ignore
export default withPrismicUnpublishedPreview(wrapPage(NotFoundPage), [
  {
    repositoryName: 'boxinc',
    linkResolver,
    componentResolver: componentResolverFromMap({
      page: PageTemplate,
    }),
  },
]);
Page.tsx
import React, { FC } from 'react';

import { graphql, PageProps } from 'gatsby';
import { withPrismicPreview } from 'gatsby-plugin-prismic-previews';
import { shape } from 'prop-types';

import { LayoutFooterFragment } from '../components/global/footer';
import {
  LayoutNavigationFragment,
  LinkFragment,
} from '../components/global/navigation';
import Page from '../components/page';
import { linkResolver } from '../utils/linkResolver';
import { wrapPage } from '../utils/wrapPage';

type PageTemplateProps = PageProps & {
  data: { [key: string]: any };
};

const PageTemplate: FC<PageTemplateProps> = ({ data }) => {
  const doc = data.allPrismicPage.nodes[0];

  if (doc) {
    return <Page {...Page.mapFragmentToProps(doc)} />;
  }

  return null;
};

PageTemplate.propTypes = {
  data: shape({}).isRequired,
};

const WrappedPage = wrapPage(PageTemplate);

WrappedPage.fragments = [
  LayoutFooterFragment,
  LayoutNavigationFragment,
  LinkFragment,
  // @ts-ignore
  ...Page.fragments,
];

// @ts-ignore
export default withPrismicPreview(WrappedPage, [
  {
    repositoryName: process.env.PRISMIC_REPOSITORY_NAME || 'boxinc',
    linkResolver,
  },
]);

export const query = graphql`
  query PageQuery($uid: String!, $lang: String!) {
    allPrismicPage(filter: { uid: { eq: $uid }, lang: { eq: $lang } }) {
      ...PageFragment
    }
    allPrismicNavigation(filter: { lang: { eq: $lang } }) {
      ...LayoutNavigationFragment
    }
    allPrismicFooter(filter: { lang: { eq: $lang } }) {
      ...LayoutFooterFragment
    }
  }
`;

When I try to preview unpublished doc through prismic, I get to the 404 page as expected, the preview is loading and after the loading nothing happens, I stay on the 404 page.

I tried console logging the nodes argument given to the componentResolver function, that returned an empty array, which doesnt sound right to me.
Also when I looked into React devTools, PrismicPreviewProvider had isBootstrap = true and it contained all the page data including the unpublished preview I wanted to view.

What am I missing here? Any help is appreciated.

Hello team, thanks for reaching out

You should be able to preview any unpublished documents if your Link Resolver and withPrismicUnpublishedPreview HOC are correctly set up.

We're currently running some tests to see if there's something wrong on our side. I'll let you know when we have more information.

Thanks

Thanks for looking into it.

This is what our link resolver looks like.

linkResolver.js
// @ts-nocheck
const { normalizeLanguage } = require('./normalizeLanguage');

module.exports = {
  linkResolver(doc) {
    const isBlogPage = (() => {
      if (doc.type === 'page' && doc.data && doc.data.is_blog) {
        return true;
      }
      return false;
    })();

    if (
      (doc.type === 'page' && !isBlogPage) ||
      doc.type === 'info-page' ||
      doc.type === 'page-core'
    ) {
      const lang = normalizeLanguage(doc.lang);
      const path = doc.uid.replace(/\./g, '/');
      return `/${lang}/${path}`;
    }

    if (doc.type === 'blog-post' || isBlogPage) {
      const lang = normalizeLanguage(doc.lang);
      const path = doc.uid.replace(/\./g, '/');
      let categorySlug;

      if (doc.data && doc.data.categories[0].category.uid) {
        categorySlug = doc.data.categories[0].category.uid;
      }

      if (categorySlug) {
        return `/${lang}/blog/${categorySlug}/${path}`;
      }

      // This shouldn't happen if the categories are set up correctly in Prismic
      return `/${lang}/blog/${path}`;
    }

    return '/';
  },
};

Hey there.

Your Link Resolver seems to be just fine. I forgot to mention earlier that Gatsby's 404 pages in development mode will appear even when a preview session is launched. This means that, when you see the 404 page, you'll need to manually go to the correct URL of the unpublished document. It should appear in a list of the same err page.

This would work normally in production. So it's only in dev mode when this odd behaviour will keep happening.

Hey.
I tried it in production, but the behavior stays the same. I launch a preview session, I get redirected to the custom 404 page with the unpublished preview HOC, preview is loading, after loading finishes, I stay on the 404 page.

Hey there, maybe there's something missing in your plugin configuration. Can you show me how you're passing the Link resolver to the plugin options? sometimes it just takes a small change in the syntax to make it work with version4 of the plugin.

linkResolver: require('./src/utils/linkResolver').linkResolver,

I ran into the same issue and was pulling my hair out trying to figure it out.

I solved it by ensuring my 404 page pulls in the data value from the props.

import * as React from "react"
import {withPrismicUnpublishedPreview} from "gatsby-plugin-prismic-previews"

import { unpublishedRepositoryConfigs } from '../utils/prismicUnpublishedPreviews'

const NotFoundPage = ({data}) => {

  return (
    <div>
      <h1>Not found</h1>
    </div>
  )
}

and in development when you preview an unpublished page you get taken to the development 404 page.
Just click on preview custom 404 page button near the top

Hope this helps

Hi, thanks for your suggestion, unfortunately it doesn't seem to work for me.

Hi, this is what our plugin config looks like

gatsby-config
/* eslint-disable no-underscore-dangle */
/*
  Gatsby can access environment variables in .env.production and .env.development files only for Client side js code.
  To access environment variables within Server side node.js code we need to load them using dotenv.
  We load also variables from .env file that are ignored by Gatsby
*/
require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
require('dotenv').config({ path: `.env` });

const { BOXINC_SITE_URL } = require('./src/config');
const { linkResolver } = require('./src/utils/linkResolver');
const { normalizeLanguage } = require('./src/utils/normalizeLanguage');

module.exports = {
  siteMetadata: {
    siteUrl: BOXINC_SITE_URL,
  },
  plugins: [
    `gatsby-plugin-image`,
    {
      resolve: `gatsby-source-prismic`,
      options: {
        repositoryName: `${process.env.PRISMIC_REPOSITORY_NAME}`,
        accessToken: `${process.env.PRISMIC_API_KEY}`,
        lang: '*',
        linkResolver: (doc) => linkResolver(doc),
        schemas: {
          /* eslint-disable global-require */
          'blog-author': require('./prismic/schema/blog-author.json'),
          'blog-category': require('./prismic/schema/blog-category.json'),
          'blog-post': require('./prismic/schema/blog-post.json'),
          'blog-posts-page': require('./prismic/schema/blog-posts-page.json'),
          footer: require('./prismic/schema/footer.json'),
          'info-page': require('./prismic/schema/info-page.json'),
          menu: require('./prismic/schema/menu.json'),
          navigation: require('./prismic/schema/navigation.json'),
          'page-core': require('./prismic/schema/page-core.json'),
          page: require('./prismic/schema/page.json'),
          'section-content': require('./prismic/schema/section-content.json'),
          advantages: {},
          'blog-post-type-1': {},
          header: {},
          nginx_redirects: require('./prismic/schema/nginx-redirects.json'),
          'redirect-rule': require('./prismic/schema/redirect-rule.json'),
          /* eslint-enable global-require */
        },
      },
    },
    {
      resolve: `gatsby-plugin-prismic-previews`,
      options: {
        repositoryName: `${process.env.PRISMIC_REPOSITORY_NAME}`,
        accessToken: `${process.env.PRISMIC_API_KEY}`,
      },
    },
    { resolve: `gatsby-plugin-react-helmet-async` },
    { resolve: `gatsby-transformer-json` },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/assets/images`,
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/locale`,
        name: `locale`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        icon: 'src/assets/images/favicon-32x32.png',
      },
    },
    {
      resolve: `gatsby-plugin-styled-components`,
      options: {},
    },

    {
      resolve: 'gatsby-plugin-react-svg',
      options: {
        rule: {
          include: `${__dirname}/src/assets/images/icons`,
        },
      },
    },
    {
      resolve: `gatsby-plugin-sitemap`,
      options: {
        query: `
          {
            allPrismicPage {
              nodes {
                path: url
                lang
                alternateLanguages: alternate_languages {
                  uid
                  lang
                }
                data {
                  categories {
                    category {
                      uid
                    }
                  }
                  metaCanonical: meta_canonical
                  metaDescription: meta_description
                  metaKeywords: meta_keywords
                  metaRobots: meta_robots
                  metaTitle: meta_title
                }
                firstPublicationDate: first_publication_date
              }
            }
            allPrismicPageCore {
              nodes {
                path: url
                lang
                alternateLanguages: alternate_languages {
                  uid
                  lang
                }
                data {
                  metaCanonical: meta_canonical
                  metaDescription: meta_description
                  metaKeywords: meta_keywords
                  metaRobots: meta_robots
                  metaTitle: meta_title
                }
              }
            }
            allPrismicBlogPost {
              nodes {
                __typename
                lang
                path: url
                data {
                  categories {
                    category {
                      uid
                    }
                  }
                  heroImage: hero_image {
                    url
                  }
                  metaKeywords: meta_keywords
                  metaTitle: meta_title
                  metaCanonical: meta_canonical
                  metaDescription: meta_description
                  metaRobots: meta_robots
                  publicationDate: release_date
                }
                alternateLanguages: alternate_languages {
                  uid
                  lang
                }
              }
             }
            }
        `,
        output: '/',
        excludes: ['de/dummy', 'en/dummy'],
        resolveSiteUrl: () => BOXINC_SITE_URL,
        resolvePages: ({
          allPrismicPage: { nodes: allPage },
          allPrismicPageCore: { nodes: allPageCore },
          allPrismicBlogPost: { nodes: allPrismicBlogPost },
        }) => [...allPage, ...allPageCore, ...allPrismicBlogPost],
        filterPages: (page, excludedRoute, _) => {
          if (page.path === excludedRoute) {
            return true;
          }

          if (
            page.data.metaRobots &&
            page.data.metaRobots.includes('noindex')
          ) {
            return true;
          }

          return false;
        },
        serialize: ({
          path,
          lang,
          alternateLanguages,
          __typename,
          data,
          firstPublicationDate,
        }) => {
          if (
            __typename === 'PrismicBlogPost' ||
            (data.categories &&
              data.categories.length > 0 &&
              data.categories[0].category.uid)
          ) {
            return {
              url: path,
              links: alternateLanguages.map(({ uid, lang: altLang }) => ({
                lang: altLang,
                url: `/${normalizeLanguage(altLang)}/blog/${
                  data.categories[0].category.uid
                }/${uid}`,
              })),
              news: data.metaTitle && {
                publication: {
                  name: 'Box Inc',
                  language: lang,
                },
                genres: 'Blog',
                publication_date: data.publicationDate || firstPublicationDate,
                title: data.metaTitle,
                keywords: data.metaKeywords,
              },
            };
          }

          return {
            url: path,
            lang,
            links: alternateLanguages.map(({ uid, lang: altLang }) => ({
              lang: altLang,
              url: `/${normalizeLanguage(altLang)}/${uid}`,
            })),
          };
        },
      },
    },
    {
      resolve: 'gatsby-plugin-brotli',
      options: {
        extensions: ['css', 'html', 'js', 'json', 'svg'],
      },
    },
    {
      resolve: 'gatsby-plugin-webpack-bundle-analyser-v2',
      options: {
        analyzerMode: 'static', // If we ever need to analyze our bundle size, just replace 'disabled' with 'static' or 'server'
        openAnalyzer: true,
      },
    },
    {
      resolve: 'nginx-redirect-rules-gen',
      options: {
        outputConfigFile: `${__dirname}/nginx/default.conf`,
        inputConfigFile: `${__dirname}/nginx/default.source.conf`,
      },
    },
  ],
};

Hi @customerservice,

I'm Angelo, the developer of the Gatsby plugins.

Everything you provided looks technically correct. Since you are seeing isBootstrapped: true in <PrismicPreviewProvider>, that means the plugin is correctly connecting to your Prismic repository and pulling all previewed documents.

The only difference I see between your Page.tsx and 404.tsx pages is the way the Prismic repository is passed. Can you confirm that you are passing the correct repository name on 404.tsx (do you use a different Prismic Repository name during production?)?

{
  repositoryName: process.env.PRISMIC_REPOSITORY_NAME || 'boxinc',
}

I tried console logging the nodes argument given to the componentResolver function, that returned an empty array, which doesnt sound right to me.

This is the reason your 404 page renders your normal 404 page and not the previewed document.

The list of matching nodes is used to determine which template to render. If you are using the componentResolverFromMap helper function, it automatically uses the first node in the list and matches its type to your template map. If you need more granular control, you can write your own function that returns a React component to render.

Since your node list is empty, withPrismicUnpublishedPreview does not have a template component to render.

The list of matching nodes is determined using the following:

  • When all documents are fetched from your repository during a preview session, it resolves its URL using your Link Resolver and assigns it to its url field. The Link Resolver provided to withPrismicPreview and withPrismicUnpublishedPreview is used.
  • When your 404 page is rendered with withPrismicUnpublishedPreview, it uses the current URL to find nodes with matching URLs.

Image you have the following documents in your repository which return the following URLs:

  • Page 1 => /page-1
  • Page 2 => /page-2
  • Blog Post 1 => /blog/post-1
  • Blog Post 2 => /blog/post-2

Now imagine you write a blog post and preview it before publishing it (i.e. an unpublished preview)

  • Blog Post 3 => /blog/post-3 (unpublished)

When you click the preview button, you should land on your /preview page. This will redirect you to /blog/post-3, which is a 404 page since it does not exist on your site.

The 404 page will look at the URL and find a document that matches. It finds Blog Post 3, sees that it is a blog_post type, and renders the BlogPost template provided to withPrismicUnpublishedPreview.

The result of the Link Resolver for the unpublished document must match the URL you land on.


If this explanation does not help solve the issue, I can take a look at your project's code and run it locally. You can send your code to me in a private message if you are comfortable with that.