Skip to content

Commit

Permalink
feat: add support for Next 13 appDir (#184)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
See the [Migration section in the README](https://github.com/sanity-io/next-sanity/tree/main#from-v2) for details.
  • Loading branch information
stipsan committed Nov 22, 2022
1 parent 251106e commit 0c80876
Show file tree
Hide file tree
Showing 42 changed files with 811 additions and 712 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -44,6 +44,9 @@ jobs:
- run: npm ci
- run: npm run lint
if: github.event.inputs.test != 'false'
# @TODO find out how to make this command work on the CI
#- run: npm run update:icons
# if: github.event.inputs.test != 'false'
- run: npm run prepublishOnly
- run: npm run test:node-esm-cjs
name: Test if Node.js ESM native modes works, without breaking CJS modes
Expand Down
254 changes: 149 additions & 105 deletions README.md
Expand Up @@ -25,11 +25,17 @@
- [Limits](#limits)
- [`next-sanity/studio` (dev-preview)](#next-sanitystudio-dev-preview)
- [Usage](#usage)
- [Next 13 `app/studio`](#next-13-appstudio)
- [Next 12 or `pages/studio`](#next-12-or-pagesstudio)
- [Opt-in to using `StudioProvider` and `StudioLayout`](#opt-in-to-using-studioprovider-and-studiolayout)
- [Customize `<ServerStyleSheetDocument />`](#customize-serverstylesheetdocument-)
- [Full-control mode](#full-control-mode)
- [`next-sanity/webhook`](#next-sanitywebhook)
- [Migrate](#migrate)
- [From `v2`](#from-v2)
- [`NextStudioGlobalStyle` is removed](#nextstudioglobalstyle-is-removed)
- [`ServerStyleSheetDocument` is removed](#serverstylesheetdocument-is-removed)
- [The internal `isWorkspaceWithTheme` and `isWorkspaces` utils are no longer exported](#the-internal-isworkspacewiththeme-and-isworkspaces-utils-are-no-longer-exported)
- [The `useBackgroundColorsFromTheme`, `useBasePath`, `useConfigWithBasePath`, and `useTextFontFamilyFromTheme`, hooks are removed](#the-usebackgroundcolorsfromtheme-usebasepath-useconfigwithbasepath-and-usetextfontfamilyfromtheme-hooks-are-removed)
- [The `NextStudioHead` component has moved from `next-sanity/studio` to `next-sanity/studio/head`](#the-nextstudiohead-component-has-moved-from-next-sanitystudio-to-next-sanitystudiohead)
- [From `v1`](#from-v1)
- [`createPreviewSubscriptionHook` is replaced with `definePreview`](#createpreviewsubscriptionhook-is-replaced-with-definepreview)
- [Before](#before)
Expand Down Expand Up @@ -450,149 +456,142 @@ The latest version of Sanity Studio allows you to embed a near-infinitely config
### Usage
The basic setup is two files:
The basic setup is 2 components, `NextStudio` and `NextStudioHead`.
`NextStudio` loads up the `import {Studio} from 'sanity'` component for you and wraps it in a Next-friendly layout.
While `NextStudioHead` sets necessary `<head>` meta tags such as `<meta name="viewport">` to ensure the responsive CSS in the Studio works as expected.
1. `pages/[[...index]].tsx`
Both the Next 13 and 12 examples uses this config file:
`sanity.config.ts`:
```tsx
// Import your sanity.config.ts file
import config from '../sanity.config'
import {NextStudio} from 'next-sanity/studio'
```ts
import {visionTool} from '@sanity/vision'
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'

export default function StudioPage() {
// Loads the Studio, with all the needed meta tags and global CSS required for it to render correctly
return <NextStudio config={config} />
}
```
import {schemaTypes} from './schemas'

The `<NextStudio />` wraps `<Studio />` component and supports forwarding all its props:
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!

```tsx
import {Studio} from 'sanity'
export default defineConfig({
basePath: '/studio', // <-- important that `basePath` matches the route you're mounting your studio from, it applies to both `/pages` and `/app`

projectId,
dataset,

plugins: [deskTool(), visionTool()],

schema: {
types: schemaTypes,
},
})
```
2. `pages/_document.tsx`
To use `sanity.cli.ts` with the same `projectId` and `dataset` as your `sanity.config.ts`:
```tsx
import {ServerStyleSheetDocument} from 'next-sanity/studio'
```ts
/* eslint-disable no-process-env */
import {loadEnvConfig} from '@next/env'
import {defineCliConfig} from 'sanity/cli'

const dev = process.env.NODE_ENV !== 'production'
loadEnvConfig(__dirname, dev, {info: () => null, error: console.error})

// Set up SSR for styled-components, ensuring there's no missing CSS when deploying a Studio in Next.js into production
export default class Document extends ServerStyleSheetDocument {}
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET

export default defineCliConfig({api: {projectId, dataset}})
```
### Opt-in to using `StudioProvider` and `StudioLayout`
Now you can run commands like `npx sanity cors add`. See `npx sanity help` for a full list of what you can do.
If you want to go lower level and have more control over the studio you can pass `StudioProvider` and `StudioLayout` from `sanity` as `children`:
#### Next 13 `app/studio`
In Next 13's `appDir` mode you use `page.tsx` to load `NextStudio`, and optionally (recommended, especially if you want great support for iPhones and other devices with display cutouts like "The Notch" or "Dynamic Island") export `NextStudioHead` in a `head.tsx`.
In routes that load `NextStudio` ensure you have `'use client'` at the top of your file.
`app/studio/[[...index]]/page.tsx`:
```tsx
'use client'

import {NextStudio} from 'next-sanity/studio'
import {StudioProvider, StudioLayout} from 'sanity'

import config from '../sanity.config'
import config from '../../../sanity.config'

function StudioPage() {
return (
<NextStudio config={config}>
<StudioProvider config={config}>
{/* Put components here and you'll have access to the same React hooks as Studio gives you when writing plugins */}
<StudioLayout />
</StudioProvider>
</NextStudio>
)
export default function StudioPage() {
// Supports the same props as `import {Studio} from 'sanity'`, `config` is required
return <NextStudio config={config} />
}
```
### Customize `<ServerStyleSheetDocument />`
You can still customize `_document.tsx`, the same way you would the default `<Document />` component from `next/document`:
`app/studio/[[...index]]/head.tsx`:
```tsx
import {ServerStyleSheetDocument} from 'next-sanity/studio'

export default class Document extends ServerStyleSheetDocument {
static async getInitialProps(ctx: DocumentContext) {
// You can still override renderPage:
const originalRenderPage = ctx.renderPage
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => <App {...props} />,
})

const initialProps = await ServerStyleSheetDocument.getInitialProps(ctx)

const extraStyles = await getStyles()
return {
...initialProps,
// Add to the default styles if you want
styles: [initialProps.styles, extraStyles],
}
}
render() {
// do the same stuff as in `next/document`
}
// Re-export `NextStudioHead` as default if you're happy with the default behavior
export {NextStudioHead as default} from 'next-sanity/studio/head'

// To customize it, use it as a children component:
import {NextStudioHead} from 'next-sanity/studio/head'

export default function CustomStudioHead() {
return (
<>
<NextStudioHead favicons={false} />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="https://www.sanity.io/static/images/favicons/favicon-32x32.png"
/>
</>
)
}
```
### Full-control mode
#### Next 12 or `pages/studio`
Using just `NextStudio` gives you a fully working Sanity Studio v3. However we recommend also using `NextStudioHead` as it ensures CSS Media Queries that target mobile devices with display cutouts (for example iPhone's "The Notch" and "Dynamic Island") and other details.
If you only need parts of what `<NextStudio />` does for you, but not all of it.
No problem. You can import any which one of the components that `<NextStudio />` is importing and assemble them in any way you want.
`/pages/studio/[[...index]].tsx`:
```tsx
import {Studio, type Config} from 'sanity'
import {NextStudioGlobalStyle, NextStudioHead} from 'next-sanity/studio'
// This implementation will only load the bare minimum of what's required for the Studio to render correctly. No favicons, fancy <meta name="theme-color"> tags or the like
export default function CustomNextStudio({config}: {config: Config}) {
import Head from 'next/head'
import {NextStudio} from 'next-sanity/studio'
import {NextStudioHead} from 'next-sanity/studio/head'

import config from '../../sanity.config'

export default function StudioPage() {
return (
<>
<Studio config={config} />
<NextStudioHead>{/* Custom extra stuff in <head> */}</NextStudioHead>
<NextStudioGlobalStyle />
<Head>
<NextStudioHead />
</Head>
<NextStudio config={config} />
</>
)
}
```
And while `<NextStudio />` have all features enabled by default allowing you to opt-out by giving it props, the inner components `<NextStudioHead />` and `<NextStudioGlobalStyle />` are opt-in.
This means that these two `StudioPage` components are functionally identical:
### Opt-in to using `StudioProvider` and `StudioLayout`
If you want to go lower level and have more control over the studio you can pass `StudioProvider` and `StudioLayout` from `sanity` as `children`:
```tsx
import {
NextStudio,
NextStudioGlobalStyle,
NextStudioHead,
useTheme,
useBackgroundColorsFromTheme,
} from 'next-sanity/studio'
import {Studio} from 'sanity'
import config from '../sanity.config'

// Turning all the features off, leaving only bare minimum required meta tags and styling
function StudioPage() {
return (
<NextStudio
config={config}
// an empty string turns off the CSS that sets a background on <html>
unstable__bg=""
unstable__noTailwindSvgFix
unstable__noFavicons
// an empty string turns off the <title> tag
unstable__document_title=""
/>
)
}
import {NextStudio} from 'next-sanity/studio'
import {StudioProvider, StudioLayout} from 'sanity'

// Since no features are enabled it works the same way
function Studiopage() {
const theme = useTheme(config)
const {themeColorLight, themeColorDark} = useBackgroundColorsFromTheme(theme)
import config from '../../../sanity.config'

function StudioPage() {
return (
<>
<Studio config={config} />
<NextStudioHead themeColorLight={themeColorLight} themeColorDark={themeColorDark} />
<NextStudioGlobalStyle />
</>
<NextStudio config={config}>
<StudioProvider config={config}>
{/* Put components here and you'll have access to the same React hooks as Studio gives you when writing plugins */}
<StudioLayout />
</StudioProvider>
</NextStudio>
)
}
```
Expand Down Expand Up @@ -635,6 +634,51 @@ export default async function revalidate(req: NextApiRequest, res: NextApiRespon
## Migrate
### From `v2`
The `v3` release only contains breaking changes on the `next-sanity/studio` imports. If you're only using `import {createClient, groq} from 'next-sanity'` or `import {definePreview, PreviewSuspense} from 'next-sanity/preview'` then there's no migration for you to do.
#### `NextStudioGlobalStyle` is removed
The layout is no longer using global CSS to set the Studio height. The switch to local CSS helps interop between Next 12 and 13 layouts.
#### `ServerStyleSheetDocument` is removed
It's no longer necessary to setup `styled-components` SSR for the Studio to render correctly.
#### The internal `isWorkspaceWithTheme` and `isWorkspaces` utils are no longer exported
The `useTheme` hook is still available if you're building abstractions that need to know what the initial workspace theme variables are.
#### The `useBackgroundColorsFromTheme`, `useBasePath`, `useConfigWithBasePath`, and `useTextFontFamilyFromTheme`, hooks are removed
You can `useTheme` to replace `useBackgroundColorsFromTheme` and `useTextFontFamilyFromTheme`:
```tsx
import {useMemo} from 'react'
import {useTheme} from 'next-sanity/studio'
import type {StudioProps} from 'sanity'
export default function MyComponent(props: Pick<StudioProps, 'config'>) {
const theme = useTheme(config)
// useBackgroundColorsFromTheme
const {themeColorLight, themeColorDark} = useMemo(
() => ({
themeColorLight: theme.color.light.default.base.bg,
themeColorDark: theme.color.dark.default.base.bg,
}),
[theme]
)
// useTextFontFamilyFromTheme
const fontFamily = useMemo(() => theme.fonts.text.family, [theme])
}
```
The reason why `useBasePath` and `useConfigWithBasePath` got removed is because Next 12 and 13 diverge too much in how they declare dynamic segments. Thus you'll need to specify `basePath` in your `sanity.config.ts` manually to match the route you're loading the studio, for the time being.
#### The `NextStudioHead` component has moved from `next-sanity/studio` to `next-sanity/studio/head`
Its props are also quite different and it now requires you to wrap it in `import Head from 'next/head'` if you're not using a `head.tsx` in `appDir`. Make sure you use TypeScript to ease the migration.
### From `v1`
#### `createPreviewSubscriptionHook` is replaced with `definePreview`
Expand Down
5 changes: 3 additions & 2 deletions app/page.tsx
Expand Up @@ -4,6 +4,7 @@ import PreviewPosts from 'app/PreviewPosts'
import {createClient} from 'app/sanity.client'
import {PreviewSuspense} from 'app/sanity.preview'
import {previewData} from 'next/headers'
import Link from 'next/link'

export default async function IndexPage() {
const thePreviewData = previewData()
Expand Down Expand Up @@ -66,12 +67,12 @@ export default async function IndexPage() {
</div>
</div>
<div className="text-center">
<a
<Link
href="/studio"
className="mx-2 my-4 inline-block rounded-full border border-gray-200 px-4 py-1 text-sm font-semibold text-gray-600 hover:border-transparent hover:bg-gray-600 hover:text-white focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2"
>
Open Studio
</a>
</Link>
</div>
</>
)
Expand Down
1 change: 1 addition & 0 deletions app/studio/[[...tool]]/head.tsx
@@ -0,0 +1 @@
export {NextStudioHead as default} from 'src/studio/head'
@@ -1,10 +1,12 @@
'use client'

import config from 'sanity.config'
import {NextStudio, useConfigWithBasePath} from 'src/studio'
import {NextStudio} from 'src/studio'

export default function StudioPage() {
return (
<NextStudio
config={useConfigWithBasePath(config)}
config={config}
// Turn off login in production so that anyone can look around in the Studio and see how it works
// eslint-disable-next-line no-process-env
unstable_noAuthBoundary={process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'}
Expand Down
14 changes: 14 additions & 0 deletions favicons.d.ts
@@ -0,0 +1,14 @@
declare module '*.ico' {
const value: string
export default value
}

declare module '*.svg' {
const value: string
export default value
}

declare module '*.png' {
const value: string
export default value
}
1 change: 0 additions & 1 deletion next-env.d.ts
@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

0 comments on commit 0c80876

Please sign in to comment.