Skip to content

Instantly share code, notes, and snippets.

@hannesl
Last active February 25, 2022 00:03
Show Gist options
  • Save hannesl/1c4c13f15dd9c7149084c4ca523d827d to your computer and use it in GitHub Desktop.
Save hannesl/1c4c13f15dd9c7149084c4ca523d827d to your computer and use it in GitHub Desktop.
An alternative, cookie-less solutions for previewing CMS content in Next.js.

This is an alternative approach to doing previews of CMS content in Next.js. The canonical solution involves a special API route that sets a cookie in the editor‘s browser and then redirects it to the normal page URL. The page then picks up the cookie and switches to preview mode.

This didn’t quite sit right with me – I’d rather use a specific preview URL and not rely on cookies for determining the preview state. It seemed like it might lead to confusing situations. I couldn’t see any reason why they would implement it like that.

Then I realized that the benefit of this is that you get to reuse all the data fetching code in your page files, and avoid having to reimplement a special Preview version of each page.

I then thought about it some more and remembered that all of those pages have a uniform API for defining their data: getStaticProps() and getServerSideProps(). What if I could create ONE preview page and have it fetch each page’s data by simply calling these functions?

Turns out that I could. The only downside is that there is some maintenance involved with the preview page/route whenever you add or modify your page files.

The following is a simplified version of the preview page I built. Please share your thoughts!

/**
* @file Supports displaying previews of pages.
*
* Should be queried like this: https://my-site/_preview/?slug=my-page&preview_key={MY_SECRET_KEY}
*/
import { GetServerSideProps, GetStaticProps, NextPage } from "next";
import _ from "lodash";
import { ParsedUrlQuery } from "querystring";
import MyCatchAll, { getStaticProps as myCatchAllStaticProps } from "./[[...slug]]";
import MyOtherPage, { getStaticProps as myOtherPageStaticProps } from "./my-other-page";
/**
* Data returned from your pages' getStaticProps() or getServerSideProps().
*/
interface PageData {
props: { [key: string]: unknown };
redirect?: unknown;
notFound?: unknown;
revalidate?: unknown;
}
/**
* An object specifying how to render a preview for a particular slug/path.
*/
interface PreviewHandler {
PageComponent: NextPage<unknown>;
pageDataCallback: GetStaticProps | GetServerSideProps;
slugValue?: (slug: string) => string;
}
/**
* Get the preview handler for a particular slug/path.
*
* @param slug The slug for which a preview is requested.
* @returns PreviewHandler
*/
const getPreviewHandler = (slug: string): PreviewHandler => {
switch (true) {
case slug === "my-other-page":
return {
PageComponent: MyOtherPage as NextPage<unknown>,
pageDataCallback: myOtherPageStaticProps,
};
default:
return {
PageComponent: MyCatchAll as NextPage<unknown>,
pageDataCallback: myCatchAllStaticProps,
slugValue: (slug) => slug,
};
}
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const slug = Array.isArray(context.query.slug)
? context.query.slug.join("/")
: context.query.slug;
if (
slug &&
context.query.preview_key &&
context.query.preview_key === process.env.MY_PREVIEW_SECRET
) {
const { pageDataCallback, slugValue } = getPreviewHandler(slug);
if (slugValue) {
context.params = context.params || {};
(context.params as ParsedUrlQuery).slug = slugValue(slug);
}
context.preview = true;
const pageData = (await pageDataCallback(context)) as PageData;
if (pageData.props) {
pageData.props.slug = slug;
}
return _.pick(pageData, "props", "redirect", "notFound");
} else {
return {
notFound: true,
};
}
};
const Preview: NextPage<{ slug: string; [key: string]: unknown }> = (props) => {
const { PageComponent } = getPreviewHandler(props.slug);
return <PageComponent {...props}></PageComponent>;
};
export default Preview;
import { GetStaticProps, NextPage } from "next";
export const getStaticProps: GetStaticProps = async ({ preview = false }) => {
const entry = await myContentFetcher("my-other-page", !!preview);
if (entry) {
return {
props: {
entry,
},
revalidate: X,
};
}
return {
notFound: true,
revalidate: X,
};
};
interface MyPageProps {
entry: MyEntryType;
}
const MyPage: NextPage<MyPageProps> = ({ entry }) => {
return <MyComponent>{/* ... */}</MyComponent>;
};
export default MyPage;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment