Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(examples): use vercel integration in cms-sanity #39323

Merged
merged 3 commits into from Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 1 addition & 3 deletions 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=
6 changes: 6 additions & 0 deletions examples/cms-sanity/.gitignore
Expand Up @@ -2,6 +2,7 @@

# dependencies
/node_modules
/studio/node_modules
/.pnp
.pnp.js

Expand All @@ -14,6 +15,7 @@

# production
/build
/studio/dist

# misc
.DS_Store
Expand All @@ -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
382 changes: 278 additions & 104 deletions examples/cms-sanity/README.md

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions 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 (
<div className="flex items-center">
<div className="w-12 h-12 relative mr-4">
<div className="relative w-12 h-12 mr-4">
<Image
src={urlForImage(picture).height(96).width(96).fit('crop').url()}
layout="fill"
src={
picture?.asset?._ref
? urlForImage(picture).height(96).width(96).fit('crop').url()
: 'https://source.unsplash.com/96x96/?face'
}
className="rounded-full"
height={96}
width={96}
alt={name}
/>
</div>
Expand Down
9 changes: 6 additions & 3 deletions 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 ? (
<div
className={cn('shadow-small', {
'hover:shadow-medium transition-shadow duration-200': slug,
})}
>
<Image
className="w-full h-auto"
layout="responsive"
width={2000}
height={1000}
alt={`Cover Image for ${title}`}
src={urlForImage(source).height(1000).width(2000).url()}
sizes="100vw"
priority={priority}
/>
</div>
) : (
Expand Down
2 changes: 2 additions & 0 deletions 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 <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
12 changes: 6 additions & 6 deletions examples/cms-sanity/components/hero-post.js
Expand Up @@ -14,22 +14,22 @@ export default function HeroPost({
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage slug={slug} title={title} image={coverImage} />
<CoverImage slug={slug} title={title} image={coverImage} priority />
</div>
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
<div className="mb-20 md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
<h3 className="mb-4 text-4xl leading-tight lg:text-6xl">
<Link href={`/posts/${slug}`}>
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="mb-4 md:mb-0 text-lg">
<div className="mb-4 text-lg md:mb-0">
<Date dateString={date} />
</div>
</div>
<div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
</div>
</section>
Expand Down
8 changes: 4 additions & 4 deletions examples/cms-sanity/components/post-header.js
Expand Up @@ -8,14 +8,14 @@ export default function PostHeader({ title, coverImage, date, author }) {
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar name={author.name} picture={author.picture} />
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage title={title} image={coverImage} />
<CoverImage title={title} image={coverImage} priority />
</div>
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar name={author.name} picture={author.picture} />
<div className="block mb-6 md:hidden">
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
<div className="mb-6 text-lg">
<Date dateString={date} />
Expand Down
8 changes: 4 additions & 4 deletions examples/cms-sanity/components/post-preview.js
Expand Up @@ -16,16 +16,16 @@ export default function PostPreview({
<div className="mb-5">
<CoverImage slug={slug} title={title} image={coverImage} />
</div>
<h3 className="text-3xl mb-3 leading-snug">
<h3 className="mb-3 text-3xl leading-snug">
<Link href={`/posts/${slug}`}>
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="text-lg mb-4">
<div className="mb-4 text-lg">
<Date dateString={date} />
</div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
{author && <Avatar name={author.name} picture={author.picture} />}
</div>
)
}
5 changes: 3 additions & 2 deletions examples/cms-sanity/lib/config.js
Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion examples/cms-sanity/lib/sanity.server.js
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion 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'],
},
}
20 changes: 11 additions & 9 deletions examples/cms-sanity/package.json
Expand Up @@ -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"
}
}
33 changes: 21 additions & 12 deletions 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
Expand All @@ -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}`)
}
41 changes: 33 additions & 8 deletions 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) {
Expand All @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions examples/cms-sanity/pages/index.js
Expand Up @@ -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 (
<>
<Layout preview={preview}>
Expand Down Expand Up @@ -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,
}
}
4 changes: 3 additions & 1 deletion examples/cms-sanity/pages/posts/[slug].js
Expand Up @@ -43,7 +43,7 @@ export default function Post({ data = {}, preview }) {
<title>
{post.title} | Next.js Blog Example with {CMS_NAME}
</title>
{post.coverImage && (
{post.coverImage?.asset?._ref && (
<meta
key="ogImage"
property="og:image"
Expand Down Expand Up @@ -85,6 +85,8 @@ export async function getStaticProps({ params, preview = false }) {
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,
}
}

Expand Down