first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
CONTENTFUL_SPACE_ID=
CONTENTFUL_ACCESS_TOKEN=
CONTENTFUL_PREVIEW_ACCESS_TOKEN=
CONTENTFUL_REVALIDATE_SECRET=

40
examples/cms-contentful/.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# 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

View File

@@ -0,0 +1,299 @@
# A statically generated blog example using Next.js and Contentful
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Contentful](https://www.contentful.com/) as the data source.
## Demo
### [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+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
- [AgilityCMS](/examples/cms-agilitycms)
- [Builder.io](/examples/cms-builder-io)
- [ButterCMS](/examples/cms-buttercms)
- [Contentful](/examples/cms-contentful)
- [Cosmic](/examples/cms-cosmic)
- [DatoCMS](/examples/cms-datocms)
- [DotCMS](/examples/cms-dotcms)
- [Drupal](/examples/cms-drupal)
- [Enterspeed](/examples/cms-enterspeed)
- [Ghost](/examples/cms-ghost)
- [GraphCMS](/examples/cms-graphcms)
- [Kontent.ai](/examples/cms-kontent-ai)
- [MakeSwift](/examples/cms-makeswift)
- [Payload](/examples/cms-payload)
- [Plasmic](/examples/cms-plasmic)
- [Prepr](/examples/cms-prepr)
- [Prismic](/examples/cms-prismic)
- [Sanity](/examples/cms-sanity)
- [Sitecore XM Cloud](/examples/cms-sitecore-xmcloud)
- [Sitefinity](/examples/cms-sitefinity)
- [Storyblok](/examples/cms-storyblok)
- [TakeShape](/examples/cms-takeshape)
- [Tina](/examples/cms-tina)
- [Umbraco](/examples/cms-umbraco)
- [Umbraco heartcore](/examples/cms-umbraco-heartcore)
- [Webiny](/examples/cms-webiny)
- [WordPress](/examples/cms-wordpress)
- [Blog Starter](/examples/blog-starter)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example cms-contentful cms-contentful-app
```
```bash
yarn create next-app --example cms-contentful cms-contentful-app
```
```bash
pnpm create next-app --example cms-contentful cms-contentful-app
```
## Configuration
### Step 1. Create an account and a space on Contentful
First, [create an account on Contentful](https://www.contentful.com/sign-up/).
After creating an account, create a new empty **space** from the [dashboard](https://app.contentful.com/) and assign to it any name of your liking.
### Step 2. Create a content model
The [content model](https://www.contentful.com/developers/docs/concepts/data-model/) defines the data structures of your application/websites. The structures are flexible and you can tailor them to your needs.
For this example you need to create a content model that defines an author and a post content type. **You can create these two by running a script or by doing it manually** to familiarize yourself with the Contentful user interface.
#### Run a script to create the content model
This project includes a setup script which you can use to set up the content model expected by the source code.
In your Contentful dashboard go to **Settings > General Settings** and copy the **Space ID**.
Next, go to **Settings > CMA tokens** and create a new token by clicking **Create personal access token**. This token has the same access rights as the logged in user. **Do not share it publicly**, you will only use it to set up your space and can delete it afterwards.
With the space ID and management access token at hand run the following command:
```
npx cross-env CONTENTFUL_SPACE_ID=YOUR_SPACE_ID CONTENTFUL_MANAGEMENT_TOKEN=XXX npm run setup
```
This command will create the needed content structures and set up your Contentful space ready to use. The output should look as follows:
```
> cms-contentful@1.0.0 setup /Users/stefan.judis/Projects/next.js/examples/cms-contentful
> node ./contentful/setup.js $CONTENTFUL_SPACE_ID $CONTENTFUL_MANAGEMENT_TOKEN
┌──────────────────────────────────────────────────┐
│ The following entities are going to be imported: │
├─────────────────────────────────┬────────────────┤
│ Content Types │ 2 │
├─────────────────────────────────┼────────────────┤
│ Editor Interfaces │ 2 │
├─────────────────────────────────┼────────────────┤
│ Locales │ 1 │
├─────────────────────────────────┼────────────────┤
│ Webhooks │ 0 │
├─────────────────────────────────┼────────────────┤
│ Entries │ 0 │
├─────────────────────────────────┼────────────────┤
│ Assets │ 0 │
└─────────────────────────────────┴────────────────┘
✔ Validating content-file
✔ Initialize client (1s)
✔ Checking if destination space already has any content and retrieving it (2s)
✔ Apply transformations to source data (1s)
✔ Push content to destination space
✔ Connecting to space (1s)
...
...
...
```
#### Create the content model manually
##### Create an `Author` content type
From your contentful space, go to **Content model** and add a new content type:
- Give it the **Name** `Author`, the **Api Identifier** should be `author`
Once the content model is saved, add these fields (you don't have to modify the settings unless specified):
- `name` - **Text** field (type **short text**). **Field ID** should be set to `name`
- `picture` - **Media** field (type **one file**). **Field ID** should be set to `picture`
Save the content type and continue.
##### Create a `post` type
From your contentful space, go to **Content model** and add another content type:
- Give it the **Name** `Post`, the **Api Identifier** should be `post`
Next, add these fields (you don't have to modify the settings unless specified):
- `title` - **Text** field (type **short text**)
- `content` - **Rich text** field
- `excerpt` - **Text** field (type **Long text, full-text search**)
- `coverImage` - **Media** field (type **one file**)
- `date` - **Date and time** field
- `slug` - **Text** field. You can optionally go to the settings of this field, and under **Appearance**, select **Slug** to display it as a slug of the `title` field.
- `author` - **Reference** field (type **one reference**)
Save the content type and continue.
### Step 3. Validate your content model
After setting up the content model (either manually or by running `npm run setup` or `yarn setup`), it should look as follows.
**Content model overview**
![Content model overview](https://github.com/vercel/next.js/assets/9113740/d3f76907-7046-4d94-b285-eb89b87aa223)
### Step 4. Populate Content
Go to the **Content** section in your space, then click on **Add entry** and select the **Author** content type:
- You just need **1 author entry**.
- Use dummy data for the text.
- For the image, you can download one from [Unsplash](https://unsplash.com/).
Next, create another entry with the content type **Post**:
- We recommend creating at least **2 post entries**.
- Use dummy data for the text.
- For images, you can download them from [Unsplash](https://unsplash.com/).
- Pick the **author** you created earlier.
**Important:** For each entry and asset, you need to click on **Publish**. If not, the entry will be in draft state.
![Published content entry](https://github.com/vercel/next.js/assets/9113740/e1b4a3fe-45f4-4851-91db-8908d3ca18e9)
### Step 5. Set up environment variables
From your contentful space, go to **Settings > API keys**. There will be an example Content delivery / preview token - you can use these API keys. (You may also create a new key.)
Next, copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
```bash
cp .env.local.example .env.local
```
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 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:
```bash
CONTENTFUL_SPACE_ID=...
CONTENTFUL_ACCESS_TOKEN=...
CONTENTFUL_PREVIEW_ACCESS_TOKEN=...
CONTENTFUL_PREVIEW_SECRET=...
CONTENTFUL_REVALIDATE_SECRET=...
```
### Step 6. Run Next.js in development mode
```bash
npm install
npm run dev
# or
yarn install
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 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/draft?secret=<CONTENTFUL_PREVIEW_SECRET>&slug={entry.fields.slug}
```
Replace `<CONTENTFUL_PREVIEW_SECRET>` with its respective value in `.env.local`.
![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:
- **Update the title**. For example, you can add `[Draft]` in front of the title.
- 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](https://github.com/vercel/next.js/assets/9113740/cc0dff9a-c57e-4ec4-85f1-22ab74af2b6b)
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
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
#### Deploy Your Local Project
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example).
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
#### Deploy from Our Template
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 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+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
In your Contentful space, go to **Settings > Webhooks** and add a new webhook:
- **Give the webhook a name**
- **Activate:** Check the activate checkbox to ensure the webhook is marked as active
- **Specify the POST URL:** Using the URL from your Vercel deployment in step 8, add the path `/api/revalidate` at the end, so it would look something like:
```
https://<YOUR_VERCEL_DEPLOYMENT_URL>/api/revalidate
```
Replace `<YOUR_VERCEL_DEPLOYMENT_URL>` with your own deployment URL as noted in the Vercel dashboard.
- **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](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](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](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](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](https://github.com/vercel/next.js/assets/9113740/ed1ffbe9-4dbf-4ec6-9c1f-39c8949c4d38)

View File

@@ -0,0 +1,6 @@
import { draftMode } from "next/headers";
export async function GET(request: Request) {
draftMode().disable();
return new Response("Draft mode is disabled");
}

View File

@@ -0,0 +1,2 @@
//@ts-ignore
export { enableDraftHandler as GET } from "@contentful/vercel-nextjs-toolkit/app-router";

View File

@@ -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() });
}

View File

@@ -0,0 +1,24 @@
import ContentfulImage from "@/lib/contentful-image";
export default function Avatar({
name,
picture,
}: {
name: string;
picture: any;
}) {
return (
<div className="flex items-center">
<div className="mr-4 w-12 h-12">
<ContentfulImage
alt={name}
className="object-cover h-full rounded-full"
height={48}
width={48}
src={picture.url}
/>
</div>
<div className="text-xl font-bold">{name}</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import ContentfulImage from "../lib/contentful-image";
import Link from "next/link";
function cn(...classes: any[]) {
return classes.filter(Boolean).join(" ");
}
export default function CoverImage({
title,
url,
slug,
}: {
title: string;
url: string;
slug?: string;
}) {
const image = (
<ContentfulImage
alt={`Cover Image for ${title}`}
priority
width={2000}
height={1000}
className={cn("shadow-small", {
"hover:shadow-medium transition-shadow duration-200": slug,
})}
src={url}
/>
);
return (
<div className="sm:mx-0">
{slug ? (
<Link href={`/posts/${slug}`} aria-label={title}>
{image}
</Link>
) : (
image
)}
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { format } from "date-fns";
export default function DateComponent({ dateString }: { dateString: string }) {
return (
<time dateTime={dateString}>
{format(new Date(dateString), "LLLL d, yyyy")}
</time>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,59 @@
import "./globals.css";
import { Inter } from "next/font/google";
import { EXAMPLE_PATH, CMS_NAME } from "@/lib/constants";
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 (
<footer className="bg-accent-1 border-t border-accent-2">
<div className="container mx-auto px-5">
<div className="py-28 flex flex-col lg:flex-row items-center">
<h3 className="text-4xl lg:text-5xl font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
Built with Next.js.
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://nextjs.org/docs"
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
>
Read Documentation
</a>
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</div>
</footer>
);
}
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.variable}>
<body>
<section className="min-h-screen">
<main>{children}</main>
<Footer />
</section>
</body>
</html>
);
}

View File

@@ -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 (
<div>
<div className="mb-5">
<CoverImage title={title} slug={slug} url={coverImage.url} />
</div>
<h3 className="text-3xl mb-3 leading-snug">
<Link href={`/posts/${slug}`} className="hover:underline">
{title}
</Link>
</h3>
<div className="text-lg mb-4">
<DateComponent dateString={date} />
</div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
);
}
export default function MoreStories({ morePosts }: { morePosts: any[] }) {
return (
<section>
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
More Stories
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
{morePosts.map((post) => (
<PostPreview
key={post.slug}
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
slug={post.slug}
excerpt={post.excerpt}
/>
))}
</div>
</section>
);
}

View File

@@ -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 (
<section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12">
<h1 className="text-6xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8">
Blog.
</h1>
<h2 className="text-center md:text-left text-lg mt-5 md:pl-8">
A statically generated blog example using{" "}
<a
href="https://nextjs.org/"
className="underline hover:text-success duration-200 transition-colors"
>
Next.js
</a>{" "}
and{" "}
<a
href={CMS_URL}
className="underline hover:text-success duration-200 transition-colors"
>
{CMS_NAME}
</a>
.
</h2>
</section>
);
}
function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}: {
title: string;
coverImage: any;
date: string;
excerpt: string;
author: any;
slug: string;
}) {
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage title={title} slug={slug} url={coverImage.url} />
</div>
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
<Link href={`/posts/${slug}`} className="hover:underline">
{title}
</Link>
</h3>
<div className="mb-4 md:mb-0 text-lg">
<Date dateString={date} />
</div>
</div>
<div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
</div>
</section>
);
}
export default async function Page() {
const { isEnabled } = draftMode();
const allPosts = await getAllPosts(isEnabled);
const heroPost = allPosts[0];
const morePosts = allPosts.slice(1);
return (
<div className="container mx-auto px-5">
<Intro />
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.coverImage}
date={heroPost.date}
author={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
)}
<MoreStories morePosts={morePosts} />
</div>
);
}

View File

@@ -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 (
<div className="container mx-auto px-5">
<h2 className="mb-20 mt-8 text-2xl font-bold leading-tight tracking-tight md:text-4xl md:tracking-tighter">
<Link href="/" className="hover:underline">
Blog
</Link>
.
</h2>
<article>
<h1 className="mb-12 text-center text-6xl font-bold leading-tight tracking-tighter md:text-left md:text-7xl md:leading-none lg:text-8xl">
{post.title}
</h1>
<div className="hidden md:mb-12 md:block">
{post.author && (
<Avatar name={post.author.name} picture={post.author.picture} />
)}
</div>
<div className="mb-8 sm:mx-0 md:mb-16">
<CoverImage title={post.title} url={post.coverImage.url} />
</div>
<div className="mx-auto max-w-2xl">
<div className="mb-6 block md:hidden">
{post.author && (
<Avatar name={post.author.name} picture={post.author.picture} />
)}
</div>
<div className="mb-6 text-lg">
<Date dateString={post.date} />
</div>
</div>
<div className="mx-auto max-w-2xl">
<div className="prose">
<Markdown content={post.content} />
</div>
</div>
</article>
<hr className="border-accent-2 mt-28 mb-24" />
<MoreStories morePosts={morePosts} />
</div>
);
}

View File

@@ -0,0 +1,120 @@
const POST_GRAPHQL_FIELDS = `
slug
title
coverImage {
url
}
date
author {
name
picture {
url
}
}
excerpt
content {
json
links {
assets {
block {
sys {
id
}
url
description
}
}
}
}
`;
async function fetchGraphQL(query: string, preview = false): Promise<any> {
return fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${
preview
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
},
body: JSON.stringify({ query }),
next: { tags: ["posts"] },
},
).then((response) => response.json());
}
function extractPost(fetchResponse: any): any {
return fetchResponse?.data?.postCollection?.items?.[0];
}
function extractPostEntries(fetchResponse: any): any[] {
return fetchResponse?.data?.postCollection?.items;
}
export async function getPreviewPostBySlug(slug: string | null): Promise<any> {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: true, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
true,
);
return extractPost(entry);
}
export async function getAllPosts(isDraftMode: boolean): Promise<any[]> {
const entries = await fetchGraphQL(
`query {
postCollection(where: { slug_exists: true }, order: date_DESC, preview: ${
isDraftMode ? "true" : "false"
}) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
isDraftMode,
);
return extractPostEntries(entries);
}
export async function getPostAndMorePosts(
slug: string,
preview: boolean,
): Promise<any> {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: ${
preview ? "true" : "false"
}, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
preview,
);
const entries = await fetchGraphQL(
`query {
postCollection(where: { slug_not_in: "${slug}" }, order: date_DESC, preview: ${
preview ? "true" : "false"
}, limit: 2) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
preview,
);
return {
post: extractPost(entry),
morePosts: extractPostEntries(entries),
};
}

View File

@@ -0,0 +1,3 @@
export const EXAMPLE_PATH = "cms-contentful";
export const CMS_NAME = "Contentful";
export const CMS_URL = "https://www.contentful.com";

View File

@@ -0,0 +1,18 @@
"use client";
import Image from "next/image";
interface ContentfulImageProps {
src: string;
width?: number;
quality?: number;
[key: string]: any; // For other props that might be passed
}
const contentfulLoader = ({ src, width, quality }: ContentfulImageProps) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
export default function ContentfulImage(props: ContentfulImageProps) {
return <Image alt={props.alt} loader={contentfulLoader} {...props} />;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import Image from "next/image";
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import { BLOCKS } from "@contentful/rich-text-types";
interface Asset {
sys: {
id: string;
};
url: string;
description: string;
}
interface AssetLink {
block: Asset[];
}
interface Content {
json: any;
links: {
assets: AssetLink;
};
}
function RichTextAsset({
id,
assets,
}: {
id: string;
assets: Asset[] | undefined;
}) {
const asset = assets?.find((asset) => asset.sys.id === id);
if (asset?.url) {
return <Image src={asset.url} layout="fill" alt={asset.description} />;
}
return null;
}
export function Markdown({ content }: { content: Content }) {
return documentToReactComponents(content.json, {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node: any) => (
<RichTextAsset
id={node.data.target.sys.id}
assets={content.links.assets.block}
/>
),
},
});
}

View File

@@ -0,0 +1,22 @@
const spaceImport = require("contentful-import");
const exportFile = require("./export.json");
const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN } = process.env;
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
throw new Error(
[
"Parameters missing...",
"Please run the setup command as follows",
"CONTENTFUL_SPACE_ID=XXX CONTENTFUL_MANAGEMENT_TOKEN=CFPAT-XXX npm run setup",
].join("\n"),
);
}
spaceImport({
spaceId: CONTENTFUL_SPACE_ID,
managementToken: CONTENTFUL_MANAGEMENT_TOKEN,
content: exportFile,
})
.then(() => console.log("The content model of your space is set up!"))
.catch((e) => console.error(e));

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
module.exports = {
images: {
loader: "custom",
},
};

View File

@@ -0,0 +1,27 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"setup": "node ./lib/setup.js"
},
"dependencies": {
"@contentful/rich-text-react-renderer": "^15.17.1",
"@contentful/rich-text-types": "^16.2.1",
"@contentful/vercel-nextjs-toolkit": "latest",
"@tailwindcss/typography": "0.5.9",
"@types/node": "^20.5.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"autoprefixer": "10.4.15",
"contentful-import": "^9.0.4",
"date-fns": "2.30.0",
"next": "latest",
"postcss": "8.4.31",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.1.6"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,21 @@
import type { Config } from "tailwindcss";
import typography from "@tailwindcss/typography";
export default {
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./lib/**/*.{ts,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-inter)"],
},
},
},
future: {
hoverOnlyWhenSupported: true,
},
plugins: [typography],
} satisfies Config;

View File

@@ -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": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}