Skip to content

Commit 0c7217b

Browse files
authored
docs(examples): use vercel integration in cms-sanity (vercel#39323)
## Feature - [x] Documentation added ## Documentation / Examples - [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) This PR updates the cms-sanity example to: - Deploy to Vercel with the [Sanity Vercel Integration](https://www.sanity.io/docs/vercel-integration) by default. - You can still opt-out of using the Sanity Vercel Integration, clicking `You can also set up manually` gives you all the information you need. - The blog itself is updated so it's much more resilient to missing data, and is setup to set `revalidate: 60` until you have a On-demand Revalidation webhook setup. - Preview Mode is now enabled on the frontpage as well. - The Sanity client setup, and webhook validation, are updated to follow our current best practices.
1 parent 911ba23 commit 0c7217b

35 files changed

+663
-246
lines changed
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
NEXT_PUBLIC_SANITY_PROJECT_ID=
22
NEXT_PUBLIC_SANITY_DATASET=
3-
SANITY_API_TOKEN=
4-
SANITY_PREVIEW_SECRET=
5-
SANITY_STUDIO_REVALIDATE_SECRET=
3+
SANITY_API_READ_TOKEN=

examples/cms-sanity/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# dependencies
44
/node_modules
5+
/studio/node_modules
56
/.pnp
67
.pnp.js
78

@@ -14,6 +15,7 @@
1415

1516
# production
1617
/build
18+
/studio/dist
1719

1820
# misc
1921
.DS_Store
@@ -34,3 +36,7 @@ yarn-error.log*
3436
# typescript
3537
*.tsbuildinfo
3638
next-env.d.ts
39+
40+
# Env files created by scripts for working locally
41+
.env
42+
studio/.env.development

examples/cms-sanity/README.md

Lines changed: 278 additions & 104 deletions
Large diffs are not rendered by default.

examples/cms-sanity/components/avatar.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import Image from 'next/image'
1+
import Image from 'next/future/image'
22
import { urlForImage } from '../lib/sanity'
33

44
export default function Avatar({ name, picture }) {
55
return (
66
<div className="flex items-center">
7-
<div className="w-12 h-12 relative mr-4">
7+
<div className="relative w-12 h-12 mr-4">
88
<Image
9-
src={urlForImage(picture).height(96).width(96).fit('crop').url()}
10-
layout="fill"
9+
src={
10+
picture?.asset?._ref
11+
? urlForImage(picture).height(96).width(96).fit('crop').url()
12+
: 'https://source.unsplash.com/96x96/?face'
13+
}
1114
className="rounded-full"
15+
height={96}
16+
width={96}
1217
alt={name}
1318
/>
1419
</div>

examples/cms-sanity/components/cover-image.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import cn from 'classnames'
2-
import Image from 'next/image'
2+
import Image from 'next/future/image'
33
import Link from 'next/link'
44
import { urlForImage } from '../lib/sanity'
55

6-
export default function CoverImage({ title, slug, image: source }) {
7-
const image = source ? (
6+
export default function CoverImage({ title, slug, image: source, priority }) {
7+
const image = source?.asset?._ref ? (
88
<div
99
className={cn('shadow-small', {
1010
'hover:shadow-medium transition-shadow duration-200': slug,
1111
})}
1212
>
1313
<Image
14+
className="w-full h-auto"
1415
layout="responsive"
1516
width={2000}
1617
height={1000}
1718
alt={`Cover Image for ${title}`}
1819
src={urlForImage(source).height(1000).width(2000).url()}
20+
sizes="100vw"
21+
priority={priority}
1922
/>
2023
</div>
2124
) : (
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { parseISO, format } from 'date-fns'
22

33
export default function Date({ dateString }) {
4+
if (!dateString) return null
5+
46
const date = parseISO(dateString)
57
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
68
}

examples/cms-sanity/components/hero-post.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ export default function HeroPost({
1414
return (
1515
<section>
1616
<div className="mb-8 md:mb-16">
17-
<CoverImage slug={slug} title={title} image={coverImage} />
17+
<CoverImage slug={slug} title={title} image={coverImage} priority />
1818
</div>
19-
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
19+
<div className="mb-20 md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 md:mb-28">
2020
<div>
21-
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
21+
<h3 className="mb-4 text-4xl leading-tight lg:text-6xl">
2222
<Link href={`/posts/${slug}`}>
2323
<a className="hover:underline">{title}</a>
2424
</Link>
2525
</h3>
26-
<div className="mb-4 md:mb-0 text-lg">
26+
<div className="mb-4 text-lg md:mb-0">
2727
<Date dateString={date} />
2828
</div>
2929
</div>
3030
<div>
31-
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
32-
<Avatar name={author.name} picture={author.picture} />
31+
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
32+
{author && <Avatar name={author.name} picture={author.picture} />}
3333
</div>
3434
</div>
3535
</section>

examples/cms-sanity/components/post-header.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ export default function PostHeader({ title, coverImage, date, author }) {
88
<>
99
<PostTitle>{title}</PostTitle>
1010
<div className="hidden md:block md:mb-12">
11-
<Avatar name={author.name} picture={author.picture} />
11+
{author && <Avatar name={author.name} picture={author.picture} />}
1212
</div>
1313
<div className="mb-8 md:mb-16 sm:mx-0">
14-
<CoverImage title={title} image={coverImage} />
14+
<CoverImage title={title} image={coverImage} priority />
1515
</div>
1616
<div className="max-w-2xl mx-auto">
17-
<div className="block md:hidden mb-6">
18-
<Avatar name={author.name} picture={author.picture} />
17+
<div className="block mb-6 md:hidden">
18+
{author && <Avatar name={author.name} picture={author.picture} />}
1919
</div>
2020
<div className="mb-6 text-lg">
2121
<Date dateString={date} />

examples/cms-sanity/components/post-preview.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ export default function PostPreview({
1616
<div className="mb-5">
1717
<CoverImage slug={slug} title={title} image={coverImage} />
1818
</div>
19-
<h3 className="text-3xl mb-3 leading-snug">
19+
<h3 className="mb-3 text-3xl leading-snug">
2020
<Link href={`/posts/${slug}`}>
2121
<a className="hover:underline">{title}</a>
2222
</Link>
2323
</h3>
24-
<div className="text-lg mb-4">
24+
<div className="mb-4 text-lg">
2525
<Date dateString={date} />
2626
</div>
27-
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
28-
<Avatar name={author.name} picture={author.picture} />
27+
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
28+
{author && <Avatar name={author.name} picture={author.picture} />}
2929
</div>
3030
)
3131
}

examples/cms-sanity/lib/config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ export const sanityConfig = {
22
// Find your project ID and dataset in `sanity.json` in your studio project
33
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
44
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
5-
useCdn: process.env.NODE_ENV !== 'production',
5+
useCdn:
6+
typeof document !== 'undefined' && process.env.NODE_ENV === 'production',
67
// useCdn == true gives fast, cheap responses using a globally distributed cache.
78
// When in production the Sanity API is only queried on build-time, and on-demand when responding to webhooks.
89
// Thus the data need to be fresh and API response time is less important.
910
// When in development/working locally, it's more important to keep costs down as hot reloading can incurr a lot of API calls
1011
// And every page load calls getStaticProps.
1112
// To get the lowest latency, lowest cost, and latest data, use the Instant Preview mode
12-
apiVersion: '2021-03-25',
13+
apiVersion: '2022-03-13',
1314
// see https://www.sanity.io/docs/api-versioning for how versioning works
1415
}

examples/cms-sanity/lib/sanity.server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ export const sanityClient = createClient(sanityConfig)
1111
export const previewClient = createClient({
1212
...sanityConfig,
1313
useCdn: false,
14-
token: process.env.SANITY_API_TOKEN,
14+
// Fallback to using the WRITE token until https://www.sanity.io/docs/vercel-integration starts shipping a READ token.
15+
// 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
16+
token:
17+
process.env.SANITY_API_READ_TOKEN || process.env.SANITY_API_WRITE_TOKEN,
1518
})
1619

1720
export const getClient = (preview) => (preview ? previewClient : sanityClient)

examples/cms-sanity/next.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
module.exports = {
2+
experimental: {
3+
images: {
4+
allowFutureImage: true,
5+
},
6+
},
27
images: {
3-
domains: ['cdn.sanity.io'],
8+
domains: ['cdn.sanity.io', 'source.unsplash.com'],
49
},
510
}

examples/cms-sanity/package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
"scripts": {
44
"dev": "next",
55
"build": "next build",
6-
"start": "next start"
6+
"start": "next start",
7+
"studio:dev": "npm --prefix studio run start",
8+
"studio:deploy": "npx vercel env pull && npm --prefix studio run deploy"
79
},
810
"dependencies": {
9-
"@portabletext/react": "^1.0.3",
11+
"@portabletext/react": "^1.0.6",
1012
"@sanity/image-url": "^1.0.1",
11-
"@sanity/webhook": "^1.0.2",
12-
"classnames": "2.3.1",
13-
"date-fns": "2.28.0",
13+
"@sanity/webhook": "^2.0.0",
14+
"classnames": "^2.3.1",
15+
"date-fns": "^2.29.1",
1416
"next": "latest",
15-
"next-sanity": "0.5.0",
17+
"next-sanity": "^0.6.0",
1618
"react": "^17.0.2",
1719
"react-dom": "^17.0.2"
1820
},
1921
"devDependencies": {
20-
"autoprefixer": "10.4.2",
21-
"postcss": "8.4.7",
22-
"tailwindcss": "^3.0.23"
22+
"autoprefixer": "^10.4.8",
23+
"postcss": "^8.4.14",
24+
"tailwindcss": "^3.1.7"
2325
}
2426
}
Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
import { postBySlugQuery } from '../../lib/queries'
22
import { previewClient } from '../../lib/sanity.server'
33

4+
function redirectToPreview(res, Location) {
5+
// Enable Preview Mode by setting the cookies
6+
res.setPreviewData({})
7+
// Redirect to a preview capable route
8+
res.writeHead(307, { Location })
9+
res.end()
10+
}
11+
412
export default async function preview(req, res) {
5-
// Check the secret and next parameters
6-
// This secret should only be known to this API route and the CMS
7-
if (
8-
req.query.secret !== process.env.SANITY_PREVIEW_SECRET ||
9-
!req.query.slug
10-
) {
11-
return res.status(401).json({ message: 'Invalid token' })
13+
const secret = process.env.SANITY_STUDIO_PREVIEW_SECRET
14+
// Only require a secret when in production
15+
if (!secret && process.env.NODE_ENV === 'production') {
16+
throw new TypeError(`Missing SANITY_STUDIO_PREVIEW_SECRET`)
17+
}
18+
// Check the secret if it's provided, enables running preview mode locally before the env var is setup
19+
if (secret && req.query.secret !== secret) {
20+
return res.status(401).json({ message: 'Invalid secret' })
21+
}
22+
// If no slug is provided open preview mode on the frontpage
23+
if (!req.query.slug) {
24+
return redirectToPreview(res, '/')
1225
}
1326

1427
// Check if the post with the given `slug` exists
@@ -21,11 +34,7 @@ export default async function preview(req, res) {
2134
return res.status(401).json({ message: 'Invalid slug' })
2235
}
2336

24-
// Enable Preview Mode by setting the cookies
25-
res.setPreviewData({})
26-
2737
// Redirect to the path from the fetched post
2838
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
29-
res.writeHead(307, { Location: `/posts/${post.slug}` })
30-
res.end()
39+
redirectToPreview(res, `/posts/${post.slug}`)
3140
}

examples/cms-sanity/pages/api/revalidate.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import { isValidRequest } from '@sanity/webhook'
1+
import { isValidSignature, SIGNATURE_HEADER_NAME } from '@sanity/webhook'
22
import { sanityClient } from '../../lib/sanity.server'
33

4-
const AUTHOR_UPDATED_QUERY = `
4+
// Next.js will by default parse the body, which can lead to invalid signatures
5+
export const config = {
6+
api: {
7+
bodyParser: false,
8+
},
9+
}
10+
11+
const AUTHOR_UPDATED_QUERY = /* groq */ `
512
*[_type == "author" && _id == $id] {
613
"slug": *[_type == "post" && references(^._id)].slug.current
714
}["slug"][]`
8-
const POST_UPDATED_QUERY = `*[_type == "post" && _id == $id].slug.current`
15+
const POST_UPDATED_QUERY = /* groq */ `*[_type == "post" && _id == $id].slug.current`
916

1017
const getQueryForType = (type) => {
1118
switch (type) {
@@ -21,14 +28,32 @@ const getQueryForType = (type) => {
2128
const log = (msg, error) =>
2229
console[error ? 'error' : 'log'](`[revalidate] ${msg}`)
2330

31+
async function readBody(readable) {
32+
const chunks = []
33+
for await (const chunk of readable) {
34+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
35+
}
36+
return Buffer.concat(chunks).toString('utf8')
37+
}
38+
2439
export default async function revalidate(req, res) {
25-
if (!isValidRequest(req, process.env.SANITY_STUDIO_REVALIDATE_SECRET)) {
26-
const invalidRequest = 'Invalid request'
27-
log(invalidRequest, true)
28-
return res.status(401).json({ message: invalidRequest })
40+
const signature = req.headers[SIGNATURE_HEADER_NAME]
41+
const body = await readBody(req) // Read the body into a string
42+
if (
43+
!isValidSignature(
44+
body,
45+
signature,
46+
process.env.SANITY_REVALIDATE_SECRET?.trim()
47+
)
48+
) {
49+
const invalidSignature = 'Invalid signature'
50+
log(invalidSignature, true)
51+
res.status(401).json({ success: false, message: invalidSignature })
52+
return
2953
}
3054

31-
const { _id: id, _type } = req.body
55+
const jsonBody = JSON.parse(body)
56+
const { _id: id, _type } = jsonBody
3257
if (typeof id !== 'string' || !id) {
3358
const invalidId = 'Invalid _id'
3459
log(invalidId, true)

examples/cms-sanity/pages/index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import Intro from '../components/intro'
66
import Layout from '../components/layout'
77
import { CMS_NAME } from '../lib/constants'
88
import { indexQuery } from '../lib/queries'
9+
import { usePreviewSubscription } from '../lib/sanity'
910
import { getClient, overlayDrafts } from '../lib/sanity.server'
1011

11-
export default function Index({ allPosts, preview }) {
12-
const heroPost = allPosts[0]
13-
const morePosts = allPosts.slice(1)
12+
export default function Index({ allPosts: initialAllPosts, preview }) {
13+
const { data: allPosts } = usePreviewSubscription(indexQuery, {
14+
initialData: initialAllPosts,
15+
enabled: preview,
16+
})
17+
const [heroPost, ...morePosts] = allPosts || []
1418
return (
1519
<>
1620
<Layout preview={preview}>
@@ -40,5 +44,7 @@ export async function getStaticProps({ preview = false }) {
4044
const allPosts = overlayDrafts(await getClient(preview).fetch(indexQuery))
4145
return {
4246
props: { allPosts, preview },
47+
// If webhooks isn't setup then attempt to re-generate in 1 minute intervals
48+
revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
4349
}
4450
}

examples/cms-sanity/pages/posts/[slug].js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Post({ data = {}, preview }) {
4343
<title>
4444
{post.title} | Next.js Blog Example with {CMS_NAME}
4545
</title>
46-
{post.coverImage && (
46+
{post.coverImage?.asset?._ref && (
4747
<meta
4848
key="ogImage"
4949
property="og:image"
@@ -85,6 +85,8 @@ export async function getStaticProps({ params, preview = false }) {
8585
morePosts: overlayDrafts(morePosts),
8686
},
8787
},
88+
// If webhooks isn't setup then attempt to re-generate in 1 minute intervals
89+
revalidate: process.env.SANITY_REVALIDATE_SECRET ? undefined : 60,
8890
}
8991
}
9092

0 commit comments

Comments
 (0)