diff --git a/examples/cms-contentful/app/posts/[slug]/page.tsx b/examples/cms-contentful/app/posts/[slug]/page.tsx index 02fc7f581875e2..2fb00f709ccbd3 100644 --- a/examples/cms-contentful/app/posts/[slug]/page.tsx +++ b/examples/cms-contentful/app/posts/[slug]/page.tsx @@ -27,26 +27,26 @@ export default async function PostPage({ return (
-

+

Blog .

-

+

{post.title}

-
+
{post.author && ( )}
-
+
-
-
+
+
{post.author && ( )} @@ -56,7 +56,7 @@ export default async function PostPage({
-
+
diff --git a/examples/cms-sanity/.env.local.example b/examples/cms-sanity/.env.local.example index 6b24096d5f9dfe..8980898bdb9ca0 100644 --- a/examples/cms-sanity/.env.local.example +++ b/examples/cms-sanity/.env.local.example @@ -1,4 +1,4 @@ +# https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= SANITY_API_READ_TOKEN= -SANITY_REVALIDATE_SECRET= diff --git a/examples/cms-sanity/.eslintrc b/examples/cms-sanity/.eslintrc new file mode 100644 index 00000000000000..7c1a3addbd42d0 --- /dev/null +++ b/examples/cms-sanity/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "next/core-web-vitals", + "root": true +} diff --git a/examples/cms-sanity/.gitignore b/examples/cms-sanity/.gitignore index 8206c2b32acdad..4719373ef79450 100644 --- a/examples/cms-sanity/.gitignore +++ b/examples/cms-sanity/.gitignore @@ -39,4 +39,4 @@ next-env.d.ts # Env files created by scripts for working locally .env -studio/.env.development \ No newline at end of file +.env.local \ No newline at end of file diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index 9042b556ff080d..4ab6e5a4a16177 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -1,109 +1,167 @@ # A statically generated blog example using Next.js and Sanity -This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Sanity](https://www.sanity.io/) as the data source. +This starter is a statically generated blog that uses Next.js App Router for the frontend and [Sanity][sanity-homepage] to handle its content. It comes with a native Sanity Studio that offers features like real-time collaboration and visual editing with live updates using [Presentation][presentation]. -You'll get: +The Studio connects to Sanity Content Lake, which gives you hosted content APIs with a flexible query language, on-demand image transformations, powerful patching, and more. You can use this starter to kick-start a blog or learn these technologies. -- Next.js deployed with the [Sanity Vercel Integration][integration]. -- Sanity Studio running on localhost and deployed in the [cloud](https://www.sanity.io/docs/deployment). -- Sub-second as-you-type previews in Next.js -- [On-demand revalidation of pages](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with [GROQ powered webhooks](https://www.sanity.io/docs/webhooks) +## Features + +- A performant, static blog with editable posts, authors, and site settings +- A native and customizable authoring environment, accessible on `yourblog.com/studio` +- Real-time and collaborative content editing with fine-grained revision history +- Side-by-side instant content preview that works across your whole site +- Support for block content and the most advanced custom fields capability in the industry +- Incremental Static Revalidation; no need to wait for a rebuild to publish new content +- Unsplash and Youtube integrations setup for easy media management +- [Sanity AI Assist preconfigured for image alt text generation](https://www.sanity.io/docs/ai-assist?utm_source=github.com&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=) +- Out of the box support for [Vercel Visual Editing](https://www.sanity.io/blog/visual-editing-sanity-vercel?utm_source=github.com&utm_medium=referral&utm_campaign=may-vercel-launch). +- A project with starter-friendly and not too heavy-handed TypeScript and Tailwind.css ## Demo -### [https://next-blog-sanity.vercel.app](https://next-blog-sanity.vercel.app) +### [https://next-blog.sanity.build](https://next-blog.sanity.build) -## Related examples +## Deploy your own -- [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](/examples/cms-kontent-ai) -- [Prepr](/examples/cms-prepr) -- [Prismic](/examples/cms-prismic) -- [Sanity](/examples/cms-sanity) -- [Sitefinity](/examples/cms-sitefinity) -- [Storyblok](/examples/cms-storyblok) -- [TakeShape](/examples/cms-takeshape) -- [Umbraco heartcore](/examples/cms-umbraco-heartcore) -- [Webiny](/examples/cms-webiny) -- [Blog Starter](/examples/blog-starter) -- [WordPress](/examples/cms-wordpress) +Use the Deploy Button below, you'll deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) as well as connect it to your Sanity dataset using [the Sanity Vercel Integration][integration]. + +[![Deploy with Vercel](https://vercel.com/button)][vercel-deploy] + +## 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-sanity next-sanity-blog +``` + +```bash +yarn create next-app --example cms-sanity next-sanity-blog +``` + +```bash +pnpm create next-app --example cms-sanity next-sanity-blog +``` # Configuration - [Step 1. Set up the environment](#step-1-set-up-the-environment) -- [Step 2. Run Next.js locally in development mode](#step-3-run-nextjs-locally-in-development-mode) + - [Reuse remote envionment variables](#reuse-remote-envionment-variables) + - [Using the Sanity CLI](#using-the-sanity-cli) + - [Creating a read token](#creating-a-read-token) +- [Step 2. Run Next.js locally in development mode](#step-2-run-nextjs-locally-in-development-mode) - [Step 3. Populate content](#step-3-populate-content) -- [Step 4. Deploy to production & use Preview Mode from anywhere](#step-4-deploy-to-production--use-preview-mode-from-anywhere) - - [If you didn't Deploy with Vercel earlier do so now](#if-you-didnt-deploy-with-vercel-earlier-do-so-now) - - [Configure CORS for production](#configure-cors-for-production) - - [Add the preview secret environment variable](#add-the-preview-secret-environment-variable) - - [How to test locally that the secret is setup correctly](#how-to-test-locally-that-the-secret-is-setup-correctly) - - [How to start Preview Mode for Next.js in production from a local Studio](#how-to-start-preview-mode-for-nextjs-in-production-from-a-local-studio) - - [If you regret sending a preview link to someone](#if-you-regret-sending-a-preview-link-to-someone) -- [Step 5. Deploy your Studio and publish from anywhere](#step-5-deploy-your-studio-and-publish-from-anywhere) -- [Step 6. Setup Revalidation Webhook](#step-6-setup-revalidation-webhook) - - [Testing the Webhook](#testing-the-webhook) +- [Step 4. Deploy to production](#step-4-deploy-to-production) - [Next steps](#next-steps) ## Step 1. Set up the environment -Use the Deploy Button below, you'll deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) as well as connect it to your Sanity dataset using [the Sanity Vercel Integration][integration]. - -[![Deploy with Vercel](https://vercel.com/button)][vercel-deploy] +### Reuse remote envionment variables -[Clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) that Vercel created for you and from the root directory of your local checkout. -Then link your clone to Vercel: +If you started with [deploying your own](#deploy-your-own) then you can run this to reuse the environment variables from the Vercel project and skip to the next step: ```bash npx vercel link +npx vercel pull ``` -Download the environment variables needed to connect Next.js and Studio to your Sanity project: +### Using the Sanity CLI + +Copy the `.env.local.example` file to `.env.local` to get started: ```bash -npx vercel env pull +cp -i .env.local.example .env.local ``` -## Step 2. Run Next.js locally in development mode +Run the setup command to get setup with a Sanity project, dataset and their relevant environment variables: ```bash -npm install && npm run dev +npm run setup ``` ```bash -yarn install && yarn dev +yarn setup ``` -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). +```bash +pnpm run setup +``` + +You'll be asked multiple questions, here's a sample output of what you can expect: + +```bash +Need to install the following packages: +sanity@3.30.1 +Ok to proceed? (y) y +You're setting up a new project! +We'll make sure you have an account with Sanity.io. +Press ctrl + C at any time to quit. + +Prefer web interfaces to terminals? +You can also set up best practice Sanity projects with +your favorite frontends on https://www.sanity.io/templates + +Looks like you already have a Sanity-account. Sweet! + +✔ Fetching existing projects +? Select project to use Templates [r0z1eifg] +? Select dataset to use blog-vercel +? Would you like to add configuration files for a Sanity project in this Next.js folder? No + +Detected framework Next.js, using prefix 'NEXT_PUBLIC_' +Found existing NEXT_PUBLIC_SANITY_PROJECT_ID, replacing value. +Found existing NEXT_PUBLIC_SANITY_DATASET, replacing value. +``` -Note: This also installs dependencies for Sanity Studio as a post-install step. +It's important that when you're asked `Would you like to add configuration files for a Sanity project in this Next.js folder?` that you answer `No` as this example is alredy setup with the required configuration files. -## Step 4. Populate content +#### Creating a read token -In another terminal start up the studio: +This far your `.env.local` file should have values for `NEXT_PUBLIC_SANITY_PROJECT_ID` and `NEXT_PUBLIC_SANITY_DATASET`. +Before you can run the project you need to setup a read token (`SANITY_API_READ_TOKEN`), it's used for authentication when Sanity Studio is live previewing your application. + +1. Go to [manage.sanity.io](https://manage.sanity.io/) and select your project. +2. Click on the `🔌 API` tab. +3. Click on `+ Add API token`. +4. Name it "next blog live preview read token" and set `Permissions` to `Viewer` and hit `Save`. +5. Copy the token and add it to your `.env.local` file. ```bash -npm run studio:dev +SANITY_API_READ_TOKEN="" ``` -Your studio should be up and running on [http://localhost:3333](http://localhost:3333)! +Your `.env.local` file should look something like this: + +```bash +NEXT_PUBLIC_SANITY_PROJECT_ID="r0z1eifg" +NEXT_PUBLIC_SANITY_DATASET="blog-vercel" +SANITY_API_READ_TOKEN="sk..." +``` -### Create content +> [!CAUTION] +> Make sure to add `.env.local` to your `.gitignore` file so you don't accidentally commit it to your repository. -Create content in Sanity Studio and live preview it in Next.js, side-by-side, by opening these URLs: +## Step 2. Run Next.js locally in development mode -- [`http://localhost:3333`](http://localhost:3333) -- [`http://localhost:3000/api/preview`](http://localhost:3000/api/preview) +```bash +npm install && npm run dev +``` + +```bash +yarn install && yarn dev +``` + +```bash +pnpm install && pnpm 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 3. Populate content + +Open your Sanity Studio that should be running on [http://localhost:3000/studio](http://localhost:3000/studio). + +By default you're taken to the [Presentation tool][presentation], which has a preview of the blog on the left hand side, and a list of documents on the right hand side.
View screenshot ✨ @@ -145,17 +203,18 @@ We're all set to do some content creation! - Create a couple more **Posts** and watch how the layout adapt to more content. -**Important:** For each post record, you need to click **Publish** after saving for it to be visible outside Preview Mode. +> [!IMPORTANT] +> For each post record, you need to click **Publish** after saving for it to be visible outside Draft Mode. In production new content is using [Time-based Revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#time-based-revalidation), which means it may take up to 1 minute before changes show up. Since a stale-while-revalidate pattern is used you may need to refresh a couple of times to see the changes. -To exit Preview Mode, you can click on _"Click here to exit preview mode"_ at the top. +## Step 4. Deploy to production -## Step 4. Deploy to production & use Preview Mode from anywhere - -### If you didn't [Deploy with Vercel earlier](#step-1-set-up-the-environment) do so now +> [!NOTE] +> If you already [deployed with Vercel earlier](#deploy-your-own) you can skip this step. To deploy your local project to Vercel, push it to [GitHub](https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-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. +> [!IMPORTANT] +> When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. After it's deployed link your local code to the Vercel project: @@ -163,173 +222,42 @@ After it's deployed link your local code to the Vercel project: npx vercel link ``` -### Configure CORS for production - -Add your `production url` to the list over CORS origins. - -
-Don't remember the production url? 🤔 - -No worries, it's easy to find out. Go to your [Vercel Dashboard](https://vercel.com/) and click on your project: - -![screenshot](https://user-images.githubusercontent.com/81981/183002637-6aa6b1d8-e0ee-4a9b-bcc0-d49799fcc984.png) - -In the screenshot above the `production url` is `https://cms-sanity.vercel.app`. - -
- -```bash -npm --prefix studio run cors:add -- [your production url] --credentials -``` - -### Add the preview secret environment variable - -It's required to set a secret that makes Preview Mode activation links unique. Otherwise anyone could see your unpublished content by just opening `[your production url]/api/preview`. -Run this and it'll prompt you for a value: - -```bash -npx vercel env add SANITY_STUDIO_PREVIEW_SECRET -``` - -The secret can be any combination of random words and letters as long as it's URL safe. -You can generate one in your DevTools console using `copy(Math.random().toString(36).substr(2, 10))` if you don't feel like inventing one. - -You should see something like this in your terminal afterwards: - -```bash -$ npx vercel env add SANITY_STUDIO_PREVIEW_SECRET -Vercel CLI 27.3.7 -? What’s the value of SANITY_STUDIO_PREVIEW_SECRET? 2whpu1jefs -? Add SANITY_STUDIO_PREVIEW_SECRET to which Environments (select multiple)? Production, Preview, Development -✅ Added Environment Variable SANITY_STUDIO_PREVIEW_SECRET to Project cms-sanity [1s] -``` - -Redeploy production to apply the secret to the preview api: - -```bash -npx vercel --prod -``` - -After it deploys it should now start preview mode if you launch `[your production url]/api/preview?secret=[your preview secret]`. You can send that preview url to people you want to show the content you're working on before you publish it. - -### How to test locally that the secret is setup correctly - -In order to test that the secret will prevent unauthorized people from activating preview mode, start by updating the local `.env` with the secret you just made: - -```bash -npx vercel env pull -``` - -Restart your Next.js and Studio processes so the secret is applied: - -```bash -npm run dev -``` - -```bash -npm run studio:dev -``` - -And now you'll get an error if `[secret]` is incorrect when you try to open `https://localhost:3000/api/preview?secret=[secret]`. - -### How to start Preview Mode for Next.js in production from a local Studio - -Run this to make the Studio open previews at `[your production url]/api/preview` instead of `http://localhost:3000/api/preview` - -```bash -SANITY_STUDIO_PREVIEW_URL=[your production url] npm run studio:dev -``` - -### If you regret sending a preview link to someone - -Revoke their access by creating a new secret: - -```bash -npx vercel env rm SANITY_STUDIO_PREVIEW_SECRET -npx vercel env add SANITY_STUDIO_PREVIEW_SECRET -npx vercel --prod -``` - -## Step 5. Deploy your Studio and publish from anywhere - -Live previewing content is fun, but collaborating on content in real-time is next-level: - -```bash -SANITY_STUDIO_PREVIEW_URL=[your production url] npm run studio:deploy -``` - -If it's successful you should see something like this in your terminal: - -```bash -SANITY_STUDIO_PREVIEW_URL="https://cms-sanity.vercel.app" npm run studio:deploy -? Studio hostname (.sanity.studio): cms-sanity - -Including the following environment variables as part of the JavaScript bundle: -- SANITY_STUDIO_PREVIEW_URL -- SANITY_STUDIO_PREVIEW_SECRET -- SANITY_STUDIO_API_PROJECT_ID -- SANITY_STUDIO_API_DATASET - -✔ Deploying to Sanity.Studio - -Success! Studio deployed to https://cms-sanity.sanity.studio/ -``` - -This snippet is stripped from verbose information, you'll see a lot of extra stuff in your terminal. - -## Step 6. Setup Revalidation Webhook - -Using GROQ Webhooks Next.js can rebuild pages that have changed content. It rebuilds so fast it can almost compete with Preview Mode. - -Create a secret and give it a value the same way you did for `SANITY_STUDIO_PREVIEW_SECRET` in [Step 4](#add-the-preview-secret-environment-variable). It's used to verify that webhook payloads came from Sanity infra, and set it as the value for `SANITY_REVALIDATE_SECRET`: - -```bash -npx vercel env add SANITY_REVALIDATE_SECRET -``` - -You should see something like this in your terminal afterwards: - -```bash -$ npx vercel env add SANITY_REVALIDATE_SECRET -Vercel CLI 27.3.7 -? What’s the value of SANITY_REVALIDATE_SECRET? jwh3nr85ft -? Add SANITY_REVALIDATE_SECRET to which Environments (select multiple)? Production, Preview, Development -✅ Added Environment Variable SANITY_REVALIDATE_SECRET to Project cms-sanity [1s] -``` - -Apply the secret to production: - -```bash -npx vercel --prod -``` - -Wormhole into the [manager](https://manage.sanity.io/) by running: - -```bash -(cd studio && npx sanity hook create) -``` - -- **Name** it "On-demand Revalidation". -- Set the **URL** to`[your production url]/api/revalidate`, for example: `https://cms-sanity.vercel.app/api/revalidate` -- Set the **Trigger on** field to -- Set the **Filter** to `_type == "post" || _type == "author"` -- Set the **Secret** to the same value you gave `SANITY_REVALIDATE_SECRET` earlier. -- Hit **Save**! - -### Testing the Webhook - -- Open the Deployment function log. (**Vercel Dashboard > Deployment > Functions** and filter by `api/revalidate`) -- Edit a Post in your Sanity Studio and publish. -- The log should start showing calls. -- And the published changes show up on the site after you reload. +> [!TIP] +> In production you can exit Draft Mode by clickin on _"Click here to exit draft mode"_ at the top. On [Preview deployments](https://vercel.com/docs/deployments/preview-deployments) you can [toggle Draft Mode in the Vercel Toolbar](https://vercel.com/docs/workflow-collaboration/draft-mode#enabling-draft-mode-in-the-vercel-toolbar). ## Next steps -- Mount your preview inside the Sanity Studio for comfortable side-by-side editing +- [Enable AI Assist](https://www.sanity.io/docs/install-and-configure-sanity-ai-assist#0a0e0c8adce9) to have [image alt text automatically generated](https://www.sanity.io/docs/install-and-configure-sanity-ai-assist?utm_source=github.com&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=#a65bfa29260c), [and many more powerful features](https://www.sanity.io/blog/sanity-ai-assist-announcement). - [Join the Sanity community](https://slack.sanity.io/) +## 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](/examples/cms-kontent-ai) +- [Prepr](/examples/cms-prepr) +- [Prismic](/examples/cms-prismic) +- [Sanity](/examples/cms-sanity) +- [Sitefinity](/examples/cms-sitefinity) +- [Storyblok](/examples/cms-storyblok) +- [TakeShape](/examples/cms-takeshape) +- [Umbraco heartcore](/examples/cms-umbraco-heartcore) +- [Webiny](/examples/cms-webiny) +- [Blog Starter](/examples/blog-starter) +- [WordPress](/examples/cms-wordpress) + [vercel-deploy]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-sanity&repository-name=cms-sanity&project-name=cms-sanity&demo-title=Blog%20using%20Next.js%20%26%20Sanity&demo-description=On-demand%20ISR%2C%20sub-second%20as-you-type%20previews&demo-url=https%3A%2F%2Fnext-blog-sanity.vercel.app%2F&demo-image=https%3A%2F%2Fuser-images.githubusercontent.com%2F110497645%2F182727236-75c02b1b-faed-4ae2-99ce-baa089f7f363.png&integration-ids=oac_hb2LITYajhRQ0i4QznmKH7gx [integration]: https://www.sanity.io/docs/vercel-integration -[`sanity.json`]: studio/sanity.json [`.env.local.example`]: .env.local.example [unsplash]: https://unsplash.com +[sanity-homepage]: https://www.sanity.io?utm_source=github.com&utm_medium=referral&utm_campaign=nextjs-v3vercelstarter +[presentation]: https://www.sanity.io/docs/presentation diff --git a/examples/cms-sanity/app/(blog)/actions.ts b/examples/cms-sanity/app/(blog)/actions.ts new file mode 100644 index 00000000000000..7b5cc9ee80a92c --- /dev/null +++ b/examples/cms-sanity/app/(blog)/actions.ts @@ -0,0 +1,12 @@ +"use server"; + +import { draftMode } from "next/headers"; + +export async function disableDraftMode() { + "use server"; + await Promise.allSettled([ + draftMode().disable(), + // Simulate a delay to show the loading state + new Promise((resolve) => setTimeout(resolve, 1000)), + ]); +} diff --git a/examples/cms-sanity/app/(blog)/alert-banner.tsx b/examples/cms-sanity/app/(blog)/alert-banner.tsx new file mode 100644 index 00000000000000..0e31c7669cf6fb --- /dev/null +++ b/examples/cms-sanity/app/(blog)/alert-banner.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useSyncExternalStore, useTransition } from "react"; + +import { disableDraftMode } from "./actions"; + +export default function AlertBanner() { + const router = useRouter(); + const [pending, startTransition] = useTransition(); + + const shouldShow = useSyncExternalStore( + subscribe, + () => window.top === window, + () => false, + ); + + if (!shouldShow) return null; + + return ( +
+
+ {pending ? ( + "Disabling draft mode..." + ) : ( + <> + {"Previewing drafts. "} + + + )} +
+
+ ); +} + +const subscribe = () => () => {}; diff --git a/examples/cms-sanity/app/(blog)/avatar.tsx b/examples/cms-sanity/app/(blog)/avatar.tsx new file mode 100644 index 00000000000000..5aa6421d01ea4e --- /dev/null +++ b/examples/cms-sanity/app/(blog)/avatar.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; + +import { Author } from "@/sanity/lib/queries"; +import { urlForImage } from "@/sanity/lib/utils"; + +export default function Avatar({ name, picture }: Author) { + return ( +
+ {picture?.asset?._ref ? ( +
+ {picture?.alt +
+ ) : ( +
By
+ )} +
{name}
+
+ ); +} diff --git a/examples/cms-sanity/app/(blog)/cover-image.tsx b/examples/cms-sanity/app/(blog)/cover-image.tsx new file mode 100644 index 00000000000000..650bad350c0606 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/cover-image.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; + +import { urlForImage } from "@/sanity/lib/utils"; + +interface CoverImageProps { + image: any; + priority?: boolean; +} + +export default function CoverImage(props: CoverImageProps) { + const { image: source, priority } = props; + const image = source?.asset?._ref ? ( + {source?.alt + ) : ( +
+ ); + + return ( +
+ {image} +
+ ); +} diff --git a/examples/cms-sanity/app/(blog)/date.tsx b/examples/cms-sanity/app/(blog)/date.tsx new file mode 100644 index 00000000000000..2ca5b6190a08f0 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/date.tsx @@ -0,0 +1,9 @@ +import { format } from "date-fns"; + +export default function DateComponent({ dateString }: { dateString: string }) { + return ( + + ); +} diff --git a/examples/cms-sanity/app/(blog)/layout.tsx b/examples/cms-sanity/app/(blog)/layout.tsx new file mode 100644 index 00000000000000..6f1d4d50920aea --- /dev/null +++ b/examples/cms-sanity/app/(blog)/layout.tsx @@ -0,0 +1,117 @@ +import "../globals.css"; + +import { toPlainText } from "@portabletext/react"; +import type { PortableTextBlock } from "@portabletext/types"; +import { SpeedInsights } from "@vercel/speed-insights/next"; +import { Metadata } from "next"; +import { VisualEditing } from "next-sanity"; +import { Inter } from "next/font/google"; +import { draftMode } from "next/headers"; +import { Suspense } from "react"; + +import AlertBanner from "./alert-banner"; +import PortableText from "./portable-text"; + +import * as demo from "@/sanity/lib/demo"; +import { sanityFetch } from "@/sanity/lib/fetch"; +import { SettingsQueryResponse, settingsQuery } from "@/sanity/lib/queries"; +import { resolveOpenGraphImage } from "@/sanity/lib/utils"; + +export async function generateMetadata(): Promise { + const settings = await sanityFetch({ + query: settingsQuery, + // Metadata should never contain stega + stega: false, + }); + const title = settings?.title || demo.title; + const description = settings?.description || demo.description; + + const ogImage = resolveOpenGraphImage(settings?.ogImage); + let metadataBase: URL | undefined = undefined; + try { + metadataBase = new URL(settings?.ogImage?.metadataBase!); + } catch { + // ignore + } + return { + metadataBase, + title: { + template: `%s | ${title}`, + default: title, + }, + description: toPlainText(description as any), + openGraph: { + images: ogImage ? [ogImage] : [], + }, + }; +} + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], + display: "swap", +}); + +async function Footer() { + const data = await sanityFetch({ + query: settingsQuery, + }); + const footer = data?.footer || ([] as PortableTextBlock[]); + + return ( + + ); +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+ {draftMode().isEnabled && } +
{children}
+ +
+ +
+ {draftMode().isEnabled && } + + + + ); +} diff --git a/examples/cms-sanity/app/(blog)/more-stories.tsx b/examples/cms-sanity/app/(blog)/more-stories.tsx new file mode 100644 index 00000000000000..4ab1ff388ec5dd --- /dev/null +++ b/examples/cms-sanity/app/(blog)/more-stories.tsx @@ -0,0 +1,52 @@ +import Link from "next/link"; + +import Avatar from "./avatar"; +import CoverImage from "./cover-image"; +import DateComponent from "./date"; + +import { sanityFetch } from "@/sanity/lib/fetch"; +import { + MoreStoriesQueryResponse, + moreStoriesQuery, +} from "@/sanity/lib/queries"; + +export default async function MoreStories(params: { + skip: string; + limit: number; +}) { + const data = await sanityFetch({ + query: moreStoriesQuery, + params, + }); + + return ( + <> +
+ {data?.map((post) => { + const { _id, title, slug, coverImage, excerpt, author } = post; + return ( +
+ + + +

+ + {title} + +

+
+ +
+ {excerpt && ( +

+ {excerpt} +

+ )} + {author && } +
+ ); + })} +
+ + ); +} diff --git a/examples/cms-sanity/app/(blog)/page.tsx b/examples/cms-sanity/app/(blog)/page.tsx new file mode 100644 index 00000000000000..56b66d07c788e0 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/page.tsx @@ -0,0 +1,112 @@ +import Link from "next/link"; +import { Suspense } from "react"; + +import Avatar from "./avatar"; +import CoverImage from "./cover-image"; +import DateComponent from "./date"; +import MoreStories from "./more-stories"; +import PortableText from "./portable-text"; + +import * as demo from "@/sanity/lib/demo"; +import { sanityFetch } from "@/sanity/lib/fetch"; +import { + HeroQueryResponse, + Post, + SettingsQueryResponse, + heroQuery, + settingsQuery, +} from "@/sanity/lib/queries"; + +function Intro(props: { title: string | null | undefined; description: any }) { + const title = props.title || demo.title; + const description = props.description?.length + ? props.description + : demo.description; + return ( +
+

+ {title || demo.title} +

+

+ +

+
+ ); +} + +function HeroPost({ + title, + slug, + excerpt, + coverImage, + date, + author, +}: Pick< + Post, + "title" | "coverImage" | "date" | "excerpt" | "author" | "slug" +>) { + return ( +
+ + + +
+
+

+ + {title} + +

+
+ +
+
+
+ {excerpt && ( +

+ {excerpt} +

+ )} + {author && } +
+
+
+ ); +} + +export default async function Page() { + const [settings, heroPost] = await Promise.all([ + sanityFetch({ + query: settingsQuery, + }), + sanityFetch({ query: heroQuery }), + ]); + + return ( +
+ + {heroPost && ( + + )} + {heroPost?._id && ( + + )} +
+ ); +} diff --git a/examples/cms-sanity/app/(blog)/portable-text.module.css b/examples/cms-sanity/app/(blog)/portable-text.module.css new file mode 100644 index 00000000000000..87f49dffcea019 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/portable-text.module.css @@ -0,0 +1,25 @@ +.portableText { + @apply text-lg leading-relaxed; +} + +.portableText p, +.portableText ul, +.portableText ol, +.portableText blockquote { + @apply my-6; +} + +.portableText h2 { + @apply mb-4 mt-12 text-3xl leading-snug; +} + +.portableText h3 { + @apply mb-4 mt-8 text-2xl leading-snug; +} + +.portableText a { + @apply text-blue-500 underline; +} +.portableText a:hover { + @apply text-blue-800; +} diff --git a/examples/cms-sanity/app/(blog)/portable-text.tsx b/examples/cms-sanity/app/(blog)/portable-text.tsx new file mode 100644 index 00000000000000..74cf8f8ec78b37 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/portable-text.tsx @@ -0,0 +1,49 @@ +/** + * This component uses Portable Text to render a post body. + * + * You can learn more about Portable Text on: + * https://www.sanity.io/docs/block-content + * https://github.com/portabletext/react-portabletext + * https://portabletext.org/ + * + */ + +import { PortableText, type PortableTextComponents } from "@portabletext/react"; +import type { PortableTextBlock } from "@portabletext/types"; + +import styles from "./portable-text.module.css"; + +export default function CustomPortableText({ + paragraphClasses, + value, +}: { + paragraphClasses?: string; + value: PortableTextBlock[]; +}) { + const components: PortableTextComponents = { + block: { + normal: ({ children }) => { + return

{children}

; + }, + }, + marks: { + link: ({ children, value }) => { + return ( + + {children} + + ); + }, + }, + }; + + return ( +
+ +
+ ); +} diff --git a/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx b/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx new file mode 100644 index 00000000000000..344d438a80d7a2 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx @@ -0,0 +1,121 @@ +import type { PortableTextBlock } from "@portabletext/types"; +import type { Metadata, ResolvingMetadata } from "next"; +import { groq } from "next-sanity"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; + +import Avatar from "../../avatar"; +import CoverImage from "../../cover-image"; +import DateComponent from "../../date"; +import MoreStories from "../../more-stories"; +import PortableText from "../../portable-text"; + +import { sanityFetch } from "@/sanity/lib/fetch"; +import { + PostQueryResponse, + SettingsQueryResponse, + postQuery, + settingsQuery, +} from "@/sanity/lib/queries"; +import { resolveOpenGraphImage } from "@/sanity/lib/utils"; +import * as demo from "@/sanity/lib/demo"; + +type Props = { + params: { slug: string }; +}; + +export async function generateStaticParams() { + return sanityFetch<{ slug: string }[]>({ + query: groq`*[_type == "post" && defined(slug.current)]{"slug": slug.current}`, + perspective: "published", + stega: false, + }); +} + +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata, +): Promise { + const post = await sanityFetch({ + query: postQuery, + params, + stega: false, + }); + const previousImages = (await parent).openGraph?.images || []; + const ogImage = resolveOpenGraphImage(post?.coverImage); + + return { + authors: post?.author?.name ? [{ name: post?.author?.name }] : [], + title: post?.title, + description: post?.excerpt, + openGraph: { + images: ogImage ? [ogImage, ...previousImages] : previousImages, + }, + } satisfies Metadata; +} + +export default async function PostPage({ params }: Props) { + const [post, settings] = await Promise.all([ + sanityFetch({ + query: postQuery, + params, + }), + sanityFetch({ + query: settingsQuery, + }), + ]); + + if (!post?._id) { + return notFound(); + } + + return ( +
+

+ + {settings?.title || demo.title} + +

+
+

+ {post.title} +

+
+ {post.author && ( + + )} +
+
+ +
+
+
+ {post.author && ( + + )} +
+
+
+ +
+
+
+ {post.content?.length && ( +
+ +
+ )} +
+ +
+ ); +} diff --git a/examples/cms-sanity/app/(sanity)/apple-icon.png b/examples/cms-sanity/app/(sanity)/apple-icon.png new file mode 100644 index 00000000000000..bc571d60bf69a9 Binary files /dev/null and b/examples/cms-sanity/app/(sanity)/apple-icon.png differ diff --git a/examples/cms-sanity/app/(sanity)/icon.ico b/examples/cms-sanity/app/(sanity)/icon.ico new file mode 100644 index 00000000000000..3f67d3e8e86ed3 Binary files /dev/null and b/examples/cms-sanity/app/(sanity)/icon.ico differ diff --git a/examples/cms-sanity/app/(sanity)/icon.png b/examples/cms-sanity/app/(sanity)/icon.png new file mode 100644 index 00000000000000..6320b50010db12 Binary files /dev/null and b/examples/cms-sanity/app/(sanity)/icon.png differ diff --git a/examples/cms-sanity/app/(sanity)/icon.svg b/examples/cms-sanity/app/(sanity)/icon.svg new file mode 100644 index 00000000000000..1beaa9b75795f3 --- /dev/null +++ b/examples/cms-sanity/app/(sanity)/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/cms-sanity/app/(sanity)/layout.tsx b/examples/cms-sanity/app/(sanity)/layout.tsx new file mode 100644 index 00000000000000..415ee7dbfb0fa2 --- /dev/null +++ b/examples/cms-sanity/app/(sanity)/layout.tsx @@ -0,0 +1,23 @@ +import "../globals.css"; + +import { Inter } from "next/font/google"; + +export { metadata, viewport } from "next-sanity/studio"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], + display: "swap", +}); + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx b/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx new file mode 100644 index 00000000000000..f3d0315aec7e68 --- /dev/null +++ b/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx @@ -0,0 +1,9 @@ +import { NextStudio } from "next-sanity/studio"; + +import config from "@/sanity.config"; + +export const dynamic = "force-static"; + +export default function StudioPage() { + return ; +} diff --git a/examples/cms-sanity/app/api/draft/route.tsx b/examples/cms-sanity/app/api/draft/route.tsx new file mode 100644 index 00000000000000..38bf40eab07bc9 --- /dev/null +++ b/examples/cms-sanity/app/api/draft/route.tsx @@ -0,0 +1,27 @@ +/** + * This file is used to allow Presentation to set the app in Draft Mode, which will load Visual Editing + * and query draft content and preview the content as it will appear once everything is published + */ + +import { validatePreviewUrl } from "@sanity/preview-url-secret"; +import { draftMode } from "next/headers"; +import { redirect } from "next/navigation"; + +import { client } from "@/sanity/lib/client"; +import { token } from "@/sanity/lib/token"; + +const clientWithToken = client.withConfig({ token }); + +export async function GET(request: Request) { + const { isValid, redirectTo = "/" } = await validatePreviewUrl( + clientWithToken, + request.url, + ); + if (!isValid) { + return new Response("Invalid secret", { status: 401 }); + } + + draftMode().enable(); + + redirect(redirectTo); +} diff --git a/examples/cms-sanity/app/favicon.ico b/examples/cms-sanity/app/favicon.ico new file mode 100644 index 00000000000000..af98450595e8b8 Binary files /dev/null and b/examples/cms-sanity/app/favicon.ico differ diff --git a/examples/cms-sanity/styles/index.css b/examples/cms-sanity/app/globals.css similarity index 52% rename from examples/cms-sanity/styles/index.css rename to examples/cms-sanity/app/globals.css index b63c4592cb2e1e..b5c61c956711f9 100644 --- a/examples/cms-sanity/styles/index.css +++ b/examples/cms-sanity/app/globals.css @@ -1,5 +1,3 @@ -/* purgecss start ignore */ @tailwind base; @tailwind components; -/* purgecss end ignore */ @tailwind utilities; diff --git a/examples/cms-sanity/components/alert.js b/examples/cms-sanity/components/alert.js deleted file mode 100644 index 8e8729368f25e4..00000000000000 --- a/examples/cms-sanity/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 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-sanity/components/avatar.js b/examples/cms-sanity/components/avatar.js deleted file mode 100644 index 2d21e70a532143..00000000000000 --- a/examples/cms-sanity/components/avatar.js +++ /dev/null @@ -1,23 +0,0 @@ -import Image from "next/image"; -import { urlForImage } from "../lib/sanity"; - -export default function Avatar({ name, picture }) { - return ( -
-
- {name} -
-
{name}
-
- ); -} diff --git a/examples/cms-sanity/components/container.js b/examples/cms-sanity/components/container.js deleted file mode 100644 index 92e559ece309f0..00000000000000 --- a/examples/cms-sanity/components/container.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Container({ children }) { - return
{children}
; -} diff --git a/examples/cms-sanity/components/cover-image.js b/examples/cms-sanity/components/cover-image.js deleted file mode 100644 index f6a47154d5c209..00000000000000 --- a/examples/cms-sanity/components/cover-image.js +++ /dev/null @@ -1,38 +0,0 @@ -import cn from "classnames"; -import Image from "next/image"; -import Link from "next/link"; -import { urlForImage } from "../lib/sanity"; - -export default function CoverImage({ title, slug, image: source, priority }) { - const image = source?.asset?._ref ? ( -
- {`Cover -
- ) : ( -
- ); - - return ( -
- {slug ? ( - - {image} - - ) : ( - image - )} -
- ); -} diff --git a/examples/cms-sanity/components/date.js b/examples/cms-sanity/components/date.js deleted file mode 100644 index 21c2f0921be3c2..00000000000000 --- a/examples/cms-sanity/components/date.js +++ /dev/null @@ -1,8 +0,0 @@ -import { parseISO, format } from "date-fns"; - -export default function Date({ dateString }) { - if (!dateString) return null; - - const date = parseISO(dateString); - return ; -} diff --git a/examples/cms-sanity/components/footer.js b/examples/cms-sanity/components/footer.js deleted file mode 100644 index 81422c7c6beb98..00000000000000 --- a/examples/cms-sanity/components/footer.js +++ /dev/null @@ -1,30 +0,0 @@ -import Container from "./container"; -import { EXAMPLE_PATH } from "../lib/constants"; - -export default function Footer() { - return ( - - ); -} diff --git a/examples/cms-sanity/components/header.js b/examples/cms-sanity/components/header.js deleted file mode 100644 index e6dea52099cd4e..00000000000000 --- a/examples/cms-sanity/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-sanity/components/hero-post.js b/examples/cms-sanity/components/hero-post.js deleted file mode 100644 index cbbab8be354111..00000000000000 --- a/examples/cms-sanity/components/hero-post.js +++ /dev/null @@ -1,37 +0,0 @@ -import Avatar from "../components/avatar"; -import Date from "../components/date"; -import CoverImage from "../components/cover-image"; -import Link from "next/link"; - -export default function HeroPost({ - title, - coverImage, - date, - excerpt, - author, - slug, -}) { - return ( -
-
- -
-
-
-

- - {title} - -

-
- -
-
-
-

{excerpt}

- {author && } -
-
-
- ); -} diff --git a/examples/cms-sanity/components/intro.js b/examples/cms-sanity/components/intro.js deleted file mode 100644 index e8578bef66be01..00000000000000 --- a/examples/cms-sanity/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-sanity/components/landing-preview.js b/examples/cms-sanity/components/landing-preview.js deleted file mode 100644 index 3ee78d059bbe25..00000000000000 --- a/examples/cms-sanity/components/landing-preview.js +++ /dev/null @@ -1,8 +0,0 @@ -import { usePreview } from "../lib/sanity"; -import { indexQuery } from "../lib/queries"; -import Landing from "./landing"; - -export default function LandingPreview({ allPosts }) { - const previewAllPosts = usePreview(null, indexQuery); - return ; -} diff --git a/examples/cms-sanity/components/landing.js b/examples/cms-sanity/components/landing.js deleted file mode 100644 index fa2b91126bd252..00000000000000 --- a/examples/cms-sanity/components/landing.js +++ /dev/null @@ -1,34 +0,0 @@ -import Layout from "./layout"; -import Head from "next/head"; -import { CMS_NAME } from "../lib/constants"; -import Container from "./container"; -import Intro from "./intro"; -import HeroPost from "./hero-post"; -import MoreStories from "./more-stories"; - -export default function Landing({ allPosts, preview }) { - const [heroPost, ...morePosts] = allPosts || []; - return ( - <> - - - {`Next.js Blog Example with ${CMS_NAME}`} - - - - {heroPost && ( - - )} - {morePosts.length > 0 && } - - - - ); -} diff --git a/examples/cms-sanity/components/layout.js b/examples/cms-sanity/components/layout.js deleted file mode 100644 index a2e161c7938d19..00000000000000 --- a/examples/cms-sanity/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}
-
-