next-sanity 9.8.6 → 9.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +428 -0
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -37,6 +37,9 @@ The all-in-one [Sanity][sanity] toolkit for production-grade content-editable Ne
37
37
  - [Debugging caching and revalidation](#debugging-caching-and-revalidation)
38
38
  - [Example implementation](#example-implementation)
39
39
  - [Visual Editing](#visual-editing)
40
+ - [Live Content API](#live-content-api)
41
+ - [Setup](#setup)
42
+ - [How does it revalidate and refresh in real time](#how-does-it-revalidate-and-refresh-in-real-time)
40
43
  - [Embedded Sanity Studio](#embedded-sanity-studio)
41
44
  - [Creating a Studio route](#creating-a-studio-route)
42
45
  - [Automatic installation of embedded Studio](#automatic-installation-of-embedded-studio)
@@ -589,6 +592,430 @@ Interactive live previews of draft content are the best way for authors to find
589
592
 
590
593
  An end-to-end tutorial of [how to configure Sanity and Next.js for Visual Editing](https://www.sanity.io/guides/nextjs-app-router-live-preview) using the same patterns demonstrated in this README is available on the Sanity Exchange.
591
594
 
595
+ ## Live Content API
596
+
597
+ [The Live Content API][live-content-api] can be used to receive real time updates in your application when viewing both draft content in contexts like Presentation tool, and published content in your user-facing production application.
598
+
599
+ > [!NOTE]
600
+ > The Live Content API is currently considered experimental and may change in the future.
601
+
602
+ ### Setup
603
+
604
+ #### 1. Configure `defineLive`
605
+
606
+ Use `defineLive` to enable automatic revalidation and refreshing of your fetched content.
607
+
608
+ ```tsx
609
+ // src/sanity/lib/live.ts
610
+
611
+ import {createClient, defineLive} from 'next-sanity'
612
+
613
+ const client = createClient({
614
+ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
615
+ dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
616
+ useCdn: true,
617
+ apiVersion: 'vX', // Target the experimental API version
618
+ stega: {studioUrl: '/studio'},
619
+ })
620
+
621
+ const token = process.env.SANITY_API_READ_TOKEN
622
+ if (!token) {
623
+ throw new Error('Missing SANITY_API_READ_TOKEN')
624
+ }
625
+
626
+ export const {sanityFetch, SanityLive} = defineLive({
627
+ client,
628
+ serverToken: token,
629
+ browserToken: token,
630
+ })
631
+ ```
632
+
633
+ The `token` passed to `defineLive` needs [Viewer rights](https://www.sanity.io/docs/roles#e2daad192df9) in order to fetch draft content.
634
+
635
+ The same token can be used as both `browserToken` and `serverToken`, as the `browserToken` is only shared with the browser when Draft Mode is enabled. Draft Mode can only be initiated by either Sanity's Presentation Tool or the Vercel Toolbar.
636
+
637
+ > Good to know:
638
+ > Enterprise plans allow the creation of custom roles with more resticted access rights than the `Viewer` role, enabling the use of a `browserToken` specifically for authenticating the Live Content API. We're working to extend this capability to all Sanity price plans.
639
+
640
+ #### 2. Render `<SanityLive />` in the root `layout.tsx`
641
+
642
+ ```tsx
643
+ // src/app/layout.tsx
644
+
645
+ import {VisualEditing} from 'next-sanity'
646
+ import {SanityLive} from '@/sanity/lib/live'
647
+
648
+ export default function RootLayout({children}: {children: React.ReactNode}) {
649
+ return (
650
+ <html lang="en">
651
+ <body>
652
+ {children}
653
+ <SanityLive />
654
+ {(await draftMode()).isEnabled && <VisualEditing />}
655
+ </body>
656
+ </html>
657
+ )
658
+ }
659
+ ```
660
+
661
+ The `<SanityLive>` component is responsible for making all `sanityFetch` calls in your application _live_, so should always be rendered. This differs from the `<VisualEditing />` component, which should only be rendered when Draft Mode is enabled.
662
+
663
+ #### 3. Fetching data with `sanityFetch`
664
+
665
+ Use `sanityFetch` to fetch data in any server component.
666
+
667
+ ```tsx
668
+ // src/app/products.tsx
669
+
670
+ import {defineQuery} from 'next-sanity'
671
+ import {sanityFetch} from '@/sanity/lib/live'
672
+
673
+ const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`)
674
+
675
+ export default async function Page() {
676
+ const {data: products} = await sanityFetch({
677
+ query: PRODUCTS_QUERY,
678
+ params: {limit: 10},
679
+ })
680
+
681
+ return (
682
+ <section>
683
+ {products.map((product) => (
684
+ <article key={product._id}>
685
+ <a href={`/product/${product.slug}`}>{product.title}</a>
686
+ </article>
687
+ ))}
688
+ </section>
689
+ )
690
+ }
691
+ ```
692
+
693
+ ### Using `generateMetadata`, `generateStaticParams` and more
694
+
695
+ `sanityFetch` can also be used in functions like `generateMetadata` in order to make updating the page title, or even its favicon, _live_.
696
+
697
+ ```ts
698
+ import {sanityFetch} from '@/sanity/lib/live'
699
+ import type {Metadata} from 'next'
700
+
701
+ export async function generateMetadata(): Promise<Metadata> {
702
+ const {data} = await sanityFetch({
703
+ query: SETTINGS_QUERY,
704
+ // Metadata should never contain stega
705
+ stega: false,
706
+ })
707
+ return {
708
+ title: {
709
+ template: `%s | ${data.title}`,
710
+ default: data.title,
711
+ },
712
+ }
713
+ }
714
+ ```
715
+
716
+ > Good to know:
717
+ > Always set `stega: false` when calling `sanityFetch` within these:
718
+ >
719
+ > - `generateMetadata`
720
+ > - `generateViewport`
721
+ > - `generateSitemaps`
722
+ > - `generateImageMetadata`
723
+
724
+ ```ts
725
+ import {sanityFetch} from '@/sanity/lib/live'
726
+
727
+ export async function generateStaticParams() {
728
+ const {data} = await sanityFetch({
729
+ query: POST_SLUGS_QUERY,
730
+ // Use the published perspective in generateStaticParams
731
+ perspective: 'published',
732
+ stega: false,
733
+ })
734
+ return data
735
+ }
736
+ ```
737
+
738
+ ### 4. Integrating with Next.js Draft Mode and Vercel Toolbar's Edit Mode
739
+
740
+ To support previewing draft content when Draft Mode is enabled, the `serverToken` passed to `defineLive` should be assigned the Viewer role, which has the ability to fetch content using the `previewDrafts` perspective.
741
+
742
+ Click the Draft Mode button in the Vercel toolbar to enable draft content:
743
+
744
+ ![image](https://github.com/user-attachments/assets/5aa3ed30-929e-48f1-a16c-8246309ec099)
745
+
746
+ With drafts enabled, you'll see the Edit Mode button show up if your Vercel plan is eligible:
747
+
748
+ ![img](https://github.com/user-attachments/assets/6ca7a9f5-e2d1-4915-83d0-8928a0a563de)
749
+
750
+ Ensure that `browserToken` is setup if you want draft content that isn't yet published to also update live.
751
+
752
+ ### 5. Integrating with Sanity Presentation Tool & Visual Editing
753
+
754
+ The `defineLive` API also supports Presentation Tool and Sanity Visual Editing.
755
+
756
+ Setup an API route that uses `defineEnableDraftMode` in your app:
757
+
758
+ ```ts
759
+ // src/app/api/draft-mode/enable/route.ts
760
+
761
+ import {client} from '@/sanity/lib/client'
762
+ import {token} from '@/sanity/lib/token'
763
+ import {defineEnableDraftMode} from 'next-sanity/draft-mode'
764
+
765
+ export const {GET} = defineEnableDraftMode({
766
+ client: client.withConfig({token}),
767
+ })
768
+ ```
769
+
770
+ The main benefit of `defineEnableDraftMode` is that it fully implements all of Sanity Presentation Tool's features, including the perspective switcher:
771
+ <img width="530" alt="image" src="https://github.com/user-attachments/assets/774d8f92-527f-4478-8089-2fb7e6a5c618">
772
+
773
+ And the Preview URL Sharing feature:
774
+ <img width="450" alt="image" src="https://github.com/user-attachments/assets/d11b38eb-389b-448f-862c-b39b3adbb7e3">
775
+
776
+ In your `sanity.config.ts`, set the `previewMode.enable` option for `presentationTool`:
777
+
778
+ ```ts
779
+ // sanity.config.ts
780
+
781
+ import {defineConfig} from 'sanity'
782
+ import {presentationTool} from 'next-sanity'
783
+
784
+ export default defineConfig({
785
+ // ...
786
+ plugins: [
787
+ // ...
788
+ presentationTool({
789
+ previewUrl: {
790
+ // ...
791
+ previewMode: {
792
+ enable: '/api/draft-mode/enable',
793
+ },
794
+ },
795
+ }),
796
+ ],
797
+ })
798
+ ```
799
+
800
+ Ensuring you have a valid viewer token setup for `defineLive.serverToken` and `defineEnableDraftMode` allows Presentation Tool to auto enable Draft Mode, and your application to pull in draft content that refreshes in real time.
801
+
802
+ The `defineLive.browserToken` option isn't required, but is recommended as it enables a faster live preview experience, both standalone and when using Presentation Tool.
803
+
804
+ ### 6. Enabling standalone live preview of draft content
805
+
806
+ Standalone live preview has the following requirements:
807
+
808
+ - `defineLive.serverToken` must be defined, otherwise only published content is fetched.
809
+ - At least one integration (Sanity Presentation Tool or Vercel Toolbar) must be setup, so Draft Mode can be enabled in your application on demand.
810
+ - `defineLive.browserToken` must be defined with a valid token.
811
+
812
+ You can verify if live preview is enabled with the `useIsLivePreview` hook:
813
+
814
+ ```tsx
815
+ 'use client'
816
+
817
+ import {useIsLivePreview} from 'next-sanity/hooks'
818
+
819
+ export function DebugLivePreview() {
820
+ const isLivePreview = useIsLivePreview()
821
+ if (isLivePreview === null) return 'Checking Live Preview...'
822
+ return isLivePreview ? 'Live Preview Enabled' : 'Live Preview Disabled'
823
+ }
824
+ ```
825
+
826
+ The following hooks can also be used to provide information about the application's current environment:
827
+
828
+ ```ts
829
+ import {
830
+ useIsPresentationTool,
831
+ useDraftModeEnvironment,
832
+ useDraftModePerspective,
833
+ } from 'next-sanity/hooks'
834
+ ```
835
+
836
+ ### Handling Layout Shift
837
+
838
+ Live components will re-render automatically as content changes. This can cause jarring layout shifts in production when items appear or disappear from a list.
839
+
840
+ To provide a better user experience, we can animate these layout changes. The following example uses `framer-motion@12.0.0-alpha.1`, which supports React Server Components:
841
+
842
+ ```tsx
843
+ // src/app/products.tsx
844
+
845
+ import {AnimatePresence} from 'framer-motion'
846
+ import * as motion from 'framer-motion/client'
847
+ import {defineQuery} from 'next-sanity'
848
+ import {sanityFetch} from '@/sanity/lib/live'
849
+
850
+ const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`)
851
+
852
+ export default async function Page() {
853
+ const {data: products} = await sanityFetch({
854
+ query: PRODUCTS_QUERY,
855
+ params: {limit: 10},
856
+ })
857
+
858
+ return (
859
+ <section>
860
+ <AnimatePresence mode="popLayout">
861
+ {products.map((product) => (
862
+ <motion.article
863
+ key={product._id}
864
+ layout="position"
865
+ animate={{opacity: 1}}
866
+ exit={{opacity: 0}}
867
+ >
868
+ <a href={`/product/${product.slug}`}>{product.title}</a>
869
+ </motion.article>
870
+ ))}
871
+ </AnimatePresence>
872
+ </section>
873
+ )
874
+ }
875
+ ```
876
+
877
+ Whilst this is an improvement, it may still lead to users attempting to click on an item as it shifts position, potentially resulting in the selection of an unintended item. We can instead require users to opt-in to changes before a layout update is triggered.
878
+
879
+ To preserve the ability to render everything on the server, we can make use of a Client Component wrapper. This can defer showing changes to the user until they've explicitly clicked to "Refresh". The example below uses `sonner` to provide toast functionality:
880
+
881
+ ```tsx
882
+ // src/app/products/products-layout-shift.tsx
883
+
884
+ 'use client'
885
+
886
+ import {useCallback, useState, useEffect} from 'react'
887
+ import isEqual from 'react-fast-compare'
888
+ import {toast} from 'sonner'
889
+
890
+ export function ProductsLayoutShift(props: {children: React.ReactNode; ids: string[]}) {
891
+ const [children, pending, startViewTransition] = useDeferredLayoutShift(props.children, props.ids)
892
+
893
+ /**
894
+ * We need to suspend layout shift for user opt-in.
895
+ */
896
+ useEffect(() => {
897
+ if (!pending) return
898
+
899
+ toast('Products have been updated', {
900
+ action: {
901
+ label: 'Refresh',
902
+ onClick: () => startViewTransition(),
903
+ },
904
+ })
905
+ }, [pending, startViewTransition])
906
+
907
+ return children
908
+ }
909
+
910
+ function useDeferredLayoutShift(children: React.ReactNode, dependencies: unknown[]) {
911
+ const [pending, setPending] = useState(false)
912
+ const [currentChildren, setCurrentChildren] = useState(children)
913
+ const [currentDependencies, setCurrentDependencies] = useState(dependencies)
914
+
915
+ if (!pending) {
916
+ if (isEqual(currentDependencies, dependencies)) {
917
+ if (currentChildren !== children) {
918
+ setCurrentChildren(children)
919
+ }
920
+ } else {
921
+ setCurrentDependencies(dependencies)
922
+ setPending(true)
923
+ }
924
+ }
925
+
926
+ const startViewTransition = useCallback(() => {
927
+ setCurrentDependencies(dependencies)
928
+ setPending(false)
929
+ }, [dependencies])
930
+
931
+ return [pending ? currentChildren : children, pending, startViewTransition] as const
932
+ }
933
+ ```
934
+
935
+ This Client Component is used to wrap the layout that should only be updated after the user has clicked the refresh button:
936
+
937
+ ```diff
938
+ // src/app/products/page.tsx
939
+
940
+ import { AnimatePresence } from "framer-motion";
941
+ import * as motion from "framer-motion/client";
942
+ import {defineQuery} from 'next-sanity'
943
+ import { sanityFetch } from "@/sanity/lib/live";
944
+ +import {ProductsLayoutShift} from './products-page-layout-shift.tsx'
945
+
946
+ const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`)
947
+
948
+ export default async function Page() {
949
+ const {data: products} = await sanityFetch({ query: PRODUCTS_QUERY, params: {limit: 10} });
950
+ + // If the list over ids change, it'll trigger the toast asking the user to opt-in to refresh
951
+ + // but if a product title has changed, perhaps to fix a typo, we update that right away
952
+ + const ids = products.map((product) => product._id)
953
+ return (
954
+ <section>
955
+ + <ProductsLayoutShift ids={ids}>
956
+ <AnimatePresence mode="popLayout">
957
+ {products.map((product) => (
958
+ <motion.article
959
+ key={product._id}
960
+ layout="position"
961
+ animate={{ opacity: 1 }}
962
+ exit={{ opacity: 0 }}
963
+ >
964
+ <a href={`/product/${product.slug}`}>{product.title}</a>
965
+ </motion.article>
966
+ ))}
967
+ </AnimatePresence>
968
+ + </ProductsLayoutShift>
969
+ </section>
970
+ );
971
+ }
972
+ ```
973
+
974
+ With this approach we've limited the use of client components to just a single component. All the server components within `<ProductsLayoutShift>` remain as server components, with all their benefits.
975
+
976
+ ## How does the Live Content API revalidate and refresh in real-time?
977
+
978
+ The architecture for `defineLive` works as follows:
979
+
980
+ 1. `sanityFetch` automatically sets `fetch.next.tags` for you using opaque tags generated by our backend, prefixed with `sanity:`.
981
+ 2. `<SanityLive />` listens to change events using the Sanity Live Content API (LCAPI).
982
+ 3. When the LCAPI emits an event, `<SanityLive />` invokes a Server Function that calls `revalidateTag(`sanity:${tag}`)`.
983
+ 4. Since it's a Server Function, Next.js will evict data fetches associated with the revalidated tag. The page is seamlessly updated with fresh content, which future visitors will also see thanks to `revalidateTag` integrating with ISR.
984
+
985
+ With this setup, as long as one visitor accesses your Next.js app after a content change, the cache is updated globally for all users, regardless of the specific URL they visit.
986
+
987
+ ### Revalidating content changes from automations
988
+
989
+ If your content operations involve scenarios where you might not always have a visitor to trigger the `revalidateTag` event, there are two ways to ensure your content is never stale:
990
+
991
+ #### A) Use a GROQ powered webhook to call `revalidateTag(sanity)`
992
+
993
+ All queries made using `sanityFetch` include the `sanity` tag in their `fetch.next.tags` array. You can use this to call `revalidateTag('sanity')` in an API route that handles a GROQ webhook payload.
994
+
995
+ This approach can be considered a "heavy hammer" so it's important to limit the webhook events that trigger it. You could also implement this in a custom component to manually purge the cache if content gets stuck.
996
+
997
+ #### B) Setup a server-side `<SanityLive />` alternative
998
+
999
+ You can setup your own long-running server, using Express for example, to listen for change events using the Sanity Live Content API. Then, create an API route in your Next.js app:
1000
+
1001
+ ```ts
1002
+ // src/app/api/revalidate-tag/route.ts
1003
+ import {revalidateTag} from 'next/cache'
1004
+
1005
+ export const POST = async (request) => {
1006
+ const {tags, isValid} = await validateRequest(request)
1007
+ if (!isValid) return new Response('No no no', {status: 400})
1008
+ for (const _tag of tags) {
1009
+ const tag = `sanity:${_tag}`
1010
+ revalidateTag(tag)
1011
+ // eslint-disable-next-line no-console
1012
+ console.log(`revalidated tag: ${tag}`)
1013
+ }
1014
+ }
1015
+ ```
1016
+
1017
+ Your Express app can then forward change events to this endpoint, ensuring your content is always up-to-date. This method guarantees that stale content is never served, even if no browser is actively viewing your app!
1018
+
592
1019
  ## Embedded Sanity Studio
593
1020
 
594
1021
  Sanity Studio is a near-infinitely configurable content editing interface that can be embedded into any React application. For Next.js, you can embed the Studio on a route (like `/studio`). The Studio will still require authentication and be available only for members of your Sanity project.
@@ -774,3 +1201,4 @@ MIT-licensed. See [LICENSE][LICENSE].
774
1201
  [vercel-content-link]: https://vercel.com/docs/workflow-collaboration/edit-mode#content-link?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
775
1202
  [sanity-next-clean-starter]: https://www.sanity.io/templates/nextjs-sanity-clean
776
1203
  [sanity-next-featured-starter]: https://www.sanity.io/templates/personal-website-with-built-in-content-editing
1204
+ [live-content-api]: https://www.sanity.io/docs/live-content-api?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-sanity",
3
- "version": "9.8.6",
3
+ "version": "9.8.8",
4
4
  "description": "Sanity.io toolkit for Next.js",
5
5
  "keywords": [
6
6
  "sanity",
@@ -139,11 +139,11 @@
139
139
  "dependencies": {
140
140
  "@portabletext/react": "^3.1.0",
141
141
  "@sanity/client": "^6.22.2",
142
- "@sanity/next-loader": "1.0.7",
143
- "@sanity/preview-kit": "5.1.9",
142
+ "@sanity/next-loader": "1.1.0",
143
+ "@sanity/preview-kit": "5.1.10",
144
144
  "@sanity/preview-url-secret": "2.0.0",
145
- "@sanity/visual-editing": "2.3.2",
146
- "groq": "^3.62.0",
145
+ "@sanity/visual-editing": "2.4.2",
146
+ "groq": "^3.62.2",
147
147
  "history": "^5.3.0"
148
148
  },
149
149
  "devDependencies": {