+
{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].
+
+[][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].
-
-[][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:
-
-
-
-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 ? (
+
+
+
+ ) : (
+ 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 ? (
+
+ ) : (
+
+ );
+
+ 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}
-
- );
-}
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 ? (
-
-
-
- ) : (
-
- );
-
- 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}
-
-
- >
- );
-}
diff --git a/examples/cms-sanity/components/markdown-styles.module.css b/examples/cms-sanity/components/markdown-styles.module.css
deleted file mode 100644
index 95d4f8b04172d6..00000000000000
--- a/examples/cms-sanity/components/markdown-styles.module.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.markdown {
- @apply text-lg leading-relaxed;
-}
-
-.markdown p,
-.markdown ul,
-.markdown ol,
-.markdown blockquote {
- @apply my-6;
-}
-
-.markdown h2 {
- @apply text-3xl mt-12 mb-4 leading-snug;
-}
-
-.markdown h3 {
- @apply text-2xl mt-8 mb-4 leading-snug;
-}
diff --git a/examples/cms-sanity/components/meta.js b/examples/cms-sanity/components/meta.js
deleted file mode 100644
index b228ae821cd18d..00000000000000
--- a/examples/cms-sanity/components/meta.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Head from "next/head";
-import { CMS_NAME, HOME_OG_IMAGE_URL } from "../lib/constants";
-
-export default function Meta() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/cms-sanity/components/more-stories.js b/examples/cms-sanity/components/more-stories.js
deleted file mode 100644
index af68ac0f2ce7be..00000000000000
--- a/examples/cms-sanity/components/more-stories.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import PostPlug from "./post-plug";
-
-export default function MoreStories({ posts }) {
- return (
-
-
- More Stories
-
-
- {posts.map((post) => (
-
- ))}
-
-
- );
-}
diff --git a/examples/cms-sanity/components/post-body.js b/examples/cms-sanity/components/post-body.js
deleted file mode 100644
index 9fd971ba60ce1e..00000000000000
--- a/examples/cms-sanity/components/post-body.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import markdownStyles from "./markdown-styles.module.css";
-import { PortableText } from "@portabletext/react";
-
-export default function PostBody({ content }) {
- return (
-
-
-
- );
-}
diff --git a/examples/cms-sanity/components/post-header.js b/examples/cms-sanity/components/post-header.js
deleted file mode 100644
index 3c38dcac6cbb0a..00000000000000
--- a/examples/cms-sanity/components/post-header.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Avatar from "../components/avatar";
-import Date from "../components/date";
-import CoverImage from "../components/cover-image";
-import PostTitle from "../components/post-title";
-
-export default function PostHeader({ title, coverImage, date, author }) {
- return (
- <>
- {title}
-
- {author && }
-
-
-
-
-
-
- {author && }
-
-
-
-
-
- >
- );
-}
diff --git a/examples/cms-sanity/components/post-plug.js b/examples/cms-sanity/components/post-plug.js
deleted file mode 100644
index b49e286b0a55e1..00000000000000
--- a/examples/cms-sanity/components/post-plug.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Avatar from "../components/avatar";
-import Date from "../components/date";
-import CoverImage from "./cover-image";
-import Link from "next/link";
-
-export default function PostPlug({
- title,
- coverImage,
- date,
- excerpt,
- author,
- slug,
-}) {
- return (
-
-
-
-
-
-
- {title}
-
-
-
-
-
- {excerpt}
- {author && }
-
- );
-}
diff --git a/examples/cms-sanity/components/post-preview.js b/examples/cms-sanity/components/post-preview.js
deleted file mode 100644
index 02691ef3ea067f..00000000000000
--- a/examples/cms-sanity/components/post-preview.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { usePreview } from "../lib/sanity";
-import { postQuery } from "../lib/queries";
-import Post from "./post";
-
-export default function PostPreview({ data }) {
- const slug = data?.post?.slug;
- const previewData = usePreview(null, postQuery, { slug });
- return ;
-}
diff --git a/examples/cms-sanity/components/post-title.js b/examples/cms-sanity/components/post-title.js
deleted file mode 100644
index a15f06616ff9f0..00000000000000
--- a/examples/cms-sanity/components/post-title.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function PostTitle({ children }) {
- return (
-
- {children}
-
- );
-}
diff --git a/examples/cms-sanity/components/post.js b/examples/cms-sanity/components/post.js
deleted file mode 100644
index 16f67e3498e3e0..00000000000000
--- a/examples/cms-sanity/components/post.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useRouter } from "next/router";
-import { urlForImage } from "../lib/sanity";
-import ErrorPage from "next/error";
-import Layout from "./layout";
-import Container from "./container";
-import Header from "./header";
-import PostTitle from "./post-title";
-import Head from "next/head";
-import { CMS_NAME } from "../lib/constants";
-import PostHeader from "./post-header";
-import PostBody from "./post-body";
-import SectionSeparator from "./section-separator";
-import MoreStories from "./more-stories";
-
-export default function Post({ data = {}, preview = false }) {
- const router = useRouter();
-
- const { post, morePosts } = data;
- const slug = post?.slug;
-
- if (!router.isFallback && !slug) {
- return ;
- }
-
- return (
-
-
-
- {router.isFallback ? (
- Loading…
- ) : (
- <>
-
-
-
- {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
-
- {post.coverImage?.asset?._ref && (
-
- )}
-
-
-
-
-
- {morePosts.length > 0 && }
- >
- )}
-
-
- );
-}
diff --git a/examples/cms-sanity/components/section-separator.js b/examples/cms-sanity/components/section-separator.js
deleted file mode 100644
index b86fb50b7d07e3..00000000000000
--- a/examples/cms-sanity/components/section-separator.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function SectionSeparator() {
- return
;
-}
diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js
deleted file mode 100644
index c8e58e78504577..00000000000000
--- a/examples/cms-sanity/lib/config.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export const sanityConfig = {
- // Find your project ID and dataset in `sanity.json` in your studio project
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
- useCdn:
- typeof document !== "undefined" && process.env.NODE_ENV === "production",
- // useCdn == true gives fast, cheap responses using a globally distributed cache.
- // When in production the Sanity API is only queried on build-time, and on-demand when responding to webhooks.
- // Thus the data need to be fresh and API response time is less important.
- // When in development/working locally, it's more important to keep costs down as hot reloading can incurr a lot of API calls
- // And every page load calls getStaticProps.
- // To get the lowest latency, lowest cost, and latest data, use the Instant Preview mode
- apiVersion: "2022-03-13",
- // see https://www.sanity.io/docs/api-versioning for how versioning works
-};
diff --git a/examples/cms-sanity/lib/constants.js b/examples/cms-sanity/lib/constants.js
deleted file mode 100644
index 3db98b71341eaf..00000000000000
--- a/examples/cms-sanity/lib/constants.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const EXAMPLE_PATH = "cms-sanity";
-export const CMS_NAME = "Sanity";
-export const CMS_URL = "https://sanity.io/";
-export const HOME_OG_IMAGE_URL =
- "https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Sanity**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTA1IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMWVtIj48dGl0bGU%2BU2FuaXR5PC90aXRsZT48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03OC4xNzkzIDcuOTkyNjFWMjEuMDAyOEg3My45MDMxVjEwLjIxMzhMNzguMTc5MyA3Ljk5MjYxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMjAuOTUxMSAyMS4zM0wzMC45NDQgMTYuMTA1MUwyOS43MTIxIDEyLjkxNDFMMjMuMTMzMiAxNS45ODIxTDIwLjk1MTEgMjEuMzNaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik03My45MDMxIDEwLjIwMjdMODQuNzQ0MyA0LjY1NDc3TDgyLjkxMjYgMS41NTcxTDczLjkwMzEgNS45NTk5N1YxMC4yMDI3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNNDMuMzcwNSA2Ljk2MjMzVjIxLjAwMjhIMzkuMjkyN1YxLjAwNzE0TDQzLjM3MDUgNi45NjIzM1oiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTI3LjEyOTkgNi4xODYxN0wyMC45NTExIDIxLjMzTDE3Ljc3MzEgMTguNTk0M0wyNS4xMzUzIDEuMDA3MTRMMjcuMTI5OSA2LjE4NjE3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTI1LjEzNTMgMS4wMDcxNEgyOS4zNDc3TDM3LjEzODYgMjEuMDAyOEgzMi44MjY5TDI1LjEzNTMgMS4wMDcxNFoiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIGQ9Ik00NC4wMDEyIDEuMDA3MTRMNTIuOTgyNCAxNC42NjgyVjIxLjAwMjhMMzkuMjkyNyAxLjAwNzE0SDQ0LjAwMTJaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNjQuOTE4MyAxLjAwNzE0SDYwLjY3MzlWMjEuMDA2M0g2NC45MTgzVjEuMDA3MTRaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNzMuOTAzMSA0LjY1NDc0SDY3LjM3VjEuMDA3MTRIODIuNTg2N0w4NC43NDQzIDQuNjU0NzRINzguMTc5M0g3My45MDMxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC41IiBkPSJNOTcuMjc1NCAxMy40MTUzVjIxLjAwMjhIOTMuMDYyOVYxMy40MTUzIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNOTMuMDYyOSAxMy40MTUyTDEwMC4xOTEgMS4wMDcxNEgxMDQuNjY2TDk3LjI3NTQgMTMuNDE1Mkg5My4wNjI5WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNOTMuMDYzIDEzLjQxNTJMODUuNzM2MyAxLjAwNzE0SDkwLjM0NTZMOTUuMzA5MiA5LjUxMDA4TDkzLjA2MyAxMy40MTUyWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTEuOTYxMjYgMy4zMTQ3OUMxLjk2MTI2IDYuMDk5MjEgMy43MTE0NSA3Ljc1NTk1IDcuMjE1MzYgOC42Mjk1NkwxMC45MjgzIDkuNDc1MzNDMTQuMjQ0NCAxMC4yMjM2IDE2LjI2MzkgMTIuMDgyMiAxNi4yNjM5IDE1LjExMDNDMTYuMjg5NyAxNi40Mjk1IDE1Ljg1MzEgMTcuNzE3MyAxNS4wMjc0IDE4Ljc1NzlDMTUuMDI3NCAxNS43MzY4IDEzLjQzNjcgMTQuMTA0NCA5LjU5OTcyIDEzLjEyMjlMNS45NTQwOSAxMi4zMDg1QzMuMDM0NzUgMTEuNjU0MSAwLjc4MTQ3OCAxMC4xMjYyIDAuNzgxNDc4IDYuODM3MDlDMC43NjYxMjMgNS41NjY5MyAxLjE4MTE2IDQuMzI3ODEgMS45NjEyNiAzLjMxNDc5IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik01Mi45ODI0IDEzLjY0MTVWMS4wMDcxNEg1Ny4wNjAyVjIxLjAwMjhINTIuOTgyNFYxMy42NDE1WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMTIuNzQ1OCAxNC4zNjg5QzE0LjMyOTQgMTUuMzY0MyAxNS4wMjM4IDE2Ljc1NjUgMTUuMDIzOCAxOC43NTQ0QzEzLjcxMyAyMC40MDQxIDExLjQxMDEgMjEuMzMgOC43MDMzMyAyMS4zM0M0LjE0NzE4IDIxLjMzIDAuOTU4NTc3IDE5LjEyNjggMC4yNSAxNS4yOTgySDQuNjI1NDdDNS4xODg3OCAxNy4wNTU5IDYuNjgwMzQgMTcuODcwMyA4LjY3MTQ0IDE3Ljg3MDNDMTEuMTAxOSAxNy44NzAzIDEyLjcxNzQgMTYuNTk2NCAxMi43NDkzIDE0LjM2MTkiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNyIgZD0iTTQuMjM1NjcgNy40NDI2N0MzLjUxMjUgNy4wMjA0NSAyLjkxOTIgNi40MTM3NSAyLjUxODczIDUuNjg2OTdDMi4xMTgyNyA0Ljk2MDE5IDEuOTI1NTggNC4xNDA0NSAxLjk2MTEzIDMuMzE0NzZDMy4yMjU5NCAxLjY3ODkxIDUuNDI2MDggMC42Nzk5OTMgOC4xMDgwNCAwLjY3OTk5M0MxMi43NDkyIDAuNjc5OTkzIDE1LjQzNDcgMy4wODg1MiAxNi4wOTcyIDYuNDc4NTZIMTEuODg4M0MxMS40MjQyIDUuMTQyMDMgMTAuMjYyMSA0LjEwMTM2IDguMTQzNDcgNC4xMDEzNkM1Ljg3OTU3IDQuMTAxMzYgNC4zMzQ4NyA1LjM5NjExIDQuMjQ2MjkgNy40NDI2NyIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPC9zdmc%2B&widths=undefined&widths=auto&heights=250&heights=150";
diff --git a/examples/cms-sanity/lib/queries.js b/examples/cms-sanity/lib/queries.js
deleted file mode 100644
index 89330900c4be1e..00000000000000
--- a/examples/cms-sanity/lib/queries.js
+++ /dev/null
@@ -1,37 +0,0 @@
-const postFields = `
- _id,
- name,
- title,
- date,
- excerpt,
- coverImage,
- "slug": slug.current,
- "author": author->{name, picture},
-`;
-
-export const indexQuery = `
-*[_type == "post"] | order(date desc, _updatedAt desc) {
- ${postFields}
-}`;
-
-export const postQuery = `
-{
- "post": *[_type == "post" && slug.current == $slug] | order(_updatedAt desc) [0] {
- content,
- ${postFields}
- },
- "morePosts": *[_type == "post" && slug.current != $slug] | order(date desc, _updatedAt desc) [0...2] {
- content,
- ${postFields}
- }
-}`;
-
-export const postSlugsQuery = `
-*[_type == "post" && defined(slug.current)][].slug.current
-`;
-
-export const postBySlugQuery = `
-*[_type == "post" && slug.current == $slug][0] {
- ${postFields}
-}
-`;
diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js
deleted file mode 100644
index 2492667bea9b60..00000000000000
--- a/examples/cms-sanity/lib/sanity.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import createImageUrlBuilder from "@sanity/image-url";
-import { definePreview } from "next-sanity/preview";
-import { sanityConfig } from "./config";
-
-export const imageBuilder = createImageUrlBuilder(sanityConfig);
-
-export const urlForImage = (source) =>
- imageBuilder.image(source).auto("format").fit("max");
-
-export const usePreview = definePreview({
- projectId: sanityConfig.projectId,
- dataset: sanityConfig.dataset,
-});
diff --git a/examples/cms-sanity/lib/sanity.server.js b/examples/cms-sanity/lib/sanity.server.js
deleted file mode 100644
index 4a0d7be3bf8cb9..00000000000000
--- a/examples/cms-sanity/lib/sanity.server.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Server-side Sanity utilities. By having these in a separate file from the
- * utilities we use on the client side, we are able to tree-shake (remove)
- * code that is not used on the client side.
- */
-import { createClient } from "next-sanity";
-import { sanityConfig } from "./config";
-
-export const sanityClient = createClient(sanityConfig);
-
-export const previewClient = createClient({
- ...sanityConfig,
- useCdn: false,
- // Fallback to using the WRITE token until https://www.sanity.io/docs/vercel-integration starts shipping a READ token.
- // As this client only exists on the server and the token is never shared with the browser, we don't risk escalating permissions to untrustworthy users
- token:
- process.env.SANITY_API_READ_TOKEN || process.env.SANITY_API_WRITE_TOKEN,
-});
-
-export const getClient = (preview) => (preview ? previewClient : sanityClient);
-
-export function overlayDrafts(docs) {
- const documents = docs || [];
- const overlayed = documents.reduce((map, doc) => {
- if (!doc._id) {
- throw new Error("Ensure that `_id` is included in query projection");
- }
-
- const isDraft = doc._id.startsWith("drafts.");
- const id = isDraft ? doc._id.slice(7) : doc._id;
- return isDraft || !map.has(id) ? map.set(id, doc) : map;
- }, new Map());
-
- return Array.from(overlayed.values());
-}
diff --git a/examples/cms-sanity/next.config.js b/examples/cms-sanity/next.config.js
index 553cc9b8363a48..44b910b28d8e8a 100644
--- a/examples/cms-sanity/next.config.js
+++ b/examples/cms-sanity/next.config.js
@@ -1,9 +1,18 @@
/** @type {import('next').NextConfig} */
module.exports = {
+ compiler: {
+ styledComponents: true,
+ },
+ logging: {
+ fetches: {
+ fullUrl: true,
+ },
+ },
+ experimental: {
+ // Used to guard against accidentally leaking SANITY_API_READ_TOKEN to the browser
+ taint: true,
+ },
images: {
- remotePatterns: [
- { hostname: "cdn.sanity.io" },
- { hostname: "source.unsplash.com" },
- ],
+ remotePatterns: [{ hostname: "cdn.sanity.io" }],
},
};
diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json
index 22addbb2a2a9b3..d3e739dadb10e5 100644
--- a/examples/cms-sanity/package.json
+++ b/examples/cms-sanity/package.json
@@ -4,23 +4,40 @@
"dev": "next",
"build": "next build",
"start": "next start",
- "studio:dev": "npm --prefix studio run start",
- "studio:deploy": "npx vercel env pull && npm --prefix studio run deploy"
+ "lint": "next lint",
+ "presetup": "echo 'about to setup env variables, follow the guide here: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli'",
+ "setup": "npx sanity@latest init --env .env.local",
+ "postsetup": "echo 'create the read token by following the rest of the guide: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#creating-a-read-token'"
},
"dependencies": {
- "@portabletext/react": "^2.0.1",
+ "@portabletext/react": "^3.0.11",
+ "@sanity/assist": "^2.0.1",
+ "@sanity/icons": "^2.10.3",
"@sanity/image-url": "^1.0.2",
- "@sanity/webhook": "^2.0.0",
- "classnames": "^2.3.1",
- "date-fns": "^2.29.3",
+ "@sanity/vision": "^3.31.0",
+ "@tailwindcss/typography": "^0.5.10",
+ "@types/node": "^20.11.24",
+ "@types/react": "^18.2.63",
+ "@types/react-dom": "^18.2.20",
+ "@vercel/speed-insights": "^1.0.10",
+ "autoprefixer": "^10.4.18",
+ "date-fns": "^3.3.1",
+ "get-youtube-id": "^1.0.1",
"next": "latest",
- "next-sanity": "^4.1.2",
- "react": "^18",
- "react-dom": "^18"
+ "next-sanity": "^8.1.5",
+ "postcss": "^8.4.35",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-lite-youtube-embed": "^2.4.0",
+ "sanity": "^3.31.0",
+ "sanity-plugin-asset-source-unsplash": "^1.1.2",
+ "server-only": "^0.0.1",
+ "styled-components": "^6.1.8",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.3.3"
},
"devDependencies": {
- "autoprefixer": "^10.4.13",
- "postcss": "^8.4.21",
- "tailwindcss": "^3.2.4"
+ "eslint": "^8.57.0",
+ "eslint-config-next": "latest"
}
}
diff --git a/examples/cms-sanity/pages/_app.js b/examples/cms-sanity/pages/_app.js
deleted file mode 100644
index aa7931ca32e943..00000000000000
--- a/examples/cms-sanity/pages/_app.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import "../styles/index.css";
-
-function MyApp({ Component, pageProps }) {
- return ;
-}
-
-export default MyApp;
diff --git a/examples/cms-sanity/pages/_document.js b/examples/cms-sanity/pages/_document.js
deleted file mode 100644
index b2fff8b4262dde..00000000000000
--- a/examples/cms-sanity/pages/_document.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/cms-sanity/pages/api/exit-preview.js b/examples/cms-sanity/pages/api/exit-preview.js
deleted file mode 100644
index 16b744e88e34aa..00000000000000
--- a/examples/cms-sanity/pages/api/exit-preview.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default async function exit(_, res) {
- // Exit Draft Mode by removing the cookie
- res.setDraftMode({ enable: false });
-
- // Redirect the user back to the index page.
- res.writeHead(307, { Location: "/" });
- res.end();
-}
diff --git a/examples/cms-sanity/pages/api/preview.js b/examples/cms-sanity/pages/api/preview.js
deleted file mode 100644
index 8035d1564cb9cc..00000000000000
--- a/examples/cms-sanity/pages/api/preview.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { postBySlugQuery } from "../../lib/queries";
-import { previewClient } from "../../lib/sanity.server";
-
-function redirectToPreview(res, Location) {
- // Enable Draft Mode by setting the cookie
- res.setDraftMode({ enable: true });
- // Redirect to a preview capable route
- res.writeHead(307, { Location });
- res.end();
-}
-
-export default async function preview(req, res) {
- const secret = process.env.SANITY_STUDIO_PREVIEW_SECRET;
- // Only require a secret when in production
- if (!secret && process.env.NODE_ENV === "production") {
- throw new TypeError(`Missing SANITY_STUDIO_PREVIEW_SECRET`);
- }
- // Check the secret if it's provided, enables running preview mode locally before the env var is setup
- if (secret && req.query.secret !== secret) {
- return res.status(401).json({ message: "Invalid secret" });
- }
- // If no slug is provided open preview mode on the frontpage
- if (!req.query.slug) {
- return redirectToPreview(res, "/");
- }
-
- // Check if the post with the given `slug` exists
- const post = await previewClient.fetch(postBySlugQuery, {
- slug: req.query.slug,
- });
-
- // If the slug doesn't exist prevent preview mode from being enabled
- if (!post) {
- return res.status(401).json({ message: "Invalid slug" });
- }
-
- // Redirect to the path from the fetched post
- // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
- redirectToPreview(res, `/posts/${post.slug}`);
-}
diff --git a/examples/cms-sanity/pages/api/revalidate.js b/examples/cms-sanity/pages/api/revalidate.js
deleted file mode 100644
index b5add334c75345..00000000000000
--- a/examples/cms-sanity/pages/api/revalidate.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook";
-import { sanityClient } from "../../lib/sanity.server";
-
-// Next.js will by default parse the body, which can lead to invalid signatures
-export const config = {
- api: {
- bodyParser: false,
- },
-};
-
-const AUTHOR_UPDATED_QUERY = /* groq */ `
- *[_type == "author" && _id == $id] {
- "slug": *[_type == "post" && references(^._id)].slug.current
- }["slug"][]`;
-const POST_UPDATED_QUERY = /* groq */ `*[_type == "post" && _id == $id].slug.current`;
-
-const getQueryForType = (type) => {
- switch (type) {
- case "author":
- return AUTHOR_UPDATED_QUERY;
- case "post":
- return POST_UPDATED_QUERY;
- default:
- throw new TypeError(`Unknown type: ${type}`);
- }
-};
-
-const log = (msg, error) =>
- console[error ? "error" : "log"](`[revalidate] ${msg}`);
-
-async function readBody(readable) {
- const chunks = [];
- for await (const chunk of readable) {
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
- }
- return Buffer.concat(chunks).toString("utf8");
-}
-
-export default async function revalidate(req, res) {
- const signature = req.headers[SIGNATURE_HEADER_NAME];
- const body = await readBody(req); // Read the body into a string
- if (
- !isValidSignature(
- body,
- signature,
- process.env.SANITY_REVALIDATE_SECRET?.trim(),
- )
- ) {
- const invalidSignature = "Invalid signature";
- log(invalidSignature, true);
- res.status(401).json({ success: false, message: invalidSignature });
- return;
- }
-
- const jsonBody = JSON.parse(body);
- const { _id: id, _type } = jsonBody;
- if (typeof id !== "string" || !id) {
- const invalidId = "Invalid _id";
- log(invalidId, true);
- return res.status(400).json({ message: invalidId });
- }
-
- log(`Querying post slug for _id '${id}', type '${_type}' ..`);
- const slug = await sanityClient.fetch(getQueryForType(_type), { id });
- const slugs = (Array.isArray(slug) ? slug : [slug]).map(
- (_slug) => `/posts/${_slug}`,
- );
- const staleRoutes = ["/", ...slugs];
-
- try {
- await Promise.all(staleRoutes.map((route) => res.revalidate(route)));
- const updatedRoutes = `Updated routes: ${staleRoutes.join(", ")}`;
- log(updatedRoutes);
- return res.status(200).json({ message: updatedRoutes });
- } catch (err) {
- log(err.message, true);
- return res.status(500).json({ message: err.message });
- }
-}
diff --git a/examples/cms-sanity/pages/index.js b/examples/cms-sanity/pages/index.js
deleted file mode 100644
index 682af4bbcfe502..00000000000000
--- a/examples/cms-sanity/pages/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { indexQuery } from "../lib/queries";
-import { getClient, overlayDrafts } from "../lib/sanity.server";
-import { PreviewSuspense } from "next-sanity/preview";
-import { lazy } from "react";
-import Landing from "../components/landing";
-
-const LandingPreview = lazy(() => import("../components/landing-preview"));
-
-export default function IndexPage({ allPosts, preview }) {
- if (preview) {
- return (
-
-
-
- );
- }
-
- return ;
-}
-
-export async function getStaticProps({ preview = false }) {
- const allPosts = overlayDrafts(await getClient(preview).fetch(indexQuery));
- return {
- props: { allPosts, preview },
- // If webhooks isn't setup then attempt to re-generate in 1 minute intervals
- revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
- };
-}
diff --git a/examples/cms-sanity/pages/posts/[slug].js b/examples/cms-sanity/pages/posts/[slug].js
deleted file mode 100644
index 2332dd291e6f59..00000000000000
--- a/examples/cms-sanity/pages/posts/[slug].js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { lazy } from "react";
-import { PreviewSuspense } from "next-sanity/preview";
-import { postQuery, postSlugsQuery } from "../../lib/queries";
-import {
- getClient,
- overlayDrafts,
- sanityClient,
-} from "../../lib/sanity.server";
-import Post from "../../components/post";
-
-const PostPreview = lazy(() => import("../../components/post-preview"));
-
-export default function PostPage({ preview, data }) {
- if (preview) {
- return (
-
-
-
- );
- }
-
- return ;
-}
-
-export async function getStaticProps({ params, preview = false }) {
- const { post, morePosts } = await getClient(preview).fetch(postQuery, {
- slug: params.slug,
- });
-
- return {
- props: {
- preview,
- data: {
- post,
- morePosts: overlayDrafts(morePosts),
- },
- },
- // If webhooks isn't setup then attempt to re-generate in 1 minute intervals
- revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
- };
-}
-
-export async function getStaticPaths() {
- const paths = await sanityClient.fetch(postSlugsQuery);
- return {
- paths: paths.map((slug) => ({ params: { slug } })),
- fallback: true,
- };
-}
diff --git a/examples/cms-sanity/postcss.config.js b/examples/cms-sanity/postcss.config.js
index 3687d286ecd7c9..12a703d900da81 100644
--- a/examples/cms-sanity/postcss.config.js
+++ b/examples/cms-sanity/postcss.config.js
@@ -1,5 +1,3 @@
-// If you want to use other PostCSS plugins, see the following:
-// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
diff --git a/examples/cms-sanity/public/favicon/android-chrome-192x192.png b/examples/cms-sanity/public/favicon/android-chrome-192x192.png
deleted file mode 100644
index 2f07282a59cdad..00000000000000
Binary files a/examples/cms-sanity/public/favicon/android-chrome-192x192.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/android-chrome-512x512.png b/examples/cms-sanity/public/favicon/android-chrome-512x512.png
deleted file mode 100644
index dbb0faea84049b..00000000000000
Binary files a/examples/cms-sanity/public/favicon/android-chrome-512x512.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/apple-touch-icon.png b/examples/cms-sanity/public/favicon/apple-touch-icon.png
deleted file mode 100644
index 8f4033b2a8b352..00000000000000
Binary files a/examples/cms-sanity/public/favicon/apple-touch-icon.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/browserconfig.xml b/examples/cms-sanity/public/favicon/browserconfig.xml
deleted file mode 100644
index 9824d87b11517d..00000000000000
--- a/examples/cms-sanity/public/favicon/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- #000000
-
-
-
diff --git a/examples/cms-sanity/public/favicon/favicon-16x16.png b/examples/cms-sanity/public/favicon/favicon-16x16.png
deleted file mode 100644
index 29deaf6716e774..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon-16x16.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/favicon-32x32.png b/examples/cms-sanity/public/favicon/favicon-32x32.png
deleted file mode 100644
index e3b4277bf093d2..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon-32x32.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/favicon.ico b/examples/cms-sanity/public/favicon/favicon.ico
deleted file mode 100644
index ea2f437d9db655..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon.ico and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/mstile-150x150.png b/examples/cms-sanity/public/favicon/mstile-150x150.png
deleted file mode 100644
index f2dfd904bf1be6..00000000000000
Binary files a/examples/cms-sanity/public/favicon/mstile-150x150.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/safari-pinned-tab.svg b/examples/cms-sanity/public/favicon/safari-pinned-tab.svg
deleted file mode 100644
index 72ab6e050cb113..00000000000000
--- a/examples/cms-sanity/public/favicon/safari-pinned-tab.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/examples/cms-sanity/public/favicon/site.webmanifest b/examples/cms-sanity/public/favicon/site.webmanifest
deleted file mode 100644
index a672d9a233c59a..00000000000000
--- a/examples/cms-sanity/public/favicon/site.webmanifest
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "Next.js",
- "short_name": "Next.js",
- "icons": [
- {
- "src": "/favicons/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/favicons/android-chrome-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "theme_color": "#000000",
- "background_color": "#000000",
- "display": "standalone"
-}
diff --git a/examples/cms-sanity/studio/sanity.cli.js b/examples/cms-sanity/sanity.cli.ts
similarity index 64%
rename from examples/cms-sanity/studio/sanity.cli.js
rename to examples/cms-sanity/sanity.cli.ts
index 84dd1a717dd3ff..44500feff30fe4 100644
--- a/examples/cms-sanity/studio/sanity.cli.js
+++ b/examples/cms-sanity/sanity.cli.ts
@@ -7,12 +7,4 @@ loadEnvConfig(__dirname, dev, { info: () => null, error: console.error });
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
-export default defineCliConfig({
- api: { projectId, dataset },
- vite: (config) => {
- return {
- ...config,
- envPrefix: ["NEXT_", "SANITY_STUDIO_", "VITE_"],
- };
- },
-});
+export default defineCliConfig({ api: { projectId, dataset } });
diff --git a/examples/cms-sanity/sanity.config.ts b/examples/cms-sanity/sanity.config.ts
new file mode 100644
index 00000000000000..6c4c9925c7903b
--- /dev/null
+++ b/examples/cms-sanity/sanity.config.ts
@@ -0,0 +1,48 @@
+"use client";
+/**
+ * This config is used to set up Sanity Studio that's mounted on the `app/(sanity)/studio/[[...tool]]/page.tsx` route
+ */
+import { assist } from "@sanity/assist";
+import { visionTool } from "@sanity/vision";
+import { PluginOptions, defineConfig } from "sanity";
+import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
+import { presentationTool } from "sanity/presentation";
+import { structureTool } from "sanity/structure";
+
+import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
+import { locate } from "@/sanity/plugins/locate";
+import { pageStructure, singletonPlugin } from "@/sanity/plugins/settings";
+import author from "@/sanity/schemas/documents/author";
+import post from "@/sanity/schemas/documents/post";
+import settings from "@/sanity/schemas/singletons/settings";
+
+export default defineConfig({
+ basePath: studioUrl,
+ projectId,
+ dataset,
+ schema: {
+ types: [
+ // Singletons
+ settings,
+ // Documents
+ post,
+ author,
+ ],
+ },
+ plugins: [
+ assist(),
+ presentationTool({
+ locate,
+ previewUrl: { previewMode: { enable: "/api/draft" } },
+ }),
+ structureTool({ structure: pageStructure([settings]) }),
+ // Configures the global "new document" button, and document actions, to suit the Settings document singleton
+ singletonPlugin([settings.name]),
+ // Add an image asset source for Unsplash
+ unsplashImageAsset(),
+ // Vision lets you query your content with GROQ in the studio
+ // https://www.sanity.io/docs/the-vision-plugin
+ process.env.NODE_ENV === "development" &&
+ visionTool({ defaultApiVersion: apiVersion }),
+ ].filter(Boolean) as PluginOptions[],
+});
diff --git a/examples/cms-sanity/sanity/lib/api.ts b/examples/cms-sanity/sanity/lib/api.ts
new file mode 100644
index 00000000000000..c84eaafb86a737
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/api.ts
@@ -0,0 +1,33 @@
+/**
+ * As this file is reused in several other files, try to keep it lean and small.
+ * Importing other npm packages here could lead to needlessly increasing the client bundle size, or end up in a server-only function that don't need it.
+ */
+
+function assertValue(v: T | undefined, errorMessage: string): T {
+ if (v === undefined) {
+ throw new Error(errorMessage);
+ }
+
+ return v;
+}
+
+export const dataset = assertValue(
+ process.env.NEXT_PUBLIC_SANITY_DATASET,
+ "Missing environment variable: NEXT_PUBLIC_SANITY_DATASET",
+);
+
+export const projectId = assertValue(
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
+ "Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID",
+);
+
+/**
+ * see https://www.sanity.io/docs/api-versioning for how versioning works
+ */
+export const apiVersion =
+ process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-28";
+
+/**
+ * Used to configure edit intent links, for Presentation Mode, as well as to configure where the Studio is mounted in the router.
+ */
+export const studioUrl = "/studio";
diff --git a/examples/cms-sanity/sanity/lib/client.ts b/examples/cms-sanity/sanity/lib/client.ts
new file mode 100644
index 00000000000000..ba149ef011293b
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/client.ts
@@ -0,0 +1,22 @@
+import { createClient } from "next-sanity";
+
+import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
+
+export const client = createClient({
+ projectId,
+ dataset,
+ apiVersion,
+ useCdn: true,
+ perspective: "published",
+ stega: {
+ studioUrl,
+ logger: console,
+ filter: (props) => {
+ if (props.sourcePath.at(-1) === "title") {
+ return true;
+ }
+
+ return props.filterDefault(props);
+ },
+ },
+});
diff --git a/examples/cms-sanity/sanity/lib/demo.ts b/examples/cms-sanity/sanity/lib/demo.ts
new file mode 100644
index 00000000000000..8c68844102ebec
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/demo.ts
@@ -0,0 +1,59 @@
+/**
+ * Demo data used as placeholders and initial values for the blog
+ */
+
+export const title = "Blog.";
+
+export const description = [
+ {
+ _key: "9f1a629887fd",
+ _type: "block",
+ children: [
+ {
+ _key: "4a58edd077880",
+ _type: "span",
+ marks: [],
+ text: "A statically generated blog example using ",
+ },
+ {
+ _key: "4a58edd077881",
+ _type: "span",
+ marks: ["ec5b66c9b1e0"],
+ text: "Next.js",
+ },
+ {
+ _key: "4a58edd077882",
+ _type: "span",
+ marks: [],
+ text: " and ",
+ },
+ {
+ _key: "4a58edd077883",
+ _type: "span",
+ marks: ["1f8991913ea8"],
+ text: "Sanity",
+ },
+ {
+ _key: "4a58edd077884",
+ _type: "span",
+ marks: [],
+ text: ".",
+ },
+ ],
+ markDefs: [
+ {
+ _key: "ec5b66c9b1e0",
+ _type: "link",
+ href: "https://nextjs.org/",
+ },
+ {
+ _key: "1f8991913ea8",
+ _type: "link",
+ href: "https://sanity.io/",
+ },
+ ],
+ style: "normal",
+ },
+];
+
+export const ogImageTitle = "A Next.js Blog with a Native Authoring Experience";
diff --git a/examples/cms-sanity/sanity/lib/fetch.ts b/examples/cms-sanity/sanity/lib/fetch.ts
new file mode 100644
index 00000000000000..91796e437cf50a
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/fetch.ts
@@ -0,0 +1,51 @@
+import type { ClientPerspective, QueryParams } from "next-sanity";
+import { draftMode } from "next/headers";
+
+import { client } from "@/sanity/lib/client";
+import { token } from "@/sanity/lib/token";
+
+/**
+ * Used to fetch data in Server Components, it has built in support for handling Draft Mode and perspectives.
+ * When using the "published" perspective then time-based revalidation is used, set to match the time-to-live on Sanity's API CDN (60 seconds)
+ * and will also fetch from the CDN.
+ * When using the "previewDrafts" perspective then the data is fetched from the live API and isn't cached, it will also fetch draft content that isn't published yet.
+ */
+export async function sanityFetch({
+ query,
+ params = {},
+ perspective = draftMode().isEnabled ? "previewDrafts" : "published",
+ /**
+ * Stega embedded Content Source Maps are used by Visual Editing by both the Sanity Presentation Tool and Vercel Visual Editing.
+ * The Sanity Presentation Tool will enable Draft Mode when loading up the live preview, and we use it as a signal for when to embed source maps.
+ * When outside of the Sanity Studio we also support the Vercel Toolbar Visual Editing feature, which is only enabled in production when it's a Vercel Preview Deployment.
+ */
+ stega = perspective === "previewDrafts" ||
+ process.env.VERCEL_ENV === "preview",
+}: {
+ query: string;
+ params?: QueryParams;
+ perspective?: Omit;
+ stega?: boolean;
+}) {
+ if (perspective === "previewDrafts") {
+ return client.fetch(query, params, {
+ stega,
+ perspective: "previewDrafts",
+ // The token is required to fetch draft content
+ token,
+ // The `previewDrafts` perspective isn't available on the API CDN
+ useCdn: false,
+ // And we can't cache the responses as it would slow down the live preview experience
+ next: { revalidate: 0 },
+ });
+ }
+ return client.fetch(query, params, {
+ stega,
+ perspective: "published",
+ // The `published` perspective is available on the API CDN
+ useCdn: true,
+ // Only enable Stega in production if it's a Vercel Preview Deployment, as the Vercel Toolbar supports Visual Editing
+ // When using the `published` perspective we use time-based revalidation to match the time-to-live on Sanity's API CDN (60 seconds)
+ next: { revalidate: 60 },
+ });
+}
diff --git a/examples/cms-sanity/sanity/lib/queries.ts b/examples/cms-sanity/sanity/lib/queries.ts
new file mode 100644
index 00000000000000..d5b8975dca8453
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/queries.ts
@@ -0,0 +1,62 @@
+import type { PortableTextBlock } from "@portabletext/types";
+import { groq } from "next-sanity";
+import type { Image } from "sanity";
+
+export const settingsQuery = groq`*[_type == "settings"][0]`;
+export interface SettingsQueryResponse {
+ title?: string;
+ description?: PortableTextBlock[];
+ footer?: PortableTextBlock[];
+ ogImage?: (Image & { alt?: string; metadataBase?: string }) | null;
+}
+
+export interface Author {
+ name: string;
+ picture?: (Image & { alt?: string | null }) | null;
+}
+export interface Post {
+ _id: string;
+ status: "draft" | "published";
+ title: string;
+ slug: string;
+ excerpt?: string | null;
+ coverImage?: (Image & { alt?: string }) | null;
+ date: string;
+ author?: Author | null;
+}
+
+const postFields = groq`
+ _id,
+ "status": select(_originalId in path("drafts.**") => "draft", "published"),
+ "title": coalesce(title, "Untitled"),
+ "slug": slug.current,
+ excerpt,
+ coverImage,
+ "date": coalesce(date, _updatedAt),
+ "author": author->{"name": coalesce(name, "Anonymous"), picture},
+`;
+
+export const heroQuery = groq`*[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] {
+ content,
+ ${postFields}
+}`;
+export type HeroQueryResponse =
+ | (Post & {
+ content?: PortableTextBlock[] | null;
+ })
+ | null;
+
+export const moreStoriesQuery = groq`*[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {
+ ${postFields}
+}`;
+export type MoreStoriesQueryResponse = Post[] | null;
+
+export const postQuery = groq`*[_type == "post" && slug.current == $slug] [0] {
+ content,
+ ${postFields}
+}`;
+export type PostQueryResponse =
+ | (Post & {
+ content?: PortableTextBlock[] | null;
+ })
+ | null;
diff --git a/examples/cms-sanity/sanity/lib/token.ts b/examples/cms-sanity/sanity/lib/token.ts
new file mode 100644
index 00000000000000..dd8757abdb724c
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/token.ts
@@ -0,0 +1,15 @@
+import "server-only";
+
+import { experimental_taintUniqueValue } from "react";
+
+export const token = process.env.SANITY_API_READ_TOKEN;
+
+if (!token) {
+ throw new Error("Missing SANITY_API_READ_TOKEN");
+}
+
+experimental_taintUniqueValue(
+ "Do not pass the sanity API read token to the client.",
+ process,
+ token,
+);
diff --git a/examples/cms-sanity/sanity/lib/utils.ts b/examples/cms-sanity/sanity/lib/utils.ts
new file mode 100644
index 00000000000000..0af9a97bed2433
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/utils.ts
@@ -0,0 +1,37 @@
+import createImageUrlBuilder from "@sanity/image-url";
+
+import { dataset, projectId } from "@/sanity/lib/api";
+
+const imageBuilder = createImageUrlBuilder({
+ projectId: projectId || "",
+ dataset: dataset || "",
+});
+
+export const urlForImage = (source: any) => {
+ // Ensure that source image contains a valid reference
+ if (!source?.asset?._ref) {
+ return undefined;
+ }
+
+ return imageBuilder?.image(source).auto("format").fit("max");
+};
+
+export function resolveOpenGraphImage(image: any, width = 1200, height = 627) {
+ if (!image) return;
+ const url = urlForImage(image)?.width(1200).height(627).fit("crop").url();
+ if (!url) return;
+ return { url, alt: image?.alt as string, width, height };
+}
+
+export function resolveHref(
+ documentType?: string,
+ slug?: string,
+): string | undefined {
+ switch (documentType) {
+ case "post":
+ return slug ? `/posts/${slug}` : undefined;
+ default:
+ console.warn("Invalid document type:", documentType);
+ return undefined;
+ }
+}
diff --git a/examples/cms-sanity/sanity/plugins/locate.ts b/examples/cms-sanity/sanity/plugins/locate.ts
new file mode 100644
index 00000000000000..5d3654bc7b905f
--- /dev/null
+++ b/examples/cms-sanity/sanity/plugins/locate.ts
@@ -0,0 +1,101 @@
+import { map, Observable } from "rxjs";
+import {
+ DocumentLocationResolver,
+ DocumentLocationsState,
+} from "sanity/presentation";
+
+import { resolveHref } from "@/sanity/lib/utils";
+
+export const locate: DocumentLocationResolver = (params, context) => {
+ if (params.type === "settings") {
+ return {
+ message: "This document is used on all pages",
+ tone: "caution",
+ } satisfies DocumentLocationsState;
+ }
+
+ if (
+ params.type === "home" ||
+ params.type === "page" ||
+ params.type === "project"
+ ) {
+ const doc$ = context.documentStore.listenQuery(
+ `*[_id==$id || references($id)]{_type,slug,title}`,
+ params,
+ { perspective: "previewDrafts" },
+ ) as Observable<
+ | {
+ _type: string;
+ slug: { current: string };
+ title: string | null;
+ }[]
+ | null
+ >;
+ return doc$.pipe(
+ map((docs) => {
+ const isReferencedBySettings = docs?.some(
+ (doc) => doc._type === "settings",
+ );
+ switch (params.type) {
+ case "home":
+ return isReferencedBySettings
+ ? ({
+ locations: [
+ {
+ title:
+ docs?.find((doc) => doc._type === "home")?.title ||
+ "Home",
+ href: resolveHref(params.type)!,
+ },
+ ],
+ tone: "positive",
+ message: "This document is used to render the front page",
+ } satisfies DocumentLocationsState)
+ : ({
+ tone: "critical",
+ message: `The top menu isn't linking to the home page. This might make it difficult for visitors to navigate your site.`,
+ } satisfies DocumentLocationsState);
+ case "page":
+ return {
+ locations: docs
+ ?.map((doc) => {
+ const href = resolveHref(doc._type, doc?.slug?.current);
+ return {
+ title: doc?.title || "Untitled",
+ href: href!,
+ };
+ })
+ .filter((doc) => doc.href !== undefined),
+ tone: isReferencedBySettings ? "positive" : "critical",
+ message: isReferencedBySettings
+ ? "The top menu is linking to this page"
+ : "The top menu isn't linking to this page. It can still be accessed if the visitor knows the URL.",
+ } satisfies DocumentLocationsState;
+ case "project":
+ return {
+ locations: docs
+ ?.map((doc) => {
+ const href = resolveHref(doc._type, doc?.slug?.current);
+ return {
+ title: doc?.title || "Untitled",
+ href: href!,
+ };
+ })
+ .filter((doc) => doc.href !== undefined),
+ tone: isReferencedBySettings ? "caution" : undefined,
+ message: isReferencedBySettings
+ ? "This document is used on all pages as it is in the top menu"
+ : undefined,
+ } satisfies DocumentLocationsState;
+ default:
+ return {
+ message: "Unable to map document type to locations",
+ tone: "critical",
+ } satisfies DocumentLocationsState;
+ }
+ }),
+ );
+ }
+
+ return null;
+};
diff --git a/examples/cms-sanity/sanity/plugins/settings.tsx b/examples/cms-sanity/sanity/plugins/settings.tsx
new file mode 100644
index 00000000000000..0720d042eaf7eb
--- /dev/null
+++ b/examples/cms-sanity/sanity/plugins/settings.tsx
@@ -0,0 +1,65 @@
+/**
+ * This plugin contains all the logic for setting up the singletons
+ */
+
+import { definePlugin, type DocumentDefinition } from "sanity";
+import { type StructureResolver } from "sanity/structure";
+
+export const singletonPlugin = (types: string[]) => {
+ return definePlugin({
+ name: "singletonPlugin",
+ document: {
+ // Hide 'Singletons (such as Settings)' from new document options
+ // https://user-images.githubusercontent.com/81981/195728798-e0c6cf7e-d442-4e58-af3a-8cd99d7fcc28.png
+ newDocumentOptions: (prev, { creationContext }) => {
+ if (creationContext.type === "global") {
+ return prev.filter(
+ (templateItem) => !types.includes(templateItem.templateId),
+ );
+ }
+
+ return prev;
+ },
+ // Removes the "duplicate" action on the Singletons (such as Home)
+ actions: (prev, { schemaType }) => {
+ if (types.includes(schemaType)) {
+ return prev.filter(({ action }) => action !== "duplicate");
+ }
+
+ return prev;
+ },
+ },
+ });
+};
+
+// The StructureResolver is how we're changing the DeskTool structure to linking to document (named Singleton)
+// like how "Home" is handled.
+export const pageStructure = (
+ typeDefArray: DocumentDefinition[],
+): StructureResolver => {
+ return (S) => {
+ // Goes through all of the singletons that were provided and translates them into something the
+ // Desktool can understand
+ const singletonItems = typeDefArray.map((typeDef) => {
+ return S.listItem()
+ .title(typeDef.title!)
+ .icon(typeDef.icon)
+ .child(
+ S.editor()
+ .id(typeDef.name)
+ .schemaType(typeDef.name)
+ .documentId(typeDef.name),
+ );
+ });
+
+ // The default root list items (except custom ones)
+ const defaultListItems = S.documentTypeListItems().filter(
+ (listItem) =>
+ !typeDefArray.find((singleton) => singleton.name === listItem.getId()),
+ );
+
+ return S.list()
+ .title("Content")
+ .items([...singletonItems, S.divider(), ...defaultListItems]);
+ };
+};
diff --git a/examples/cms-sanity/sanity/schemas/documents/author.ts b/examples/cms-sanity/sanity/schemas/documents/author.ts
new file mode 100644
index 00000000000000..cd0f867772d5ba
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/documents/author.ts
@@ -0,0 +1,37 @@
+import { UserIcon } from "@sanity/icons";
+import { defineField, defineType } from "sanity";
+
+export default defineType({
+ name: "author",
+ title: "Author",
+ icon: UserIcon,
+ type: "document",
+ fields: [
+ defineField({
+ name: "name",
+ title: "Name",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "picture",
+ title: "Picture",
+ type: "image",
+ fields: [
+ {
+ name: "alt",
+ type: "string",
+ title: "Alternative text",
+ description: "Important for SEO and accessiblity.",
+ },
+ ],
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ validation: (rule) => rule.required(),
+ }),
+ ],
+});
diff --git a/examples/cms-sanity/sanity/schemas/documents/post.ts b/examples/cms-sanity/sanity/schemas/documents/post.ts
new file mode 100644
index 00000000000000..2b359796e146e3
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/documents/post.ts
@@ -0,0 +1,102 @@
+import { BookIcon } from "@sanity/icons";
+import { format, parseISO } from "date-fns";
+import { defineField, defineType } from "sanity";
+
+import authorType from "./author";
+
+/**
+ * This file is the schema definition for a post.
+ *
+ * Here you'll be able to edit the different fields that appear when you
+ * create or edit a post in the studio.
+ *
+ * Here you can see the different schema types that are available:
+
+ https://www.sanity.io/docs/schema-types
+
+ */
+
+export default defineType({
+ name: "post",
+ title: "Post",
+ icon: BookIcon,
+ type: "document",
+ fields: [
+ defineField({
+ name: "title",
+ title: "Title",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "slug",
+ title: "Slug",
+ type: "slug",
+ description: "A slug is required for the post to show up in the preview",
+ options: {
+ source: "title",
+ maxLength: 96,
+ isUnique: (value, context) => context.defaultIsUnique(value, context),
+ },
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "content",
+ title: "Content",
+ type: "array",
+ of: [{ type: "block" }],
+ }),
+ defineField({
+ name: "excerpt",
+ title: "Excerpt",
+ type: "text",
+ }),
+ defineField({
+ name: "coverImage",
+ title: "Cover Image",
+ type: "image",
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ fields: [
+ {
+ name: "alt",
+ type: "string",
+ title: "Alternative text",
+ description: "Important for SEO and accessiblity.",
+ },
+ ],
+ }),
+ defineField({
+ name: "date",
+ title: "Date",
+ type: "datetime",
+ initialValue: () => new Date().toISOString(),
+ }),
+ defineField({
+ name: "author",
+ title: "Author",
+ type: "reference",
+ to: [{ type: authorType.name }],
+ }),
+ ],
+ preview: {
+ select: {
+ title: "title",
+ author: "author.name",
+ date: "date",
+ media: "coverImage",
+ },
+ prepare({ title, media, author, date }) {
+ const subtitles = [
+ author && `by ${author}`,
+ date && `on ${format(parseISO(date), "LLL d, yyyy")}`,
+ ].filter(Boolean);
+
+ return { title, media, subtitle: subtitles.join(" ") };
+ },
+ },
+});
diff --git a/examples/cms-sanity/sanity/schemas/singletons/settings.tsx b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx
new file mode 100644
index 00000000000000..5859a3da2d31ef
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx
@@ -0,0 +1,121 @@
+import { CogIcon } from "@sanity/icons";
+import { defineArrayMember, defineField, defineType } from "sanity";
+import * as demo from "@/sanity/lib/demo";
+
+export default defineType({
+ name: "settings",
+ title: "Settings",
+ type: "document",
+ icon: CogIcon,
+ fields: [
+ defineField({
+ name: "title",
+ description: "This field is the title of your blog.",
+ title: "Title",
+ type: "string",
+ initialValue: demo.title,
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "description",
+ description:
+ "Used both for the description tag for SEO, and the blog subheader.",
+ title: "Description",
+ type: "array",
+ initialValue: demo.description,
+ of: [
+ defineArrayMember({
+ type: "block",
+ options: {},
+ styles: [],
+ lists: [],
+ marks: {
+ decorators: [],
+ annotations: [
+ defineField({
+ type: "object",
+ name: "link",
+ fields: [
+ {
+ type: "string",
+ name: "href",
+ title: "URL",
+ validation: (rule) => rule.required(),
+ },
+ ],
+ }),
+ ],
+ },
+ }),
+ ],
+ }),
+ defineField({
+ name: "footer",
+ description:
+ "This is a block of text that will be displayed at the bottom of the page.",
+ title: "Footer Info",
+ type: "array",
+ of: [
+ defineArrayMember({
+ type: "block",
+ marks: {
+ annotations: [
+ {
+ name: "link",
+ type: "object",
+ title: "Link",
+ fields: [
+ {
+ name: "href",
+ type: "url",
+ title: "Url",
+ },
+ ],
+ },
+ ],
+ },
+ }),
+ ],
+ }),
+ defineField({
+ name: "ogImage",
+ title: "Open Graph Image",
+ type: "image",
+ description: "Displayed on social cards and search engine results.",
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ fields: [
+ defineField({
+ name: "alt",
+ description: "Important for accessibility and SEO.",
+ title: "Alternative text",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "metadataBase",
+ type: "url",
+ description: (
+
+ More information
+
+ ),
+ }),
+ ],
+ }),
+ ],
+ preview: {
+ prepare() {
+ return {
+ title: "Settings",
+ };
+ },
+ },
+});
diff --git a/examples/cms-sanity/studio/.gitignore b/examples/cms-sanity/studio/.gitignore
deleted file mode 100644
index d0c88091a7f2ef..00000000000000
--- a/examples/cms-sanity/studio/.gitignore
+++ /dev/null
@@ -1,35 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# production
-/dist
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
-
-.sanity
diff --git a/examples/cms-sanity/studio/copyEnv.js b/examples/cms-sanity/studio/copyEnv.js
deleted file mode 100644
index c447b4161ac3b0..00000000000000
--- a/examples/cms-sanity/studio/copyEnv.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const fs = require("fs");
-
-if (fs.existsSync("../.env")) {
- fs.copyFileSync("../.env", ".env.development");
-} else if (fs.existsSync("../.env.local")) {
- fs.copyFileSync("../.env.local", ".env.development");
-} else {
- throw new Error("No .env or .env.local file found at root of the project");
-}
diff --git a/examples/cms-sanity/studio/package.json b/examples/cms-sanity/studio/package.json
deleted file mode 100644
index d3591e67c02de8..00000000000000
--- a/examples/cms-sanity/studio/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "private": true,
- "scripts": {
- "start": "sanity dev",
- "dev": "sanity dev",
- "build": "sanity build",
- "cors:add": "npx sanity cors add",
- "deploy": "sanity deploy",
- "prestart": "npm run env",
- "predeploy": "npm run env",
- "env": "node copyEnv.js"
- },
- "dependencies": {
- "sanity": "^3.2.6",
- "@sanity/vision": "^3.2.6",
- "prop-types": "^15.7",
- "react": "^18",
- "react-dom": "^18",
- "sanity-plugin-asset-source-unsplash": "^1.0.6",
- "styled-components": "^5.2.0"
- },
- "devDependencies": {
- "autoprefixer": "^10.4.13",
- "postcss": "^8.4.21",
- "tailwindcss": "^3.2.4"
- }
-}
diff --git a/examples/cms-sanity/studio/plugins/.gitkeep b/examples/cms-sanity/studio/plugins/.gitkeep
deleted file mode 100644
index 2419d3d0600bd4..00000000000000
--- a/examples/cms-sanity/studio/plugins/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-User-specific packages can be placed here
diff --git a/examples/cms-sanity/studio/resolveProductionUrl.js b/examples/cms-sanity/studio/resolveProductionUrl.js
deleted file mode 100644
index f95c3d7d1908fa..00000000000000
--- a/examples/cms-sanity/studio/resolveProductionUrl.js
+++ /dev/null
@@ -1,21 +0,0 @@
-let productionUrl;
-try {
- productionUrl = new URL(
- import.meta.env.SANITY_STUDIO_PREVIEW_URL || "http://localhost:3000",
- );
-} catch (err) {
- console.error("Invalid productionUrl", err);
-}
-
-export function resolveProductionUrl(prev, { document }) {
- if (!productionUrl || !document.slug?.current) {
- return prev;
- }
- const searchParams = new URLSearchParams();
- searchParams.set(
- "secret",
- import.meta.env.SANITY_STUDIO_PREVIEW_SECRET || "",
- );
- searchParams.set("slug", document.slug.current);
- return `${productionUrl.origin}/api/preview?${searchParams}`;
-}
diff --git a/examples/cms-sanity/studio/sanity.config.js b/examples/cms-sanity/studio/sanity.config.js
deleted file mode 100644
index 7f718f17e0ff21..00000000000000
--- a/examples/cms-sanity/studio/sanity.config.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { visionTool } from "@sanity/vision";
-import { defineConfig } from "sanity";
-import { deskTool } from "sanity/desk";
-import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
-
-import { resolveProductionUrl } from "./resolveProductionUrl";
-import { author } from "./schemas/author";
-import { post } from "./schemas/post";
-
-const title =
- import.meta.env.NEXT_PUBLIC_SANITY_PROJECT_TITLE ||
- "Next.js Blog with Sanity.io";
-const projectId = import.meta.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
-const dataset = import.meta.env.NEXT_PUBLIC_SANITY_DATASET;
-
-export default defineConfig({
- basePath: "/",
- projectId: projectId || "",
- dataset: dataset || "",
- title,
- schema: {
- // If you want more content types, you can add them to this array
- types: [author, post],
- },
- document: {
- productionUrl: resolveProductionUrl,
- },
- plugins: [
- deskTool({}),
- // Add an image asset source for Unsplash
- unsplashImageAsset(),
- // Vision lets you query your content with GROQ in the studio
- // https://www.sanity.io/docs/the-vision-plugin
- visionTool(),
- ],
-});
diff --git a/examples/cms-sanity/studio/schemas/author.js b/examples/cms-sanity/studio/schemas/author.js
deleted file mode 100644
index 6c05e957cc645a..00000000000000
--- a/examples/cms-sanity/studio/schemas/author.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export const author = {
- name: "author",
- title: "Author",
- type: "document",
- fields: [
- {
- name: "name",
- title: "Name",
- type: "string",
- validation: (Rule) => Rule.required(),
- },
- {
- name: "picture",
- title: "Picture",
- type: "image",
- options: { hotspot: true },
- validation: (Rule) => Rule.required(),
- },
- ],
-};
diff --git a/examples/cms-sanity/studio/schemas/post.js b/examples/cms-sanity/studio/schemas/post.js
deleted file mode 100644
index 99556226f1f3f1..00000000000000
--- a/examples/cms-sanity/studio/schemas/post.js
+++ /dev/null
@@ -1,64 +0,0 @@
-export const post = {
- name: "post",
- title: "Post",
- type: "document",
- fields: [
- {
- name: "title",
- title: "Title",
- type: "string",
- validation: (Rule) => Rule.required(),
- },
- {
- name: "slug",
- title: "Slug",
- type: "slug",
- options: {
- source: "title",
- maxLength: 96,
- },
- validation: (Rule) => Rule.required(),
- },
- {
- name: "content",
- title: "Content",
- type: "array",
- of: [{ type: "block" }],
- },
- {
- name: "excerpt",
- title: "Excerpt",
- type: "string",
- },
- {
- name: "coverImage",
- title: "Cover Image",
- type: "image",
- options: {
- hotspot: true,
- },
- },
- {
- name: "date",
- title: "Date",
- type: "datetime",
- },
- {
- name: "author",
- title: "Author",
- type: "reference",
- to: [{ type: "author" }],
- },
- ],
- preview: {
- select: {
- title: "title",
- author: "author.name",
- media: "coverImage",
- },
- prepare(selection) {
- const { author } = selection;
- return { ...selection, subtitle: author && `by ${author}` };
- },
- },
-};
diff --git a/examples/cms-sanity/studio/static/.gitkeep b/examples/cms-sanity/studio/static/.gitkeep
deleted file mode 100644
index 37178a72a546a0..00000000000000
--- a/examples/cms-sanity/studio/static/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-Files placed here will be served by the Sanity server under the `/static`-prefix
diff --git a/examples/cms-sanity/studio/static/favicon.ico b/examples/cms-sanity/studio/static/favicon.ico
deleted file mode 100644
index 7305cdbf5c90a2..00000000000000
Binary files a/examples/cms-sanity/studio/static/favicon.ico and /dev/null differ
diff --git a/examples/cms-sanity/studio/tailwind.config.js b/examples/cms-sanity/studio/tailwind.config.js
deleted file mode 100644
index 8b4fa5cb74a54e..00000000000000
--- a/examples/cms-sanity/studio/tailwind.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./components/**/*.{js,ts,jsx,tsx}"],
- theme: {},
- plugins: [],
-};
diff --git a/examples/cms-sanity/tailwind.config.js b/examples/cms-sanity/tailwind.config.js
deleted file mode 100644
index a8f65a525d2cc4..00000000000000
--- a/examples/cms-sanity/tailwind.config.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- "./pages/**/*.{js,ts,jsx,tsx}",
- "./components/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {
- colors: {
- "accent-1": "#FAFAFA",
- "accent-2": "#EAEAEA",
- "accent-7": "#333",
- success: "#0070f3",
- cyan: "#79FFE1",
- },
- spacing: {
- 28: "7rem",
- },
- letterSpacing: {
- tighter: "-.04em",
- },
- lineHeight: {
- tight: 1.2,
- },
- fontSize: {
- "5xl": "2.5rem",
- "6xl": "2.75rem",
- "7xl": "4.5rem",
- "8xl": "6.25rem",
- },
- boxShadow: {
- small: "0 5px 10px rgba(0, 0, 0, 0.12)",
- medium: "0 8px 30px rgba(0, 0, 0, 0.12)",
- },
- },
- },
- plugins: [],
-};
diff --git a/examples/cms-sanity/tailwind.config.ts b/examples/cms-sanity/tailwind.config.ts
new file mode 100644
index 00000000000000..99eb4e61d89fd9
--- /dev/null
+++ b/examples/cms-sanity/tailwind.config.ts
@@ -0,0 +1,17 @@
+import type { Config } from "tailwindcss";
+import typography from "@tailwindcss/typography";
+
+export default {
+ content: ["./app/**/*.{ts,tsx}", "./sanity/**/*.{ts,tsx}"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-inter)"],
+ },
+ },
+ },
+ future: {
+ hoverOnlyWhenSupported: true,
+ },
+ plugins: [typography],
+} satisfies Config;
diff --git a/examples/cms-sanity/tsconfig.json b/examples/cms-sanity/tsconfig.json
new file mode 100644
index 00000000000000..e06a4454ab0627
--- /dev/null
+++ b/examples/cms-sanity/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
{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].
+
+[][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].
-
-[][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.
.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 (
+
+ ) : (
+
+ );
+
+ return (
+ {
+ 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 (
+ <>
+
+
+
+
+ );
+}
+
+function HeroPost({
+ title,
+ slug,
+ excerpt,
+ coverImage,
+ date,
+ author,
+}: Pick<
+ Post,
+ "title" | "coverImage" | "date" | "excerpt" | "author" | "slug"
+>) {
+ return (
+
+
+
+
+
+ );
+}
+
+export default async function Page() {
+ const [settings, heroPost] = await Promise.all([
+ sanityFetch({
+ query: settingsQuery,
+ }),
+ sanityFetch({ query: heroQuery }),
+ ]);
+
+ return (
+ {
+ 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 (
+ ;
+}
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 (
-
-
- );
-}
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 (
-
-
- );
-}
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 (
- <>
-
-
-
- );
-}
diff --git a/examples/cms-sanity/components/post-body.js b/examples/cms-sanity/components/post-body.js
deleted file mode 100644
index 9fd971ba60ce1e..00000000000000
--- a/examples/cms-sanity/components/post-body.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import markdownStyles from "./markdown-styles.module.css";
-import { PortableText } from "@portabletext/react";
-
-export default function PostBody({ content }) {
- return (
- {title}
- ;
-}
diff --git a/examples/cms-sanity/components/post-title.js b/examples/cms-sanity/components/post-title.js
deleted file mode 100644
index a15f06616ff9f0..00000000000000
--- a/examples/cms-sanity/components/post-title.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function PostTitle({ children }) {
- return (
- ;
- }
-
- return (
-
-
-
- {router.isFallback ? (
- Loading…
- ) : (
- <>
-
-
-
- {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
-
- {post.coverImage?.asset?._ref && (
-
- )}
-
-
-
-
-
- {morePosts.length > 0 && }
- >
- )}
-
-
- );
-}
diff --git a/examples/cms-sanity/components/section-separator.js b/examples/cms-sanity/components/section-separator.js
deleted file mode 100644
index b86fb50b7d07e3..00000000000000
--- a/examples/cms-sanity/components/section-separator.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function SectionSeparator() {
- return
; -} diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js deleted file mode 100644 index c8e58e78504577..00000000000000 --- a/examples/cms-sanity/lib/config.js +++ /dev/null @@ -1,15 +0,0 @@ -export const sanityConfig = { - // Find your project ID and dataset in `sanity.json` in your studio project - dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production", - projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, - useCdn: - typeof document !== "undefined" && process.env.NODE_ENV === "production", - // useCdn == true gives fast, cheap responses using a globally distributed cache. - // When in production the Sanity API is only queried on build-time, and on-demand when responding to webhooks. - // Thus the data need to be fresh and API response time is less important. - // When in development/working locally, it's more important to keep costs down as hot reloading can incurr a lot of API calls - // And every page load calls getStaticProps. - // To get the lowest latency, lowest cost, and latest data, use the Instant Preview mode - apiVersion: "2022-03-13", - // see https://www.sanity.io/docs/api-versioning for how versioning works -}; diff --git a/examples/cms-sanity/lib/constants.js b/examples/cms-sanity/lib/constants.js deleted file mode 100644 index 3db98b71341eaf..00000000000000 --- a/examples/cms-sanity/lib/constants.js +++ /dev/null @@ -1,5 +0,0 @@ -export const EXAMPLE_PATH = "cms-sanity"; -export const CMS_NAME = "Sanity"; -export const CMS_URL = "https://sanity.io/"; -export const HOME_OG_IMAGE_URL = - "https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Sanity**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTA1IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMWVtIj48dGl0bGU%2BU2FuaXR5PC90aXRsZT48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03OC4xNzkzIDcuOTkyNjFWMjEuMDAyOEg3My45MDMxVjEwLjIxMzhMNzguMTc5MyA3Ljk5MjYxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMjAuOTUxMSAyMS4zM0wzMC45NDQgMTYuMTA1MUwyOS43MTIxIDEyLjkxNDFMMjMuMTMzMiAxNS45ODIxTDIwLjk1MTEgMjEuMzNaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik03My45MDMxIDEwLjIwMjdMODQuNzQ0MyA0LjY1NDc3TDgyLjkxMjYgMS41NTcxTDczLjkwMzEgNS45NTk5N1YxMC4yMDI3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNNDMuMzcwNSA2Ljk2MjMzVjIxLjAwMjhIMzkuMjkyN1YxLjAwNzE0TDQzLjM3MDUgNi45NjIzM1oiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTI3LjEyOTkgNi4xODYxN0wyMC45NTExIDIxLjMzTDE3Ljc3MzEgMTguNTk0M0wyNS4xMzUzIDEuMDA3MTRMMjcuMTI5OSA2LjE4NjE3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTI1LjEzNTMgMS4wMDcxNEgyOS4zNDc3TDM3LjEzODYgMjEuMDAyOEgzMi44MjY5TDI1LjEzNTMgMS4wMDcxNFoiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIGQ9Ik00NC4wMDEyIDEuMDA3MTRMNTIuOTgyNCAxNC42NjgyVjIxLjAwMjhMMzkuMjkyNyAxLjAwNzE0SDQ0LjAwMTJaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNjQuOTE4MyAxLjAwNzE0SDYwLjY3MzlWMjEuMDA2M0g2NC45MTgzVjEuMDA3MTRaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNzMuOTAzMSA0LjY1NDc0SDY3LjM3VjEuMDA3MTRIODIuNTg2N0w4NC43NDQzIDQuNjU0NzRINzguMTc5M0g3My45MDMxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC41IiBkPSJNOTcuMjc1NCAxMy40MTUzVjIxLjAwMjhIOTMuMDYyOVYxMy40MTUzIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNOTMuMDYyOSAxMy40MTUyTDEwMC4xOTEgMS4wMDcxNEgxMDQuNjY2TDk3LjI3NTQgMTMuNDE1Mkg5My4wNjI5WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNOTMuMDYzIDEzLjQxNTJMODUuNzM2MyAxLjAwNzE0SDkwLjM0NTZMOTUuMzA5MiA5LjUxMDA4TDkzLjA2MyAxMy40MTUyWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTEuOTYxMjYgMy4zMTQ3OUMxLjk2MTI2IDYuMDk5MjEgMy43MTE0NSA3Ljc1NTk1IDcuMjE1MzYgOC42Mjk1NkwxMC45MjgzIDkuNDc1MzNDMTQuMjQ0NCAxMC4yMjM2IDE2LjI2MzkgMTIuMDgyMiAxNi4yNjM5IDE1LjExMDNDMTYuMjg5NyAxNi40Mjk1IDE1Ljg1MzEgMTcuNzE3MyAxNS4wMjc0IDE4Ljc1NzlDMTUuMDI3NCAxNS43MzY4IDEzLjQzNjcgMTQuMTA0NCA5LjU5OTcyIDEzLjEyMjlMNS45NTQwOSAxMi4zMDg1QzMuMDM0NzUgMTEuNjU0MSAwLjc4MTQ3OCAxMC4xMjYyIDAuNzgxNDc4IDYuODM3MDlDMC43NjYxMjMgNS41NjY5MyAxLjE4MTE2IDQuMzI3ODEgMS45NjEyNiAzLjMxNDc5IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik01Mi45ODI0IDEzLjY0MTVWMS4wMDcxNEg1Ny4wNjAyVjIxLjAwMjhINTIuOTgyNFYxMy42NDE1WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMTIuNzQ1OCAxNC4zNjg5QzE0LjMyOTQgMTUuMzY0MyAxNS4wMjM4IDE2Ljc1NjUgMTUuMDIzOCAxOC43NTQ0QzEzLjcxMyAyMC40MDQxIDExLjQxMDEgMjEuMzMgOC43MDMzMyAyMS4zM0M0LjE0NzE4IDIxLjMzIDAuOTU4NTc3IDE5LjEyNjggMC4yNSAxNS4yOTgySDQuNjI1NDdDNS4xODg3OCAxNy4wNTU5IDYuNjgwMzQgMTcuODcwMyA4LjY3MTQ0IDE3Ljg3MDNDMTEuMTAxOSAxNy44NzAzIDEyLjcxNzQgMTYuNTk2NCAxMi43NDkzIDE0LjM2MTkiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNyIgZD0iTTQuMjM1NjcgNy40NDI2N0MzLjUxMjUgNy4wMjA0NSAyLjkxOTIgNi40MTM3NSAyLjUxODczIDUuNjg2OTdDMi4xMTgyNyA0Ljk2MDE5IDEuOTI1NTggNC4xNDA0NSAxLjk2MTEzIDMuMzE0NzZDMy4yMjU5NCAxLjY3ODkxIDUuNDI2MDggMC42Nzk5OTMgOC4xMDgwNCAwLjY3OTk5M0MxMi43NDkyIDAuNjc5OTkzIDE1LjQzNDcgMy4wODg1MiAxNi4wOTcyIDYuNDc4NTZIMTEuODg4M0MxMS40MjQyIDUuMTQyMDMgMTAuMjYyMSA0LjEwMTM2IDguMTQzNDcgNC4xMDEzNkM1Ljg3OTU3IDQuMTAxMzYgNC4zMzQ4NyA1LjM5NjExIDQuMjQ2MjkgNy40NDI2NyIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPC9zdmc%2B&widths=undefined&widths=auto&heights=250&heights=150"; diff --git a/examples/cms-sanity/lib/queries.js b/examples/cms-sanity/lib/queries.js deleted file mode 100644 index 89330900c4be1e..00000000000000 --- a/examples/cms-sanity/lib/queries.js +++ /dev/null @@ -1,37 +0,0 @@ -const postFields = ` - _id, - name, - title, - date, - excerpt, - coverImage, - "slug": slug.current, - "author": author->{name, picture}, -`; - -export const indexQuery = ` -*[_type == "post"] | order(date desc, _updatedAt desc) { - ${postFields} -}`; - -export const postQuery = ` -{ - "post": *[_type == "post" && slug.current == $slug] | order(_updatedAt desc) [0] { - content, - ${postFields} - }, - "morePosts": *[_type == "post" && slug.current != $slug] | order(date desc, _updatedAt desc) [0...2] { - content, - ${postFields} - } -}`; - -export const postSlugsQuery = ` -*[_type == "post" && defined(slug.current)][].slug.current -`; - -export const postBySlugQuery = ` -*[_type == "post" && slug.current == $slug][0] { - ${postFields} -} -`; diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js deleted file mode 100644 index 2492667bea9b60..00000000000000 --- a/examples/cms-sanity/lib/sanity.js +++ /dev/null @@ -1,13 +0,0 @@ -import createImageUrlBuilder from "@sanity/image-url"; -import { definePreview } from "next-sanity/preview"; -import { sanityConfig } from "./config"; - -export const imageBuilder = createImageUrlBuilder(sanityConfig); - -export const urlForImage = (source) => - imageBuilder.image(source).auto("format").fit("max"); - -export const usePreview = definePreview({ - projectId: sanityConfig.projectId, - dataset: sanityConfig.dataset, -}); diff --git a/examples/cms-sanity/lib/sanity.server.js b/examples/cms-sanity/lib/sanity.server.js deleted file mode 100644 index 4a0d7be3bf8cb9..00000000000000 --- a/examples/cms-sanity/lib/sanity.server.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Server-side Sanity utilities. By having these in a separate file from the - * utilities we use on the client side, we are able to tree-shake (remove) - * code that is not used on the client side. - */ -import { createClient } from "next-sanity"; -import { sanityConfig } from "./config"; - -export const sanityClient = createClient(sanityConfig); - -export const previewClient = createClient({ - ...sanityConfig, - useCdn: false, - // Fallback to using the WRITE token until https://www.sanity.io/docs/vercel-integration starts shipping a READ token. - // As this client only exists on the server and the token is never shared with the browser, we don't risk escalating permissions to untrustworthy users - token: - process.env.SANITY_API_READ_TOKEN || process.env.SANITY_API_WRITE_TOKEN, -}); - -export const getClient = (preview) => (preview ? previewClient : sanityClient); - -export function overlayDrafts(docs) { - const documents = docs || []; - const overlayed = documents.reduce((map, doc) => { - if (!doc._id) { - throw new Error("Ensure that `_id` is included in query projection"); - } - - const isDraft = doc._id.startsWith("drafts."); - const id = isDraft ? doc._id.slice(7) : doc._id; - return isDraft || !map.has(id) ? map.set(id, doc) : map; - }, new Map()); - - return Array.from(overlayed.values()); -} diff --git a/examples/cms-sanity/next.config.js b/examples/cms-sanity/next.config.js index 553cc9b8363a48..44b910b28d8e8a 100644 --- a/examples/cms-sanity/next.config.js +++ b/examples/cms-sanity/next.config.js @@ -1,9 +1,18 @@ /** @type {import('next').NextConfig} */ module.exports = { + compiler: { + styledComponents: true, + }, + logging: { + fetches: { + fullUrl: true, + }, + }, + experimental: { + // Used to guard against accidentally leaking SANITY_API_READ_TOKEN to the browser + taint: true, + }, images: { - remotePatterns: [ - { hostname: "cdn.sanity.io" }, - { hostname: "source.unsplash.com" }, - ], + remotePatterns: [{ hostname: "cdn.sanity.io" }], }, }; diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 22addbb2a2a9b3..d3e739dadb10e5 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -4,23 +4,40 @@ "dev": "next", "build": "next build", "start": "next start", - "studio:dev": "npm --prefix studio run start", - "studio:deploy": "npx vercel env pull && npm --prefix studio run deploy" + "lint": "next lint", + "presetup": "echo 'about to setup env variables, follow the guide here: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli'", + "setup": "npx sanity@latest init --env .env.local", + "postsetup": "echo 'create the read token by following the rest of the guide: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#creating-a-read-token'" }, "dependencies": { - "@portabletext/react": "^2.0.1", + "@portabletext/react": "^3.0.11", + "@sanity/assist": "^2.0.1", + "@sanity/icons": "^2.10.3", "@sanity/image-url": "^1.0.2", - "@sanity/webhook": "^2.0.0", - "classnames": "^2.3.1", - "date-fns": "^2.29.3", + "@sanity/vision": "^3.31.0", + "@tailwindcss/typography": "^0.5.10", + "@types/node": "^20.11.24", + "@types/react": "^18.2.63", + "@types/react-dom": "^18.2.20", + "@vercel/speed-insights": "^1.0.10", + "autoprefixer": "^10.4.18", + "date-fns": "^3.3.1", + "get-youtube-id": "^1.0.1", "next": "latest", - "next-sanity": "^4.1.2", - "react": "^18", - "react-dom": "^18" + "next-sanity": "^8.1.5", + "postcss": "^8.4.35", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-lite-youtube-embed": "^2.4.0", + "sanity": "^3.31.0", + "sanity-plugin-asset-source-unsplash": "^1.1.2", + "server-only": "^0.0.1", + "styled-components": "^6.1.8", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" }, "devDependencies": { - "autoprefixer": "^10.4.13", - "postcss": "^8.4.21", - "tailwindcss": "^3.2.4" + "eslint": "^8.57.0", + "eslint-config-next": "latest" } } diff --git a/examples/cms-sanity/pages/_app.js b/examples/cms-sanity/pages/_app.js deleted file mode 100644 index aa7931ca32e943..00000000000000 --- a/examples/cms-sanity/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import "../styles/index.css"; - -function MyApp({ Component, pageProps }) { - return ;
-}
-
-export default MyApp;
diff --git a/examples/cms-sanity/pages/_document.js b/examples/cms-sanity/pages/_document.js
deleted file mode 100644
index b2fff8b4262dde..00000000000000
--- a/examples/cms-sanity/pages/_document.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/cms-sanity/pages/api/exit-preview.js b/examples/cms-sanity/pages/api/exit-preview.js
deleted file mode 100644
index 16b744e88e34aa..00000000000000
--- a/examples/cms-sanity/pages/api/exit-preview.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default async function exit(_, res) {
- // Exit Draft Mode by removing the cookie
- res.setDraftMode({ enable: false });
-
- // Redirect the user back to the index page.
- res.writeHead(307, { Location: "/" });
- res.end();
-}
diff --git a/examples/cms-sanity/pages/api/preview.js b/examples/cms-sanity/pages/api/preview.js
deleted file mode 100644
index 8035d1564cb9cc..00000000000000
--- a/examples/cms-sanity/pages/api/preview.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { postBySlugQuery } from "../../lib/queries";
-import { previewClient } from "../../lib/sanity.server";
-
-function redirectToPreview(res, Location) {
- // Enable Draft Mode by setting the cookie
- res.setDraftMode({ enable: true });
- // Redirect to a preview capable route
- res.writeHead(307, { Location });
- res.end();
-}
-
-export default async function preview(req, res) {
- const secret = process.env.SANITY_STUDIO_PREVIEW_SECRET;
- // Only require a secret when in production
- if (!secret && process.env.NODE_ENV === "production") {
- throw new TypeError(`Missing SANITY_STUDIO_PREVIEW_SECRET`);
- }
- // Check the secret if it's provided, enables running preview mode locally before the env var is setup
- if (secret && req.query.secret !== secret) {
- return res.status(401).json({ message: "Invalid secret" });
- }
- // If no slug is provided open preview mode on the frontpage
- if (!req.query.slug) {
- return redirectToPreview(res, "/");
- }
-
- // Check if the post with the given `slug` exists
- const post = await previewClient.fetch(postBySlugQuery, {
- slug: req.query.slug,
- });
-
- // If the slug doesn't exist prevent preview mode from being enabled
- if (!post) {
- return res.status(401).json({ message: "Invalid slug" });
- }
-
- // Redirect to the path from the fetched post
- // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
- redirectToPreview(res, `/posts/${post.slug}`);
-}
diff --git a/examples/cms-sanity/pages/api/revalidate.js b/examples/cms-sanity/pages/api/revalidate.js
deleted file mode 100644
index b5add334c75345..00000000000000
--- a/examples/cms-sanity/pages/api/revalidate.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook";
-import { sanityClient } from "../../lib/sanity.server";
-
-// Next.js will by default parse the body, which can lead to invalid signatures
-export const config = {
- api: {
- bodyParser: false,
- },
-};
-
-const AUTHOR_UPDATED_QUERY = /* groq */ `
- *[_type == "author" && _id == $id] {
- "slug": *[_type == "post" && references(^._id)].slug.current
- }["slug"][]`;
-const POST_UPDATED_QUERY = /* groq */ `*[_type == "post" && _id == $id].slug.current`;
-
-const getQueryForType = (type) => {
- switch (type) {
- case "author":
- return AUTHOR_UPDATED_QUERY;
- case "post":
- return POST_UPDATED_QUERY;
- default:
- throw new TypeError(`Unknown type: ${type}`);
- }
-};
-
-const log = (msg, error) =>
- console[error ? "error" : "log"](`[revalidate] ${msg}`);
-
-async function readBody(readable) {
- const chunks = [];
- for await (const chunk of readable) {
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
- }
- return Buffer.concat(chunks).toString("utf8");
-}
-
-export default async function revalidate(req, res) {
- const signature = req.headers[SIGNATURE_HEADER_NAME];
- const body = await readBody(req); // Read the body into a string
- if (
- !isValidSignature(
- body,
- signature,
- process.env.SANITY_REVALIDATE_SECRET?.trim(),
- )
- ) {
- const invalidSignature = "Invalid signature";
- log(invalidSignature, true);
- res.status(401).json({ success: false, message: invalidSignature });
- return;
- }
-
- const jsonBody = JSON.parse(body);
- const { _id: id, _type } = jsonBody;
- if (typeof id !== "string" || !id) {
- const invalidId = "Invalid _id";
- log(invalidId, true);
- return res.status(400).json({ message: invalidId });
- }
-
- log(`Querying post slug for _id '${id}', type '${_type}' ..`);
- const slug = await sanityClient.fetch(getQueryForType(_type), { id });
- const slugs = (Array.isArray(slug) ? slug : [slug]).map(
- (_slug) => `/posts/${_slug}`,
- );
- const staleRoutes = ["/", ...slugs];
-
- try {
- await Promise.all(staleRoutes.map((route) => res.revalidate(route)));
- const updatedRoutes = `Updated routes: ${staleRoutes.join(", ")}`;
- log(updatedRoutes);
- return res.status(200).json({ message: updatedRoutes });
- } catch (err) {
- log(err.message, true);
- return res.status(500).json({ message: err.message });
- }
-}
diff --git a/examples/cms-sanity/pages/index.js b/examples/cms-sanity/pages/index.js
deleted file mode 100644
index 682af4bbcfe502..00000000000000
--- a/examples/cms-sanity/pages/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { indexQuery } from "../lib/queries";
-import { getClient, overlayDrafts } from "../lib/sanity.server";
-import { PreviewSuspense } from "next-sanity/preview";
-import { lazy } from "react";
-import Landing from "../components/landing";
-
-const LandingPreview = lazy(() => import("../components/landing-preview"));
-
-export default function IndexPage({ allPosts, preview }) {
- if (preview) {
- return (
-
-
-
- );
- }
-
- return ;
-}
-
-export async function getStaticProps({ preview = false }) {
- const allPosts = overlayDrafts(await getClient(preview).fetch(indexQuery));
- return {
- props: { allPosts, preview },
- // If webhooks isn't setup then attempt to re-generate in 1 minute intervals
- revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
- };
-}
diff --git a/examples/cms-sanity/pages/posts/[slug].js b/examples/cms-sanity/pages/posts/[slug].js
deleted file mode 100644
index 2332dd291e6f59..00000000000000
--- a/examples/cms-sanity/pages/posts/[slug].js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { lazy } from "react";
-import { PreviewSuspense } from "next-sanity/preview";
-import { postQuery, postSlugsQuery } from "../../lib/queries";
-import {
- getClient,
- overlayDrafts,
- sanityClient,
-} from "../../lib/sanity.server";
-import Post from "../../components/post";
-
-const PostPreview = lazy(() => import("../../components/post-preview"));
-
-export default function PostPage({ preview, data }) {
- if (preview) {
- return (
-
-
-
- );
- }
-
- return ;
-}
-
-export async function getStaticProps({ params, preview = false }) {
- const { post, morePosts } = await getClient(preview).fetch(postQuery, {
- slug: params.slug,
- });
-
- return {
- props: {
- preview,
- data: {
- post,
- morePosts: overlayDrafts(morePosts),
- },
- },
- // If webhooks isn't setup then attempt to re-generate in 1 minute intervals
- revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
- };
-}
-
-export async function getStaticPaths() {
- const paths = await sanityClient.fetch(postSlugsQuery);
- return {
- paths: paths.map((slug) => ({ params: { slug } })),
- fallback: true,
- };
-}
diff --git a/examples/cms-sanity/postcss.config.js b/examples/cms-sanity/postcss.config.js
index 3687d286ecd7c9..12a703d900da81 100644
--- a/examples/cms-sanity/postcss.config.js
+++ b/examples/cms-sanity/postcss.config.js
@@ -1,5 +1,3 @@
-// If you want to use other PostCSS plugins, see the following:
-// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
diff --git a/examples/cms-sanity/public/favicon/android-chrome-192x192.png b/examples/cms-sanity/public/favicon/android-chrome-192x192.png
deleted file mode 100644
index 2f07282a59cdad..00000000000000
Binary files a/examples/cms-sanity/public/favicon/android-chrome-192x192.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/android-chrome-512x512.png b/examples/cms-sanity/public/favicon/android-chrome-512x512.png
deleted file mode 100644
index dbb0faea84049b..00000000000000
Binary files a/examples/cms-sanity/public/favicon/android-chrome-512x512.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/apple-touch-icon.png b/examples/cms-sanity/public/favicon/apple-touch-icon.png
deleted file mode 100644
index 8f4033b2a8b352..00000000000000
Binary files a/examples/cms-sanity/public/favicon/apple-touch-icon.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/browserconfig.xml b/examples/cms-sanity/public/favicon/browserconfig.xml
deleted file mode 100644
index 9824d87b11517d..00000000000000
--- a/examples/cms-sanity/public/favicon/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- #000000
-
-
-
diff --git a/examples/cms-sanity/public/favicon/favicon-16x16.png b/examples/cms-sanity/public/favicon/favicon-16x16.png
deleted file mode 100644
index 29deaf6716e774..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon-16x16.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/favicon-32x32.png b/examples/cms-sanity/public/favicon/favicon-32x32.png
deleted file mode 100644
index e3b4277bf093d2..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon-32x32.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/favicon.ico b/examples/cms-sanity/public/favicon/favicon.ico
deleted file mode 100644
index ea2f437d9db655..00000000000000
Binary files a/examples/cms-sanity/public/favicon/favicon.ico and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/mstile-150x150.png b/examples/cms-sanity/public/favicon/mstile-150x150.png
deleted file mode 100644
index f2dfd904bf1be6..00000000000000
Binary files a/examples/cms-sanity/public/favicon/mstile-150x150.png and /dev/null differ
diff --git a/examples/cms-sanity/public/favicon/safari-pinned-tab.svg b/examples/cms-sanity/public/favicon/safari-pinned-tab.svg
deleted file mode 100644
index 72ab6e050cb113..00000000000000
--- a/examples/cms-sanity/public/favicon/safari-pinned-tab.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/examples/cms-sanity/public/favicon/site.webmanifest b/examples/cms-sanity/public/favicon/site.webmanifest
deleted file mode 100644
index a672d9a233c59a..00000000000000
--- a/examples/cms-sanity/public/favicon/site.webmanifest
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "Next.js",
- "short_name": "Next.js",
- "icons": [
- {
- "src": "/favicons/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/favicons/android-chrome-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "theme_color": "#000000",
- "background_color": "#000000",
- "display": "standalone"
-}
diff --git a/examples/cms-sanity/studio/sanity.cli.js b/examples/cms-sanity/sanity.cli.ts
similarity index 64%
rename from examples/cms-sanity/studio/sanity.cli.js
rename to examples/cms-sanity/sanity.cli.ts
index 84dd1a717dd3ff..44500feff30fe4 100644
--- a/examples/cms-sanity/studio/sanity.cli.js
+++ b/examples/cms-sanity/sanity.cli.ts
@@ -7,12 +7,4 @@ loadEnvConfig(__dirname, dev, { info: () => null, error: console.error });
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
-export default defineCliConfig({
- api: { projectId, dataset },
- vite: (config) => {
- return {
- ...config,
- envPrefix: ["NEXT_", "SANITY_STUDIO_", "VITE_"],
- };
- },
-});
+export default defineCliConfig({ api: { projectId, dataset } });
diff --git a/examples/cms-sanity/sanity.config.ts b/examples/cms-sanity/sanity.config.ts
new file mode 100644
index 00000000000000..6c4c9925c7903b
--- /dev/null
+++ b/examples/cms-sanity/sanity.config.ts
@@ -0,0 +1,48 @@
+"use client";
+/**
+ * This config is used to set up Sanity Studio that's mounted on the `app/(sanity)/studio/[[...tool]]/page.tsx` route
+ */
+import { assist } from "@sanity/assist";
+import { visionTool } from "@sanity/vision";
+import { PluginOptions, defineConfig } from "sanity";
+import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
+import { presentationTool } from "sanity/presentation";
+import { structureTool } from "sanity/structure";
+
+import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
+import { locate } from "@/sanity/plugins/locate";
+import { pageStructure, singletonPlugin } from "@/sanity/plugins/settings";
+import author from "@/sanity/schemas/documents/author";
+import post from "@/sanity/schemas/documents/post";
+import settings from "@/sanity/schemas/singletons/settings";
+
+export default defineConfig({
+ basePath: studioUrl,
+ projectId,
+ dataset,
+ schema: {
+ types: [
+ // Singletons
+ settings,
+ // Documents
+ post,
+ author,
+ ],
+ },
+ plugins: [
+ assist(),
+ presentationTool({
+ locate,
+ previewUrl: { previewMode: { enable: "/api/draft" } },
+ }),
+ structureTool({ structure: pageStructure([settings]) }),
+ // Configures the global "new document" button, and document actions, to suit the Settings document singleton
+ singletonPlugin([settings.name]),
+ // Add an image asset source for Unsplash
+ unsplashImageAsset(),
+ // Vision lets you query your content with GROQ in the studio
+ // https://www.sanity.io/docs/the-vision-plugin
+ process.env.NODE_ENV === "development" &&
+ visionTool({ defaultApiVersion: apiVersion }),
+ ].filter(Boolean) as PluginOptions[],
+});
diff --git a/examples/cms-sanity/sanity/lib/api.ts b/examples/cms-sanity/sanity/lib/api.ts
new file mode 100644
index 00000000000000..c84eaafb86a737
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/api.ts
@@ -0,0 +1,33 @@
+/**
+ * As this file is reused in several other files, try to keep it lean and small.
+ * Importing other npm packages here could lead to needlessly increasing the client bundle size, or end up in a server-only function that don't need it.
+ */
+
+function assertValue(v: T | undefined, errorMessage: string): T {
+ if (v === undefined) {
+ throw new Error(errorMessage);
+ }
+
+ return v;
+}
+
+export const dataset = assertValue(
+ process.env.NEXT_PUBLIC_SANITY_DATASET,
+ "Missing environment variable: NEXT_PUBLIC_SANITY_DATASET",
+);
+
+export const projectId = assertValue(
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
+ "Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID",
+);
+
+/**
+ * see https://www.sanity.io/docs/api-versioning for how versioning works
+ */
+export const apiVersion =
+ process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-28";
+
+/**
+ * Used to configure edit intent links, for Presentation Mode, as well as to configure where the Studio is mounted in the router.
+ */
+export const studioUrl = "/studio";
diff --git a/examples/cms-sanity/sanity/lib/client.ts b/examples/cms-sanity/sanity/lib/client.ts
new file mode 100644
index 00000000000000..ba149ef011293b
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/client.ts
@@ -0,0 +1,22 @@
+import { createClient } from "next-sanity";
+
+import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
+
+export const client = createClient({
+ projectId,
+ dataset,
+ apiVersion,
+ useCdn: true,
+ perspective: "published",
+ stega: {
+ studioUrl,
+ logger: console,
+ filter: (props) => {
+ if (props.sourcePath.at(-1) === "title") {
+ return true;
+ }
+
+ return props.filterDefault(props);
+ },
+ },
+});
diff --git a/examples/cms-sanity/sanity/lib/demo.ts b/examples/cms-sanity/sanity/lib/demo.ts
new file mode 100644
index 00000000000000..8c68844102ebec
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/demo.ts
@@ -0,0 +1,59 @@
+/**
+ * Demo data used as placeholders and initial values for the blog
+ */
+
+export const title = "Blog.";
+
+export const description = [
+ {
+ _key: "9f1a629887fd",
+ _type: "block",
+ children: [
+ {
+ _key: "4a58edd077880",
+ _type: "span",
+ marks: [],
+ text: "A statically generated blog example using ",
+ },
+ {
+ _key: "4a58edd077881",
+ _type: "span",
+ marks: ["ec5b66c9b1e0"],
+ text: "Next.js",
+ },
+ {
+ _key: "4a58edd077882",
+ _type: "span",
+ marks: [],
+ text: " and ",
+ },
+ {
+ _key: "4a58edd077883",
+ _type: "span",
+ marks: ["1f8991913ea8"],
+ text: "Sanity",
+ },
+ {
+ _key: "4a58edd077884",
+ _type: "span",
+ marks: [],
+ text: ".",
+ },
+ ],
+ markDefs: [
+ {
+ _key: "ec5b66c9b1e0",
+ _type: "link",
+ href: "https://nextjs.org/",
+ },
+ {
+ _key: "1f8991913ea8",
+ _type: "link",
+ href: "https://sanity.io/",
+ },
+ ],
+ style: "normal",
+ },
+];
+
+export const ogImageTitle = "A Next.js Blog with a Native Authoring Experience";
diff --git a/examples/cms-sanity/sanity/lib/fetch.ts b/examples/cms-sanity/sanity/lib/fetch.ts
new file mode 100644
index 00000000000000..91796e437cf50a
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/fetch.ts
@@ -0,0 +1,51 @@
+import type { ClientPerspective, QueryParams } from "next-sanity";
+import { draftMode } from "next/headers";
+
+import { client } from "@/sanity/lib/client";
+import { token } from "@/sanity/lib/token";
+
+/**
+ * Used to fetch data in Server Components, it has built in support for handling Draft Mode and perspectives.
+ * When using the "published" perspective then time-based revalidation is used, set to match the time-to-live on Sanity's API CDN (60 seconds)
+ * and will also fetch from the CDN.
+ * When using the "previewDrafts" perspective then the data is fetched from the live API and isn't cached, it will also fetch draft content that isn't published yet.
+ */
+export async function sanityFetch({
+ query,
+ params = {},
+ perspective = draftMode().isEnabled ? "previewDrafts" : "published",
+ /**
+ * Stega embedded Content Source Maps are used by Visual Editing by both the Sanity Presentation Tool and Vercel Visual Editing.
+ * The Sanity Presentation Tool will enable Draft Mode when loading up the live preview, and we use it as a signal for when to embed source maps.
+ * When outside of the Sanity Studio we also support the Vercel Toolbar Visual Editing feature, which is only enabled in production when it's a Vercel Preview Deployment.
+ */
+ stega = perspective === "previewDrafts" ||
+ process.env.VERCEL_ENV === "preview",
+}: {
+ query: string;
+ params?: QueryParams;
+ perspective?: Omit;
+ stega?: boolean;
+}) {
+ if (perspective === "previewDrafts") {
+ return client.fetch(query, params, {
+ stega,
+ perspective: "previewDrafts",
+ // The token is required to fetch draft content
+ token,
+ // The `previewDrafts` perspective isn't available on the API CDN
+ useCdn: false,
+ // And we can't cache the responses as it would slow down the live preview experience
+ next: { revalidate: 0 },
+ });
+ }
+ return client.fetch(query, params, {
+ stega,
+ perspective: "published",
+ // The `published` perspective is available on the API CDN
+ useCdn: true,
+ // Only enable Stega in production if it's a Vercel Preview Deployment, as the Vercel Toolbar supports Visual Editing
+ // When using the `published` perspective we use time-based revalidation to match the time-to-live on Sanity's API CDN (60 seconds)
+ next: { revalidate: 60 },
+ });
+}
diff --git a/examples/cms-sanity/sanity/lib/queries.ts b/examples/cms-sanity/sanity/lib/queries.ts
new file mode 100644
index 00000000000000..d5b8975dca8453
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/queries.ts
@@ -0,0 +1,62 @@
+import type { PortableTextBlock } from "@portabletext/types";
+import { groq } from "next-sanity";
+import type { Image } from "sanity";
+
+export const settingsQuery = groq`*[_type == "settings"][0]`;
+export interface SettingsQueryResponse {
+ title?: string;
+ description?: PortableTextBlock[];
+ footer?: PortableTextBlock[];
+ ogImage?: (Image & { alt?: string; metadataBase?: string }) | null;
+}
+
+export interface Author {
+ name: string;
+ picture?: (Image & { alt?: string | null }) | null;
+}
+export interface Post {
+ _id: string;
+ status: "draft" | "published";
+ title: string;
+ slug: string;
+ excerpt?: string | null;
+ coverImage?: (Image & { alt?: string }) | null;
+ date: string;
+ author?: Author | null;
+}
+
+const postFields = groq`
+ _id,
+ "status": select(_originalId in path("drafts.**") => "draft", "published"),
+ "title": coalesce(title, "Untitled"),
+ "slug": slug.current,
+ excerpt,
+ coverImage,
+ "date": coalesce(date, _updatedAt),
+ "author": author->{"name": coalesce(name, "Anonymous"), picture},
+`;
+
+export const heroQuery = groq`*[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] {
+ content,
+ ${postFields}
+}`;
+export type HeroQueryResponse =
+ | (Post & {
+ content?: PortableTextBlock[] | null;
+ })
+ | null;
+
+export const moreStoriesQuery = groq`*[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {
+ ${postFields}
+}`;
+export type MoreStoriesQueryResponse = Post[] | null;
+
+export const postQuery = groq`*[_type == "post" && slug.current == $slug] [0] {
+ content,
+ ${postFields}
+}`;
+export type PostQueryResponse =
+ | (Post & {
+ content?: PortableTextBlock[] | null;
+ })
+ | null;
diff --git a/examples/cms-sanity/sanity/lib/token.ts b/examples/cms-sanity/sanity/lib/token.ts
new file mode 100644
index 00000000000000..dd8757abdb724c
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/token.ts
@@ -0,0 +1,15 @@
+import "server-only";
+
+import { experimental_taintUniqueValue } from "react";
+
+export const token = process.env.SANITY_API_READ_TOKEN;
+
+if (!token) {
+ throw new Error("Missing SANITY_API_READ_TOKEN");
+}
+
+experimental_taintUniqueValue(
+ "Do not pass the sanity API read token to the client.",
+ process,
+ token,
+);
diff --git a/examples/cms-sanity/sanity/lib/utils.ts b/examples/cms-sanity/sanity/lib/utils.ts
new file mode 100644
index 00000000000000..0af9a97bed2433
--- /dev/null
+++ b/examples/cms-sanity/sanity/lib/utils.ts
@@ -0,0 +1,37 @@
+import createImageUrlBuilder from "@sanity/image-url";
+
+import { dataset, projectId } from "@/sanity/lib/api";
+
+const imageBuilder = createImageUrlBuilder({
+ projectId: projectId || "",
+ dataset: dataset || "",
+});
+
+export const urlForImage = (source: any) => {
+ // Ensure that source image contains a valid reference
+ if (!source?.asset?._ref) {
+ return undefined;
+ }
+
+ return imageBuilder?.image(source).auto("format").fit("max");
+};
+
+export function resolveOpenGraphImage(image: any, width = 1200, height = 627) {
+ if (!image) return;
+ const url = urlForImage(image)?.width(1200).height(627).fit("crop").url();
+ if (!url) return;
+ return { url, alt: image?.alt as string, width, height };
+}
+
+export function resolveHref(
+ documentType?: string,
+ slug?: string,
+): string | undefined {
+ switch (documentType) {
+ case "post":
+ return slug ? `/posts/${slug}` : undefined;
+ default:
+ console.warn("Invalid document type:", documentType);
+ return undefined;
+ }
+}
diff --git a/examples/cms-sanity/sanity/plugins/locate.ts b/examples/cms-sanity/sanity/plugins/locate.ts
new file mode 100644
index 00000000000000..5d3654bc7b905f
--- /dev/null
+++ b/examples/cms-sanity/sanity/plugins/locate.ts
@@ -0,0 +1,101 @@
+import { map, Observable } from "rxjs";
+import {
+ DocumentLocationResolver,
+ DocumentLocationsState,
+} from "sanity/presentation";
+
+import { resolveHref } from "@/sanity/lib/utils";
+
+export const locate: DocumentLocationResolver = (params, context) => {
+ if (params.type === "settings") {
+ return {
+ message: "This document is used on all pages",
+ tone: "caution",
+ } satisfies DocumentLocationsState;
+ }
+
+ if (
+ params.type === "home" ||
+ params.type === "page" ||
+ params.type === "project"
+ ) {
+ const doc$ = context.documentStore.listenQuery(
+ `*[_id==$id || references($id)]{_type,slug,title}`,
+ params,
+ { perspective: "previewDrafts" },
+ ) as Observable<
+ | {
+ _type: string;
+ slug: { current: string };
+ title: string | null;
+ }[]
+ | null
+ >;
+ return doc$.pipe(
+ map((docs) => {
+ const isReferencedBySettings = docs?.some(
+ (doc) => doc._type === "settings",
+ );
+ switch (params.type) {
+ case "home":
+ return isReferencedBySettings
+ ? ({
+ locations: [
+ {
+ title:
+ docs?.find((doc) => doc._type === "home")?.title ||
+ "Home",
+ href: resolveHref(params.type)!,
+ },
+ ],
+ tone: "positive",
+ message: "This document is used to render the front page",
+ } satisfies DocumentLocationsState)
+ : ({
+ tone: "critical",
+ message: `The top menu isn't linking to the home page. This might make it difficult for visitors to navigate your site.`,
+ } satisfies DocumentLocationsState);
+ case "page":
+ return {
+ locations: docs
+ ?.map((doc) => {
+ const href = resolveHref(doc._type, doc?.slug?.current);
+ return {
+ title: doc?.title || "Untitled",
+ href: href!,
+ };
+ })
+ .filter((doc) => doc.href !== undefined),
+ tone: isReferencedBySettings ? "positive" : "critical",
+ message: isReferencedBySettings
+ ? "The top menu is linking to this page"
+ : "The top menu isn't linking to this page. It can still be accessed if the visitor knows the URL.",
+ } satisfies DocumentLocationsState;
+ case "project":
+ return {
+ locations: docs
+ ?.map((doc) => {
+ const href = resolveHref(doc._type, doc?.slug?.current);
+ return {
+ title: doc?.title || "Untitled",
+ href: href!,
+ };
+ })
+ .filter((doc) => doc.href !== undefined),
+ tone: isReferencedBySettings ? "caution" : undefined,
+ message: isReferencedBySettings
+ ? "This document is used on all pages as it is in the top menu"
+ : undefined,
+ } satisfies DocumentLocationsState;
+ default:
+ return {
+ message: "Unable to map document type to locations",
+ tone: "critical",
+ } satisfies DocumentLocationsState;
+ }
+ }),
+ );
+ }
+
+ return null;
+};
diff --git a/examples/cms-sanity/sanity/plugins/settings.tsx b/examples/cms-sanity/sanity/plugins/settings.tsx
new file mode 100644
index 00000000000000..0720d042eaf7eb
--- /dev/null
+++ b/examples/cms-sanity/sanity/plugins/settings.tsx
@@ -0,0 +1,65 @@
+/**
+ * This plugin contains all the logic for setting up the singletons
+ */
+
+import { definePlugin, type DocumentDefinition } from "sanity";
+import { type StructureResolver } from "sanity/structure";
+
+export const singletonPlugin = (types: string[]) => {
+ return definePlugin({
+ name: "singletonPlugin",
+ document: {
+ // Hide 'Singletons (such as Settings)' from new document options
+ // https://user-images.githubusercontent.com/81981/195728798-e0c6cf7e-d442-4e58-af3a-8cd99d7fcc28.png
+ newDocumentOptions: (prev, { creationContext }) => {
+ if (creationContext.type === "global") {
+ return prev.filter(
+ (templateItem) => !types.includes(templateItem.templateId),
+ );
+ }
+
+ return prev;
+ },
+ // Removes the "duplicate" action on the Singletons (such as Home)
+ actions: (prev, { schemaType }) => {
+ if (types.includes(schemaType)) {
+ return prev.filter(({ action }) => action !== "duplicate");
+ }
+
+ return prev;
+ },
+ },
+ });
+};
+
+// The StructureResolver is how we're changing the DeskTool structure to linking to document (named Singleton)
+// like how "Home" is handled.
+export const pageStructure = (
+ typeDefArray: DocumentDefinition[],
+): StructureResolver => {
+ return (S) => {
+ // Goes through all of the singletons that were provided and translates them into something the
+ // Desktool can understand
+ const singletonItems = typeDefArray.map((typeDef) => {
+ return S.listItem()
+ .title(typeDef.title!)
+ .icon(typeDef.icon)
+ .child(
+ S.editor()
+ .id(typeDef.name)
+ .schemaType(typeDef.name)
+ .documentId(typeDef.name),
+ );
+ });
+
+ // The default root list items (except custom ones)
+ const defaultListItems = S.documentTypeListItems().filter(
+ (listItem) =>
+ !typeDefArray.find((singleton) => singleton.name === listItem.getId()),
+ );
+
+ return S.list()
+ .title("Content")
+ .items([...singletonItems, S.divider(), ...defaultListItems]);
+ };
+};
diff --git a/examples/cms-sanity/sanity/schemas/documents/author.ts b/examples/cms-sanity/sanity/schemas/documents/author.ts
new file mode 100644
index 00000000000000..cd0f867772d5ba
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/documents/author.ts
@@ -0,0 +1,37 @@
+import { UserIcon } from "@sanity/icons";
+import { defineField, defineType } from "sanity";
+
+export default defineType({
+ name: "author",
+ title: "Author",
+ icon: UserIcon,
+ type: "document",
+ fields: [
+ defineField({
+ name: "name",
+ title: "Name",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "picture",
+ title: "Picture",
+ type: "image",
+ fields: [
+ {
+ name: "alt",
+ type: "string",
+ title: "Alternative text",
+ description: "Important for SEO and accessiblity.",
+ },
+ ],
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ validation: (rule) => rule.required(),
+ }),
+ ],
+});
diff --git a/examples/cms-sanity/sanity/schemas/documents/post.ts b/examples/cms-sanity/sanity/schemas/documents/post.ts
new file mode 100644
index 00000000000000..2b359796e146e3
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/documents/post.ts
@@ -0,0 +1,102 @@
+import { BookIcon } from "@sanity/icons";
+import { format, parseISO } from "date-fns";
+import { defineField, defineType } from "sanity";
+
+import authorType from "./author";
+
+/**
+ * This file is the schema definition for a post.
+ *
+ * Here you'll be able to edit the different fields that appear when you
+ * create or edit a post in the studio.
+ *
+ * Here you can see the different schema types that are available:
+
+ https://www.sanity.io/docs/schema-types
+
+ */
+
+export default defineType({
+ name: "post",
+ title: "Post",
+ icon: BookIcon,
+ type: "document",
+ fields: [
+ defineField({
+ name: "title",
+ title: "Title",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "slug",
+ title: "Slug",
+ type: "slug",
+ description: "A slug is required for the post to show up in the preview",
+ options: {
+ source: "title",
+ maxLength: 96,
+ isUnique: (value, context) => context.defaultIsUnique(value, context),
+ },
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "content",
+ title: "Content",
+ type: "array",
+ of: [{ type: "block" }],
+ }),
+ defineField({
+ name: "excerpt",
+ title: "Excerpt",
+ type: "text",
+ }),
+ defineField({
+ name: "coverImage",
+ title: "Cover Image",
+ type: "image",
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ fields: [
+ {
+ name: "alt",
+ type: "string",
+ title: "Alternative text",
+ description: "Important for SEO and accessiblity.",
+ },
+ ],
+ }),
+ defineField({
+ name: "date",
+ title: "Date",
+ type: "datetime",
+ initialValue: () => new Date().toISOString(),
+ }),
+ defineField({
+ name: "author",
+ title: "Author",
+ type: "reference",
+ to: [{ type: authorType.name }],
+ }),
+ ],
+ preview: {
+ select: {
+ title: "title",
+ author: "author.name",
+ date: "date",
+ media: "coverImage",
+ },
+ prepare({ title, media, author, date }) {
+ const subtitles = [
+ author && `by ${author}`,
+ date && `on ${format(parseISO(date), "LLL d, yyyy")}`,
+ ].filter(Boolean);
+
+ return { title, media, subtitle: subtitles.join(" ") };
+ },
+ },
+});
diff --git a/examples/cms-sanity/sanity/schemas/singletons/settings.tsx b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx
new file mode 100644
index 00000000000000..5859a3da2d31ef
--- /dev/null
+++ b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx
@@ -0,0 +1,121 @@
+import { CogIcon } from "@sanity/icons";
+import { defineArrayMember, defineField, defineType } from "sanity";
+import * as demo from "@/sanity/lib/demo";
+
+export default defineType({
+ name: "settings",
+ title: "Settings",
+ type: "document",
+ icon: CogIcon,
+ fields: [
+ defineField({
+ name: "title",
+ description: "This field is the title of your blog.",
+ title: "Title",
+ type: "string",
+ initialValue: demo.title,
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "description",
+ description:
+ "Used both for the description tag for SEO, and the blog subheader.",
+ title: "Description",
+ type: "array",
+ initialValue: demo.description,
+ of: [
+ defineArrayMember({
+ type: "block",
+ options: {},
+ styles: [],
+ lists: [],
+ marks: {
+ decorators: [],
+ annotations: [
+ defineField({
+ type: "object",
+ name: "link",
+ fields: [
+ {
+ type: "string",
+ name: "href",
+ title: "URL",
+ validation: (rule) => rule.required(),
+ },
+ ],
+ }),
+ ],
+ },
+ }),
+ ],
+ }),
+ defineField({
+ name: "footer",
+ description:
+ "This is a block of text that will be displayed at the bottom of the page.",
+ title: "Footer Info",
+ type: "array",
+ of: [
+ defineArrayMember({
+ type: "block",
+ marks: {
+ annotations: [
+ {
+ name: "link",
+ type: "object",
+ title: "Link",
+ fields: [
+ {
+ name: "href",
+ type: "url",
+ title: "Url",
+ },
+ ],
+ },
+ ],
+ },
+ }),
+ ],
+ }),
+ defineField({
+ name: "ogImage",
+ title: "Open Graph Image",
+ type: "image",
+ description: "Displayed on social cards and search engine results.",
+ options: {
+ hotspot: true,
+ aiAssist: {
+ imageDescriptionField: "alt",
+ },
+ },
+ fields: [
+ defineField({
+ name: "alt",
+ description: "Important for accessibility and SEO.",
+ title: "Alternative text",
+ type: "string",
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: "metadataBase",
+ type: "url",
+ description: (
+
+ More information
+
+ ),
+ }),
+ ],
+ }),
+ ],
+ preview: {
+ prepare() {
+ return {
+ title: "Settings",
+ };
+ },
+ },
+});
diff --git a/examples/cms-sanity/studio/.gitignore b/examples/cms-sanity/studio/.gitignore
deleted file mode 100644
index d0c88091a7f2ef..00000000000000
--- a/examples/cms-sanity/studio/.gitignore
+++ /dev/null
@@ -1,35 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# production
-/dist
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
-
-.sanity
diff --git a/examples/cms-sanity/studio/copyEnv.js b/examples/cms-sanity/studio/copyEnv.js
deleted file mode 100644
index c447b4161ac3b0..00000000000000
--- a/examples/cms-sanity/studio/copyEnv.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const fs = require("fs");
-
-if (fs.existsSync("../.env")) {
- fs.copyFileSync("../.env", ".env.development");
-} else if (fs.existsSync("../.env.local")) {
- fs.copyFileSync("../.env.local", ".env.development");
-} else {
- throw new Error("No .env or .env.local file found at root of the project");
-}
diff --git a/examples/cms-sanity/studio/package.json b/examples/cms-sanity/studio/package.json
deleted file mode 100644
index d3591e67c02de8..00000000000000
--- a/examples/cms-sanity/studio/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "private": true,
- "scripts": {
- "start": "sanity dev",
- "dev": "sanity dev",
- "build": "sanity build",
- "cors:add": "npx sanity cors add",
- "deploy": "sanity deploy",
- "prestart": "npm run env",
- "predeploy": "npm run env",
- "env": "node copyEnv.js"
- },
- "dependencies": {
- "sanity": "^3.2.6",
- "@sanity/vision": "^3.2.6",
- "prop-types": "^15.7",
- "react": "^18",
- "react-dom": "^18",
- "sanity-plugin-asset-source-unsplash": "^1.0.6",
- "styled-components": "^5.2.0"
- },
- "devDependencies": {
- "autoprefixer": "^10.4.13",
- "postcss": "^8.4.21",
- "tailwindcss": "^3.2.4"
- }
-}
diff --git a/examples/cms-sanity/studio/plugins/.gitkeep b/examples/cms-sanity/studio/plugins/.gitkeep
deleted file mode 100644
index 2419d3d0600bd4..00000000000000
--- a/examples/cms-sanity/studio/plugins/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-User-specific packages can be placed here
diff --git a/examples/cms-sanity/studio/resolveProductionUrl.js b/examples/cms-sanity/studio/resolveProductionUrl.js
deleted file mode 100644
index f95c3d7d1908fa..00000000000000
--- a/examples/cms-sanity/studio/resolveProductionUrl.js
+++ /dev/null
@@ -1,21 +0,0 @@
-let productionUrl;
-try {
- productionUrl = new URL(
- import.meta.env.SANITY_STUDIO_PREVIEW_URL || "http://localhost:3000",
- );
-} catch (err) {
- console.error("Invalid productionUrl", err);
-}
-
-export function resolveProductionUrl(prev, { document }) {
- if (!productionUrl || !document.slug?.current) {
- return prev;
- }
- const searchParams = new URLSearchParams();
- searchParams.set(
- "secret",
- import.meta.env.SANITY_STUDIO_PREVIEW_SECRET || "",
- );
- searchParams.set("slug", document.slug.current);
- return `${productionUrl.origin}/api/preview?${searchParams}`;
-}
diff --git a/examples/cms-sanity/studio/sanity.config.js b/examples/cms-sanity/studio/sanity.config.js
deleted file mode 100644
index 7f718f17e0ff21..00000000000000
--- a/examples/cms-sanity/studio/sanity.config.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { visionTool } from "@sanity/vision";
-import { defineConfig } from "sanity";
-import { deskTool } from "sanity/desk";
-import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
-
-import { resolveProductionUrl } from "./resolveProductionUrl";
-import { author } from "./schemas/author";
-import { post } from "./schemas/post";
-
-const title =
- import.meta.env.NEXT_PUBLIC_SANITY_PROJECT_TITLE ||
- "Next.js Blog with Sanity.io";
-const projectId = import.meta.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
-const dataset = import.meta.env.NEXT_PUBLIC_SANITY_DATASET;
-
-export default defineConfig({
- basePath: "/",
- projectId: projectId || "",
- dataset: dataset || "",
- title,
- schema: {
- // If you want more content types, you can add them to this array
- types: [author, post],
- },
- document: {
- productionUrl: resolveProductionUrl,
- },
- plugins: [
- deskTool({}),
- // Add an image asset source for Unsplash
- unsplashImageAsset(),
- // Vision lets you query your content with GROQ in the studio
- // https://www.sanity.io/docs/the-vision-plugin
- visionTool(),
- ],
-});
diff --git a/examples/cms-sanity/studio/schemas/author.js b/examples/cms-sanity/studio/schemas/author.js
deleted file mode 100644
index 6c05e957cc645a..00000000000000
--- a/examples/cms-sanity/studio/schemas/author.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export const author = {
- name: "author",
- title: "Author",
- type: "document",
- fields: [
- {
- name: "name",
- title: "Name",
- type: "string",
- validation: (Rule) => Rule.required(),
- },
- {
- name: "picture",
- title: "Picture",
- type: "image",
- options: { hotspot: true },
- validation: (Rule) => Rule.required(),
- },
- ],
-};
diff --git a/examples/cms-sanity/studio/schemas/post.js b/examples/cms-sanity/studio/schemas/post.js
deleted file mode 100644
index 99556226f1f3f1..00000000000000
--- a/examples/cms-sanity/studio/schemas/post.js
+++ /dev/null
@@ -1,64 +0,0 @@
-export const post = {
- name: "post",
- title: "Post",
- type: "document",
- fields: [
- {
- name: "title",
- title: "Title",
- type: "string",
- validation: (Rule) => Rule.required(),
- },
- {
- name: "slug",
- title: "Slug",
- type: "slug",
- options: {
- source: "title",
- maxLength: 96,
- },
- validation: (Rule) => Rule.required(),
- },
- {
- name: "content",
- title: "Content",
- type: "array",
- of: [{ type: "block" }],
- },
- {
- name: "excerpt",
- title: "Excerpt",
- type: "string",
- },
- {
- name: "coverImage",
- title: "Cover Image",
- type: "image",
- options: {
- hotspot: true,
- },
- },
- {
- name: "date",
- title: "Date",
- type: "datetime",
- },
- {
- name: "author",
- title: "Author",
- type: "reference",
- to: [{ type: "author" }],
- },
- ],
- preview: {
- select: {
- title: "title",
- author: "author.name",
- media: "coverImage",
- },
- prepare(selection) {
- const { author } = selection;
- return { ...selection, subtitle: author && `by ${author}` };
- },
- },
-};
diff --git a/examples/cms-sanity/studio/static/.gitkeep b/examples/cms-sanity/studio/static/.gitkeep
deleted file mode 100644
index 37178a72a546a0..00000000000000
--- a/examples/cms-sanity/studio/static/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-Files placed here will be served by the Sanity server under the `/static`-prefix
diff --git a/examples/cms-sanity/studio/static/favicon.ico b/examples/cms-sanity/studio/static/favicon.ico
deleted file mode 100644
index 7305cdbf5c90a2..00000000000000
Binary files a/examples/cms-sanity/studio/static/favicon.ico and /dev/null differ
diff --git a/examples/cms-sanity/studio/tailwind.config.js b/examples/cms-sanity/studio/tailwind.config.js
deleted file mode 100644
index 8b4fa5cb74a54e..00000000000000
--- a/examples/cms-sanity/studio/tailwind.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./components/**/*.{js,ts,jsx,tsx}"],
- theme: {},
- plugins: [],
-};
diff --git a/examples/cms-sanity/tailwind.config.js b/examples/cms-sanity/tailwind.config.js
deleted file mode 100644
index a8f65a525d2cc4..00000000000000
--- a/examples/cms-sanity/tailwind.config.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- "./pages/**/*.{js,ts,jsx,tsx}",
- "./components/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {
- colors: {
- "accent-1": "#FAFAFA",
- "accent-2": "#EAEAEA",
- "accent-7": "#333",
- success: "#0070f3",
- cyan: "#79FFE1",
- },
- spacing: {
- 28: "7rem",
- },
- letterSpacing: {
- tighter: "-.04em",
- },
- lineHeight: {
- tight: 1.2,
- },
- fontSize: {
- "5xl": "2.5rem",
- "6xl": "2.75rem",
- "7xl": "4.5rem",
- "8xl": "6.25rem",
- },
- boxShadow: {
- small: "0 5px 10px rgba(0, 0, 0, 0.12)",
- medium: "0 8px 30px rgba(0, 0, 0, 0.12)",
- },
- },
- },
- plugins: [],
-};
diff --git a/examples/cms-sanity/tailwind.config.ts b/examples/cms-sanity/tailwind.config.ts
new file mode 100644
index 00000000000000..99eb4e61d89fd9
--- /dev/null
+++ b/examples/cms-sanity/tailwind.config.ts
@@ -0,0 +1,17 @@
+import type { Config } from "tailwindcss";
+import typography from "@tailwindcss/typography";
+
+export default {
+ content: ["./app/**/*.{ts,tsx}", "./sanity/**/*.{ts,tsx}"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-inter)"],
+ },
+ },
+ },
+ future: {
+ hoverOnlyWhenSupported: true,
+ },
+ plugins: [typography],
+} satisfies Config;
diff --git a/examples/cms-sanity/tsconfig.json b/examples/cms-sanity/tsconfig.json
new file mode 100644
index 00000000000000..e06a4454ab0627
--- /dev/null
+++ b/examples/cms-sanity/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
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. - -
-
-
-```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 (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: - - - -In the screenshot above the `production url` is `https://cms-sanity.vercel.app`. - -
+
+ );
+}
+
+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 (
+
+ {pending ? (
+ "Disabling draft mode..."
+ ) : (
+ <>
+ {"Previewing drafts. "}
+
+ >
+ )}
+
+
+ {picture?.asset?._ref ? (
+
+ );
+}
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 ? (
+
+
+
+ ) : (
+ By
+ )}
+ {name}
+
+ {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
+ {data?.map((post) => {
+ const { _id, title, slug, coverImage, excerpt, author } = post;
+ return (
+
+
+
+
+ }
+
+ );
+ })}
+
+ >
+ );
+}
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} + +
+
+
+
+ {excerpt && (
+ + {excerpt} +
+ )} + {author &&+ {title || demo.title} +
+
+
+
+
+
+
+
+ + + {title} + +
+
+
+
+
+ {excerpt && (
+ }
+
+ + {excerpt} +
+ )} + {author &&
+
+ {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
+
+
+
+
+ );
+}
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 + + {settings?.title || demo.title} + +
++ {post.title} +
+
+ {post.author && (
+
+ )}
+
+
+
+
+
+
+ {post.content?.length && (
+
+ {post.author && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ )}
+
-
-
-
- );
-}
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 (
-
- {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/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
-
-
- {name}
- {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 ? (
-
-
-
- ) : (
-
- );
-
- 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 &&- Blog. -
-- A statically generated blog example using{" "} - - Next.js - {" "} - and{" "} - - {CMS_NAME} - - . -
-
-
- {children}
-
-
- >
- );
-}
diff --git a/examples/cms-sanity/components/markdown-styles.module.css b/examples/cms-sanity/components/markdown-styles.module.css
deleted file mode 100644
index 95d4f8b04172d6..00000000000000
--- a/examples/cms-sanity/components/markdown-styles.module.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.markdown {
- @apply text-lg leading-relaxed;
-}
-
-.markdown p,
-.markdown ul,
-.markdown ol,
-.markdown blockquote {
- @apply my-6;
-}
-
-.markdown h2 {
- @apply text-3xl mt-12 mb-4 leading-snug;
-}
-
-.markdown h3 {
- @apply text-2xl mt-8 mb-4 leading-snug;
-}
diff --git a/examples/cms-sanity/components/meta.js b/examples/cms-sanity/components/meta.js
deleted file mode 100644
index b228ae821cd18d..00000000000000
--- a/examples/cms-sanity/components/meta.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Head from "next/head";
-import { CMS_NAME, HOME_OG_IMAGE_URL } from "../lib/constants";
-
-export default function Meta() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/cms-sanity/components/more-stories.js b/examples/cms-sanity/components/more-stories.js
deleted file mode 100644
index af68ac0f2ce7be..00000000000000
--- a/examples/cms-sanity/components/more-stories.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import PostPlug from "./post-plug";
-
-export default function MoreStories({ posts }) {
- return (
- - More Stories -
-
- {posts.map((post) => (
-
- ))}
-
-
-
-
- );
-}
diff --git a/examples/cms-sanity/components/post-header.js b/examples/cms-sanity/components/post-header.js
deleted file mode 100644
index 3c38dcac6cbb0a..00000000000000
--- a/examples/cms-sanity/components/post-header.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Avatar from "../components/avatar";
-import Date from "../components/date";
-import CoverImage from "../components/cover-image";
-import PostTitle from "../components/post-title";
-
-export default function PostHeader({ title, coverImage, date, author }) {
- return (
- <>
-
- {author && }
-
-
-
-
-
-
- >
- );
-}
diff --git a/examples/cms-sanity/components/post-plug.js b/examples/cms-sanity/components/post-plug.js
deleted file mode 100644
index b49e286b0a55e1..00000000000000
--- a/examples/cms-sanity/components/post-plug.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Avatar from "../components/avatar";
-import Date from "../components/date";
-import CoverImage from "./cover-image";
-import Link from "next/link";
-
-export default function PostPlug({
- title,
- coverImage,
- date,
- excerpt,
- author,
- slug,
-}) {
- return (
-
- {author && }
-
-
-
-
-
- }
-
- );
-}
diff --git a/examples/cms-sanity/components/post-preview.js b/examples/cms-sanity/components/post-preview.js
deleted file mode 100644
index 02691ef3ea067f..00000000000000
--- a/examples/cms-sanity/components/post-preview.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { usePreview } from "../lib/sanity";
-import { postQuery } from "../lib/queries";
-import Post from "./post";
-
-export default function PostPreview({ data }) {
- const slug = data?.post?.slug;
- const previewData = usePreview(null, postQuery, { slug });
- return
-
-
- - - {title} - -
-
-
-
- {excerpt}
- {author &&- {children} -
- ); -} diff --git a/examples/cms-sanity/components/post.js b/examples/cms-sanity/components/post.js deleted file mode 100644 index 16f67e3498e3e0..00000000000000 --- a/examples/cms-sanity/components/post.js +++ /dev/null @@ -1,65 +0,0 @@ -import { useRouter } from "next/router"; -import { urlForImage } from "../lib/sanity"; -import ErrorPage from "next/error"; -import Layout from "./layout"; -import Container from "./container"; -import Header from "./header"; -import PostTitle from "./post-title"; -import Head from "next/head"; -import { CMS_NAME } from "../lib/constants"; -import PostHeader from "./post-header"; -import PostBody from "./post-body"; -import SectionSeparator from "./section-separator"; -import MoreStories from "./more-stories"; - -export default function Post({ data = {}, preview = false }) { - const router = useRouter(); - - const { post, morePosts } = data; - const slug = post?.slug; - - if (!router.isFallback && !slug) { - return; -} diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js deleted file mode 100644 index c8e58e78504577..00000000000000 --- a/examples/cms-sanity/lib/config.js +++ /dev/null @@ -1,15 +0,0 @@ -export const sanityConfig = { - // Find your project ID and dataset in `sanity.json` in your studio project - dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production", - projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, - useCdn: - typeof document !== "undefined" && process.env.NODE_ENV === "production", - // useCdn == true gives fast, cheap responses using a globally distributed cache. - // When in production the Sanity API is only queried on build-time, and on-demand when responding to webhooks. - // Thus the data need to be fresh and API response time is less important. - // When in development/working locally, it's more important to keep costs down as hot reloading can incurr a lot of API calls - // And every page load calls getStaticProps. - // To get the lowest latency, lowest cost, and latest data, use the Instant Preview mode - apiVersion: "2022-03-13", - // see https://www.sanity.io/docs/api-versioning for how versioning works -}; diff --git a/examples/cms-sanity/lib/constants.js b/examples/cms-sanity/lib/constants.js deleted file mode 100644 index 3db98b71341eaf..00000000000000 --- a/examples/cms-sanity/lib/constants.js +++ /dev/null @@ -1,5 +0,0 @@ -export const EXAMPLE_PATH = "cms-sanity"; -export const CMS_NAME = "Sanity"; -export const CMS_URL = "https://sanity.io/"; -export const HOME_OG_IMAGE_URL = - "https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Sanity**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTA1IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMWVtIj48dGl0bGU%2BU2FuaXR5PC90aXRsZT48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03OC4xNzkzIDcuOTkyNjFWMjEuMDAyOEg3My45MDMxVjEwLjIxMzhMNzguMTc5MyA3Ljk5MjYxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMjAuOTUxMSAyMS4zM0wzMC45NDQgMTYuMTA1MUwyOS43MTIxIDEyLjkxNDFMMjMuMTMzMiAxNS45ODIxTDIwLjk1MTEgMjEuMzNaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik03My45MDMxIDEwLjIwMjdMODQuNzQ0MyA0LjY1NDc3TDgyLjkxMjYgMS41NTcxTDczLjkwMzEgNS45NTk5N1YxMC4yMDI3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNNDMuMzcwNSA2Ljk2MjMzVjIxLjAwMjhIMzkuMjkyN1YxLjAwNzE0TDQzLjM3MDUgNi45NjIzM1oiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTI3LjEyOTkgNi4xODYxN0wyMC45NTExIDIxLjMzTDE3Ljc3MzEgMTguNTk0M0wyNS4xMzUzIDEuMDA3MTRMMjcuMTI5OSA2LjE4NjE3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTI1LjEzNTMgMS4wMDcxNEgyOS4zNDc3TDM3LjEzODYgMjEuMDAyOEgzMi44MjY5TDI1LjEzNTMgMS4wMDcxNFoiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIGQ9Ik00NC4wMDEyIDEuMDA3MTRMNTIuOTgyNCAxNC42NjgyVjIxLjAwMjhMMzkuMjkyNyAxLjAwNzE0SDQ0LjAwMTJaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNjQuOTE4MyAxLjAwNzE0SDYwLjY3MzlWMjEuMDA2M0g2NC45MTgzVjEuMDA3MTRaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNzMuOTAzMSA0LjY1NDc0SDY3LjM3VjEuMDA3MTRIODIuNTg2N0w4NC43NDQzIDQuNjU0NzRINzguMTc5M0g3My45MDMxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC41IiBkPSJNOTcuMjc1NCAxMy40MTUzVjIxLjAwMjhIOTMuMDYyOVYxMy40MTUzIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNOTMuMDYyOSAxMy40MTUyTDEwMC4xOTEgMS4wMDcxNEgxMDQuNjY2TDk3LjI3NTQgMTMuNDE1Mkg5My4wNjI5WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNOTMuMDYzIDEzLjQxNTJMODUuNzM2MyAxLjAwNzE0SDkwLjM0NTZMOTUuMzA5MiA5LjUxMDA4TDkzLjA2MyAxMy40MTUyWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTEuOTYxMjYgMy4zMTQ3OUMxLjk2MTI2IDYuMDk5MjEgMy43MTE0NSA3Ljc1NTk1IDcuMjE1MzYgOC42Mjk1NkwxMC45MjgzIDkuNDc1MzNDMTQuMjQ0NCAxMC4yMjM2IDE2LjI2MzkgMTIuMDgyMiAxNi4yNjM5IDE1LjExMDNDMTYuMjg5NyAxNi40Mjk1IDE1Ljg1MzEgMTcuNzE3MyAxNS4wMjc0IDE4Ljc1NzlDMTUuMDI3NCAxNS43MzY4IDEzLjQzNjcgMTQuMTA0NCA5LjU5OTcyIDEzLjEyMjlMNS45NTQwOSAxMi4zMDg1QzMuMDM0NzUgMTEuNjU0MSAwLjc4MTQ3OCAxMC4xMjYyIDAuNzgxNDc4IDYuODM3MDlDMC43NjYxMjMgNS41NjY5MyAxLjE4MTE2IDQuMzI3ODEgMS45NjEyNiAzLjMxNDc5IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik01Mi45ODI0IDEzLjY0MTVWMS4wMDcxNEg1Ny4wNjAyVjIxLjAwMjhINTIuOTgyNFYxMy42NDE1WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMTIuNzQ1OCAxNC4zNjg5QzE0LjMyOTQgMTUuMzY0MyAxNS4wMjM4IDE2Ljc1NjUgMTUuMDIzOCAxOC43NTQ0QzEzLjcxMyAyMC40MDQxIDExLjQxMDEgMjEuMzMgOC43MDMzMyAyMS4zM0M0LjE0NzE4IDIxLjMzIDAuOTU4NTc3IDE5LjEyNjggMC4yNSAxNS4yOTgySDQuNjI1NDdDNS4xODg3OCAxNy4wNTU5IDYuNjgwMzQgMTcuODcwMyA4LjY3MTQ0IDE3Ljg3MDNDMTEuMTAxOSAxNy44NzAzIDEyLjcxNzQgMTYuNTk2NCAxMi43NDkzIDE0LjM2MTkiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNyIgZD0iTTQuMjM1NjcgNy40NDI2N0MzLjUxMjUgNy4wMjA0NSAyLjkxOTIgNi40MTM3NSAyLjUxODczIDUuNjg2OTdDMi4xMTgyNyA0Ljk2MDE5IDEuOTI1NTggNC4xNDA0NSAxLjk2MTEzIDMuMzE0NzZDMy4yMjU5NCAxLjY3ODkxIDUuNDI2MDggMC42Nzk5OTMgOC4xMDgwNCAwLjY3OTk5M0MxMi43NDkyIDAuNjc5OTkzIDE1LjQzNDcgMy4wODg1MiAxNi4wOTcyIDYuNDc4NTZIMTEuODg4M0MxMS40MjQyIDUuMTQyMDMgMTAuMjYyMSA0LjEwMTM2IDguMTQzNDcgNC4xMDEzNkM1Ljg3OTU3IDQuMTAxMzYgNC4zMzQ4NyA1LjM5NjExIDQuMjQ2MjkgNy40NDI2NyIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPC9zdmc%2B&widths=undefined&widths=auto&heights=250&heights=150"; diff --git a/examples/cms-sanity/lib/queries.js b/examples/cms-sanity/lib/queries.js deleted file mode 100644 index 89330900c4be1e..00000000000000 --- a/examples/cms-sanity/lib/queries.js +++ /dev/null @@ -1,37 +0,0 @@ -const postFields = ` - _id, - name, - title, - date, - excerpt, - coverImage, - "slug": slug.current, - "author": author->{name, picture}, -`; - -export const indexQuery = ` -*[_type == "post"] | order(date desc, _updatedAt desc) { - ${postFields} -}`; - -export const postQuery = ` -{ - "post": *[_type == "post" && slug.current == $slug] | order(_updatedAt desc) [0] { - content, - ${postFields} - }, - "morePosts": *[_type == "post" && slug.current != $slug] | order(date desc, _updatedAt desc) [0...2] { - content, - ${postFields} - } -}`; - -export const postSlugsQuery = ` -*[_type == "post" && defined(slug.current)][].slug.current -`; - -export const postBySlugQuery = ` -*[_type == "post" && slug.current == $slug][0] { - ${postFields} -} -`; diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js deleted file mode 100644 index 2492667bea9b60..00000000000000 --- a/examples/cms-sanity/lib/sanity.js +++ /dev/null @@ -1,13 +0,0 @@ -import createImageUrlBuilder from "@sanity/image-url"; -import { definePreview } from "next-sanity/preview"; -import { sanityConfig } from "./config"; - -export const imageBuilder = createImageUrlBuilder(sanityConfig); - -export const urlForImage = (source) => - imageBuilder.image(source).auto("format").fit("max"); - -export const usePreview = definePreview({ - projectId: sanityConfig.projectId, - dataset: sanityConfig.dataset, -}); diff --git a/examples/cms-sanity/lib/sanity.server.js b/examples/cms-sanity/lib/sanity.server.js deleted file mode 100644 index 4a0d7be3bf8cb9..00000000000000 --- a/examples/cms-sanity/lib/sanity.server.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Server-side Sanity utilities. By having these in a separate file from the - * utilities we use on the client side, we are able to tree-shake (remove) - * code that is not used on the client side. - */ -import { createClient } from "next-sanity"; -import { sanityConfig } from "./config"; - -export const sanityClient = createClient(sanityConfig); - -export const previewClient = createClient({ - ...sanityConfig, - useCdn: false, - // Fallback to using the WRITE token until https://www.sanity.io/docs/vercel-integration starts shipping a READ token. - // As this client only exists on the server and the token is never shared with the browser, we don't risk escalating permissions to untrustworthy users - token: - process.env.SANITY_API_READ_TOKEN || process.env.SANITY_API_WRITE_TOKEN, -}); - -export const getClient = (preview) => (preview ? previewClient : sanityClient); - -export function overlayDrafts(docs) { - const documents = docs || []; - const overlayed = documents.reduce((map, doc) => { - if (!doc._id) { - throw new Error("Ensure that `_id` is included in query projection"); - } - - const isDraft = doc._id.startsWith("drafts."); - const id = isDraft ? doc._id.slice(7) : doc._id; - return isDraft || !map.has(id) ? map.set(id, doc) : map; - }, new Map()); - - return Array.from(overlayed.values()); -} diff --git a/examples/cms-sanity/next.config.js b/examples/cms-sanity/next.config.js index 553cc9b8363a48..44b910b28d8e8a 100644 --- a/examples/cms-sanity/next.config.js +++ b/examples/cms-sanity/next.config.js @@ -1,9 +1,18 @@ /** @type {import('next').NextConfig} */ module.exports = { + compiler: { + styledComponents: true, + }, + logging: { + fetches: { + fullUrl: true, + }, + }, + experimental: { + // Used to guard against accidentally leaking SANITY_API_READ_TOKEN to the browser + taint: true, + }, images: { - remotePatterns: [ - { hostname: "cdn.sanity.io" }, - { hostname: "source.unsplash.com" }, - ], + remotePatterns: [{ hostname: "cdn.sanity.io" }], }, }; diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 22addbb2a2a9b3..d3e739dadb10e5 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -4,23 +4,40 @@ "dev": "next", "build": "next build", "start": "next start", - "studio:dev": "npm --prefix studio run start", - "studio:deploy": "npx vercel env pull && npm --prefix studio run deploy" + "lint": "next lint", + "presetup": "echo 'about to setup env variables, follow the guide here: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli'", + "setup": "npx sanity@latest init --env .env.local", + "postsetup": "echo 'create the read token by following the rest of the guide: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#creating-a-read-token'" }, "dependencies": { - "@portabletext/react": "^2.0.1", + "@portabletext/react": "^3.0.11", + "@sanity/assist": "^2.0.1", + "@sanity/icons": "^2.10.3", "@sanity/image-url": "^1.0.2", - "@sanity/webhook": "^2.0.0", - "classnames": "^2.3.1", - "date-fns": "^2.29.3", + "@sanity/vision": "^3.31.0", + "@tailwindcss/typography": "^0.5.10", + "@types/node": "^20.11.24", + "@types/react": "^18.2.63", + "@types/react-dom": "^18.2.20", + "@vercel/speed-insights": "^1.0.10", + "autoprefixer": "^10.4.18", + "date-fns": "^3.3.1", + "get-youtube-id": "^1.0.1", "next": "latest", - "next-sanity": "^4.1.2", - "react": "^18", - "react-dom": "^18" + "next-sanity": "^8.1.5", + "postcss": "^8.4.35", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-lite-youtube-embed": "^2.4.0", + "sanity": "^3.31.0", + "sanity-plugin-asset-source-unsplash": "^1.1.2", + "server-only": "^0.0.1", + "styled-components": "^6.1.8", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" }, "devDependencies": { - "autoprefixer": "^10.4.13", - "postcss": "^8.4.21", - "tailwindcss": "^3.2.4" + "eslint": "^8.57.0", + "eslint-config-next": "latest" } } diff --git a/examples/cms-sanity/pages/_app.js b/examples/cms-sanity/pages/_app.js deleted file mode 100644 index aa7931ca32e943..00000000000000 --- a/examples/cms-sanity/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import "../styles/index.css"; - -function MyApp({ Component, pageProps }) { - return