diff --git a/examples/cms-contentful/README.md b/examples/cms-contentful/README.md index d2dd3e85e777..6120452bf9ed 100644 --- a/examples/cms-contentful/README.md +++ b/examples/cms-contentful/README.md @@ -4,13 +4,13 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas ## Demo -### [https://next-blog-contentful.vercel.app/](https://next-blog-contentful.vercel.app/) +### [https://app-router-contentful.vercel.app/](https://app-router-contentful.vercel.app/) ## Deploy your own Using the Deploy Button below, you'll deploy the Next.js project as well as connect it to your Contentful space using the Vercel Contentful Integration. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-contentful&project-name=nextjs-contentful-blog&repository-name=nextjs-contentful-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Preview+Mode&demo-url=https%3A%2F%2Fnext-blog-contentful.vercel.app%2F&demo-image=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Fv1625705016%2Ffront%2Fexamples%2FCleanShot_2021-07-07_at_19.43.15_2x.png&integration-ids=oac_aZtAZpDfT1lX3zrnWy7KT9VA&env=CONTENTFUL_PREVIEW_SECRET&envDescription=Any%20URL%20friendly%20value%20to%20secure%20Preview%20Mode) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-contentful&project-name=nextjs-contentful-blog&repository-name=nextjs-contentful-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Draft+Mode&demo-url=https%3A%2F%2Fnext-blog-contentful.vercel.app%2F&demo-image=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Fv1625705016%2Ffront%2Fexamples%2FCleanShot_2021-07-07_at_19.43.15_2x.png&integration-ids=oac_aZtAZpDfT1lX3zrnWy7KT9VA&env=CONTENTFUL_PREVIEW_SECRET&envDescription=Any%20URL%20friendly%20value%20to%20secure%20Draft%20Mode) ### Related examples @@ -152,7 +152,7 @@ After setting up the content model (either manually or by running `npm run setup **Content model overview** -![Content model overview](./docs/content-model-overview.png) +![Content model overview](https://github.com/vercel/next.js/assets/9113740/d3f76907-7046-4d94-b285-eb89b87aa223) ### Step 4. Populate Content @@ -171,7 +171,7 @@ Next, create another entry with the content type **Post**: **Important:** For each entry and asset, you need to click on **Publish**. If not, the entry will be in draft state. -![Published content entry](./docs/content-entry-publish.png) +![Published content entry](https://github.com/vercel/next.js/assets/9113740/e1b4a3fe-45f4-4851-91db-8908d3ca18e9) ### Step 5. Set up environment variables @@ -188,7 +188,7 @@ Then set each variable on `.env.local`: - `CONTENTFUL_SPACE_ID` should be the **Space ID** field of your API Key - `CONTENTFUL_ACCESS_TOKEN` should be the **[Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) - access token** field of your API key - `CONTENTFUL_PREVIEW_ACCESS_TOKEN` should be the **[Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/) - access token** field of your API key -- `CONTENTFUL_PREVIEW_SECRET` should be any value you want. It must be URL friendly as the dashboard will send it as a query parameter to enable preview mode +- `CONTENTFUL_PREVIEW_SECRET` should be any value you want. It must be URL friendly as the dashboard will send it as a query parameter to enable Next.js Draft Mode - - `CONTENTFUL_REVALIDATE_SECRET` should be any value you want. This will be the value you pass in as a secret header from the Contentful Webhook settings to use **[On-Demand Revalidation](https://vercel.com/docs/concepts/next.js/incremental-static-regeneration#on-demand-revalidation)** Your `.env.local` file should look like this: @@ -215,19 +215,19 @@ yarn dev Your blog should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions). -### Step 7. Try preview mode +### Step 7. Try Draft Mode In your Contentful space, go to **Settings > Content preview** and add a new content preview for development. The **Name** field may be anything, like `Development`. Then, under **Content preview URLs**, check **Post** and set its value to: ``` -http://localhost:3000/api/preview?secret=&slug={entry.fields.slug} +http://localhost:3000/api/draft?secret=&slug={entry.fields.slug} ``` Replace `` with its respective value in `.env.local`. -![Content preview setup](./docs/content-preview-setup.png) +![Content preview setup](https://github.com/vercel/next.js/assets/9113740/f1383d68-ea2b-4adf-974f-235b8c098745) Once saved, go to one of the posts you've created and: @@ -235,9 +235,9 @@ Once saved, go to one of the posts you've created and: - The state of the post will switch to **CHANGED** automatically. **Do not** publish it. By doing this, the post will be in draft state. - In the sidebar, you will see the **Open preview** button. Click on it! -![Content entry overview](./docs/content-entry-preview.png) +![Content entry overview](https://github.com/vercel/next.js/assets/9113740/cc0dff9a-c57e-4ec4-85f1-22ab74af2b6b) -You will now be able to see the updated title. To exit preview mode, you can click on **Click here to exit preview mode** at the top of the page. +You will now be able to see the updated title. To manually exit Draft Mode, you can navigate to `/api/disable-draft` in the browser. ### Step 8. Deploy on Vercel @@ -253,9 +253,9 @@ To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [ Alternatively, you can deploy using our template by clicking on the Deploy button below. -This will deploy the Next.js project as well as connect it to your Contentful space using the Vercel Contentful Integration. If you are using Preview Mode, make sure to add `CONTENTFUL_PREVIEW_SECRET` as an [Environment Variable](https://vercel.com/docs/environment-variables) as well. +This will deploy the Next.js project as well as connect it to your Contentful space using the Vercel Contentful Integration. If you are using Draft Mode, make sure to add `CONTENTFUL_PREVIEW_SECRET` as an [Environment Variable](https://vercel.com/docs/concepts/projects/environment-variables) as well. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-contentful&project-name=nextjs-contentful-blog&repository-name=nextjs-contentful-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Preview+Mode&demo-url=https%3A%2F%2Fnext-blog-contentful.vercel.app%2F&demo-image=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Fv1625705016%2Ffront%2Fexamples%2FCleanShot_2021-07-07_at_19.43.15_2x.png&integration-ids=oac_aZtAZpDfT1lX3zrnWy7KT9VA&env=CONTENTFUL_PREVIEW_SECRET,CONTENTFUL_REVALIDATE_SECRET&envDescription=Any%20URL%20friendly%20value%20to%20secure%20Your%20App) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-contentful&project-name=nextjs-contentful-blog&repository-name=nextjs-contentful-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Draft+Mode&demo-url=https%3A%2F%2Fnext-blog-contentful.vercel.app%2F&demo-image=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Fv1625705016%2Ffront%2Fexamples%2FCleanShot_2021-07-07_at_19.43.15_2x.png&integration-ids=oac_aZtAZpDfT1lX3zrnWy7KT9VA&env=CONTENTFUL_PREVIEW_SECRET,CONTENTFUL_REVALIDATE_SECRET&envDescription=Any%20URL%20friendly%20value%20to%20secure%20Your%20App) ### Step 9. Try using On-Demand Revalidation @@ -273,21 +273,21 @@ In your Contentful space, go to **Settings > Webhooks** and add a new webhook: - **Specify Triggers:** You can choose to trigger for all events or specific events only, such as the Publishing and Unpublishing of Entries and Assets, as shown below. - ![Content webhook url](./docs/content-webhook-url.png) + ![Content webhook url](https://github.com/vercel/next.js/assets/9113740/c8df492a-57d6-42a1-8a3c-b0de3d6ad42f) - **Specify Secret Header:** Add a secret header named `x-vercel-reval-key` and enter the value of the `CONTENTFUL_REVALIDATE_SECRET` from before. - ![Content secret header](./docs/content-secret-header.png) + ![Content secret header](https://github.com/vercel/next.js/assets/9113740/574935e6-0d31-4e4f-b914-8b01bdf03d5e) - **Set Content type:** Set content type to `application/json` in the dropdown. - ![Content publish changes](./docs/content-content-type.png) + ![Content publish changes](https://github.com/vercel/next.js/assets/9113740/78bd856c-ece1-4bf3-a330-1d544abd858d) - **Edit post:** Now, try editing the title of one of your blog posts in Contentful and click Publish. You should see the changed reflected in the website you just deployed, all without triggering a build! Behind the scenes a call was made to the revalidate api that triggers a revalidation of both the landing page and the specific post that was changed. - ![Content publish changes](./docs/content-publish-changes.png) + ![Content publish changes](https://github.com/vercel/next.js/assets/9113740/ad96bfa7-89c1-4e46-9d9c-9067176c9769) - **Verify:** You can verify if your request was made successfully by checking the webhook request log on Contentful and checking for a successful 200 status code, or by having your functions tab open on Vercel when committing the change (log drains may also be used). If you are experiencing issues with the api call, ensure you have correctly entered in the value for environment variable `CONTENTFUL_REVALIDATE_SECRET` within your Vercel deployment. - ![Content successful request](./docs/content-successful-request.png) + ![Content successful request](https://github.com/vercel/next.js/assets/9113740/ed1ffbe9-4dbf-4ec6-9c1f-39c8949c4d38) diff --git a/examples/cms-contentful/app/api/disable-draft/route.ts b/examples/cms-contentful/app/api/disable-draft/route.ts new file mode 100644 index 000000000000..40282bae836f --- /dev/null +++ b/examples/cms-contentful/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/cms-contentful/app/api/draft/route.ts b/examples/cms-contentful/app/api/draft/route.ts new file mode 100644 index 000000000000..16dd1f3126ad --- /dev/null +++ b/examples/cms-contentful/app/api/draft/route.ts @@ -0,0 +1,22 @@ +import { draftMode } from 'next/headers' +import { redirect } from 'next/navigation' +import { getPreviewPostBySlug } from '../../../lib/api' + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const secret = searchParams.get('secret') + const slug = searchParams.get('slug') + + if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET) { + return new Response('Invalid token', { status: 401 }) + } + + const post = await getPreviewPostBySlug(slug) + + if (!post) { + return new Response('Invalid slug', { status: 401 }) + } + + draftMode().enable() + redirect(`/posts/${post.slug}`) +} diff --git a/examples/cms-contentful/app/api/revalidate/route.ts b/examples/cms-contentful/app/api/revalidate/route.ts new file mode 100644 index 000000000000..21cbcca25b14 --- /dev/null +++ b/examples/cms-contentful/app/api/revalidate/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from 'next/server' +import { revalidateTag } from 'next/cache' + +export async function POST(request: NextRequest) { + const requestHeaders = new Headers(request.headers) + const secret = requestHeaders.get('x-vercel-reval-key') + + if (secret !== process.env.CONTENTFUL_REVALIDATE_SECRET) { + return NextResponse.json({ message: 'Invalid secret' }, { status: 401 }) + } + + revalidateTag('posts') + + return NextResponse.json({ revalidated: true, now: Date.now() }) +} diff --git a/examples/cms-contentful/app/avatar.tsx b/examples/cms-contentful/app/avatar.tsx new file mode 100644 index 000000000000..0f9f8f49e014 --- /dev/null +++ b/examples/cms-contentful/app/avatar.tsx @@ -0,0 +1,24 @@ +import ContentfulImage from '@/lib/contentful-image' + +export default function Avatar({ + name, + picture, +}: { + name: string + picture: any +}) { + return ( +
+
+ +
+
{name}
+
+ ) +} diff --git a/examples/cms-contentful/components/cover-image.js b/examples/cms-contentful/app/cover-image.tsx similarity index 64% rename from examples/cms-contentful/components/cover-image.js rename to examples/cms-contentful/app/cover-image.tsx index 0b4a4f41295b..494271f9e523 100644 --- a/examples/cms-contentful/components/cover-image.js +++ b/examples/cms-contentful/app/cover-image.tsx @@ -1,13 +1,25 @@ -import ContentfulImage from './contentful-image' +import ContentfulImage from '../lib/contentful-image' import Link from 'next/link' -import cn from 'classnames' -export default function CoverImage({ title, url, slug }) { +function cn(...classes: any[]) { + return classes.filter(Boolean).join(' ') +} + +export default function CoverImage({ + title, + url, + slug, +}: { + title: string + url: string + slug?: string +}) { const image = ( {format(new Date(dateString), 'LLLL d, yyyy')} diff --git a/examples/cms-contentful/app/favicon.ico b/examples/cms-contentful/app/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/examples/cms-contentful/app/favicon.ico differ diff --git a/examples/cms-contentful/styles/index.css b/examples/cms-contentful/app/globals.css similarity index 52% rename from examples/cms-contentful/styles/index.css rename to examples/cms-contentful/app/globals.css index b63c4592cb2e..b5c61c956711 100644 --- a/examples/cms-contentful/styles/index.css +++ b/examples/cms-contentful/app/globals.css @@ -1,5 +1,3 @@ -/* purgecss start ignore */ @tailwind base; @tailwind components; -/* purgecss end ignore */ @tailwind utilities; diff --git a/examples/cms-contentful/components/footer.js b/examples/cms-contentful/app/layout.tsx similarity index 53% rename from examples/cms-contentful/components/footer.js rename to examples/cms-contentful/app/layout.tsx index da9eed88ec26..2d8409b1cd8f 100644 --- a/examples/cms-contentful/components/footer.js +++ b/examples/cms-contentful/app/layout.tsx @@ -1,17 +1,29 @@ -import Container from './container' -import { EXAMPLE_PATH } from '../lib/constants' +import './globals.css' +import { Inter } from 'next/font/google' +import { EXAMPLE_PATH, CMS_NAME } from '@/lib/constants' -export default function Footer() { +export const metadata = { + title: `Next.js and ${CMS_NAME} Example`, + description: `This is a blog built with Next.js and ${CMS_NAME}.`, +} + +const inter = Inter({ + variable: '--font-inter', + subsets: ['latin'], + display: 'swap', +}) + +function Footer() { return ( ) } + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +
+
{children}
+
+
+ + + ) +} diff --git a/examples/cms-contentful/app/more-stories.tsx b/examples/cms-contentful/app/more-stories.tsx new file mode 100644 index 000000000000..e2f1b98c4638 --- /dev/null +++ b/examples/cms-contentful/app/more-stories.tsx @@ -0,0 +1,61 @@ +import Link from 'next/link' +import Avatar from './avatar' +import DateComponent from './date' +import CoverImage from './cover-image' + +function PostPreview({ + title, + coverImage, + date, + excerpt, + author, + slug, +}: { + title: string + coverImage: any + date: string + excerpt: string + author: any + slug: string +}) { + return ( +
+
+ +
+

+ + {title} + +

+
+ +
+

{excerpt}

+ {author && } +
+ ) +} + +export default function MoreStories({ morePosts }: { morePosts: any[] }) { + return ( +
+

+ More Stories +

+
+ {morePosts.map((post) => ( + + ))} +
+
+ ) +} diff --git a/examples/cms-contentful/app/page.tsx b/examples/cms-contentful/app/page.tsx new file mode 100644 index 000000000000..9c2430075015 --- /dev/null +++ b/examples/cms-contentful/app/page.tsx @@ -0,0 +1,101 @@ +import Link from 'next/link' +import { draftMode } from 'next/headers' + +import Date from './date' +import CoverImage from './cover-image' +import Avatar from './avatar' +import MoreStories from './more-stories' + +import { getAllPosts } from '@/lib/api' +import { CMS_NAME, CMS_URL } from '@/lib/constants' + +function Intro() { + return ( +
+

+ Blog. +

+

+ A statically generated blog example using{' '} + + Next.js + {' '} + and{' '} + + {CMS_NAME} + + . +

+
+ ) +} + +function HeroPost({ + title, + coverImage, + date, + excerpt, + author, + slug, +}: { + title: string + coverImage: any + date: string + excerpt: string + author: any + slug: string +}) { + return ( +
+
+ +
+
+
+

+ + {title} + +

+
+ +
+
+
+

{excerpt}

+ {author && } +
+
+
+ ) +} + +export default async function Page() { + const { isEnabled } = draftMode() + const allPosts = await getAllPosts(isEnabled) + const heroPost = allPosts[0] + const morePosts = allPosts.slice(1) + + return ( +
+ + {heroPost && ( + + )} + +
+ ) +} diff --git a/examples/cms-contentful/app/posts/[slug]/page.tsx b/examples/cms-contentful/app/posts/[slug]/page.tsx new file mode 100644 index 000000000000..e71d0f5b80a0 --- /dev/null +++ b/examples/cms-contentful/app/posts/[slug]/page.tsx @@ -0,0 +1,69 @@ +import Link from 'next/link' +import { draftMode } from 'next/headers' + +import MoreStories from '../../more-stories' +import Avatar from '../../avatar' +import Date from '../../date' +import CoverImage from '../../cover-image' + +import { Markdown } from '@/lib/markdown' +import { getAllPosts, getPostAndMorePosts } from '@/lib/api' + +export async function generateStaticParams() { + const allPosts = await getAllPosts(false) + + return allPosts.map((post) => ({ + slug: post.slug, + })) +} + +export default async function PostPage({ + params, +}: { + params: { slug: string } +}) { + const { isEnabled } = draftMode() + const { post, morePosts } = await getPostAndMorePosts(params.slug, isEnabled) + + return ( +
+

+ + Blog + + . +

+
+

+ {post.title} +

+
+ {post.author && ( + + )} +
+
+ +
+
+
+ {post.author && ( + + )} +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ ) +} diff --git a/examples/cms-contentful/components/alert.js b/examples/cms-contentful/components/alert.js deleted file mode 100644 index 2994d59fbd32..000000000000 --- a/examples/cms-contentful/components/alert.js +++ /dev/null @@ -1,42 +0,0 @@ -import Container from './container' -import cn from 'classnames' -import { EXAMPLE_PATH } from '../lib/constants' - -export default function Alert({ preview }) { - return ( -
- -
- {preview ? ( - <> - This is page is a preview.{' '} - - Click here - {' '} - to exit preview mode. - - ) : ( - <> - The source code for this blog is{' '} - - available on GitHub - - . - - )} -
-
-
- ) -} diff --git a/examples/cms-contentful/components/avatar.js b/examples/cms-contentful/components/avatar.js deleted file mode 100644 index f242405068d6..000000000000 --- a/examples/cms-contentful/components/avatar.js +++ /dev/null @@ -1,17 +0,0 @@ -import ContentfulImage from './contentful-image' - -export default function Avatar({ name, picture }) { - return ( -
-
- -
-
{name}
-
- ) -} diff --git a/examples/cms-contentful/components/container.js b/examples/cms-contentful/components/container.js deleted file mode 100644 index fc1c29dfb074..000000000000 --- a/examples/cms-contentful/components/container.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Container({ children }) { - return
{children}
-} diff --git a/examples/cms-contentful/components/contentful-image.js b/examples/cms-contentful/components/contentful-image.js deleted file mode 100644 index a6d6e691f751..000000000000 --- a/examples/cms-contentful/components/contentful-image.js +++ /dev/null @@ -1,11 +0,0 @@ -import Image from 'next/image' - -const contentfulLoader = ({ src, width, quality }) => { - return `${src}?w=${width}&q=${quality || 75}` -} - -const ContentfulImage = (props) => { - return -} - -export default ContentfulImage diff --git a/examples/cms-contentful/components/header.js b/examples/cms-contentful/components/header.js deleted file mode 100644 index 05cb9af247f7..000000000000 --- a/examples/cms-contentful/components/header.js +++ /dev/null @@ -1,12 +0,0 @@ -import Link from 'next/link' - -export default function Header() { - return ( -

- - Blog - - . -

- ) -} diff --git a/examples/cms-contentful/components/hero-post.js b/examples/cms-contentful/components/hero-post.js deleted file mode 100644 index 6999cbb3a1c5..000000000000 --- a/examples/cms-contentful/components/hero-post.js +++ /dev/null @@ -1,37 +0,0 @@ -import Link from 'next/link' -import Avatar from '../components/avatar' -import DateComponent from '../components/date' -import CoverImage from '../components/cover-image' - -export default function HeroPost({ - title, - coverImage, - date, - excerpt, - author, - slug, -}) { - return ( -
-
- -
-
-
-

- - {title} - -

-
- -
-
-
-

{excerpt}

- {author && } -
-
-
- ) -} diff --git a/examples/cms-contentful/components/intro.js b/examples/cms-contentful/components/intro.js deleted file mode 100644 index 5931b3c5961b..000000000000 --- a/examples/cms-contentful/components/intro.js +++ /dev/null @@ -1,28 +0,0 @@ -import { CMS_NAME, CMS_URL } from '../lib/constants' - -export default function Intro() { - return ( -
-

- Blog. -

-

- A statically generated blog example using{' '} - - Next.js - {' '} - and{' '} - - {CMS_NAME} - - . -

-
- ) -} diff --git a/examples/cms-contentful/components/layout.js b/examples/cms-contentful/components/layout.js deleted file mode 100644 index 99d95353131e..000000000000 --- a/examples/cms-contentful/components/layout.js +++ /dev/null @@ -1,16 +0,0 @@ -import Alert from '../components/alert' -import Footer from '../components/footer' -import Meta from '../components/meta' - -export default function Layout({ preview, children }) { - return ( - <> - -
- -
{children}
-
-