├── .gitignore ├── README.md ├── blog-frontend ├── .env.local.example ├── .gitignore ├── README.md ├── components │ ├── alert.js │ ├── avatar.js │ ├── comments.js │ ├── container.js │ ├── cover-image.js │ ├── date.js │ ├── footer.js │ ├── form.js │ ├── header.js │ ├── hero-post.js │ ├── intro.js │ ├── layout.js │ ├── markdown-styles.module.css │ ├── meta.js │ ├── more-stories.js │ ├── post-body.js │ ├── post-header.js │ ├── post-preview.js │ ├── post-title.js │ └── section-separator.js ├── lib │ ├── api.js │ ├── constants.js │ └── sanity.js ├── package.json ├── pages │ ├── _app.js │ ├── _document.js │ ├── api │ │ ├── createComment.js │ │ ├── exit-preview.js │ │ └── preview.js │ ├── index.js │ └── posts │ │ └── [slug].js ├── postcss.config.js ├── public │ └── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest ├── schemas │ └── schema.js ├── styles │ └── index.css ├── tailwind.config.js └── yarn.lock └── studio ├── README.md ├── config ├── .checksums └── @sanity │ ├── data-aspects.json │ ├── default-layout.json │ ├── default-login.json │ └── form-builder.json ├── package.json ├── plugins └── .gitkeep ├── sanity.json ├── schemas ├── author.js ├── blockContent.js ├── category.js ├── comment.js ├── post.js └── schema.js ├── static ├── .gitkeep └── favicon.ico ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Next.js blog with comment section 2 | 3 | This is a demo of how to add a simple comment section to blog post using [Next.js](https://nextjs.org), [Sanity.io](https://www.sanity.io), and [Vercel](https://vercel.com). 4 | 5 | [You can watch the walkthrough here](https://youtu.be/NzUNMUHxvZ4) 6 | 7 | This repository is just a demo and will not be maintained. You're always welcome to join [our community](https://slack.sanity.io/?utm_source=github&utm_medium=readme&utm_campaign=community) and ask for help there if you're stuck. 8 | -------------------------------------------------------------------------------- /blog-frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SANITY_PROJECT_ID= 2 | SANITY_API_TOKEN= 3 | SANITY_PREVIEW_SECRET= -------------------------------------------------------------------------------- /blog-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /blog-frontend/README.md: -------------------------------------------------------------------------------- 1 | # A statically generated blog example using Next.js and Sanity 2 | 3 | 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. 4 | 5 | ## Demo 6 | 7 | ### [https://next-blog-sanity.now.sh/](https://next-blog-sanity.now.sh/) 8 | 9 | ## Deploy your own 10 | 11 | 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): 12 | 13 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/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) 14 | 15 | ### Related examples 16 | 17 | - [WordPress](/examples/cms-wordpress) 18 | - [DatoCMS](/examples/cms-datocms) 19 | - [TakeShape](/examples/cms-takeshape) 20 | - [Prismic](/examples/cms-prismic) 21 | - [Contentful](/examples/cms-contentful) 22 | - [Strapi](/examples/cms-strapi) 23 | - [Agility CMS](/examples/cms-agilitycms) 24 | - [Cosmic](/examples/cms-cosmic) 25 | - [ButterCMS](/examples/cms-buttercms) 26 | - [Storyblok](/examples/cms-storyblok) 27 | - [GraphCMS](/examples/cms-graphcms) 28 | - [Blog Starter](/examples/blog-starter) 29 | 30 | ## How to use 31 | 32 | ### Using `create-next-app` 33 | 34 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: 35 | 36 | ```bash 37 | npx create-next-app --example cms-sanity cms-sanity-app 38 | # or 39 | yarn create next-app --example cms-sanity cms-sanity-app 40 | ``` 41 | 42 | ### Download manually 43 | 44 | Download the example: 45 | 46 | ```bash 47 | curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/cms-sanity 48 | cd cms-sanity 49 | ``` 50 | 51 | ## Configuration 52 | 53 | ### Step 1. Create an account and a project on Sanity 54 | 55 | First, [create an account on Sanity](https://sanity.io). 56 | 57 | After creating an account, install the Sanity cli from npm `npm i -g @sanity/cli`. 58 | 59 | ### Step 2. Create a new Sanity project 60 | 61 | In a separate folder run `sanity init` to initialize a new studio project. 62 | 63 | This will be where we manage our data. 64 | 65 | 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. 66 | 67 | ### Step 3. Generate an API token 68 | 69 | 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. 70 | 71 | ### Step 4. Set up environment variables 72 | 73 | Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git): 74 | 75 | ```bash 76 | cp .env.local.example .env.local 77 | ``` 78 | 79 | Then set each variable on `.env.local`: 80 | 81 | - `NEXT_PUBLIC_SANITY_PROJECT_ID` should be the `projectId` value from the `sanity.json` file created in step 2. 82 | - `SANITY_API_TOKEN` should be the API token generated in the previous step. 83 | - `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). 84 | 85 | Your `.env.local` file should look like this: 86 | 87 | ```bash 88 | NEXT_PUBLIC_SANITY_PROJECT_ID=... 89 | SANITY_API_TOKEN=... 90 | SANITY_PREVIEW_SECRET=... 91 | ``` 92 | 93 | ### Step 5. Prepare project for previewing 94 | 95 | Go to https://www.sanity.io/docs/preview-content-on-site and follow the three steps on that page. It should be done inside the studio project generated in Step 2. 96 | 97 | When you get to the second step about creating a file called `resolveProductionUrl.js`, copy the following instead: 98 | 99 | ```js 100 | const previewSecret = 'MY_SECRET' // Copy the string you used for SANITY_PREVIEW_SECRET 101 | const projectUrl = 'http://localhost:3000' 102 | 103 | export default function resolveProductionUrl(document) { 104 | return `${projectUrl}/api/preview?secret=${previewSecret}&slug=${document.slug.current}` 105 | } 106 | ``` 107 | 108 | ### Step 6. Copy the schema file 109 | 110 | After initializing your Sanity studio project there should be a `schemas` folder. 111 | 112 | 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. 113 | 114 | ### Step 7. Populate Content 115 | 116 | To add some content go to your Sanity studio project directory and run `sanity start`. 117 | 118 | After the project has started and you have navigated to the URL given in the terminal, select **Author** and create a new record. 119 | 120 | - You just need **1 Author record**. 121 | - Use dummy data for the text. 122 | - For the image, you can download one from [Unsplash](https://unsplash.com/). 123 | 124 | Next, select **Post** and create a new record. 125 | 126 | - We recommend creating at least **2 Post records**. 127 | - Use dummy data for the text. 128 | - You can write markdown for the **Content** field. 129 | - For the images, you can download ones from [Unsplash](https://unsplash.com/). 130 | - Pick the **Author** you created earlier. 131 | 132 | **Important:** For each post record, you need to click **Publish** after saving. If not, the post will be in the draft state. 133 | 134 | ### Step 8. Run Next.js in development mode 135 | 136 | ```bash 137 | npm install 138 | npm run dev 139 | 140 | # or 141 | 142 | yarn install 143 | yarn dev 144 | ``` 145 | 146 | 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). 147 | 148 | ### Step 9. Try preview mode 149 | 150 | On Sanity, go to one of the posts you've created and: 151 | 152 | - **Update the title**. For example, you can add `[Draft]` in front of the title. 153 | - 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. 154 | 155 | 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)). 156 | 157 | 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)) 158 | 159 | 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. 160 | 161 | ### Step 10. Deploy on Vercel 162 | 163 | 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)). 164 | 165 | #### Deploy Your Local Project 166 | 167 | To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import/git?utm_source=github&utm_medium=readme&utm_campaign=next-example). 168 | 169 | **Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. 170 | 171 | #### Deploy from Our Template 172 | 173 | Alternatively, you can deploy using our template by clicking on the Deploy button below. 174 | 175 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/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) 176 | -------------------------------------------------------------------------------- /blog-frontend/components/alert.js: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | import cn from 'classnames' 3 | import { EXAMPLE_PATH } from '../lib/constants' 4 | 5 | export default function Alert({ preview }) { 6 | return ( 7 |
13 | 14 |
15 | {preview ? ( 16 | <> 17 | This page is a preview.{' '} 18 | 22 | Click here 23 | {' '} 24 | to exit preview mode. 25 | 26 | ) : ( 27 | <> 28 | The source code for this blog is{' '} 29 | 33 | available on GitHub 34 | 35 | . 36 | 37 | )} 38 |
39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /blog-frontend/components/avatar.js: -------------------------------------------------------------------------------- 1 | export default function Avatar({ name, picture }) { 2 | return ( 3 |
4 | {name} 5 |
{name}
6 |
7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /blog-frontend/components/comments.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import Date from './date' 3 | 4 | export function Comments({ comments = [] }) { 5 | return ( 6 | 7 |

Comments:

8 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /blog-frontend/components/container.js: -------------------------------------------------------------------------------- 1 | export default function Container({ children }) { 2 | return
{children}
3 | } 4 | -------------------------------------------------------------------------------- /blog-frontend/components/cover-image.js: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import Link from 'next/link' 3 | import { imageBuilder } from '../lib/sanity' 4 | 5 | export default function CoverImage({ title, url, slug }) { 6 | const image = ( 7 | {`Cover 16 | ) 17 | 18 | return ( 19 |
20 | {slug ? ( 21 | 22 | {image} 23 | 24 | ) : ( 25 | image 26 | )} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /blog-frontend/components/date.js: -------------------------------------------------------------------------------- 1 | import { isValid, parseISO, format } from 'date-fns' 2 | 3 | export default function Date({ dateString }) { 4 | if (!isValid(parseISO(dateString))) { 5 | return 'No date' 6 | } 7 | const date = parseISO(dateString) 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /blog-frontend/components/footer.js: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | import { EXAMPLE_PATH } from '../lib/constants' 3 | 4 | export default function Footer() { 5 | return ( 6 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /blog-frontend/components/form.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | import { useForm } from 'react-hook-form' 3 | 4 | export function Form ({_id}) { 5 | const [formData, setFormData] = useState() 6 | const [isSubmitting, setIsSubmitting] = useState(false) 7 | const [hasSubmitted, setHasSubmitted] = useState(false) 8 | const { register, handleSubmit, watch, errors } = useForm() 9 | const onSubmit = async data => { 10 | setIsSubmitting(true) 11 | let response 12 | setFormData(data) 13 | try { 14 | response = await fetch('/api/createComment', { 15 | method: 'POST', 16 | body: JSON.stringify(data), 17 | type: 'application/json' 18 | }) 19 | setIsSubmitting(false) 20 | setHasSubmitted(true) 21 | } catch (err) { 22 | setFormData(err) 23 | } 24 | } 25 | 26 | if (isSubmitting) { 27 | return

Submitting comment…

28 | } 29 | if (hasSubmitted) { 30 | return ( 31 | <> 32 |

Thanks for your comment!

33 | 40 | ) 41 | } 42 | 43 | return ( 44 |
45 | 46 | 50 | 54 | 58 | {/* errors will return when field validation fails */} 59 | {errors.exampleRequired && This field is required} 60 | 61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /blog-frontend/components/header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function Header() { 4 | return ( 5 |

6 | 7 | Blog 8 | 9 | . 10 |

11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /blog-frontend/components/hero-post.js: -------------------------------------------------------------------------------- 1 | import Avatar from '../components/avatar' 2 | import Date from '../components/date' 3 | import CoverImage from '../components/cover-image' 4 | import Link from 'next/link' 5 | 6 | export default function HeroPost({ 7 | title, 8 | coverImage, 9 | date, 10 | excerpt, 11 | author, 12 | slug, 13 | }) { 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 |
21 |

22 | 23 | {title} 24 | 25 |

26 |
27 | 28 |
29 |
30 |
31 |

{excerpt}

32 | 33 |
34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /blog-frontend/components/intro.js: -------------------------------------------------------------------------------- 1 | import { CMS_NAME, CMS_URL } from '../lib/constants' 2 | 3 | export default function Intro() { 4 | return ( 5 |
6 |

7 | Blog. 8 |

9 |

10 | A statically generated blog example using{' '} 11 | 15 | Next.js 16 | {' '} 17 | and{' '} 18 | 22 | {CMS_NAME} 23 | 24 | . 25 |

26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /blog-frontend/components/layout.js: -------------------------------------------------------------------------------- 1 | import Alert from '../components/alert' 2 | import Footer from '../components/footer' 3 | import Meta from '../components/meta' 4 | 5 | export default function Layout({ preview, children }) { 6 | return ( 7 | <> 8 | 9 |
10 | 11 |
{children}
12 |
13 |