From 0f5e2b454c8442ae45b091810c7a2d1ce79860b4 Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Wed, 3 Aug 2022 03:23:54 +0200 Subject: [PATCH] docs(examples): use vercel integration in cms-sanity --- examples/cms-sanity/.env.local.example | 4 +- examples/cms-sanity/.gitignore | 6 + examples/cms-sanity/README.md | 382 +++++++++++++----- examples/cms-sanity/components/avatar.js | 13 +- examples/cms-sanity/components/cover-image.js | 9 +- examples/cms-sanity/components/date.js | 2 + examples/cms-sanity/components/hero-post.js | 12 +- examples/cms-sanity/components/post-header.js | 8 +- .../cms-sanity/components/post-preview.js | 8 +- examples/cms-sanity/lib/config.js | 5 +- examples/cms-sanity/lib/sanity.server.js | 5 +- examples/cms-sanity/next.config.js | 7 +- examples/cms-sanity/package.json | 20 +- examples/cms-sanity/pages/api/preview.js | 33 +- examples/cms-sanity/pages/api/revalidate.js | 41 +- examples/cms-sanity/pages/index.js | 12 +- examples/cms-sanity/pages/posts/[slug].js | 4 +- examples/cms-sanity/schemas/schema.js | 81 ---- examples/cms-sanity/studio/.gitignore | 32 ++ examples/cms-sanity/studio/config/.checksums | 8 + .../studio/config/@sanity/data-aspects.json | 3 + .../studio/config/@sanity/default-layout.json | 6 + .../studio/config/@sanity/default-login.json | 8 + .../studio/config/@sanity/form-builder.json | 5 + .../studio/config/@sanity/vision.json | 3 + examples/cms-sanity/studio/package.json | 26 ++ examples/cms-sanity/studio/plugins/.gitkeep | 1 + .../cms-sanity/studio/resolveProductionUrl.js | 18 + examples/cms-sanity/studio/sanity.json | 29 ++ examples/cms-sanity/studio/schemas/author.js | 20 + examples/cms-sanity/studio/schemas/post.js | 64 +++ examples/cms-sanity/studio/schemas/schema.js | 23 ++ examples/cms-sanity/studio/static/.gitkeep | 1 + examples/cms-sanity/studio/static/favicon.ico | Bin 0 -> 1150 bytes examples/cms-sanity/studio/tsconfig.json | 10 + 35 files changed, 663 insertions(+), 246 deletions(-) delete mode 100644 examples/cms-sanity/schemas/schema.js create mode 100644 examples/cms-sanity/studio/.gitignore create mode 100644 examples/cms-sanity/studio/config/.checksums create mode 100644 examples/cms-sanity/studio/config/@sanity/data-aspects.json create mode 100644 examples/cms-sanity/studio/config/@sanity/default-layout.json create mode 100644 examples/cms-sanity/studio/config/@sanity/default-login.json create mode 100644 examples/cms-sanity/studio/config/@sanity/form-builder.json create mode 100644 examples/cms-sanity/studio/config/@sanity/vision.json create mode 100644 examples/cms-sanity/studio/package.json create mode 100644 examples/cms-sanity/studio/plugins/.gitkeep create mode 100644 examples/cms-sanity/studio/resolveProductionUrl.js create mode 100644 examples/cms-sanity/studio/sanity.json create mode 100644 examples/cms-sanity/studio/schemas/author.js create mode 100644 examples/cms-sanity/studio/schemas/post.js create mode 100644 examples/cms-sanity/studio/schemas/schema.js create mode 100644 examples/cms-sanity/studio/static/.gitkeep create mode 100644 examples/cms-sanity/studio/static/favicon.ico create mode 100644 examples/cms-sanity/studio/tsconfig.json diff --git a/examples/cms-sanity/.env.local.example b/examples/cms-sanity/.env.local.example index a246ea991e7c..84f39e3cd4a5 100644 --- a/examples/cms-sanity/.env.local.example +++ b/examples/cms-sanity/.env.local.example @@ -1,5 +1,3 @@ NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= -SANITY_API_TOKEN= -SANITY_PREVIEW_SECRET= -SANITY_STUDIO_REVALIDATE_SECRET= +SANITY_API_READ_TOKEN= diff --git a/examples/cms-sanity/.gitignore b/examples/cms-sanity/.gitignore index c87c9b392c02..4cebee738fa5 100644 --- a/examples/cms-sanity/.gitignore +++ b/examples/cms-sanity/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +/studio/node_modules /.pnp .pnp.js @@ -14,6 +15,7 @@ # production /build +/studio/dist # misc .DS_Store @@ -34,3 +36,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Env files created by scripts for working locally +.env +studio/.env.development \ No newline at end of file diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index c352df705477..b46c0fd2774b 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -4,21 +4,16 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas You'll get: -- Sanity Studio running on localhost +- 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) ## Demo -### [https://next-blog-sanity.vercel.app/](https://next-blog-sanity.vercel.app/) +### [https://next-blog-sanity.vercel.app](https://next-blog-sanity.vercel.app) -## Deploy your own - -Once you have access to [the environment variables you'll need](#step-4-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET,SANITY_STUDIO_REVALIDATE_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) - -### Related examples +## Related examples - [WordPress](/examples/cms-wordpress) - [DatoCMS](/examples/cms-datocms) @@ -37,9 +32,55 @@ Once you have access to [the environment variables you'll need](#step-4-set-up-e - [Blog Starter](/examples/blog-starter) - [Builder.io](/examples/cms-builder-io) -## How to use +# Configuration + +- [Step 1. Set up the environment](#step-1-set-up-the-environment) +- [Step 2. Configure CORS for localhost](#step-2-configure-cors-for-localhost) +- [Step 3. Run Next.js locally in development mode](#step-3-run-nextjs-locally-in-development-mode) +- [Step 4. Populate content](#step-4-populate-content) +- [Step 5. Deploy to production & use Preview Mode from anywhere](#step-5-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 6. Deploy your Studio and publish from anywhere](#step-6-deploy-your-studio-and-publish-from-anywhere) +- [Step 7. Setup Revalidation Webhook](#step-7-setup-revalidation-webhook) + - [Testing the Webhook](#testing-the-webhook) +- [Next steps](#next-steps) + +## Step 1. Set up the environment + +Use the Deploy Button below, you'll deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) as well as connect it to your Sanity dataset using [the Sanity Vercel Integration][integration]. + +[![Deploy with Vercel](https://vercel.com/button)][vercel-deploy] + +[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: + +```bash +npx vercel link +``` + +Download the environment variables needed to connect Next.js and Studio to your Sanity project: + +```bash +npx vercel env pull +``` + +
+You can also set up manually + +- [Bootstrap the example](#bootstrap-the-example) +- [Connect to a Sanity project](#connect-to-a-sanity-project) +- [Set up environment variables](#set-up-environment-variables) + +If using the [integration] isn't an option. Or maybe you want to work locally first and deploy to Vercel later. Whatever the reason this guide shows you how to setup manually. -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: +### Bootstrap the example + +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): ```bash npx create-next-app --example cms-sanity cms-sanity-app @@ -53,174 +94,307 @@ yarn create next-app --example cms-sanity cms-sanity-app pnpm create next-app --example cms-sanity cms-sanity-app ``` -## Configuration +### Connect to a Sanity project -### Step 1. Create an account and a project on Sanity +Run this to select from your existing Sanity projects, or create a new one: -First, [create an account on Sanity](https://sanity.io). +```bash +(cd studio && npx @sanity/cli init) +``` -After creating an account, install the Sanity cli from npm `npm i -g @sanity/cli`. +The CLI will update [`sanity.json`] with the project ID and dataset name. -### Step 2. Create a new Sanity project +### Set up environment variables -In a separate folder run `sanity init` to initialize a new studio project. +Copy the [`.env.local.example`] file in this directory to `.env.local` (which will be ignored by Git): -This will be where we manage our data. +```bash +cp .env.local.example .env.local +``` -When going through the init phase make sure to select **Yes** to the **Use the default dataset configuration** step and select **Clean project with no predefined schemas** for the **Select project template** step. +Then set these variables in `.env.local`: -### Step 3. Generate an API token +- `NEXT_PUBLIC_SANITY_PROJECT_ID` should be the `projectId` value from [`sanity.json`]. +- `NEXT_PUBLIC_SANITY_DATASET` should be the `dataset` value from [`sanity.json`]. +- `SANITY_API_READ_TOKEN` create an API token with `read-only` permissions: + - Run this to open your project settings or go to https://manage.sanity.io/ and open your project: + ```bash + (cd studio && npx @sanity/cli manage) + ``` + - Go to **API** and the **Tokens** section at the bottom, launch its **Add API token** button. + - Name it `SANITY_API_READ_TOKEN`, set **Permissions** to `Viewer`. + - Hit **Save** and you can copy/paste the token. -Log into https://manage.sanity.io/ and choose the project you just created. Then from **Settings**, select **API**, then click **Add New Token** and create a token with the **Read** permission. +Your `.env.local` file should look like this: -### Step 4. Set up environment variables +```bash +NEXT_PUBLIC_SANITY_PROJECT_ID=... +NEXT_PUBLIC_SANITY_DATASET=... +SANITY_API_READ_TOKEN=... +``` + +
-Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git): +## Step 2. Configure CORS for localhost + +Needed for live previewing unpublished/draft content. ```bash -cp .env.local.example .env.local +npm --prefix studio run cors:add -- http://localhost:3000 --credentials ``` -Then set each variable on `.env.local`: +## Step 3. Run Next.js locally in development mode -- `NEXT_PUBLIC_SANITY_PROJECT_ID` should be the `projectId` value from the `sanity.json` file created in step 2. -- `NEXT_PUBLIC_SANITY_DATASET` should be the `dataset` value from the `sanity.json` file created in step 2 - defaults to `production` if not set. -- `SANITY_API_TOKEN` should be the API token generated in the previous step. -- `SANITY_PREVIEW_SECRET` can be any random string (but avoid spaces), like `MY_SECRET` - this is used for [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode). -- `SANITY_STUDIO_REVALIDATE_SECRET` should be setup the same way as `SANITY_PREVIEW_SECRET` - this is used for [on-demand revalidation](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with [webhooks](https://www.sanity.io/docs/webhooks). +```bash +npm install && npm run dev +``` -Your `.env.local` file should look like this: +```bash +yarn install && yarn dev +``` + +Your blog should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions). + +## Step 4. Populate content + +In another terminal start up the studio: ```bash -NEXT_PUBLIC_SANITY_PROJECT_ID=... -NEXT_PUBLIC_SANITY_DATASET=... -SANITY_API_TOKEN=... -SANITY_PREVIEW_SECRET=... -SANITY_STUDIO_REVALIDATE_SECRET=... +npm run studio:dev ``` -### Step 5. Prepare the project for previewing +Your studio should be up and running on [http://localhost:3333](http://localhost:3333)! + +Create content in Sanity Studio and live preview it in Next.js, side-by-side, by opening these URLs: + +- [`http://localhost:3333`](http://localhost:3333) +- [`http://localhost:3000/api/preview`](http://localhost:3000/api/preview) + +
+View screenshot ✨ + +![screenshot](https://user-images.githubusercontent.com/81981/182991870-7a0f6e54-b35e-4728-922b-409fcf1d6cc3.png) + +
+ +We're all set to do some content creation! + +- Click on the **"Create new document"** button top left and select **Post** +- Type some dummy data for the **Title** +- **Generate** a **Slug** +
+ View screenshot ✨ + + ![screenshot](https://user-images.githubusercontent.com/81981/182993687-b6313086-f60a-4b36-b038-4c1c63b53c54.png) + +
+ +- Set the **Date** +- Select a **Cover Image** from [Unsplash]. +
+ View screenshot ✨ + + ![screenshot](https://user-images.githubusercontent.com/81981/182994571-f204c41c-e1e3-44f4-82b3-99fefbd25bec.png) + +
-5.1. Install the `@sanity/production-preview` plugin with `sanity install @sanity/production-preview`. +- Let's create an **Author** inline, click **Create new**. +- Give the **Author** a **Name**. +- After selecting a **Picture** of a **face** from [Unsplash], set a hotspot to ensure pixel-perfect cropping. +
+ View screenshot ✨ -5.2. Create a file called `resolveProductionUrl.js` (we'll get back to that file in a bit). + ![screenshot](https://user-images.githubusercontent.com/81981/182995772-33d63e45-4920-48c5-aa47-ccb7ce10170c.png) -5.3. Open your studio's sanity.json, and add the following entry to the parts-array: +
-```diff -{ - "plugins": [ - "@sanity/production-preview" - ], - "parts": [ - //... -+ { -+ "implements": "part:@sanity/production-preview/resolve-production-url", -+ "path": "./resolveProductionUrl.js" -+ } - ] -} +- 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. + +To exit Preview Mode, you can click on _"Click here to exit preview mode"_ at the top. + +## Step 5. 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 + +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. + +After it's deployed link your local code to the Vercel project: + +```bash +npx vercel link ``` -Now, go back to `resolveProductionUrl.js` and add a function that will receive the full document that was selected for previewing: +### Configure CORS for production + +Add your `production url` to the list over CORS origins. + +
+Don't remember the production url? 🤔 + +No worries, it's easy to find out. Go to your [Vercel Dashboard](https://vercel.com/) and click on your project: + +![screenshot](https://user-images.githubusercontent.com/81981/183002637-6aa6b1d8-e0ee-4a9b-bcc0-d49799fcc984.png) -```js -const previewSecret = 'MY_SECRET' // Copy the string you used for SANITY_PREVIEW_SECRET -const projectUrl = 'http://localhost:3000' +In the screenshot above the `production url` is `https://cms-sanity.vercel.app`. -export default function resolveProductionUrl(document) { - return `${projectUrl}/api/preview?secret=${previewSecret}&slug=${document.slug.current}` -} +
+ +```bash +npm --prefix studio run cors:add -- [your production url] --credentials ``` -For more information on live previewing check the [full guide.](https://www.sanity.io/guides/nextjs-live-preview) +### Add the preview secret environment variable -### Step 6. Copy the schema file +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: -After initializing your Sanity studio project there should be a `schemas` folder. +```bash +npx vercel env add SANITY_STUDIO_PREVIEW_SECRET +``` -Replace the contents of `schema.js` in the Sanity studio project directory with [`./schemas/schema.js`](./schemas/schema.js) in this example directory. This will set up the schema we’ll use this for this example. +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. -### Step 7. Populate Content +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] +``` -To add some content go to your Sanity studio project directory and run `sanity start`. +Redeploy production to apply the secret to the preview api: -After the project has started and you have navigated to the URL given in the terminal, select **Author** and create a new record. +```bash +npx vercel --prod +``` -- You just need **1 Author record**. -- Use dummy data for the text. -- For the image, you can download one from [Unsplash](https://unsplash.com/). +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. -Next, select **Post** and create a new record. +### How to test locally that the secret is setup correctly -- We recommend creating at least **2 Post records**. -- Use dummy data for the text. -- You can write markdown for the **Content** field. -- For the images, you can download ones from [Unsplash](https://unsplash.com/). -- Pick the **Author** you created earlier. +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: -**Important:** For each post record, you need to click **Publish** after saving. If not, the post will be in the draft state. +```bash +npx vercel env pull +``` -### Step 8. Run Next.js in development mode +Restart your Next.js and Studio processes so the secret is applied: ```bash -npm install npm run dev +``` -# or +```bash +npm run studio:dev +``` -yarn install -yarn 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 ``` -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). +### If you regret sending a preview link to someone -### Step 9. Try preview mode +Revoke their access by creating a new secret: -On Sanity, go to one of the posts you've created and: +```bash +npx vercel env rm SANITY_STUDIO_PREVIEW_SECRET +npx vercel env add SANITY_STUDIO_PREVIEW_SECRET +npx vercel --prod +``` -- **Update the title**. For example, you can add `[Draft]` in front of the title. -- As you edit the document it will be saved as a draft, but **DO NOT** click **Publish**. By doing this, the post will be in the draft state. +## Step 6. Deploy your Studio and publish from anywhere -Now, if you go to the post page on localhost, you won't see the updated title. However, if you use the **Preview Mode**, you'll be able to see the change ([Documentation](https://nextjs.org/docs/advanced-features/preview-mode)). +Live previewing content is fun, but collaborating on content in real-time is next-level: -To view the preview, go to the post edit page on Sanity, click the three dots above the document and select **Open preview** ([see the instruction here](https://www.sanity.io/docs/preview-content-on-site)) +```bash +SANITY_STUDIO_PREVIEW_URL=[your production url] npm run studio:deploy +``` -You should now be able to see the updated title. To exit Preview Mode, you can click on _"Click here to exit preview mode"_ at the top. +If it's successful you should see something like this in your terminal: -### Step 10. Deploy on Vercel +```bash +SANITY_STUDIO_PREVIEW_URL="https://cms-sanity.vercel.app" npm run studio:deploy +? Studio hostname (.sanity.studio): cms-sanity -You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). +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 -#### Deploy Your Local Project +✔ Deploying to Sanity.Studio -To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example). +Success! Studio deployed to https://cms-sanity.sanity.studio/ +``` -**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. +This snippet is stripped from verbose information, you'll see a lot of extra stuff in your terminal. -#### Deploy from Our Template +## Step 7. Setup Revalidation Webhook -Alternatively, you can deploy using our template by clicking on the Deploy button below. +Using GROQ Webhooks Next.js can rebuild pages that have changed content. It rebuilds so fast it can almost compete with Preview Mode. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET,SANITY_STUDIO_REVALIDATE_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) +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`: -### Step 11. Setup Revalidation Webhook +```bash +npx vercel env add SANITY_REVALIDATE_SECRET +``` -- Open your Sanity manager, go to **API**, and **Create new webhook**. -- Set the **URL** to use the Vercel app url from [Step 10](#step-10-deploy-on-vercel) and append `/api/revalidate`, for example: `https://cms-sanity.vercel.app/api/revalidate` +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/cli 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_STUDIO_REVALIDATE_SECRET` earlier. +- Set the **Secret** to the same value you gave `SANITY_REVALIDATE_SECRET` earlier. - Hit **Save**! -#### Testing the Webhook +### 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. -### Next steps +## Next steps - Mount your preview inside the Sanity Studio for comfortable side-by-side editing - [Join the Sanity community](https://slack.sanity.io/) + +[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 diff --git a/examples/cms-sanity/components/avatar.js b/examples/cms-sanity/components/avatar.js index bbf429823789..a7df741c533c 100644 --- a/examples/cms-sanity/components/avatar.js +++ b/examples/cms-sanity/components/avatar.js @@ -1,14 +1,19 @@ -import Image from 'next/image' +import Image from 'next/future/image' import { urlForImage } from '../lib/sanity' export default function Avatar({ name, picture }) { return (
-
+
{name}
diff --git a/examples/cms-sanity/components/cover-image.js b/examples/cms-sanity/components/cover-image.js index 818e8eef5e47..a53c7b917113 100644 --- a/examples/cms-sanity/components/cover-image.js +++ b/examples/cms-sanity/components/cover-image.js @@ -1,21 +1,24 @@ import cn from 'classnames' -import Image from 'next/image' +import Image from 'next/future/image' import Link from 'next/link' import { urlForImage } from '../lib/sanity' -export default function CoverImage({ title, slug, image: source }) { - const image = source ? ( +export default function CoverImage({ title, slug, image: source, priority }) { + const image = source?.asset?._ref ? (
{`Cover
) : ( diff --git a/examples/cms-sanity/components/date.js b/examples/cms-sanity/components/date.js index eac5681378bf..882b66e59eb6 100644 --- a/examples/cms-sanity/components/date.js +++ b/examples/cms-sanity/components/date.js @@ -1,6 +1,8 @@ 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/hero-post.js b/examples/cms-sanity/components/hero-post.js index 4d81ed0850ac..9b0c0c4311e3 100644 --- a/examples/cms-sanity/components/hero-post.js +++ b/examples/cms-sanity/components/hero-post.js @@ -14,22 +14,22 @@ export default function HeroPost({ return (
- +
-
+
-

+

{title}

-
+
-

{excerpt}

- +

{excerpt}

+ {author && }
diff --git a/examples/cms-sanity/components/post-header.js b/examples/cms-sanity/components/post-header.js index ee2900e50bdc..ebf92b24454f 100644 --- a/examples/cms-sanity/components/post-header.js +++ b/examples/cms-sanity/components/post-header.js @@ -8,14 +8,14 @@ export default function PostHeader({ title, coverImage, date, author }) { <> {title}
- + {author && }
- +
-
- +
+ {author && }
diff --git a/examples/cms-sanity/components/post-preview.js b/examples/cms-sanity/components/post-preview.js index efa3a0fb16b7..8cb438298d83 100644 --- a/examples/cms-sanity/components/post-preview.js +++ b/examples/cms-sanity/components/post-preview.js @@ -16,16 +16,16 @@ export default function PostPreview({
-

+

{title}

-
+
-

{excerpt}

- +

{excerpt}

+ {author && }
) } diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js index c28d1f9c1b86..8db086784d6a 100644 --- a/examples/cms-sanity/lib/config.js +++ b/examples/cms-sanity/lib/config.js @@ -2,13 +2,14 @@ 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: process.env.NODE_ENV !== 'production', + 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: '2021-03-25', + apiVersion: '2022-03-13', // see https://www.sanity.io/docs/api-versioning for how versioning works } diff --git a/examples/cms-sanity/lib/sanity.server.js b/examples/cms-sanity/lib/sanity.server.js index 081409e35ef0..259d11888a8d 100644 --- a/examples/cms-sanity/lib/sanity.server.js +++ b/examples/cms-sanity/lib/sanity.server.js @@ -11,7 +11,10 @@ export const sanityClient = createClient(sanityConfig) export const previewClient = createClient({ ...sanityConfig, useCdn: false, - token: process.env.SANITY_API_TOKEN, + // 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 ddon'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) diff --git a/examples/cms-sanity/next.config.js b/examples/cms-sanity/next.config.js index da5f2c88763e..5e537bb53804 100644 --- a/examples/cms-sanity/next.config.js +++ b/examples/cms-sanity/next.config.js @@ -1,5 +1,10 @@ module.exports = { + experimental: { + images: { + allowFutureImage: true, + }, + }, images: { - domains: ['cdn.sanity.io'], + domains: ['cdn.sanity.io', 'source.unsplash.com'], }, } diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 529122e867f4..6f369cc2785d 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -3,22 +3,24 @@ "scripts": { "dev": "next", "build": "next build", - "start": "next start" + "start": "next start", + "studio:dev": "npm --prefix studio run start", + "studio:deploy": "npx vercel env pull && npm --prefix studio run deploy" }, "dependencies": { - "@portabletext/react": "^1.0.3", + "@portabletext/react": "^1.0.6", "@sanity/image-url": "^1.0.1", - "@sanity/webhook": "^1.0.2", - "classnames": "2.3.1", - "date-fns": "2.28.0", + "@sanity/webhook": "^2.0.0", + "classnames": "^2.3.1", + "date-fns": "^2.29.1", "next": "latest", - "next-sanity": "0.5.0", + "next-sanity": "^0.6.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { - "autoprefixer": "10.4.2", - "postcss": "8.4.7", - "tailwindcss": "^3.0.23" + "autoprefixer": "^10.4.8", + "postcss": "^8.4.14", + "tailwindcss": "^3.1.7" } } diff --git a/examples/cms-sanity/pages/api/preview.js b/examples/cms-sanity/pages/api/preview.js index e6cc1eecf261..538c71d53487 100644 --- a/examples/cms-sanity/pages/api/preview.js +++ b/examples/cms-sanity/pages/api/preview.js @@ -1,14 +1,27 @@ import { postBySlugQuery } from '../../lib/queries' import { previewClient } from '../../lib/sanity.server' +function redirectToPreview(res, Location) { + // Enable Preview Mode by setting the cookies + res.setPreviewData({}) + // Redirect to a preview capable route + res.writeHead(307, { Location }) + res.end() +} + export default async function preview(req, res) { - // Check the secret and next parameters - // This secret should only be known to this API route and the CMS - if ( - req.query.secret !== process.env.SANITY_PREVIEW_SECRET || - !req.query.slug - ) { - return res.status(401).json({ message: 'Invalid token' }) + 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 @@ -21,11 +34,7 @@ export default async function preview(req, res) { return res.status(401).json({ message: 'Invalid slug' }) } - // Enable Preview Mode by setting the cookies - res.setPreviewData({}) - // Redirect to the path from the fetched post // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities - res.writeHead(307, { Location: `/posts/${post.slug}` }) - res.end() + redirectToPreview(res, `/posts/${post.slug}`) } diff --git a/examples/cms-sanity/pages/api/revalidate.js b/examples/cms-sanity/pages/api/revalidate.js index e346590cac43..96cac3a9a3c1 100644 --- a/examples/cms-sanity/pages/api/revalidate.js +++ b/examples/cms-sanity/pages/api/revalidate.js @@ -1,11 +1,18 @@ -import { isValidRequest } from '@sanity/webhook' +import { isValidSignature, SIGNATURE_HEADER_NAME } from '@sanity/webhook' import { sanityClient } from '../../lib/sanity.server' -const AUTHOR_UPDATED_QUERY = ` +// 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 = `*[_type == "post" && _id == $id].slug.current` +const POST_UPDATED_QUERY = /* groq */ `*[_type == "post" && _id == $id].slug.current` const getQueryForType = (type) => { switch (type) { @@ -21,14 +28,32 @@ const getQueryForType = (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) { - if (!isValidRequest(req, process.env.SANITY_STUDIO_REVALIDATE_SECRET)) { - const invalidRequest = 'Invalid request' - log(invalidRequest, true) - return res.status(401).json({ message: invalidRequest }) + 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 { _id: id, _type } = req.body + const jsonBody = JSON.parse(body) + const { _id: id, _type } = jsonBody if (typeof id !== 'string' || !id) { const invalidId = 'Invalid _id' log(invalidId, true) diff --git a/examples/cms-sanity/pages/index.js b/examples/cms-sanity/pages/index.js index 615cfa8e50e7..bd9d90f62fe4 100644 --- a/examples/cms-sanity/pages/index.js +++ b/examples/cms-sanity/pages/index.js @@ -6,11 +6,15 @@ import Intro from '../components/intro' import Layout from '../components/layout' import { CMS_NAME } from '../lib/constants' import { indexQuery } from '../lib/queries' +import { usePreviewSubscription } from '../lib/sanity' import { getClient, overlayDrafts } from '../lib/sanity.server' -export default function Index({ allPosts, preview }) { - const heroPost = allPosts[0] - const morePosts = allPosts.slice(1) +export default function Index({ allPosts: initialAllPosts, preview }) { + const { data: allPosts } = usePreviewSubscription(indexQuery, { + initialData: initialAllPosts, + enabled: preview, + }) + const [heroPost, ...morePosts] = allPosts || [] return ( <> @@ -40,5 +44,7 @@ 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 index d7b2f07e048e..770b38bced45 100644 --- a/examples/cms-sanity/pages/posts/[slug].js +++ b/examples/cms-sanity/pages/posts/[slug].js @@ -43,7 +43,7 @@ export default function Post({ data = {}, preview }) { {post.title} | Next.js Blog Example with {CMS_NAME} - {post.coverImage && ( + {post.coverImage?.asset?._ref && ( 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 new file mode 100644 index 000000000000..02639b02a1cc --- /dev/null +++ b/examples/cms-sanity/studio/schemas/post.js @@ -0,0 +1,64 @@ +export default { + 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/schemas/schema.js b/examples/cms-sanity/studio/schemas/schema.js new file mode 100644 index 000000000000..2972a54b252d --- /dev/null +++ b/examples/cms-sanity/studio/schemas/schema.js @@ -0,0 +1,23 @@ +// First, we must import the schema creator +import createSchema from 'part:@sanity/base/schema-creator' + +// Then import schema types from any plugins that might expose them +import schemaTypes from 'all:part:@sanity/base/schema-type' + +// We import object and document schemas +// import blockContent from './blockContent' +import post from './post' +import author from './author' + +// Then we give our schema to the builder and provide the result to Sanity +export default createSchema({ + // We name our schema + name: 'default', + // Then proceed to concatenate our document type + // to the ones provided by any plugins that are installed + types: schemaTypes.concat([ + /* Your types here! */ + post, + author, + ]), +}) diff --git a/examples/cms-sanity/studio/static/.gitkeep b/examples/cms-sanity/studio/static/.gitkeep new file mode 100644 index 000000000000..37178a72a546 --- /dev/null +++ b/examples/cms-sanity/studio/static/.gitkeep @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000000000000000000000000000000000..7305cdbf5c90a28ce1786cd0f85e8cad9f37e5ba GIT binary patch literal 1150 zcma)*y-R{o6vj_c)DUS6K`k}eu-5cQA<#OdgnLGzB$wTd$c)i|fbVYR4R)9Zj)&s8x&#UD_cjfVTR;fqG zH$kq(JyRPrd%&GpUgDO?`+U9udSu|viKiw{eM#2?D}g|u+vIf)H-~N=|4TEzU5!2W z;oHTpXLHw1YjNm@@hza&J-^kwRq}mHV@C9Pj!86+mfjwzr^4ZIuj$h{ToioT^|t&T zE{2yI);QqCLB*!eb6zLZ7X5zzNTZ{^f-bIJVuoj!O(YVX^@{d0yvN`M?|d_*_Jf;H za2NFRs2lp;Svv~!`KRhgCe&lPKLz==$#Z8Ma7Q)%otR{;2z{5yFTi(=E4Dfh^fUDS zd(WyjP(m|lu5F)dZ`M4Z&x7^?1&{)9{RZq>&A&!PN