diff --git a/examples/cms-sanity/.env.local.example b/examples/cms-sanity/.env.local.example index 6bc5dc43020b..a246ea991e7c 100644 --- a/examples/cms-sanity/.env.local.example +++ b/examples/cms-sanity/.env.local.example @@ -2,3 +2,4 @@ NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= SANITY_API_TOKEN= SANITY_PREVIEW_SECRET= +SANITY_STUDIO_REVALIDATE_SECRET= diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index e7b5d268d7f9..d74846361b8e 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -6,6 +6,7 @@ You'll get: - Sanity Studio running on localhost - 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 @@ -15,7 +16,7 @@ You'll get: 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&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) +[![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 @@ -80,6 +81,7 @@ Then set each variable on `.env.local`: - `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). Your `.env.local` file should look like this: @@ -88,6 +90,7 @@ NEXT_PUBLIC_SANITY_PROJECT_ID=... NEXT_PUBLIC_SANITY_DATASET=... SANITY_API_TOKEN=... SANITY_PREVIEW_SECRET=... +SANITY_STUDIO_REVALIDATE_SECRET=... ``` ### Step 5. Prepare the project for previewing @@ -193,9 +196,25 @@ To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [ Alternatively, you can deploy using our template by clicking on the Deploy button below. -[![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&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) +[![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) -#### Next steps +### Step 11. Setup Revalidation Webhook + +- 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` +- 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. +- 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. + +### Next steps -- Invalidate your routes in production [on-demand](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with GROQ powered webhooks - Mount your preview inside the Sanity Studio for comfortable side-by-side editing +- [Join the Sanity community](https://slack.sanity.io/) diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js index b4b0101e5494..c28d1f9c1b86 100644 --- a/examples/cms-sanity/lib/config.js +++ b/examples/cms-sanity/lib/config.js @@ -2,10 +2,13 @@ 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: process.env.NODE_ENV !== 'production', // useCdn == true gives fast, cheap responses using a globally distributed cache. - // Set this to false if your application require the freshest possible - // data always (potentially slightly slower and a bit more expensive). + // 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', // see https://www.sanity.io/docs/api-versioning for how versioning works } diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 8b576f61ae8c..529122e867f4 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -8,6 +8,7 @@ "dependencies": { "@portabletext/react": "^1.0.3", "@sanity/image-url": "^1.0.1", + "@sanity/webhook": "^1.0.2", "classnames": "2.3.1", "date-fns": "2.28.0", "next": "latest", diff --git a/examples/cms-sanity/pages/api/revalidate.js b/examples/cms-sanity/pages/api/revalidate.js new file mode 100644 index 000000000000..9e2af5b3ebbd --- /dev/null +++ b/examples/cms-sanity/pages/api/revalidate.js @@ -0,0 +1,56 @@ +import { isValidRequest } from '@sanity/webhook' +import { sanityClient } from '../../lib/sanity.server' + +const AUTHOR_UPDATED_QUERY = ` + *[_type == "author" && _id == $id] { + "slug": *[_type == "post" && references(^._id)].slug.current + }["slug"][]` +const POST_UPDATED_QUERY = `*[_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}`) + +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 { _id: id, _type } = req.body + 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.unstable_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 }) + } +}