Skip to content

Commit

Permalink
Merge pull request #118 from contentful/feat/next-js-example
Browse files Browse the repository at this point in the history
feat: next js example [TOL-1162]
  • Loading branch information
YvesRijckaert authored May 17, 2023
2 parents 29be2a9 + 1bf976d commit 37cd061
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 6 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,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 implementation in the [examples/nextjs](./examples/nextjs/) folder.

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.

1. Add the @contentful/live-preview package to your project
Expand Down Expand Up @@ -259,14 +261,14 @@ const CustomApp = ({ Component, pageProps }) => (
)
```

This provides the posibility to only enable live updates and the inspector mode inside the preview mode:
This provides the possibility to only enable live updates and inspector mode inside draft mode:

```tsx
import '@contentful/live-preview/style.css';
import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react';

const CustomApp = ({ Component, pageProps }) => (
<ContentfulLivePreviewProvider locale="en-US" enableInspectorMode={pageProps.previewActive} enableLiveUpdates={pageProps.previewActive}>
<ContentfulLivePreviewProvider locale="en-US" enableInspectorMode={pageProps.draftMode} enableLiveUpdates={pageProps.draftMode}>
<Component {...pageProps}>
</ContentfulLivePreviewProvider>
)
Expand Down Expand Up @@ -299,7 +301,7 @@ export default function BlogPost: ({ blogPost }) {
}
```

> It doesn't matter if the data is loaded with getServerSideProps, getStaticProps or if you load it in any other way.<br>It's necessary that the provided information to `useContentfulLiveUpdate` contains the `sys.id` for identifation and only non-transformed fields can be updated.<br>(For GraphQL also the `__typename` needs to be provided)
> It doesn't matter if the data is loaded with getServerSideProps, getStaticProps or if you load it in any other way.<br>It's necessary that the provided information to `useContentfulLiveUpdate` contains the `sys.id` for identification and only non-transformed fields can be updated.<br>(For GraphQL also the `__typename` needs to be provided)

**Tip:** If you want to tag multiple fields of an entry, you can also provide initial arguments to the hook:

Expand All @@ -317,13 +319,13 @@ export default function BlogPost: ({ blogPost }) {
)
```
4. Enable preview mode
4. Enable draft mode
We suggest using the [preview mode](https://nextjs.org/docs/advanced-features/preview-mode) and the [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/) for the best experience.
We suggest using the [draft mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) and the [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/) for the best experience.
For a full guide checkout this [free course](https://www.contentful.com/nextjs-starter-guide/)
5. In Contentful, define the preview environment and configure the preview URL for your Next.js application. Once you open an entry with a configured preview URL, you can use the live preview and all its features.
5. In Contentful, configure the draft URL for your Next.js application in the Content preview settings. Once you open an entry with a configured preview URL, you can use the live preview and all its features.
That's it! You should now be able to use the Contentful live preview SDK with Next.js.
Expand Down
8 changes: 8 additions & 0 deletions examples/nextjs/.env.local.example
Original file line number Diff line number Diff line change
@@ -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=
35 changes: 35 additions & 0 deletions examples/nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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

# 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
43 changes: 43 additions & 0 deletions examples/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Next.js 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. The SDK provides live preview functionality for content changes and the inspector mode for your Contentful space.

## 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=<CONTENTFUL_PREVIEW_SECRET>&slug={entry.fields.slug}`

Replace `<CONTENTFUL_PREVIEW_SECRET>` 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.
94 changes: 94 additions & 0 deletions examples/nextjs/lib/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const POST_GRAPHQL_FIELDS = `
__typename
sys {
id
}
slug
title
description
`;

async function fetchGraphQL(query, draftMode = false) {
return fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${
draftMode
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
},
body: JSON.stringify({ query }),
}
).then((response) => response.json());
}

function extractPost(fetchResponse) {
return fetchResponse?.data?.postCollection?.items?.[0];
}

function extractPostEntries(fetchResponse) {
return fetchResponse?.data?.postCollection?.items;
}

export async function getPreviewPostBySlug(slug) {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: true, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
true
);
return extractPost(entry);
}

export async function getAllPostsWithSlug() {
const entries = await fetchGraphQL(
`query {
postCollection(where: { slug_exists: true }) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`
);
return extractPostEntries(entries);
}

export async function getAllPostsForHome(draftMode) {
const entries = await fetchGraphQL(
`query {
postCollection(preview: ${draftMode ? "true" : "false"}) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
draftMode
);
return extractPostEntries(entries);
}

export async function getPost(slug, draftMode) {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: ${
draftMode ? "true" : "false"
}, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
draftMode
);
return {
post: extractPost(entry),
};
}
19 changes: 19 additions & 0 deletions examples/nextjs/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'Content-Security-Policy',
value: `frame-ancestors 'self' https://app.contentful.com`,
},
],
},
];
},
};
14 changes: 14 additions & 0 deletions examples/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@contentful/live-preview": "^2.0.2",
"next": "^13.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
12 changes: 12 additions & 0 deletions examples/nextjs/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ContentfulLivePreviewProvider } from "@contentful/live-preview/react";
import "@contentful/live-preview/style.css";

function App({ Component, pageProps }) {
return (
<ContentfulLivePreviewProvider locale="en-US" enableInspectorMode={pageProps.draftMode} enableLiveUpdates={pageProps.draftMode}>
<Component {...pageProps} />
</ContentfulLivePreviewProvider>
);
}

export default App;
13 changes: 13 additions & 0 deletions examples/nextjs/pages/_document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
3 changes: 3 additions & 0 deletions examples/nextjs/pages/api/disable-draft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.setDraftMode({ enable: false });
}
35 changes: 35 additions & 0 deletions examples/nextjs/pages/api/draft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getPreviewPostBySlug } from "../../lib/api";

export default async function draft(req, res) {
const { secret, slug } = req.query;

// Check the secret and next parameters
// This secret should only be known to this API route and Contentful
if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET || !slug) {
return res.status(401).json({ message: "Invalid token" });
}

// Fetch the post to check if the provided `slug` exists
const post = await getPreviewPostBySlug(slug);

// If the slug doesn't exist prevent draft mode from being enabled
if (!post) {
return res.status(401).json({ message: "Invalid slug" });
}

// Enable Draft Mode by setting the cookie
res.setDraftMode({ enable: true });

// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
// res.writeHead(307, { Location: `/posts/${post.slug}` })
const url = `/posts/${post.slug}`;
res.setHeader("Content-Type", "text/html");
res.write(
`<!DOCTYPE html><html><head><meta http-equiv="Refresh" content="0; url=${url}" />
<script>window.location.href = '${url}'</script>
</head>
</html>`
);
res.end();
}
23 changes: 23 additions & 0 deletions examples/nextjs/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getAllPostsForHome } from "../lib/api";
import Head from "next/head";
import Link from "next/link";

export default function Index({ posts }) {
return (
<>
<Head>
<title>Contentful live preview example with Next.js</title>
</Head>
{posts.map((post) => (
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
))}
</>
);
}

export async function getStaticProps({ draftMode = false }) {
const posts = (await getAllPostsForHome(draftMode)) ?? [];
return {
props: { posts },
};
}
Loading

0 comments on commit 37cd061

Please sign in to comment.