diff --git a/README.md b/README.md index 6bd1bf6a..b0630e92 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,8 @@ That's it! You should now be able to use the Contentful Live Preview SDK with va #### Integration with Next.js -You can find an example for the NextJS Pages Router implementation in the [examples/nextjs-graphql](./examples/nextjs-graphql/) folder. If you are using the app router you can look at this [example](./examples/nextjs-13-app-router-graphql/) instead. +You can find an example for the NextJS Pages Router implementation in the [examples/nextjs-graphql](./examples/nextjs-graphql/) folder. +If you are using the app router you can look at this [example](./examples/nextjs-13-app-router-graphql/) or for only serverside rendering this [example](./examples/next-13-app-router-ssr/) instead. To use the Contentful Live Preview SDK with [Next.js](https://nextjs.org), you can either use one of the Contentful starter templates, or do the following steps to add it to an existing project. diff --git a/examples/next-13-app-router-ssr/.env.local.example b/examples/next-13-app-router-ssr/.env.local.example new file mode 100644 index 00000000..2611e44f --- /dev/null +++ b/examples/next-13-app-router-ssr/.env.local.example @@ -0,0 +1,8 @@ +# This is the Space ID from your Contentful space. +CONTENTFUL_SPACE_ID= +# This is the Content Delivery API - access token, which is used for fetching published data from your Contentful space. +CONTENTFUL_ACCESS_TOKEN= +# This is the Content Preview API - access token, which is used for fetching draft data from your Contentful space. +CONTENTFUL_PREVIEW_ACCESS_TOKEN= +# This can be any value you want. It must be URL friendly as it will be send as a query parameter to enable draft mode. +CONTENTFUL_PREVIEW_SECRET= \ No newline at end of file diff --git a/examples/next-13-app-router-ssr/.eslintrc.json b/examples/next-13-app-router-ssr/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/examples/next-13-app-router-ssr/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/next-13-app-router-ssr/.gitignore b/examples/next-13-app-router-ssr/.gitignore new file mode 100644 index 00000000..fef7b6fe --- /dev/null +++ b/examples/next-13-app-router-ssr/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +/public/live-preview.mjs +/public/live-preview.umd.js + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.next +.env diff --git a/examples/next-13-app-router-ssr/README.md b/examples/next-13-app-router-ssr/README.md new file mode 100644 index 00000000..6d0160ba --- /dev/null +++ b/examples/next-13-app-router-ssr/README.md @@ -0,0 +1,43 @@ +# Next.js GraphQL Contentful live preview SDK example + +This is an example project that demonstrates how to use the `@contentful/live-preview` SDK with a Next.js application that uses the GraphQL API. The live preview SDK will reload the page after **related** content changes and enables the inspector mode for your Contentful space. +It's important that you use the CPA (Content Preview API) when using this functionality. + +## 1. Installation + +Install the dependencies: + +```bash +npm install +``` + +## 2. Environment variables + +To run this project, you will need to add the following environment variables to your `.env.local` file: + +- `CONTENTFUL_SPACE_ID`: This is the Space ID from your Contentful space. +- `CONTENTFUL_ACCESS_TOKEN`: This is the Content Delivery API - access token, which is used for fetching **published** data from your Contentful space. +- `CONTENTFUL_PREVIEW_ACCESS_TOKEN`: This is the Content Preview API - access token, which is used for fetching **draft** data from your Contentful space. +- `CONTENTFUL_PREVIEW_SECRET`: This can be any value you want. It must be URL friendly as it will be send as a query parameter to enable draft mode. + +## 3. Setting up the content model + +You will need to set up a content model within your Contentful space. For this project, we need a `Post` content type with the following fields: + +- `slug` +- `title` +- `description` + +Once you've set up the `Post` content model, you can populate it with some example entries. + +## 4. Setting up Content preview URL + +In order to enable the live preview feature in your local development environment, you need to set up the Content preview URL in your Contentful space. + +`http://localhost:3000/api/draft?secret=&slug={entry.fields.slug}` + +Replace `` with its respective value in `.env.local`. + +## 5. Running the project locally + +To run the project locally, you can use the `npm run dev` command. You can now use the live preview feature. diff --git a/examples/next-13-app-router-ssr/app/api/disable-draft/route.ts b/examples/next-13-app-router-ssr/app/api/disable-draft/route.ts new file mode 100644 index 00000000..128e6852 --- /dev/null +++ b/examples/next-13-app-router-ssr/app/api/disable-draft/route.ts @@ -0,0 +1,6 @@ +import { draftMode } from 'next/headers'; + +export async function GET(request: Request) { + draftMode().disable(); + return new Response('Draft mode is disabled'); +} diff --git a/examples/next-13-app-router-ssr/app/api/draft/route.ts b/examples/next-13-app-router-ssr/app/api/draft/route.ts new file mode 100644 index 00000000..ed9a54c1 --- /dev/null +++ b/examples/next-13-app-router-ssr/app/api/draft/route.ts @@ -0,0 +1,45 @@ +// route handler with secret and slug +import { getPreviewPostBySlug } from '@/lib/api-graphql'; +import { cookies, draftMode } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export async function GET(request: Request) { + // Parse query string parameters + const { searchParams } = new URL(request.url); + const secret = searchParams.get('secret'); + const slug = searchParams.get('slug'); + + // This secret should only be known to this route handler and the CMS + if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET || !slug) { + return new Response('Invalid token', { status: 401 }); + } + + // Fetch the headless CMS to check if the provided `slug` exists + // getPostBySlug would implement the required fetching logic to the headless CMS + const post = await getPreviewPostBySlug(slug); + + // If the slug doesn't exist prevent draft mode from being enabled + if (!post) { + return new Response('Invalid slug', { status: 401 }); + } + + // Enable Draft Mode by setting the cookie + draftMode().enable(); + + // Override cookie header for draft mode for usage in live-preview + // https://github.com/vercel/next.js/issues/49927 + const cookieStore = cookies(); + const cookie = cookieStore.get('__prerender_bypass')!; + cookies().set({ + name: '__prerender_bypass', + value: cookie?.value, + httpOnly: true, + path: '/', + secure: true, + sameSite: 'none', + }); + + // Redirect to the path from the fetched post + // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities + redirect(`/posts/${post.slug}`); +} diff --git a/examples/next-13-app-router-ssr/app/components/post-layout.tsx b/examples/next-13-app-router-ssr/app/components/post-layout.tsx new file mode 100644 index 00000000..2c6040bf --- /dev/null +++ b/examples/next-13-app-router-ssr/app/components/post-layout.tsx @@ -0,0 +1,27 @@ +import { Post } from '../../lib/api-graphql'; +import { ContentfulLivePreview } from '@contentful/live-preview'; + +export default function PostLayout({ post }: { post: Post }) { + return ( + <> +

+ {post.title} +

+

+ {post.description} +

+ + ); +} diff --git a/examples/next-13-app-router-ssr/app/favicon.ico b/examples/next-13-app-router-ssr/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/examples/next-13-app-router-ssr/app/favicon.ico differ diff --git a/examples/next-13-app-router-ssr/app/layout.tsx b/examples/next-13-app-router-ssr/app/layout.tsx new file mode 100644 index 00000000..b78127a0 --- /dev/null +++ b/examples/next-13-app-router-ssr/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import { draftMode } from 'next/headers'; +import Script from 'next/script'; + +import '@contentful/live-preview/style.css'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'Next app with app router and reload entry change', + description: 'Generated by create next app', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + const { isEnabled } = draftMode(); + + return ( + + {children} + {isEnabled &&