├── .env.example ├── .gitignore ├── README.md ├── package.json ├── pnpm-lock.yaml ├── sanity.cli.ts ├── src ├── app.d.ts ├── app.html ├── hooks.server.ts ├── lib │ ├── components │ │ ├── Header.svelte │ │ └── PreviewBanner.svelte │ ├── config │ │ ├── app.ts │ │ ├── environment.ts │ │ └── sanity │ │ │ ├── client.ts │ │ │ ├── components │ │ │ └── PostsPreview.tsx │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── queries.ts │ │ │ ├── sanity.config.ts │ │ │ ├── schemas │ │ │ ├── author.ts │ │ │ ├── post.ts │ │ │ └── settings.ts │ │ │ └── sveltekit │ │ │ ├── aborter.ts │ │ │ ├── currentUser.ts │ │ │ ├── previewSubscriptionStore.ts │ │ │ └── types.ts │ ├── types │ │ └── index.ts │ └── utils │ │ ├── index.ts │ │ ├── preview-cookies.ts │ │ └── sanity-studio.svelte └── routes │ ├── (app) │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ └── posts │ │ └── [slug] │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── (studio) │ └── studio │ │ └── [...catchall] │ │ ├── +page.server.ts │ │ └── +page@(studio).svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ └── api │ ├── exit-preview │ └── +server.ts │ └── preview │ └── +server.ts ├── static ├── avatar.jpg ├── dark.svg ├── favicon.png └── light.svg ├── svelte.config.js ├── tsconfig.json ├── vite.config.js └── windi.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | VITE_SANITY_PROJECT_ID="" 2 | VITE_SANITY_DATASET="production" 3 | 4 | ## Also available on the client side, because studio is a SPA. 5 | VITE_SANITY_PREVIEW_SECRET="any-random-string" 6 | 7 | SANITY_API_READ_TOKEN="" 8 | SANITY_API_WRITE_TOKEN="" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmultiplehats%2Fsveltekit-sanity-v3&env=VITE_SANITY_PROJECT_ID,VITE_SANITY_DATASET,VITE_SANITY_PREVIEW_SECRET,SANITY_API_READ_TOKEN,SANITY_API_WRITE_TOKEN&envDescription=These%20API%20keys%20are%20needed%20from%20Sanity%20to%20run%20this%20app.&project-name=my-sveltekit-sanity-v3&repo-name=my-svelte-sanity-v3) 2 | 3 | # Sveltekit x Sanity Studio v3 4 | 5 | Hi there 👋! This is a repo for my [talk on YouTube](https://www.youtube.com/watch?v=xELXz553LCY), from the [Sanity.io Virtual Meetup - Autumn 2022](https://www.meetup.com/meetup-group-dvjyrjdv/events/289456759/). 6 | 7 | ## Features 8 | 9 | ### ✨ Embedding Sanity V3 in a Sveltekit app 10 | 11 | When I was working on a new project that involved [Sveltekit](https://kit.svelte.dev/) and Sanity I got curious and wanted to know whether I could directly embed the Sanity Studio V3 (Release Candiate) into a SvelteKit app. I was living on the edge already, so I might as well embrace it 🌈 12 | 13 | ### 👀 Side-by-side Instant Content preview. 14 | 15 | I also go over on how we use Sanity's Side-by-side Instant Content preview feature with Sveltekit. And how you can easily implement this in your own SvelteKit applications. The code ([createPreviewSubscriptionStore](https://github.com/multiplehats/sveltekit-sanity-v3/blob/main/src/lib/config/sanity/sveltekit/previewSubscriptionStore.ts#L10)) is mostly inspired from [Sanity's toolkit for Next.js](https://github.com/sanity-io/next-sanity). 16 | 17 | #### Learn more 18 | - [Introduction to Sanity Studio v3](https://beta.sanity.io/docs/platform/studio/v2-to-v3) 19 | - [Sanity Studio V3 Announcement](https://www.sanity.io/blog/sanity-studio-v3-developer-preview) 20 | 21 | ## Developing 22 | 23 | Once you've created a project and installed dependencies with `pnpm install`. Make sure you have added all the environment variables (see env.example). 24 | 25 | ```bash 26 | pnpm dev 27 | 28 | # or start the server and open the app in a new browser tab 29 | pnpm dev -- --open 30 | ``` 31 | 32 | ## Building & Previewing 33 | 34 | To build the project, run: 35 | 36 | ```bash 37 | pnpm build 38 | ``` 39 | 40 | To preview the build, run: 41 | ```bash 42 | pnpm preview 43 | ``` 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daghappie", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev --port 3100", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", 12 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "next", 16 | "@sveltejs/kit": "^1.0.1", 17 | "@types/react": "^18.0.26", 18 | "@types/react-dom": "^18.0.10", 19 | "@typescript-eslint/eslint-plugin": "^5.47.1", 20 | "@typescript-eslint/parser": "^5.47.1", 21 | "eslint": "^8.30.0", 22 | "eslint-config-prettier": "^8.5.0", 23 | "eslint-plugin-svelte3": "^4.0.0", 24 | "prettier": "^2.8.1", 25 | "prettier-plugin-svelte": "^2.9.0", 26 | "svelte": "^3.55.0", 27 | "svelte-check": "^2.10.3", 28 | "svelte-preprocess": "^4.10.7", 29 | "svelte2tsx": "^0.5.23", 30 | "ts-node": "^10.9.1", 31 | "tslib": "^2.4.1", 32 | "typescript": "^4.9.4", 33 | "vite": "^4.0.3", 34 | "vite-plugin-windicss": "^1.8.10", 35 | "windicss": "^3.5.6" 36 | }, 37 | "dependencies": { 38 | "@sanity/client": "^3.4.1", 39 | "@sanity/groq-store": "^1.1.4", 40 | "@sanity/icons": "^1.3.10", 41 | "@sanity/image-url": "^1.0.1", 42 | "@sanity/ui": "^0.37.22", 43 | "@sanity/vision": "3.0.0-dev-preview.22", 44 | "@sanity/webhook": "^2.0.0", 45 | "groq": "^2.33.2", 46 | "react": "^18.2.0", 47 | "react-dom": "^18.2.0", 48 | "sanity": "^3.1.2", 49 | "sanity-plugin-asset-source-unsplash": "^1.0.1" 50 | }, 51 | "type": "module" 52 | } 53 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import { createCliConfig } from 'sanity/cli'; 2 | 3 | const projectId = import.meta.env.VITE_SANITY_PROJECT_ID; 4 | const dataset = import.meta.env.VITE_SANITY_DATASET; 5 | 6 | export default createCliConfig({ api: { projectId, dataset } }); 7 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App { 5 | interface Locals { 6 | previewMode: boolean; 7 | } 8 | // interface PageData {} 9 | // interface Platform {} 10 | // interface PrivateEnv {} 11 | // interface PublicEnv {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | %sveltekit.body% 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { getPreviewCookie } from '$lib/utils'; 2 | import type { Handle } from '@sveltejs/kit'; 3 | 4 | export const handle: Handle = async ({ event, resolve }) => { 5 | const previewModeCookie = getPreviewCookie(event.cookies); 6 | 7 | event.locals.previewMode = false; 8 | 9 | if (previewModeCookie === 'true') { 10 | event.locals.previewMode = true; 11 | } 12 | 13 | const response = await resolve(event); 14 | 15 | return response; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |
6 |
7 |
8 | 9 | 10 | 11 |
12 | 13 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/PreviewBanner.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if !embedded} 10 |
11 |
12 |
13 |
14 | {#if $data?.profileImage} 15 | 16 | {/if} 17 | 18 |

19 | 20 | This page is a draft. 21 | 24 |

25 |
26 | 27 | 36 |
37 |
38 |
39 | {/if} 40 | -------------------------------------------------------------------------------- /src/lib/config/app.ts: -------------------------------------------------------------------------------- 1 | const app = { 2 | appName: 'Sanity Virtual Talk', 3 | 4 | }; 5 | 6 | export { app as default }; 7 | -------------------------------------------------------------------------------- /src/lib/config/environment.ts: -------------------------------------------------------------------------------- 1 | export const isDev = import.meta.env.DEV; 2 | export const isProd = import.meta.env.PROD; 3 | 4 | -------------------------------------------------------------------------------- /src/lib/config/sanity/client.ts: -------------------------------------------------------------------------------- 1 | import sanityClient from '@sanity/client'; 2 | import type { ClientConfig, SanityClient } from '@sanity/client'; 3 | import { env } from '$env/dynamic/private'; 4 | import { sanityConfig } from './config'; 5 | 6 | const createClient = (config: ClientConfig): SanityClient => { 7 | return sanityClient(config); 8 | } 9 | 10 | export const previewClient = createClient({ 11 | ...sanityConfig, 12 | useCdn: false, 13 | token: env.SANITY_API_READ_TOKEN || env.SANITY_API_WRITE_TOKEN || '', 14 | }); 15 | export const client = createClient(sanityConfig); 16 | export const getSanityServerClient = (usePreview: boolean) => (usePreview ? previewClient : client); 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export function overlayDrafts(docs: any[]): any[] { 20 | const documents = docs || []; 21 | const overlayed = documents.reduce((map, doc) => { 22 | if (!doc._id) { 23 | throw new Error('Ensure that `_id` is included in query projection'); 24 | } 25 | 26 | const isDraft = doc._id.startsWith('drafts.'); 27 | const id = isDraft ? doc._id.slice(7) : doc._id; 28 | return isDraft || !map.has(id) ? map.set(id, doc) : map; 29 | }, new Map()); 30 | 31 | return Array.from(overlayed.values()); 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/config/sanity/components/PostsPreview.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component is responsible for rendering a preview of a post inside the Studio. 3 | * It's imported in `sanity.config.ts´ and used as a component in the defaultDocumentNode function. 4 | */ 5 | import { Card, Text } from '@sanity/ui'; 6 | import React from 'react'; 7 | 8 | export function PostsPreview(props: unknown) { 9 | // @ts-expect-error - TODO: Fix this 10 | if (!props.document.displayed.slug) { 11 | return ( 12 | 13 | Please add a slug to the post to see the preview! 14 | 15 | ); 16 | } 17 | 18 | return ( 19 | 20 | {/* @ts-expect-error - TODO: Fix this */} 21 |