From ba426c629f7ad93130762fa8cdf563bd3ac76d77 Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Mon, 14 Oct 2024 16:01:03 +0200 Subject: [PATCH] examples: bump deps in Sanity Example Update Sanity example to next v15 Update Sanity example with Live Content API --- examples/cms-sanity/.env.local.example | 4 + examples/cms-sanity/.gitignore | 4 +- examples/cms-sanity/README.md | 2 +- .../cms-sanity/app/(blog)/alert-banner.tsx | 52 ----- examples/cms-sanity/app/(blog)/avatar.tsx | 3 +- examples/cms-sanity/app/(blog)/cors.ts | 29 +++ .../cms-sanity/app/(blog)/cover-image.tsx | 11 +- .../app/(blog)/draft-mode-toast.tsx | 55 ++++++ .../app/(blog)/hero-layout-shift.tsx | 46 +++++ examples/cms-sanity/app/(blog)/layout.tsx | 70 +++---- .../app/(blog)/more-stories-layout-shift.tsx | 39 ++++ .../cms-sanity/app/(blog)/more-stories.tsx | 99 ++++++---- examples/cms-sanity/app/(blog)/onboarding.tsx | 1 - examples/cms-sanity/app/(blog)/page.tsx | 179 +++++++++++------- .../cms-sanity/app/(blog)/portable-text.tsx | 62 +++++- .../posts/[slug]/content-layout-shift.tsx | 44 +++++ .../app/(blog)/posts/[slug]/page.tsx | 145 +++++++++----- .../app/(blog)/use-deferred-transition.tsx | 39 ++++ examples/cms-sanity/app/(sanity)/layout.tsx | 1 - .../app/(sanity)/studio/[[...tool]]/page.tsx | 3 +- .../app/api/draft-mode/enable/route.ts | 3 +- examples/cms-sanity/app/globals.css | 8 + examples/cms-sanity/package.json | 64 ++++--- examples/cms-sanity/sanity.config.ts | 33 ++-- examples/cms-sanity/sanity.types.ts | 22 ++- examples/cms-sanity/sanity/lib/api.ts | 2 +- examples/cms-sanity/sanity/lib/client.ts | 8 +- .../cms-sanity/sanity/lib/dataAttribute.ts | 16 ++ examples/cms-sanity/sanity/lib/demo.ts | 43 +++-- examples/cms-sanity/sanity/lib/fetch.ts | 58 ------ examples/cms-sanity/sanity/lib/live.ts | 11 ++ examples/cms-sanity/sanity/lib/queries.ts | 5 +- examples/cms-sanity/sanity/lib/utils.ts | 16 +- examples/cms-sanity/sanity/plugins/assist.ts | 1 - .../sanity/schemas/documents/author.ts | 7 + .../sanity/schemas/documents/post.ts | 8 +- .../sanity/schemas/singletons/settings.tsx | 3 +- examples/cms-sanity/schema.json | 14 ++ examples/cms-sanity/tailwind.config.ts | 2 +- 39 files changed, 804 insertions(+), 408 deletions(-) delete mode 100644 examples/cms-sanity/app/(blog)/alert-banner.tsx create mode 100644 examples/cms-sanity/app/(blog)/cors.ts create mode 100644 examples/cms-sanity/app/(blog)/draft-mode-toast.tsx create mode 100644 examples/cms-sanity/app/(blog)/hero-layout-shift.tsx create mode 100644 examples/cms-sanity/app/(blog)/more-stories-layout-shift.tsx create mode 100644 examples/cms-sanity/app/(blog)/posts/[slug]/content-layout-shift.tsx create mode 100644 examples/cms-sanity/app/(blog)/use-deferred-transition.tsx create mode 100644 examples/cms-sanity/sanity/lib/dataAttribute.ts delete mode 100644 examples/cms-sanity/sanity/lib/fetch.ts create mode 100644 examples/cms-sanity/sanity/lib/live.ts diff --git a/examples/cms-sanity/.env.local.example b/examples/cms-sanity/.env.local.example index 8980898bdb9ca..4c7341c8a102a 100644 --- a/examples/cms-sanity/.env.local.example +++ b/examples/cms-sanity/.env.local.example @@ -2,3 +2,7 @@ NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= SANITY_API_READ_TOKEN= +# Silence log messages meant for onboarding +# NEXT_PUBLIC_SANITY_STEGA_LOGGER=false +# Debug Next.js cache behavior +# NEXT_PRIVATE_DEBUG_CACHE=true diff --git a/examples/cms-sanity/.gitignore b/examples/cms-sanity/.gitignore index 83b42e1bc5d44..ac6da6740a838 100644 --- a/examples/cms-sanity/.gitignore +++ b/examples/cms-sanity/.gitignore @@ -43,4 +43,6 @@ next-env.d.ts # Env files created by scripts for working locally .env -.env.local \ No newline at end of file +.env.local +.env.local.* +!.env.local.example \ No newline at end of file diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index 168a2938b2816..c7b0ace7511ad 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -301,7 +301,7 @@ npx vercel link - [WordPress](/examples/cms-wordpress) - [Blog Starter](/examples/blog-starter) -[vercel-deploy]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-sanity&repository-name=cms-sanity&project-name=cms-sanity&demo-title=Blog%20using%20Next.js%20%26%20Sanity&demo-description=Real-time%20updates%2C%20seamless%20editing%2C%20no%20rebuild%20delays.&demo-url=https%3A%2F%2Fnext-blog.sanity.build%2F&demo-image=https%3A%2F%2Fgithub.com%2Fsanity-io%2Fnext-sanity%2Fassets%2F81981%2Fb81296a9-1f53-4eec-8948-3cb51aca1259&integration-ids=oac_hb2LITYajhRQ0i4QznmKH7gx +[vercel-deploy]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-sanity&repository-name=cms-sanity&project-name=cms-sanity&demo-title=Blog%20using%20Next.js%20%26%20Sanity&demo-description=Real-time%20updates%2C%20seamless%20editing%2C%20no%20rebuild%20delays.&demo-url=https%3A%2F%2Fnext-blog.sanity.build%2F&demo-image=%2F%2Fgithub.com%2Fsanity-io%2Fnext-sanity%2Fassets%2F81981%2Fb81296a9-1f53-4eec-8948-3cb51aca1259&integration-ids=oac_hb2LITYajhRQ0i4QznmKH7gx [integration]: https://www.sanity.io/docs/vercel-integration [`.env.local.example`]: .env.local.example [unsplash]: https://unsplash.com diff --git a/examples/cms-sanity/app/(blog)/alert-banner.tsx b/examples/cms-sanity/app/(blog)/alert-banner.tsx deleted file mode 100644 index 567bf34e2cd22..0000000000000 --- a/examples/cms-sanity/app/(blog)/alert-banner.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useSyncExternalStore, useTransition } from "react"; - -import { disableDraftMode } from "./actions"; - -const emptySubscribe = () => () => {}; - -export default function AlertBanner() { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - - const shouldShow = useSyncExternalStore( - emptySubscribe, - () => window.top === window, - () => false, - ); - - if (!shouldShow) return null; - - return ( -
-
- {pending ? ( - "Disabling draft mode..." - ) : ( - <> - {"Previewing drafts. "} - - - )} -
-
- ); -} diff --git a/examples/cms-sanity/app/(blog)/avatar.tsx b/examples/cms-sanity/app/(blog)/avatar.tsx index f0942804aca9e..becc6fdb611f8 100644 --- a/examples/cms-sanity/app/(blog)/avatar.tsx +++ b/examples/cms-sanity/app/(blog)/avatar.tsx @@ -1,7 +1,6 @@ -import { Image } from "next-sanity/image"; - import type { Author } from "@/sanity.types"; import { urlForImage } from "@/sanity/lib/utils"; +import { Image } from "next-sanity/image"; interface Props { name: string; diff --git a/examples/cms-sanity/app/(blog)/cors.ts b/examples/cms-sanity/app/(blog)/cors.ts new file mode 100644 index 0000000000000..79fba97ecbf12 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/cors.ts @@ -0,0 +1,29 @@ +"use client"; + +import { isCorsOriginError } from "next-sanity"; +import { toast } from "sonner"; + +export function handleError(error: unknown) { + if (isCorsOriginError(error)) { + const { addOriginUrl } = error; + toast.error(`Sanity Live couldn't connect`, { + description: `Your origin is blocked by CORS policy`, + duration: Infinity, + action: addOriginUrl + ? { + label: "Manage", + onClick: () => window.open(addOriginUrl.toString(), "_blank"), + } + : undefined, + }); + } else if (error instanceof Error) { + console.error(error); + toast.error(error.name, { description: error.message, duration: Infinity }); + } else { + console.error(error); + toast.error("Unknown error", { + description: "Check the console for more details", + duration: Infinity, + }); + } +} diff --git a/examples/cms-sanity/app/(blog)/cover-image.tsx b/examples/cms-sanity/app/(blog)/cover-image.tsx index 97057ec8c941c..61af3dc9e327d 100644 --- a/examples/cms-sanity/app/(blog)/cover-image.tsx +++ b/examples/cms-sanity/app/(blog)/cover-image.tsx @@ -1,6 +1,6 @@ -import { Image } from "next-sanity/image"; - import { urlForImage } from "@/sanity/lib/utils"; +import * as motion from "framer-motion/client"; +import { Image } from "next-sanity/image"; interface CoverImageProps { image: any; @@ -24,8 +24,11 @@ export default function CoverImage(props: CoverImageProps) { ); return ( -
+ {image} -
+ ); } diff --git a/examples/cms-sanity/app/(blog)/draft-mode-toast.tsx b/examples/cms-sanity/app/(blog)/draft-mode-toast.tsx new file mode 100644 index 0000000000000..a8c49e21570a8 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/draft-mode-toast.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { + useDraftModeEnvironment, + useIsPresentationTool, +} from "next-sanity/hooks"; +import { useRouter } from "next/navigation"; +import { useEffect, useTransition } from "react"; +import { toast } from "sonner"; +import { disableDraftMode } from "./actions"; + +export default function DraftModeToast() { + const isPresentationTool = useIsPresentationTool(); + const env = useDraftModeEnvironment(); + const router = useRouter(); + const [pending, startTransition] = useTransition(); + + useEffect(() => { + if (isPresentationTool === false) { + /** + * We delay the toast in case we're inside Presentation Tool + */ + const toastId = toast("Draft Mode Enabled", { + id: "draft-mode-toast", + description: + env === "live" + ? "Content is live, refreshing automatically" + : "Refresh manually to see changes", + duration: Infinity, + action: { + label: "Disable", + onClick: () => + startTransition(async () => { + await disableDraftMode(); + startTransition(() => router.refresh()); + }), + }, + }); + return () => { + toast.dismiss(toastId); + }; + } + }, [env, router, isPresentationTool]); + + useEffect(() => { + if (pending) { + const toastId = toast.loading("Disabling draft mode..."); + return () => { + toast.dismiss(toastId); + }; + } + }, [pending]); + + return null; +} diff --git a/examples/cms-sanity/app/(blog)/hero-layout-shift.tsx b/examples/cms-sanity/app/(blog)/hero-layout-shift.tsx new file mode 100644 index 0000000000000..cff61dcd0a6ff --- /dev/null +++ b/examples/cms-sanity/app/(blog)/hero-layout-shift.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useEffect } from "react"; +import { toast } from "sonner"; +import { useDeferredLayoutShift } from "./use-deferred-transition"; + +/** + * Suspends layout shift for the hero post when a new post is published. + * On changes it'll require opt-in form the user before the post is shown. + * If the post itself is edited, it'll refresh automatically to allow fixing typos. + */ + +export function HeroLayoutShift(props: { + children: React.ReactNode; + id: string; +}) { + const [children, pending, startViewTransition] = useDeferredLayoutShift( + props.children, + [props.id], + ); + + /** + * We need to suspend layout shift for user opt-in. + */ + useEffect(() => { + if (!pending) return; + + toast("A new post is available", { + id: "hero-layout-shift", + duration: Infinity, + action: { + label: "Refresh", + onClick: () => { + requestAnimationFrame(() => + document + .querySelector("article") + ?.scrollIntoView({ behavior: "smooth", block: "nearest" }), + ); + startViewTransition(); + }, + }, + }); + }, [pending, startViewTransition]); + + return children; +} diff --git a/examples/cms-sanity/app/(blog)/layout.tsx b/examples/cms-sanity/app/(blog)/layout.tsx index b6954df828195..e3ee79b679242 100644 --- a/examples/cms-sanity/app/(blog)/layout.tsx +++ b/examples/cms-sanity/app/(blog)/layout.tsx @@ -1,25 +1,26 @@ import "../globals.css"; - +import * as demo from "@/sanity/lib/demo"; +import { sanityFetch, SanityLive } from "@/sanity/lib/live"; +import { settingsQuery } from "@/sanity/lib/queries"; +import { resolveOpenGraphImage } from "@/sanity/lib/utils"; +import { Analytics } from "@vercel/analytics/react"; import { SpeedInsights } from "@vercel/speed-insights/next"; +import { AnimatePresence } from "framer-motion"; import type { Metadata } from "next"; import { - VisualEditing, toPlainText, + VisualEditing, type PortableTextBlock, } from "next-sanity"; import { Inter } from "next/font/google"; import { draftMode } from "next/headers"; - -import AlertBanner from "./alert-banner"; +import { Toaster } from "sonner"; +import { handleError } from "./cors"; +import DraftModeToast from "./draft-mode-toast"; import PortableText from "./portable-text"; -import * as demo from "@/sanity/lib/demo"; -import { sanityFetch } from "@/sanity/lib/fetch"; -import { settingsQuery } from "@/sanity/lib/queries"; -import { resolveOpenGraphImage } from "@/sanity/lib/utils"; - export async function generateMetadata(): Promise { - const settings = await sanityFetch({ + const { data: settings } = await sanityFetch({ query: settingsQuery, // Metadata should never contain stega stega: false, @@ -60,49 +61,36 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - const data = await sanityFetch({ query: settingsQuery }); - const footer = data?.footer || []; const { isEnabled: isDraftMode } = await draftMode(); + const { data } = await sanityFetch({ query: settingsQuery }); + const footer = data?.footer || []; return (
- {isDraftMode && } -
{children}
+
+ {children} +
- {isDraftMode && } + + {isDraftMode && ( + <> + + + + )} + + ); diff --git a/examples/cms-sanity/app/(blog)/more-stories-layout-shift.tsx b/examples/cms-sanity/app/(blog)/more-stories-layout-shift.tsx new file mode 100644 index 0000000000000..155843752d5b5 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/more-stories-layout-shift.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useEffect } from "react"; +import { toast } from "sonner"; +import { useDeferredLayoutShift } from "./use-deferred-transition"; + +/** + * Suspends layout shift for the more stories section when a new post is published. + * On changes it'll require opt-in form the user before the post is shown. + * If the post itself is edited, it'll refresh automatically to allow fixing typos. + */ + +export function MoreStoriesLayoutShift(props: { + children: React.ReactNode; + ids: string[]; +}) { + const [children, pending, startViewTransition] = useDeferredLayoutShift( + props.children, + props.ids, + ); + + /** + * We need to suspend layout shift for user opt-in. + */ + useEffect(() => { + if (!pending) return; + + toast("More stories have been published", { + id: "more-stories-layout-shift", + duration: Infinity, + action: { + label: "Refresh", + onClick: () => startViewTransition(), + }, + }); + }, [pending, startViewTransition]); + + return children; +} diff --git a/examples/cms-sanity/app/(blog)/more-stories.tsx b/examples/cms-sanity/app/(blog)/more-stories.tsx index ea7a993ae42fd..0d706959d2edb 100644 --- a/examples/cms-sanity/app/(blog)/more-stories.tsx +++ b/examples/cms-sanity/app/(blog)/more-stories.tsx @@ -1,46 +1,81 @@ +import { sanityFetch } from "@/sanity/lib/live"; +import { moreStoriesQuery } from "@/sanity/lib/queries"; +import { AnimatePresence } from "framer-motion"; +import * as motion from "framer-motion/client"; import Link from "next/link"; - import Avatar from "./avatar"; import CoverImage from "./cover-image"; import DateComponent from "./date"; - -import { sanityFetch } from "@/sanity/lib/fetch"; -import { moreStoriesQuery } from "@/sanity/lib/queries"; +import { MoreStoriesLayoutShift } from "./more-stories-layout-shift"; export default async function MoreStories(params: { skip: string; limit: number; }) { - const data = await sanityFetch({ query: moreStoriesQuery, params }); + const { data } = await sanityFetch({ query: moreStoriesQuery, params }); return ( - <> -
- {data?.map((post) => { - const { _id, title, slug, coverImage, excerpt, author } = post; - return ( -
- - - -

- - {title} +
+ post._id) ?? []} + > + + {data?.map((post) => { + const { _id, title, slug, coverImage, excerpt, author } = post; + return ( + + + -

-
- -
- {excerpt && ( -

- {excerpt} -

- )} - {author && } -
- ); - })} -
- + + + {title} + + + + + + + {excerpt && ( +

+ {excerpt} +

+ )} +
+ + {author && ( + + )} + + + ); + })} + + + ); } diff --git a/examples/cms-sanity/app/(blog)/onboarding.tsx b/examples/cms-sanity/app/(blog)/onboarding.tsx index 2971b2bacb638..0055b1cf5bccd 100644 --- a/examples/cms-sanity/app/(blog)/onboarding.tsx +++ b/examples/cms-sanity/app/(blog)/onboarding.tsx @@ -4,7 +4,6 @@ * This file is used for onboarding when you don't have any posts yet and are using the template for the first time. * Once you have content, and know where to go to access the Sanity Studio and create content, you can delete this file. */ - import Link from "next/link"; import { useSyncExternalStore } from "react"; diff --git a/examples/cms-sanity/app/(blog)/page.tsx b/examples/cms-sanity/app/(blog)/page.tsx index 58b2a538169a6..1f7c1d9e090e1 100644 --- a/examples/cms-sanity/app/(blog)/page.tsx +++ b/examples/cms-sanity/app/(blog)/page.tsx @@ -1,39 +1,109 @@ +import type { HeroQueryResult } from "@/sanity.types"; +import { dataAttribute } from "@/sanity/lib/dataAttribute"; +import * as demo from "@/sanity/lib/demo"; +import { sanityFetch } from "@/sanity/lib/live"; +import { heroQuery, settingsQuery } from "@/sanity/lib/queries"; +import { AnimatePresence, LayoutGroup } from "framer-motion"; +import * as motion from "framer-motion/client"; import Link from "next/link"; import { Suspense } from "react"; - import Avatar from "./avatar"; import CoverImage from "./cover-image"; import DateComponent from "./date"; +import { HeroLayoutShift } from "./hero-layout-shift"; import MoreStories from "./more-stories"; import Onboarding from "./onboarding"; import PortableText from "./portable-text"; -import type { HeroQueryResult } from "@/sanity.types"; -import * as demo from "@/sanity/lib/demo"; -import { sanityFetch } from "@/sanity/lib/fetch"; -import { heroQuery, settingsQuery } from "@/sanity/lib/queries"; +export default async function Page() { + const [{ data: settings }, { data: heroPost }] = await Promise.all([ + sanityFetch({ + query: settingsQuery, + }), + sanityFetch({ query: heroQuery }), + ]); + + return ( +
+ + {heroPost ? ( + + + + + + + + + ) : ( + + )} +
+ ); +} function Intro(props: { title: string | null | undefined; description: any }) { - const title = props.title || demo.title; - const description = props.description?.length - ? props.description - : demo.description; + const editable = dataAttribute({ type: "settings", id: "settings" }); return ( -
-

- {title || demo.title} -

-

+
+ + {props.title || demo.title} + + -

+
); } function HeroPost({ + _id, title, slug, excerpt, @@ -42,70 +112,47 @@ function HeroPost({ author, }: Pick< Exclude, - "title" | "coverImage" | "date" | "excerpt" | "author" | "slug" + "_id" | "title" | "coverImage" | "date" | "excerpt" | "author" | "slug" >) { return ( -
- - + <> + +
-

+

{title}

-
+
- {excerpt && ( -

- {excerpt} -

- )} - {author && } +
+ {excerpt && ( +

+ {excerpt} +

+ )} +
+
+ {author && } +
-
- ); -} - -export default async function Page() { - const [settings, heroPost] = await Promise.all([ - sanityFetch({ - query: settingsQuery, - }), - sanityFetch({ query: heroQuery }), - ]); - - return ( -
- - {heroPost ? ( - - ) : ( - - )} - {heroPost?._id && ( - - )} -
+ ); } diff --git a/examples/cms-sanity/app/(blog)/portable-text.tsx b/examples/cms-sanity/app/(blog)/portable-text.tsx index 7ec07c5692146..a09a2ad514621 100644 --- a/examples/cms-sanity/app/(blog)/portable-text.tsx +++ b/examples/cms-sanity/app/(blog)/portable-text.tsx @@ -10,9 +10,10 @@ import { PortableText, - type PortableTextComponents, type PortableTextBlock, + type PortableTextComponents, } from "next-sanity"; +import type { CSSProperties } from "react"; export default function CustomPortableText({ className, @@ -23,17 +24,56 @@ export default function CustomPortableText({ }) { const components: PortableTextComponents = { block: { - h5: ({ children }) => ( -
{children}
+ normal: ({ children, value }) => ( +

{children}

+ ), + blockquote: ({ children, value }) => ( +
{children}
+ ), + h1: ({ children, value }) => ( +

{children}

+ ), + h2: ({ children, value }) => ( +

{children}

+ ), + h3: ({ children, value }) => ( +

{children}

+ ), + h4: ({ children, value }) => ( +

{children}

+ ), + h5: ({ children, value }) => ( +
+ {children} +
), - h6: ({ children }) => ( -
{children}
+ h6: ({ children, value }) => ( +
+ {children} +
), }, + list: { + number: ({ children, value }) => ( +
    {children}
+ ), + bullet: ({ children, value }) => ( +
    {children}
+ ), + }, + listItem: ({ children, value }) => ( +
  • + {children} +
  • + ), marks: { link: ({ children, value }) => { return ( - + {children} ); @@ -47,3 +87,13 @@ export default function CustomPortableText({ ); } + +function getViewTransitionName(value: string | undefined) { + return value ? `pt-${value}` : undefined; +} + +function style(value: string | undefined): CSSProperties { + return { + viewTransitionName: getViewTransitionName(value), + }; +} diff --git a/examples/cms-sanity/app/(blog)/posts/[slug]/content-layout-shift.tsx b/examples/cms-sanity/app/(blog)/posts/[slug]/content-layout-shift.tsx new file mode 100644 index 0000000000000..dec1b839ac839 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/posts/[slug]/content-layout-shift.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { startTransition, useEffect } from "react"; +import { flushSync } from "react-dom"; +import { toast } from "sonner"; +import { useDeferredLayoutShift } from "../../use-deferred-transition"; + +export function ContentLayoutShift(props: { + children: React.ReactNode; + rev: string; +}) { + const [children, pending, startViewTransition] = useDeferredLayoutShift( + props.children, + [props.rev], + ); + + /** + * We need to suspend layout shift for user opt-in. + */ + useEffect(() => { + if (!pending) return; + + toast("This post has been updated", { + id: `post-content-layout-shift`, + duration: Infinity, + action: { + label: "Refresh", + onClick: () => { + const update = () => startViewTransition(); + if ( + "startViewTransition" in document && + typeof document.startViewTransition === "function" + ) { + document.startViewTransition(() => flushSync(() => update())); + } else { + startTransition(() => update()); + } + }, + }, + }); + }, [pending, startViewTransition]); + + return children; +} diff --git a/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx b/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx index 125c419f18c56..10ca5fde6ebc9 100644 --- a/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx +++ b/examples/cms-sanity/app/(blog)/posts/[slug]/page.tsx @@ -1,20 +1,18 @@ -import { defineQuery } from "next-sanity"; +import * as demo from "@/sanity/lib/demo"; +import { sanityFetch } from "@/sanity/lib/live"; +import { postQuery, settingsQuery } from "@/sanity/lib/queries"; +import { resolveOpenGraphImage } from "@/sanity/lib/utils"; +import * as motion from "framer-motion/client"; import type { Metadata, ResolvingMetadata } from "next"; -import { type PortableTextBlock } from "next-sanity"; +import { defineQuery, type PortableTextBlock } from "next-sanity"; import Link from "next/link"; -import { notFound } from "next/navigation"; import { Suspense } from "react"; - import Avatar from "../../avatar"; import CoverImage from "../../cover-image"; import DateComponent from "../../date"; import MoreStories from "../../more-stories"; import PortableText from "../../portable-text"; - -import * as demo from "@/sanity/lib/demo"; -import { sanityFetch } from "@/sanity/lib/fetch"; -import { postQuery, settingsQuery } from "@/sanity/lib/queries"; -import { resolveOpenGraphImage } from "@/sanity/lib/utils"; +import { ContentLayoutShift } from "./content-layout-shift"; type Props = { params: Promise<{ slug: string }>; @@ -25,18 +23,19 @@ const postSlugs = defineQuery( ); export async function generateStaticParams() { - return await sanityFetch({ + const { data } = await sanityFetch({ query: postSlugs, perspective: "published", stega: false, }); + return data; } export async function generateMetadata( { params }: Props, parent: ResolvingMetadata, ): Promise { - const post = await sanityFetch({ + const { data: post } = await sanityFetch({ query: postQuery, params, stega: false, @@ -55,60 +54,102 @@ export async function generateMetadata( } export default async function PostPage({ params }: Props) { - const [post, settings] = await Promise.all([ + const [{ data: post }, { data: settings }] = await Promise.all([ sanityFetch({ query: postQuery, params }), sanityFetch({ query: settingsQuery }), ]); - if (!post?._id) { - return notFound(); - } - return (
    -

    - + + {settings?.title || demo.title} -

    -
    -

    - {post.title} -

    -
    - {post.author && ( - - )} -
    -
    - -
    -
    -
    - {post.author && ( - - )} -
    -
    -
    - + + {post?._id ? ( +
    + + + {post.title} + + + {post.author && ( + + )} + +
    + +
    +
    + + {post.author && ( + + )} + +
    + + + +
    -
    -
    - {post.content?.length && ( - - )} -
    + + {post.content?.length && ( + + )} + + + + ) : ( +

    + 404 - Post Not Found +

    + )}
    diff --git a/examples/cms-sanity/app/(blog)/use-deferred-transition.tsx b/examples/cms-sanity/app/(blog)/use-deferred-transition.tsx new file mode 100644 index 0000000000000..8343406c89128 --- /dev/null +++ b/examples/cms-sanity/app/(blog)/use-deferred-transition.tsx @@ -0,0 +1,39 @@ +import { useIsLivePreview } from "next-sanity/hooks"; +import { useCallback, useState } from "react"; +import isEqual from "react-fast-compare"; + +export function useDeferredLayoutShift( + children: React.ReactNode, + dependencies: unknown[], +) { + const [pending, setPending] = useState(false); + const [currentChildren, setCurrentChildren] = useState(children); + const [currentDependencies, setCurrentDependencies] = useState(dependencies); + + if (!pending) { + if (isEqual(currentDependencies, dependencies)) { + if (currentChildren !== children) { + setCurrentChildren(children); + } + } else { + setCurrentDependencies(dependencies); + setPending(true); + } + } + + const startViewTransition = useCallback(() => { + setCurrentDependencies(dependencies); + setPending(false); + }, [dependencies]); + + /** + * If we are in live preview mode then we can skip suspending layout shift. + */ + const isLivePreview = useIsLivePreview() === true; + + return [ + pending && !isLivePreview ? currentChildren : children, + pending && !isLivePreview, + startViewTransition, + ] as const; +} diff --git a/examples/cms-sanity/app/(sanity)/layout.tsx b/examples/cms-sanity/app/(sanity)/layout.tsx index 0e8faaea8bcba..0809afe2d132f 100644 --- a/examples/cms-sanity/app/(sanity)/layout.tsx +++ b/examples/cms-sanity/app/(sanity)/layout.tsx @@ -1,5 +1,4 @@ import "../globals.css"; - import { Inter } from "next/font/google"; const inter = Inter({ diff --git a/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx b/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx index f3d0315aec7e6..2a59585c16277 100644 --- a/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx +++ b/examples/cms-sanity/app/(sanity)/studio/[[...tool]]/page.tsx @@ -1,6 +1,5 @@ -import { NextStudio } from "next-sanity/studio"; - import config from "@/sanity.config"; +import { NextStudio } from "next-sanity/studio"; export const dynamic = "force-static"; diff --git a/examples/cms-sanity/app/api/draft-mode/enable/route.ts b/examples/cms-sanity/app/api/draft-mode/enable/route.ts index 91986b76f3fb0..40947c20c1f18 100644 --- a/examples/cms-sanity/app/api/draft-mode/enable/route.ts +++ b/examples/cms-sanity/app/api/draft-mode/enable/route.ts @@ -1,7 +1,6 @@ -import { defineEnableDraftMode } from "next-sanity/draft-mode"; - import { client } from "@/sanity/lib/client"; import { token } from "@/sanity/lib/token"; +import { defineEnableDraftMode } from "next-sanity/draft-mode"; export const { GET } = defineEnableDraftMode({ client: client.withConfig({ token }), diff --git a/examples/cms-sanity/app/globals.css b/examples/cms-sanity/app/globals.css index b5c61c956711f..04172992f81a4 100644 --- a/examples/cms-sanity/app/globals.css +++ b/examples/cms-sanity/app/globals.css @@ -1,3 +1,11 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@media (prefers-reduced-motion) { + ::view-transition-group(*), + ::view-transition-old(*), + ::view-transition-new(*) { + animation: none !important; + } +} diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index a0a9becc69ff4..f8fcd8d394c66 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -1,44 +1,58 @@ { "private": true, "scripts": { - "predev": "npm run typegen", - "dev": "next --turbo", - "prebuild": "npm run typegen", "build": "next build", - "start": "next start", + "predev": "npm run typegen", + "dev": "next --turbopack", + "format": "prettier --cache --write .", "lint": "next lint", "presetup": "echo 'about to setup env variables, follow the guide here: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli'", "setup": "npx sanity@latest init --env .env.local", "postsetup": "echo 'create the read token by following the rest of the guide: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#creating-a-read-token'", + "start": "next start", "typegen": "sanity schema extract && sanity typegen generate" }, + "prettier": { + "plugins": [ + "@ianvs/prettier-plugin-sort-imports", + "prettier-plugin-packagejson", + "prettier-plugin-tailwindcss" + ] + }, "dependencies": { - "@sanity/assist": "^3.0.8", - "@sanity/icons": "^3.4.0", - "@sanity/image-url": "^1.0.2", - "@sanity/preview-url-secret": "^2.0.0", - "@sanity/vision": "^3.62.0", + "@sanity/assist": "^3.0.9", + "@sanity/client": "^6.24.1", + "@sanity/icons": "^3.5.5", + "@sanity/image-url": "^1.1.0", + "@sanity/vision": "^3.68.2", "@tailwindcss/typography": "^0.5.15", - "@types/node": "^22.7.8", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", - "@vercel/speed-insights": "^1.0.13", + "@types/node": "^22.10.2", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vercel/analytics": "^1.4.1", + "@vercel/speed-insights": "^1.1.0", "autoprefixer": "^10.4.20", "date-fns": "^4.1.0", - "next": "^15.0.0", - "next-sanity": "^9.7.0", - "postcss": "^8.4.47", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "sanity": "^3.62.0", - "sanity-plugin-asset-source-unsplash": "^3.0.1", - "server-only": "^0.0.1", + "framer-motion": "^11.15.0", + "next": "^15.1.2", + "next-sanity": "^9.8.29", + "postcss": "^8.4.49", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-fast-compare": "^3.2.2", + "sanity": "^3.68.2", + "sanity-plugin-asset-source-unsplash": "^3.0.2", + "sonner": "^1.7.1", "styled-components": "^6.1.13", - "tailwindcss": "^3.4.14", - "typescript": "5.6.3" + "tailwindcss": "^3.4.17", + "typescript": "5.7.2" }, "devDependencies": { - "eslint": "^9.13.0", - "eslint-config-next": "^15.0.0" + "@ianvs/prettier-plugin-sort-imports": "^4.4.0", + "eslint": "^9.17.0", + "eslint-config-next": "^15.1.2", + "prettier": "^3.4.2", + "prettier-plugin-packagejson": "^2.5.6", + "prettier-plugin-tailwindcss": "^0.6.9" } } diff --git a/examples/cms-sanity/sanity.config.ts b/examples/cms-sanity/sanity.config.ts index 1372daaacf802..17d1b84681fea 100644 --- a/examples/cms-sanity/sanity.config.ts +++ b/examples/cms-sanity/sanity.config.ts @@ -1,26 +1,25 @@ "use client"; + /** * This config is used to set up Sanity Studio that's mounted on the `app/(sanity)/studio/[[...tool]]/page.tsx` route */ +import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; +import { assistWithPresets } from "@/sanity/plugins/assist"; +import { pageStructure, singletonPlugin } from "@/sanity/plugins/settings"; +import author from "@/sanity/schemas/documents/author"; +import post from "@/sanity/schemas/documents/post"; +import settings from "@/sanity/schemas/singletons/settings"; import { visionTool } from "@sanity/vision"; -import { PluginOptions, defineConfig } from "sanity"; +import { defineConfig, PluginOptions } from "sanity"; import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash"; import { - presentationTool, defineDocuments, defineLocations, + presentationTool, type DocumentLocation, } from "sanity/presentation"; import { structureTool } from "sanity/structure"; -import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; -import { pageStructure, singletonPlugin } from "@/sanity/plugins/settings"; -import { assistWithPresets } from "@/sanity/plugins/assist"; -import author from "@/sanity/schemas/documents/author"; -import post from "@/sanity/schemas/documents/post"; -import settings from "@/sanity/schemas/singletons/settings"; -import { resolveHref } from "@/sanity/lib/utils"; - const homeLocation = { title: "Home", href: "/", @@ -45,7 +44,7 @@ export default defineConfig({ mainDocuments: defineDocuments([ { route: "/posts/:slug", - filter: `_type == "post" && slug.current == $slug`, + filter: `_type == "post" && (slug.current == $slug || _id == $slug)`, }, ]), locations: { @@ -61,12 +60,14 @@ export default defineConfig({ }, resolve: (doc) => ({ locations: [ - { - title: doc?.title || "Untitled", - href: resolveHref("post", doc?.slug)!, - }, + doc + ? { + title: doc?.title || "Untitled", + href: `/posts/${doc.slug}`, + } + : null, homeLocation, - ], + ].filter(Boolean) as DocumentLocation[], }), }), }, diff --git a/examples/cms-sanity/sanity.types.ts b/examples/cms-sanity/sanity.types.ts index 37df1b51da46c..f4294f5c3718a 100644 --- a/examples/cms-sanity/sanity.types.ts +++ b/examples/cms-sanity/sanity.types.ts @@ -1,3 +1,6 @@ +// Query TypeMap +import "@sanity/client"; + /** * --------------------------------------------------------------------------------- * This file has been generated by Sanity TypeGen. @@ -105,6 +108,7 @@ export type Post = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; }; date?: string; @@ -133,6 +137,7 @@ export type Author = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; }; }; @@ -517,6 +522,7 @@ export type HeroQueryResult = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; date: string; @@ -532,12 +538,13 @@ export type HeroQueryResult = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; } | null; } | null; // Variable: moreStoriesQuery -// Query: *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } +// Query: *[_type == "post" && _id != $skip && defined(slug.current) && slug.current != $skip] | order(date desc, _updatedAt desc) [0...$limit] { _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } export type MoreStoriesQueryResult = Array<{ _id: string; status: "draft" | "published"; @@ -554,6 +561,7 @@ export type MoreStoriesQueryResult = Array<{ hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; date: string; @@ -569,13 +577,15 @@ export type MoreStoriesQueryResult = Array<{ hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; } | null; }>; // Variable: postQuery -// Query: *[_type == "post" && slug.current == $slug] [0] { content, _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } +// Query: *[_type == "post" && (slug.current == $slug)] [0] { _rev, content, _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } export type PostQueryResult = { + _rev: string; content: Array<{ children?: Array<{ marks?: Array; @@ -609,6 +619,7 @@ export type PostQueryResult = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; date: string; @@ -624,6 +635,7 @@ export type PostQueryResult = { hotspot?: SanityImageHotspot; crop?: SanityImageCrop; alt?: string; + imagePrompt?: string; _type: "image"; } | null; } | null; @@ -636,14 +648,12 @@ export type PostSlugsResult = Array<{ slug: string | null; }>; -// Query TypeMap -import "@sanity/client"; declare module "@sanity/client" { interface SanityQueries { '*[_type == "settings"][0]': SettingsQueryResult; '\n *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] {\n content,\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': HeroQueryResult; - '\n *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': MoreStoriesQueryResult; - '\n *[_type == "post" && slug.current == $slug] [0] {\n content,\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': PostQueryResult; + '\n *[_type == "post" && _id != $skip && defined(slug.current) && slug.current != $skip] | order(date desc, _updatedAt desc) [0...$limit] {\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': MoreStoriesQueryResult; + '\n *[_type == "post" && (slug.current == $slug)] [0] {\n _rev,\n content,\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': PostQueryResult; '*[_type == "post" && defined(slug.current)]{"slug": slug.current}': PostSlugsResult; } } diff --git a/examples/cms-sanity/sanity/lib/api.ts b/examples/cms-sanity/sanity/lib/api.ts index c84eaafb86a73..7c7e1b08d1f74 100644 --- a/examples/cms-sanity/sanity/lib/api.ts +++ b/examples/cms-sanity/sanity/lib/api.ts @@ -25,7 +25,7 @@ export const projectId = assertValue( * see https://www.sanity.io/docs/api-versioning for how versioning works */ export const apiVersion = - process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-28"; + process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-10-22"; /** * Used to configure edit intent links, for Presentation Mode, as well as to configure where the Studio is mounted in the router. diff --git a/examples/cms-sanity/sanity/lib/client.ts b/examples/cms-sanity/sanity/lib/client.ts index ba149ef011293..0a1c04b8e775b 100644 --- a/examples/cms-sanity/sanity/lib/client.ts +++ b/examples/cms-sanity/sanity/lib/client.ts @@ -1,6 +1,5 @@ -import { createClient } from "next-sanity"; - import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; +import { createClient } from "next-sanity"; export const client = createClient({ projectId, @@ -10,7 +9,10 @@ export const client = createClient({ perspective: "published", stega: { studioUrl, - logger: console, + logger: + process.env.NEXT_PUBLIC_SANITY_STEGA_LOGGER === "false" + ? undefined + : console, filter: (props) => { if (props.sourcePath.at(-1) === "title") { return true; diff --git a/examples/cms-sanity/sanity/lib/dataAttribute.ts b/examples/cms-sanity/sanity/lib/dataAttribute.ts new file mode 100644 index 0000000000000..0d83cab7a3feb --- /dev/null +++ b/examples/cms-sanity/sanity/lib/dataAttribute.ts @@ -0,0 +1,16 @@ +import { studioUrl as baseUrl, dataset, projectId } from "@/sanity/lib/api"; +import { createDataAttribute } from "next-sanity"; + +export function dataAttribute( + node: Omit< + Parameters[0], + "baseUrl" | "workspace" | "tool" | "projectId" | "dataset" + >, +) { + return createDataAttribute({ + baseUrl, + projectId, + dataset, + ...node, + }); +} diff --git a/examples/cms-sanity/sanity/lib/demo.ts b/examples/cms-sanity/sanity/lib/demo.ts index 8c68844102ebe..da5814a971920 100644 --- a/examples/cms-sanity/sanity/lib/demo.ts +++ b/examples/cms-sanity/sanity/lib/demo.ts @@ -2,7 +2,7 @@ * Demo data used as placeholders and initial values for the blog */ -export const title = "Blog."; +export const title = "🍋 Fresh ✨"; export const description = [ { @@ -13,47 +13,62 @@ export const description = [ _key: "4a58edd077880", _type: "span", marks: [], - text: "A statically generated blog example using ", + text: "Self-updating blog with ", }, { _key: "4a58edd077881", _type: "span", marks: ["ec5b66c9b1e0"], - text: "Next.js", + text: "Sanity Live Content", }, { _key: "4a58edd077882", _type: "span", marks: [], - text: " and ", + text: " & ", }, { _key: "4a58edd077883", _type: "span", marks: ["1f8991913ea8"], - text: "Sanity", - }, - { - _key: "4a58edd077884", - _type: "span", - marks: [], - text: ".", + text: "Next.js", }, ], markDefs: [ { _key: "ec5b66c9b1e0", _type: "link", - href: "https://nextjs.org/", + href: "https://www.sanity.io/live", }, { _key: "1f8991913ea8", _type: "link", - href: "https://sanity.io/", + href: "https://nextjs.org/", }, ], style: "normal", }, ]; -export const ogImageTitle = "A Next.js Blog with a Native Authoring Experience"; +export const footer = [ + { + _type: "block", + _key: "9066f0e0c422", + style: "normal", + markDefs: [ + { + _type: "link", + _key: "068d472d2618", + href: "https://github.com/vercel/next.js/tree/canary/examples/cms-sanity", + }, + ], + children: [ + { + _type: "span", + _key: "2839a5d28445", + marks: ["068d472d2618"], + text: "View on GitHub", + }, + ], + }, +]; diff --git a/examples/cms-sanity/sanity/lib/fetch.ts b/examples/cms-sanity/sanity/lib/fetch.ts deleted file mode 100644 index 3c11d0d4c5ae7..0000000000000 --- a/examples/cms-sanity/sanity/lib/fetch.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { ClientPerspective, QueryParams } from "next-sanity"; -import { draftMode } from "next/headers"; - -import { client } from "@/sanity/lib/client"; -import { token } from "@/sanity/lib/token"; - -/** - * Used to fetch data in Server Components, it has built in support for handling Draft Mode and perspectives. - * When using the "published" perspective then time-based revalidation is used, set to match the time-to-live on Sanity's API CDN (60 seconds) - * and will also fetch from the CDN. - * When using the "previewDrafts" perspective then the data is fetched from the live API and isn't cached, it will also fetch draft content that isn't published yet. - */ -export async function sanityFetch({ - query, - params = {}, - perspective: _perspective, - /** - * Stega embedded Content Source Maps are used by Visual Editing by both the Sanity Presentation Tool and Vercel Visual Editing. - * The Sanity Presentation Tool will enable Draft Mode when loading up the live preview, and we use it as a signal for when to embed source maps. - * When outside of the Sanity Studio we also support the Vercel Toolbar Visual Editing feature, which is only enabled in production when it's a Vercel Preview Deployment. - */ - stega: _stega, -}: { - query: QueryString; - params?: QueryParams | Promise; - perspective?: Omit; - stega?: boolean; -}) { - const perspective = - _perspective || (await draftMode()).isEnabled - ? "previewDrafts" - : "published"; - const stega = - _stega || - perspective === "previewDrafts" || - process.env.VERCEL_ENV === "preview"; - if (perspective === "previewDrafts") { - return client.fetch(query, await params, { - stega, - perspective: "previewDrafts", - // The token is required to fetch draft content - token, - // The `previewDrafts` perspective isn't available on the API CDN - useCdn: false, - // And we can't cache the responses as it would slow down the live preview experience - next: { revalidate: 0 }, - }); - } - return client.fetch(query, await params, { - stega, - perspective: "published", - // The `published` perspective is available on the API CDN - useCdn: true, - // Only enable Stega in production if it's a Vercel Preview Deployment, as the Vercel Toolbar supports Visual Editing - // When using the `published` perspective we use time-based revalidation to match the time-to-live on Sanity's API CDN (60 seconds) - next: { revalidate: 60 }, - }); -} diff --git a/examples/cms-sanity/sanity/lib/live.ts b/examples/cms-sanity/sanity/lib/live.ts new file mode 100644 index 0000000000000..44af0b6fa4995 --- /dev/null +++ b/examples/cms-sanity/sanity/lib/live.ts @@ -0,0 +1,11 @@ +import { defineLive } from "next-sanity"; +import { client } from "./client"; +import { token } from "./token"; + +export const { sanityFetch, SanityLive } = defineLive({ + client, + // Required for showing draft content when the Sanity Presentation Tool is used, or to enable the Vercel Toolbar Edit Mode + serverToken: token, + // Required for stand-alone live previews, the token is only shared to the brwoser if it's a valid Next.js Draft Mode session + browserToken: token, +}); diff --git a/examples/cms-sanity/sanity/lib/queries.ts b/examples/cms-sanity/sanity/lib/queries.ts index 12d45e4cf882a..f177bc81abb1e 100644 --- a/examples/cms-sanity/sanity/lib/queries.ts +++ b/examples/cms-sanity/sanity/lib/queries.ts @@ -21,13 +21,14 @@ export const heroQuery = defineQuery(` `); export const moreStoriesQuery = defineQuery(` - *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { + *[_type == "post" && _id != $skip && defined(slug.current) && slug.current != $skip] | order(date desc, _updatedAt desc) [0...$limit] { ${postFields} } `); export const postQuery = defineQuery(` - *[_type == "post" && slug.current == $slug] [0] { + *[_type == "post" && (slug.current == $slug)] [0] { + _rev, content, ${postFields} } diff --git a/examples/cms-sanity/sanity/lib/utils.ts b/examples/cms-sanity/sanity/lib/utils.ts index 0af9a97bed243..1119286e78522 100644 --- a/examples/cms-sanity/sanity/lib/utils.ts +++ b/examples/cms-sanity/sanity/lib/utils.ts @@ -1,6 +1,5 @@ -import createImageUrlBuilder from "@sanity/image-url"; - import { dataset, projectId } from "@/sanity/lib/api"; +import createImageUrlBuilder from "@sanity/image-url"; const imageBuilder = createImageUrlBuilder({ projectId: projectId || "", @@ -22,16 +21,3 @@ export function resolveOpenGraphImage(image: any, width = 1200, height = 627) { if (!url) return; return { url, alt: image?.alt as string, width, height }; } - -export function resolveHref( - documentType?: string, - slug?: string, -): string | undefined { - switch (documentType) { - case "post": - return slug ? `/posts/${slug}` : undefined; - default: - console.warn("Invalid document type:", documentType); - return undefined; - } -} diff --git a/examples/cms-sanity/sanity/plugins/assist.ts b/examples/cms-sanity/sanity/plugins/assist.ts index a4fa23b750284..3881cd333feba 100644 --- a/examples/cms-sanity/sanity/plugins/assist.ts +++ b/examples/cms-sanity/sanity/plugins/assist.ts @@ -3,7 +3,6 @@ */ import { assist } from "@sanity/assist"; - import postType from "../schemas/documents/post"; export const assistWithPresets = () => diff --git a/examples/cms-sanity/sanity/schemas/documents/author.ts b/examples/cms-sanity/sanity/schemas/documents/author.ts index 04a27b474beb2..4ce72eebbef61 100644 --- a/examples/cms-sanity/sanity/schemas/documents/author.ts +++ b/examples/cms-sanity/sanity/schemas/documents/author.ts @@ -32,11 +32,18 @@ export default defineType({ }); }, }, + { + type: "text", + name: "imagePrompt", + title: "Image prompt", + rows: 2, + }, ], options: { hotspot: true, aiAssist: { imageDescriptionField: "alt", + imageInstructionField: "imagePrompt", }, }, validation: (rule) => rule.required(), diff --git a/examples/cms-sanity/sanity/schemas/documents/post.ts b/examples/cms-sanity/sanity/schemas/documents/post.ts index efb1bdaf82ae4..3fba59c0f2b8d 100644 --- a/examples/cms-sanity/sanity/schemas/documents/post.ts +++ b/examples/cms-sanity/sanity/schemas/documents/post.ts @@ -1,7 +1,6 @@ import { DocumentTextIcon } from "@sanity/icons"; import { format, parseISO } from "date-fns"; import { defineField, defineType } from "sanity"; - import authorType from "./author"; /** @@ -59,6 +58,7 @@ export default defineType({ hotspot: true, aiAssist: { imageDescriptionField: "alt", + imageInstructionField: "imagePrompt", }, }, fields: [ @@ -76,6 +76,12 @@ export default defineType({ }); }, }, + { + type: "text", + name: "imagePrompt", + title: "Image prompt", + rows: 2, + }, ], validation: (rule) => rule.required(), }), diff --git a/examples/cms-sanity/sanity/schemas/singletons/settings.tsx b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx index 2c0c780a5d7c3..63c4e690fad57 100644 --- a/examples/cms-sanity/sanity/schemas/singletons/settings.tsx +++ b/examples/cms-sanity/sanity/schemas/singletons/settings.tsx @@ -1,8 +1,7 @@ +import * as demo from "@/sanity/lib/demo"; import { CogIcon } from "@sanity/icons"; import { defineArrayMember, defineField, defineType } from "sanity"; -import * as demo from "@/sanity/lib/demo"; - export default defineType({ name: "settings", title: "Settings", diff --git a/examples/cms-sanity/schema.json b/examples/cms-sanity/schema.json index 7db0c69906ace..d56377cecaa35 100644 --- a/examples/cms-sanity/schema.json +++ b/examples/cms-sanity/schema.json @@ -619,6 +619,13 @@ }, "optional": true }, + "imagePrompt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, "_type": { "type": "objectAttribute", "value": { @@ -769,6 +776,13 @@ }, "optional": true }, + "imagePrompt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, "_type": { "type": "objectAttribute", "value": { diff --git a/examples/cms-sanity/tailwind.config.ts b/examples/cms-sanity/tailwind.config.ts index 99eb4e61d89fd..02ef19dd4eb18 100644 --- a/examples/cms-sanity/tailwind.config.ts +++ b/examples/cms-sanity/tailwind.config.ts @@ -1,5 +1,5 @@ -import type { Config } from "tailwindcss"; import typography from "@tailwindcss/typography"; +import type { Config } from "tailwindcss"; export default { content: ["./app/**/*.{ts,tsx}", "./sanity/**/*.{ts,tsx}"],