├── studio ├── plugins │ └── .gitkeep ├── .eslintrc ├── config │ ├── @sanity │ │ ├── data-aspects.json │ │ ├── default-layout.json │ │ └── default-login.json │ └── .checksums ├── static │ └── .gitkeep ├── sanity.cli.ts ├── schemas │ ├── index.ts │ ├── category.ts │ ├── author.ts │ ├── post.ts │ └── blockContent.ts ├── sanity.config.ts ├── .gitignore ├── tsconfig.json ├── README.md └── package.json ├── frontend ├── .eslintrc.json ├── public │ ├── favicon.ico │ ├── vercel.svg │ ├── thirteen.svg │ └── next.svg ├── next.config.js ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── hello.ts │ ├── index.tsx │ └── post │ │ └── [slug].tsx ├── client.ts ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── .gitignore ├── .editorconfig └── README.md /studio/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | User-specific packages can be placed here 2 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /studio/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio" 3 | } 4 | -------------------------------------------------------------------------------- /studio/config/@sanity/data-aspects.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOptions": {} 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | out 5 | .DS_Store 6 | sanitytutorialblog -------------------------------------------------------------------------------- /studio/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /studio/config/@sanity/default-layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolSwitcher": { 3 | "order": [], 4 | "hidden": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/tutorial-sanity-blog-react-next/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /studio/config/@sanity/default-login.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": { 3 | "mode": "append", 4 | "redirectOnSingle": false, 5 | "entries": [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | 3 | export default function App({ Component, pageProps }: AppProps) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /studio/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {defineCliConfig} from 'sanity/cli' 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: 'your-project-id', 6 | dataset: 'production' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /studio/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import blockContent from './blockContent' 2 | import category from './category' 3 | import post from './post' 4 | import author from './author' 5 | 6 | export const schemaTypes = [post, author, category, blockContent] 7 | -------------------------------------------------------------------------------- /frontend/client.ts: -------------------------------------------------------------------------------- 1 | import sanityClient from '@sanity/client' 2 | 3 | export default sanityClient({ 4 | projectId: 'your-project-id', // you can find this in sanity.json 5 | dataset: 'production', // or the name you chose in step 1 6 | useCdn: true // `false` if you want to ensure fresh data 7 | }) -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /frontend/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /studio/config/.checksums: -------------------------------------------------------------------------------- 1 | { 2 | "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!", 3 | "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea", 4 | "@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f", 5 | "@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6" 6 | } 7 | -------------------------------------------------------------------------------- /studio/schemas/category.ts: -------------------------------------------------------------------------------- 1 | import {defineField, defineType} from 'sanity' 2 | 3 | export default defineType({ 4 | name: 'category', 5 | title: 'Category', 6 | type: 'document', 7 | fields: [ 8 | defineField({ 9 | name: 'title', 10 | title: 'Title', 11 | type: 'string', 12 | }), 13 | defineField({ 14 | name: 'description', 15 | title: 'Description', 16 | type: 'text', 17 | }), 18 | ], 19 | }) 20 | -------------------------------------------------------------------------------- /studio/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'sanity' 2 | import {deskTool} from 'sanity/desk' 3 | import {visionTool} from '@sanity/vision' 4 | import {schemaTypes} from './schemas' 5 | 6 | export default defineConfig({ 7 | name: 'default', 8 | title: 'sanity-tutorial-blog', 9 | 10 | projectId: 'your-project-id', 11 | dataset: 'your-dataset-name', 12 | 13 | plugins: [deskTool(), visionTool()], 14 | 15 | schema: { 16 | types: schemaTypes, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /studio/.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 | # Compiled Sanity Studio 9 | /dist 10 | 11 | # Temporary Sanity runtime, generated by the CLI on every dev server start 12 | /.sanity 13 | 14 | # Logs 15 | /logs 16 | *.log 17 | 18 | # Coverage directory used by testing tools 19 | /coverage 20 | 21 | # Misc 22 | .DS_Store 23 | *.pem 24 | 25 | # Typescript 26 | *.tsbuildinfo 27 | -------------------------------------------------------------------------------- /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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "13.1.6", 13 | "@portabletext/react": "^2.0.1", 14 | "@sanity/client": "^4.0.1", 15 | "@sanity/image-url": "^1.0.2", 16 | "@types/node": "18.11.18", 17 | "@types/react": "18.0.27", 18 | "@types/react-dom": "18.0.10", 19 | "eslint": "8.33.0", 20 | "eslint-config-next": "13.1.6", 21 | "groq": "^3.2.6", 22 | "next": "13.1.6", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "4.9.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /studio/README.md: -------------------------------------------------------------------------------- 1 | # Sanity Blogging Content Studio 2 | 3 | Congratulations, you have now installed the Sanity Content Studio, an open source real-time content editing environment connected to the Sanity backend. 4 | 5 | Now you can do the following things: 6 | 7 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) 8 | - Check out the example frontend: [React/Next.js](https://github.com/sanity-io/tutorial-sanity-blog-react-next) 9 | - [Read the blog post about this template](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=readme) 10 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) 11 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) 12 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import groq from 'groq' 3 | import client from '../client' 4 | 5 | const Index = ({posts}) => { 6 | return ( 7 |
8 |

Welcome to a blog!

9 | {posts.length > 0 && posts.map( 10 | ({ _id, title = '', slug = '', publishedAt = '' }) => 11 | slug && ( 12 |
  • 13 | 14 | {title} 15 | {' '} 16 | ({new Date(publishedAt).toDateString()}) 17 |
  • 18 | ) 19 | )} 20 |
    21 | ) 22 | } 23 | 24 | export async function getStaticProps() { 25 | const posts = await client.fetch(groq` 26 | *[_type == "post" && publishedAt < now()] | order(publishedAt desc) 27 | `) 28 | return { 29 | props: { 30 | posts 31 | } 32 | } 33 | } 34 | 35 | export default Index -------------------------------------------------------------------------------- /studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remove-me-sanity-tutorial-blog", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "package.json", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "dev": "sanity dev", 9 | "start": "sanity start", 10 | "build": "sanity build", 11 | "deploy": "sanity deploy", 12 | "deploy-graphql": "sanity graphql deploy" 13 | }, 14 | "keywords": [ 15 | "sanity" 16 | ], 17 | "dependencies": { 18 | "@sanity/vision": "^3.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-is": "^18.2.0", 22 | "sanity": "^3.0.0", 23 | "styled-components": "^5.2.0" 24 | }, 25 | "devDependencies": { 26 | "@sanity/eslint-config-studio": "^2.0.1", 27 | "eslint": "^8.6.0", 28 | "prettier": "^2.8.3", 29 | "typescript": "^4.0.0" 30 | }, 31 | "prettier": { 32 | "semi": false, 33 | "printWidth": 100, 34 | "bracketSpacing": false, 35 | "singleQuote": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /studio/schemas/author.ts: -------------------------------------------------------------------------------- 1 | import {defineField, defineType} from 'sanity' 2 | 3 | export default defineType({ 4 | name: 'author', 5 | title: 'Author', 6 | type: 'document', 7 | fields: [ 8 | defineField({ 9 | name: 'name', 10 | title: 'Name', 11 | type: 'string', 12 | }), 13 | defineField({ 14 | name: 'slug', 15 | title: 'Slug', 16 | type: 'slug', 17 | options: { 18 | source: 'name', 19 | maxLength: 96, 20 | }, 21 | }), 22 | defineField({ 23 | name: 'image', 24 | title: 'Image', 25 | type: 'image', 26 | options: { 27 | hotspot: true, 28 | }, 29 | }), 30 | defineField({ 31 | name: 'bio', 32 | title: 'Bio', 33 | type: 'array', 34 | of: [ 35 | { 36 | title: 'Block', 37 | type: 'block', 38 | styles: [{title: 'Normal', value: 'normal'}], 39 | lists: [], 40 | }, 41 | ], 42 | }), 43 | ], 44 | preview: { 45 | select: { 46 | title: 'name', 47 | media: 'image', 48 | }, 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /frontend/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blogging with Sanity and Next.js 2 | 3 | [Read the tutorial](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=github&github_campaing=rbt) 4 | 5 | ## Get started 6 | 7 | ```sh 8 | # Install the Sanity command line interface 9 | ~/ 10 | > npm i -g @sanity/cli 11 | 12 | # Initiate your own project in the studio folder 13 | ~/this-blog/studio 14 | > sanity init 15 | 16 | # Add a CORS-origin rule to allow the frontend to request data 17 | ~/this-blog/studio 18 | > sanity cors add http://localhost:3000 --no-credentials 19 | 20 | # Insert the projectId and dataset name from Sanity in client.js 21 | ~/this-blog/web 22 | > nano client.js 23 | 24 | # Install frontend dependencies 25 | ~/this-blog/web 26 | > npm install 27 | 28 | # Run Next.js in development mode 29 | ~/this-blog/web 30 | > npm run dev 31 | ``` 32 | 33 | ## Deploy on vercel 34 | 35 | ```sh 36 | ~/this-blog/web 37 | > npm i -g vercel 38 | > vercel login 39 | > vercel 40 | ``` 41 | 42 | ## Deploy as a static site on Netlify 43 | 44 | [Read the tutorial](https://www.sanity.io/blog/tutorial-host-your-sanity-based-next-js-project-on-netlify?utm_source=github&utm_campaign=netlifyexport) 45 | 46 | ```sh 47 | ~/this-blog/web 48 | npm run export 49 | # exports your site as static files in /out 50 | ``` 51 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /studio/schemas/post.ts: -------------------------------------------------------------------------------- 1 | import {defineField, defineType} from 'sanity' 2 | 3 | export default defineType({ 4 | name: 'post', 5 | title: 'Post', 6 | type: 'document', 7 | fields: [ 8 | defineField({ 9 | name: 'title', 10 | title: 'Title', 11 | type: 'string', 12 | }), 13 | defineField({ 14 | name: 'slug', 15 | title: 'Slug', 16 | type: 'slug', 17 | options: { 18 | source: 'title', 19 | maxLength: 96, 20 | }, 21 | }), 22 | defineField({ 23 | name: 'author', 24 | title: 'Author', 25 | type: 'reference', 26 | to: {type: 'author'}, 27 | }), 28 | defineField({ 29 | name: 'mainImage', 30 | title: 'Main image', 31 | type: 'image', 32 | options: { 33 | hotspot: true, 34 | }, 35 | }), 36 | defineField({ 37 | name: 'categories', 38 | title: 'Categories', 39 | type: 'array', 40 | of: [{type: 'reference', to: {type: 'category'}}], 41 | }), 42 | defineField({ 43 | name: 'publishedAt', 44 | title: 'Published at', 45 | type: 'datetime', 46 | }), 47 | defineField({ 48 | name: 'body', 49 | title: 'Body', 50 | type: 'blockContent', 51 | }), 52 | ], 53 | 54 | preview: { 55 | select: { 56 | title: 'title', 57 | author: 'author.name', 58 | media: 'mainImage', 59 | }, 60 | prepare(selection) { 61 | const {author} = selection 62 | return {...selection, subtitle: author && `by ${author}`} 63 | }, 64 | }, 65 | }) 66 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /studio/schemas/blockContent.ts: -------------------------------------------------------------------------------- 1 | import {defineType, defineArrayMember} from 'sanity' 2 | 3 | /** 4 | * This is the schema definition for the rich text fields used for 5 | * for this blog studio. When you import it in schemas.js it can be 6 | * reused in other parts of the studio with: 7 | * { 8 | * name: 'someName', 9 | * title: 'Some title', 10 | * type: 'blockContent' 11 | * } 12 | */ 13 | export default defineType({ 14 | title: 'Block Content', 15 | name: 'blockContent', 16 | type: 'array', 17 | of: [ 18 | defineArrayMember({ 19 | title: 'Block', 20 | type: 'block', 21 | // Styles let you set what your user can mark up blocks with. These 22 | // correspond with HTML tags, but you can set any title or value 23 | // you want and decide how you want to deal with it where you want to 24 | // use your content. 25 | styles: [ 26 | {title: 'Normal', value: 'normal'}, 27 | {title: 'H1', value: 'h1'}, 28 | {title: 'H2', value: 'h2'}, 29 | {title: 'H3', value: 'h3'}, 30 | {title: 'H4', value: 'h4'}, 31 | {title: 'Quote', value: 'blockquote'}, 32 | ], 33 | lists: [{title: 'Bullet', value: 'bullet'}], 34 | // Marks let you mark up inline text in the block editor. 35 | marks: { 36 | // Decorators usually describe a single property – e.g. a typographic 37 | // preference or highlighting by editors. 38 | decorators: [ 39 | {title: 'Strong', value: 'strong'}, 40 | {title: 'Emphasis', value: 'em'}, 41 | ], 42 | // Annotations can be any object structure – e.g. a link or a footnote. 43 | annotations: [ 44 | { 45 | title: 'URL', 46 | name: 'link', 47 | type: 'object', 48 | fields: [ 49 | { 50 | title: 'URL', 51 | name: 'href', 52 | type: 'url', 53 | }, 54 | ], 55 | }, 56 | ], 57 | }, 58 | }), 59 | // You can add additional types here. Note that you can't use 60 | // primitive types such as 'string' and 'number' in the same array 61 | // as a block type. 62 | defineArrayMember({ 63 | type: 'image', 64 | options: {hotspot: true}, 65 | }), 66 | ], 67 | }) 68 | -------------------------------------------------------------------------------- /frontend/pages/post/[slug].tsx: -------------------------------------------------------------------------------- 1 | // [slug].tsx 2 | 3 | import groq from 'groq' 4 | import imageUrlBuilder from '@sanity/image-url' 5 | import {PortableText} from '@portabletext/react' 6 | import client from '../../client' 7 | 8 | function urlFor (source) { 9 | return imageUrlBuilder(client).image(source) 10 | } 11 | 12 | const ptComponents = { 13 | types: { 14 | image: ({ value }) => { 15 | if (!value?.asset?._ref) { 16 | return null 17 | } 18 | return ( 19 | {value.alt 24 | ) 25 | } 26 | } 27 | } 28 | 29 | const Post = ({post}) => { 30 | const { 31 | title = 'Missing title', 32 | name = 'Missing name', 33 | categories, 34 | authorImage, 35 | body = [] 36 | } = post 37 | return ( 38 |
    39 |

    {title}

    40 | By {name} 41 | {categories && ( 42 |
      43 | Posted in 44 | {categories.map(category =>
    • {category}
    • )} 45 |
    46 | )} 47 | {authorImage && ( 48 |
    49 | {`${name}'s 55 |
    56 | )} 57 | 61 |
    62 | ) 63 | } 64 | 65 | const query = groq`*[_type == "post" && slug.current == $slug][0]{ 66 | title, 67 | "name": author->name, 68 | "categories": categories[]->title, 69 | "authorImage": author->image, 70 | body 71 | }` 72 | export async function getStaticPaths() { 73 | const paths = await client.fetch( 74 | groq`*[_type == "post" && defined(slug.current)][].slug.current` 75 | ) 76 | 77 | return { 78 | paths: paths.map((slug) => ({params: {slug}})), 79 | fallback: true, 80 | } 81 | } 82 | 83 | export async function getStaticProps(context) { 84 | // It's important to default the slug so that it doesn't return "undefined" 85 | const { slug = "" } = context.params 86 | const post = await client.fetch(query, { slug }) 87 | return { 88 | props: { 89 | post 90 | } 91 | } 92 | } 93 | export default Post --------------------------------------------------------------------------------