Skip to content

Commit

Permalink
docs: explain setup with apollo for CSM graphQL (#763)
Browse files Browse the repository at this point in the history
* docs: explain setup with apollo for CSM graphQL

* docs(content-source-maps): add example for content-source-maps with apollo GraphQL []

* docs(content-source-maps): update tip for apollo/client []

---------

Co-authored-by: Chris Helgert <[email protected]>
  • Loading branch information
YvesRijckaert and chrishelgert authored Oct 30, 2024
1 parent f173594 commit e26c7e0
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/content-source-maps-apollo/.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/content-source-maps-apollo/.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
15 changes: 15 additions & 0 deletions examples/content-source-maps-apollo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
> ⚠️ **This feature is currently in alpha for selected customers.**
# Next.js App Router GraphQL Content Source Maps example

This is an example project that demonstrates how to use Content Source Maps with a Next.js App Router application. It is using the GraphQL API of Contentful.

## What?

[Inspector mode](https://www.contentful.com/developers/docs/tutorials/general/live-preview/) helps to simplify navigation within complex content structures. However, setup costs for complex pages can be time-consuming. To address this, Content Source Maps automates tagging and enables integration with [Vercel’s Content Links](https://vercel.com/docs/workflow-collaboration/edit-mode#content-link) feature, enhancing usability and collaboration.

## How?

It uses the GraphQL query level directive `@contentSourceMaps` to generate Content Source Maps for preview content. This content is parsed through the `encodeGraphQLResponse` function from the Live Preview SDK. It returns with the content that includes the hidden metadata to enable live preview inspector mode and Vercel Content Links.

For more information around Content Source Maps check out the [README](https://github.com/contentful/live-preview/tree/main/packages/content-source-maps) for Content Source Maps.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { draftMode } from 'next/headers';

export async function GET(request: Request) {
draftMode().disable();
return new Response('Draft mode is disabled');
}
36 changes: 36 additions & 0 deletions examples/content-source-maps-apollo/app/api/enable-draft/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 bypass = searchParams.get('x-vercel-protection-bypass');

if (!secret) {
return new Response('Missing parameters', { status: 400 });
}

// This secret should only be known to this route handler and the CMS
if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET) {
return new Response('Invalid token', { 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(`/?x-vercel-protection-bypass=${bypass}&x-vercel-set-bypass-cookie=samesitenone`);
}
Binary file not shown.
28 changes: 28 additions & 0 deletions examples/content-source-maps-apollo/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata } from 'next';
import { draftMode } from 'next/headers';
import { ContentfulPreviewProvider } from '../components/contentful-preview-provider';

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const { isEnabled } = draftMode();

return (
<ContentfulPreviewProvider
locale="en-US"
enableInspectorMode={isEnabled}
enableLiveUpdates={isEnabled}
>
<html lang="en">
<body>{children}</body>
</html>
</ContentfulPreviewProvider>
);
}
20 changes: 20 additions & 0 deletions examples/content-source-maps-apollo/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { draftMode } from 'next/headers';
import { getAllPostsForHome } from '../lib/api-graphql';

export default async function Home() {
const { isEnabled } = draftMode();
const posts = await getAllPostsForHome(isEnabled);

return (
<main>
<ul>
{posts &&
posts.map((post, i) => (
<li key={i}>
<h1>{post.title}</h1>
</li>
))}
</ul>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import { ContentfulLivePreviewInitConfig } from '@contentful/live-preview';
import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react';
import { PropsWithChildren } from 'react';

export function ContentfulPreviewProvider({
children,
...props
}: PropsWithChildren<ContentfulLivePreviewInitConfig>) {
return <ContentfulLivePreviewProvider {...props}>{children}</ContentfulLivePreviewProvider>;
}
101 changes: 101 additions & 0 deletions examples/content-source-maps-apollo/lib/api-graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ApolloClient, ApolloLink, from, gql, HttpLink, InMemoryCache } from '@apollo/client';
import { encodeGraphQLResponse } from '@contentful/live-preview';

interface Sys {
id: string;
}

export interface Post {
__typename: string;
sys: Sys;
title: string;
}

const POST_GRAPHQL_FIELDS = `
__typename
sys {
id
}
title
`;

interface PostCollection {
items: Post[];
}

interface FetchResponse {
data?: {
postCollection?: PostCollection;
};
}

async function fetchGraphQL(query: string, draftMode = false): Promise<any> {
try {
// https://github.com/apollographql/apollo-feature-requests/issues/117
const client = new ApolloClient({
cache: new InMemoryCache(),
link: from([
new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
response.data = {
...response.data,
extensions: response.extensions,
};
return response;
});
}),
new HttpLink({
uri: `https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${
draftMode
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
},
}),
]),
});

const result = await client.query({
query: gql(query),
// Important: With cache the extensions are stil stripped away
fetchPolicy: draftMode ? 'no-cache' : 'cache-first',
});

if (result.errors) {
console.error(result.errors);
throw new Error(`GraphQL query errors: ${result.errors.map((e) => e.message).join(', ')}`);
}

const { extensions, ...data } = result.data;

return { data, extensions };
} catch (error) {
console.error(`Error in fetchGraphQL: ${error}`);
return { errors: [(error as any).message] };
}
}

function extractPostEntries(fetchResponse: FetchResponse): Post[] | undefined {
return fetchResponse?.data?.postCollection?.items;
}

export async function getAllPostsForHome(draftMode: boolean): Promise<Post[] | undefined> {
const entries = await fetchGraphQL(
`query @contentSourceMaps {
postCollection(preview: ${draftMode ? 'true' : 'false'}, limit: 2) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
draftMode,
);

// Conditionally encode the entries only in draft mode
const finalEntries = draftMode ? encodeGraphQLResponse(entries) : entries;

return extractPostEntries(finalEntries);
}
19 changes: 19 additions & 0 deletions examples/content-source-maps-apollo/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 http://localhost:3001/`,
},
],
},
];
},
};
19 changes: 19 additions & 0 deletions examples/content-source-maps-apollo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@contentful/live-preview": "^4.2.2",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"@apollo/client": "3.11.8",
"next": "14.2.10",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.0.4"
}
}
28 changes: 28 additions & 0 deletions examples/content-source-maps-apollo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
2 changes: 2 additions & 0 deletions packages/content-source-maps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ const dataWithAutoTagging = encodeCPAResponse(data);
<ContentfulLivePreviewProvider experimental={{ ignoreManuallyTaggedElements: true }} />
```

- For usage with @apollo/client, a custom link is needed to add the extensions to forward the extionsions to the response. [Example](../../examples/content-source-maps-apollo/lib/api-graphql.ts)

## Limitations

- Markdown support is currently in development.
Expand Down

0 comments on commit e26c7e0

Please sign in to comment.