Skip to content

Releases: sanity-io/next-sanity

next-sanity@13.0.0-cache-components.59

15 May 16:56
2561285

Choose a tag to compare

Pre-release

Major Changes

  • #3536 7c18db6 Thanks @stipsan! - Replace the revalidateSyncTags prop on <SanityLive> with action

    The prop was initially introduced to allow overriding the default cache invalidation behavior when a live event was received. Back then the revalidateTag function did not have an second argument that allows configuring the revalidatino behavior, and we did not have support for Invalidate Sync Tags Functions.
    Both platforms have evolved, and the name revalidateSyncTags is no longer accurate, and its default behavior is not ideal.

    This API change is only a breaking change for you if you either:

    • Passed a custom function to revalidateSyncTags on <SanityLive>
    • Rely on the default behavior of how cache tags were invalidated when a live event was received

    The default behavior that you might have relied on is that when a live event was received:

    In practice this meant that if you published a change to your nextjs app while at least one visitor was connected to <SanityLive> and not in draft mode, then they would be guaranteed to see the change within a few seconds.
    If you were in Presentation Tool or otherwise in draft mode, and nobody else observed the change, then they would eventually see the change if they manually refresh the page a couple of times, or something else triggered a refresh() event after the cache is updated. This is a side-effect of using revalidateTag with the max cache profile instead of updateTag, and was a change we made in #3432 to avoid the impact of the Next.js regression reported by Sanity to Vercel in vercel/next.js#93210 as draftMode generally implies that <SanityLive> will enable includeDrafts and thus see live events for draft content, and not just published content, and thus receive far more events than if you were not in draft mode.

    The new behavior further mitiates the impact we've seen in vercel/next.js#93210 by:

    • using revalidateTag with the max cache profile instead of updateTag
    • when in draft mode, we don't updateTag nor revalidateTag at all, we just call refresh()

    In practice this means that by default content changes are no longer guaranteed to be seen by all visitors within a few seconds, they may need to refresh, or trigger a navigation event, before the new content is visible. This makes the default experience "less live", and is a trade-off we're making until nextjs addresses the regression reported in vercel/next.js#93210 and gives us a path to guaranteed live content updates pushed to all visitors that is also cost efficient.

    Opting in to guaranteed live content updates

    The recommended way to opt in to guaranteed live content updates is to implement a Invalidate Sync Tags Function (example) that calls a /api/revalidate-tags endpoint in your app (example).

    We recommend using revalidateTag with the max cache profile to invalidate the cache tags, and use dedicated client components if you want to drive specific experiences that have to be real-time (example), but if you want to have the same instant experience as next-sanity@12 you can set {expire: 0} in your route handler:

    export async function POST(request: Request) {
      const { tags } = (await request.json()) as { tags?: string[] };
    
      for (const tag of tags) {
        // `sanityFetch` returned by `defineLive` from `next-sanity/live` prefixes its `cacheTag` calls with `sanity:`, so we need to add the same prefix here
        revalidateTag(`sanity:${tag}`, { expire: 0 });
      }
    
      return Response.json({ revalidated: tags });
    }

    You then set waitFor="function" on your <SanityLive> component, when you are on a deployment that also handles incoming events to /api/revalidate-tags:

    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>
            {children}
            <SanityLive
              waitFor={
                process.env.VERCEL_ENV === "production" ? "function" : undefined
              }
            />
          </body>
        </html>
      );
    }

    Restore the previous default behavior

    If you want to restore the previous default behavior without implementing an Invalidate Sync Tags Function, you may do so by setting a custom action prop to <SanityLive>:

    import { revalidateTag, updateTag } from "next/cache";
    import { draftMode } from "next/headers";
    import { parseTags } from "next-sanity/live";
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>
            {children}
            <SanityLive
              action={async (unsafeTags) => {
                "use server";
    
                const { isEnabled: isDraftMode } = await draftMode();
                const { tags } = parseTags(unsafeTags);
                const logTags: string[] = [];
                for (const tag of tags) {
                  if (isDraftMode) {
                    revalidateTag(tag, "max");
                  } else {
                    updateTag(tag);
                  }
                  logTags.push(tag);
                }
    
                console.log(
                  `<SanityLive /> ${
                    isDraftMode
                      ? `revalidated tags: ${logTags.join(
                          ", "
                        )} with cache profile "max" `
                      : `updated tags: ${logTags.join(
                          ", "
                        )} and revalidated tag: "sanity:fetch-sync-tags" with cache profile "max"`
                  }`
                );
    
                if (isDraftMode) {
                  return "refresh";
                }
              }}
            />
          </body>
        </html>
      );
    }

    waitFor="function" no longer ignores custom actions

    Previously setting waitFor="function" would ignore any custom revalidateSyncTags function and always call router.refresh() internally.
    That's not the case with action anymore, so if your implementation looks like this today:

    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>
            {children}
            <SanityLive
              revalidateSyncTags={async (tags) => {
                "use server";
                console.log(
                  "this only logs if `waitFor` is not set to `function`",
                  { tags }
                );
              }}
              waitFor={
                process.env.VERCEL_ENV === "production" ? "function" : undefined
              }
            />
          </body>
        </html>
      );
    }

    You need to conditionally set action to your own function, or the string 'refresh', if you want it to work the same way, as action is always respected:

    import { parseTags } from "next-sanity/live";
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const isProduction = process.env.VERCEL_ENV === "production";
      return (
        <html lang="en">
          <body>
            {children}
            <SanityLive
              action={
                isProduction
                  ? "refresh"
                  : async (unsafeTags) => {
                      "use server";
                      const { tagsWithoutPrefix } = parseTags(unsafeTags);
                      console.log(
                        "this only logs if `waitFor` is not set to `function`",
                        { tags }
                      );
                    }
              }
              waitFor={isProduction ? "function" : undefined}
            />
          </body>
        </html>
      );
    }

Patch Changes

  • #3535 c90a84c Thanks @stipsan! - Allow enabling stega: true on sanityFetch if serverToken is provided

    Previously, if you tried to enable stega on perspective: 'published' it would not work:

    const {data} =await sanityF...
Read more

next-sanity@13.0.0-cache-components.58

13 May 18:27
f92e5e8

Choose a tag to compare

Pre-release

Patch Changes

next-sanity@13.0.0-cache-components.57

12 May 15:01
ea64745

Choose a tag to compare

Pre-release

Major Changes

  • #3527 3572c9a Thanks @stipsan! - Remove the stega option from defineLive

    When the client given to defineLive has a stega.studioUrl configured, and draftMode().isEnabled is true, then sanityFetch calls would use true as the default value for its stega option.

    To opt-out of stega being set by default in draft mode, you have 3 options:

    1. Do not define stega.studioUrl in the client config
    2. Set stega: false in the sanityFetch call itself
    3. Set stega: false in the defineLive call.

    With this change you no longer have option 3, and you have to use option 2 or 1.

  • #3526 4ea57cb Thanks @stipsan! - Remove the deprecated fetchOptions option from defineLive

    This option was used to set a time-based revalidation as a fallback strategy for when content might change in the dataset without an active browser session connected to <SanityLive>, thus making the cached content stale.
    The downside to this approach was that time-based revalidation ended up causing many unnecessary ISR writes, since they trigger based on a fixed interval rather than on the actual content changes.

    Now that we have Invalidate Sync Tags support in Sanity Functions, the preferred fallback approach is to use it to call a /api/revalidate-tags endpoint in your app so that the content is eventually fresh even if the content change happened without an active browser session connected to <SanityLive>.

12.4.5

Patch Changes

  • 9446c27 Thanks @stipsan! - Fix a regression introduced in v12.4.0 (#3430) that lead to live preview sometimes failing to refresh and show draft content automatically

next-sanity@13.0.0-cache-components.56

11 May 19:03
efeaa28

Choose a tag to compare

Pre-release

Major Changes

  • #3518 ee00392 Thanks @stipsan! - Throw errors during render if onError is not defined on <SanityLive>

    If you were already handling errors in your own onError callback, then this is not a breaking change for you.

    The new behavior is to avoid silent failures and instead throw errors during render so they can be caught by the nearest React error boundary.

    Restore previous behavior

    The previous behavior would console.warn or console.error the error, and continue rendering the component. If you prefer this behavior you can restore it this way:

    Create a client-functions.ts file with 'use client' and export a onError function like so:

    // app/client-functions.ts
    "use client";
    
    import { isCorsOriginError, type SanityLiveOnError } from "next-sanity/live";
    
    export const onError: SanityLiveOnError = (
      error,
      { includeDrafts, waitFor }
    ) => {
      if (isCorsOriginError(error)) {
        console.warn(
          `Sanity Live is unable to connect to the Sanity API as the current origin - ${window.origin} - is not in the list of allowed CORS origins for this Sanity Project.`,
          error.addOriginUrl && `Add it here:`,
          error.addOriginUrl?.toString()
        );
      } else {
        console.error("Sanity Live encountered an error:", error, {
          includeDrafts,
          waitFor,
        });
      }
    };

    Then in your layout.tsx file, import the onError function and pass it to <SanityLive>:

    // app/layout.tsx
    import { onError } from "./client-functions";
    import { SanityLive } from "#sanity/live";
    
    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <>
          {children}
          <SanityLive onError={onError} />
        </>
      );
    }

    Customize the error boundary

    Using the unstable_catchError API you can create an error boundary that can handle errors and offer retry logic. Here's an example that uses the sonner library to show error toasts that adapt to the error type:

    // app/SanityLiveErrorBoundary.tsx
    "use client";
    
    import { isCorsOriginError } from "next-sanity/live";
    import { unstable_catchError, type ErrorInfo } from "next/error";
    import { useEffect } from "react";
    import { toast } from "sonner";
    
    function SanityLiveErrorBoundary(
      _props: {},
      { error, unstable_retry }: ErrorInfo
    ) {
      useEffect(() => {
        let toastId: string | number | undefined;
        if (isCorsOriginError(error)) {
          const { addOriginUrl } = error;
          toastId = toast.warning(`Sanity Live couldn't connect`, {
            description: `${
              new URL(window.origin).host
            } is blocked by CORS policy`,
            richColors: true,
            duration: Infinity,
            action: addOriginUrl
              ? {
                  label: "Manage",
                  onClick: (event) => {
                    event.preventDefault();
                    window.open(addOriginUrl.toString(), "_blank");
                  },
                }
              : { label: "Retry", onClick: () => unstable_retry() },
            cancel: addOriginUrl
              ? { label: "Retry", onClick: () => unstable_retry() }
              : undefined,
          });
        } else if (error instanceof Error) {
          console.error(error);
          toastId = toast.error(error.message, {
            richColors: true,
            duration: Infinity,
            action: { label: "Retry", onClick: () => unstable_retry() },
          });
        } else {
          console.error(error);
          toastId = toast.error("Unknown error", {
            description: "Check the console for more details",
            richColors: true,
            duration: Infinity,
            action: { label: "Retry", onClick: () => unstable_retry() },
          });
        }
    
        return () => {
          toast.dismiss(toastId);
        };
      }, [error, unstable_retry]);
    
      return null;
    }
    
    export default unstable_catchError(SanityLiveErrorBoundary);

    Then in your layout.tsx file wrap the SanityLive component in the error boundary:

    // app/layout.tsx
    import SanityLiveErrorBoundary from "./SanityLiveErrorBoundary";
    import { SanityLive } from "#sanity/live";
    
    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <>
          {children}
          <SanityLiveErrorBoundary>
            <SanityLive />
          </SanityLiveErrorBoundary>
        </>
      );
    }
  • #3518 ee00392 Thanks @stipsan! - Renamed 3 type exports on next-sanity/live:

    • DefinedSanityFetchType to DefinedFetchType
    • DefinedSanityLiveProps to DefinedLiveProps
    • DefineSanityLiveOptions to DefineLiveOptions

12.4.5

Patch Changes

  • 9446c27 Thanks @stipsan! - Fix a regression introduced in v12.4.0 (#3430) that lead to live preview sometimes failing to refresh and show draft content automatically

next-sanity@13.0.0-cache-components.55

11 May 16:57
1a47578

Choose a tag to compare

Pre-release

Minor Changes

  • #3511 e05f69d Thanks @stipsan! - Add onReconnect & onRestart props on <SanityLive>, by default they call router.refresh()

  • #3515 2096572 Thanks @stipsan! - Add onWelcome prop to <SanityLive>

    The default behavior is to log a welcome message to the console, here's how to customize it:

    // app/client-functions.ts
    "use client";
    
    import type { SanityLiveOnWelcome } from "next-sanity/live";
    
    export const onWelcome: SanityLiveOnWelcome = (
      event,
      { includeDrafts, waitFor }
    ) => {
      console.info(
        `<SanityLive${
          includeDrafts ? " includeDrafts" : ""
        }> is connected and listening for live events to ${
          includeDrafts
            ? "all content including drafts and version documents in content releases"
            : "published content"
        }.${
          waitFor === "function"
            ? " Events will be delayed until after a Sanity Function has processed them."
            : ""
        }`
      );
    };
    // app/layout.tsx
    import { onWelcome } from "./client-functions";
    import { SanityLive } from "#sanity/live";
    
    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <>
          {children}
          <SanityLive onWelcome={onWelcome} />
        </>
      );
    }

    To disable default welcome message, pass onWelcome={false}.

12.4.5

Patch Changes

  • 9446c27 Thanks @stipsan! - Fix a regression introduced in v12.4.0 (#3430) that lead to live preview sometimes failing to refresh and show draft content automatically

next-sanity@13.0.0-cache-components.54

08 May 16:55
79c948e

Choose a tag to compare

Pre-release

Patch Changes

next-sanity@13.0.0-cache-components.53

08 May 16:37
b8972dd

Choose a tag to compare

Pre-release

Patch Changes

next-sanity@13.0.0-cache-components.52

08 May 15:36
dd5210f

Choose a tag to compare

Pre-release

Patch Changes

  • #3109 a4b1ede Thanks @stipsan! - Simplify Sanity Live cache tag handling to use a single cacheTagPrefix across published and draft flows.

    parseTags() now follows the single-prefix model and includes tagsWithoutPrefix as a convenience in the parsed result.

  • #3109 fbdeeef Thanks @stipsan! - No longer auto refresh in when editing documents

next-sanity@13.0.0-cache-components.51

08 May 14:35
9e035ce

Choose a tag to compare

Pre-release

Major Changes

  • #3493 0f1d162 Thanks @stipsan! - Remove refreshOnMount prop from <SanityLive>

    This prop was false by default, so if you weren't using it you won't be affected by this change.

    If you were using it, here's how you can add back the same functionality:

    Create a new RefreshOnMount component:

    // app/RefreshOnMount.tsx
    import { useRouter } from "next/navigation";
    import { useEffect, useReducer, startTransition } from "react";
    
    /**
     * Handles refreshing the page when the page is mounted,
     * in case the content changes at a high enough frequency that by
     * the time the page started streaming, and the <SanityLive> component sets
     * up the EventSource connection, content might have changed.
     */
    export function RefreshOnMount() {
      const router = useRouter();
      const [mounted, mount] = useReducer(() => true, false);
    
      useEffect(() => {
        if (!mounted) {
          startTransition(() => {
            mount();
            router.refresh();
          });
        }
      }, [mounted, router]);
    
      return null;
    }

    Then update your layout to include it:

    // app/layout.tsx
    import {SanityLive} from '#sanity/live'
    +import {DebugStatus} from './RefreshOnMount'
    
    export default function Layout({children}: {children: React.ReactNode}) {
      return (
        <>
          {children}
    -      <SanityLive refreshOnMount />
    +      <SanityLive />
    +      <RefreshOnMount />
        </>
      )
    }
  • #3495 4630270 Thanks @stipsan! - Remove refreshOnFocus prop from <SanityLive>

    This prop was enabled by default for published content in top-level windows, so if you relied on it you can add it back like this:

    Create a new RefreshOnFocus component:

    // app/RefreshOnFocus.tsx
    "use client";
    
    import { useRouter } from "next/navigation";
    import { startTransition, useEffect } from "react";
    
    const focusThrottleInterval = 5_000;
    
    export function RefreshOnFocus() {
      const router = useRouter();
    
      useEffect(() => {
        // If inside an iframe then don't refresh on focus
        if (window.self !== window.top) return;
    
        const controller = new AbortController();
        let nextFocusRevalidatedAt = 0;
        const callback = () => {
          const now = Date.now();
          if (
            now > nextFocusRevalidatedAt &&
            document.visibilityState !== "hidden"
          ) {
            startTransition(() => router.refresh());
            nextFocusRevalidatedAt = now + focusThrottleInterval;
          }
        };
    
        const { signal } = controller;
        document.addEventListener("visibilitychange", callback, {
          passive: true,
          signal,
        });
        window.addEventListener("focus", callback, { passive: true, signal });
    
        return () => controller.abort();
      }, [router]);
    
      return null;
    }

    Then update your layout to include it:

    // app/layout.tsx
    import {SanityLive} from '#sanity/live'
    +import {RefreshOnFocus} from './RefreshOnFocus'
    
    export default function Layout({children}: {children: React.ReactNode}) {
      return (
        <>
          {children}
          <SanityLive />
    +      <RefreshOnFocus />
        </>
      )
    }

    The motivation for removing this feature is that most users saw this as unexpected behavior, especially since when using browser debug tools window focus events trigger often and paired with how v16 behaves differently with link prefetching and the router.refresh() call it's bedt to remove it.

  • #3494 3ad0422 Thanks @stipsan! - Remove refreshOnReconnect prop from <SanityLive>

    It was removed as Next.js itself is working first class support for handling network connectivity changes, it can already be tested using experimental.useOffline, and so it no longer makes sense for us to implement it ourselves.

    This prop was true by default, and only used when not in draft mode, so if you relied on this behavior you can add it back like this:

    Create a new RefreshOnReconnect component:

    // app/RefreshOnReconnect.tsx
    "use client";
    
    import { useRouter } from "next/navigation";
    import { startTransition, useEffect } from "react";
    
    export function RefreshOnReconnect() {
      const router = useRouter();
    
      useEffect(() => {
        const controller = new AbortController();
        const { signal } = controller;
    
        window.addEventListener(
          "online",
          () => startTransition(() => router.refresh()),
          {
            passive: true,
            signal,
          }
        );
    
        return () => controller.abort();
      }, [router]);
    
      return null;
    }

    Then update your layout to include it:

    // app/layout.tsx
    import {SanityLive} from '#sanity/live'
    +import {RefreshOnReconnect} from './RefreshOnReconnect'
    
    export default async function Layout({children}: {children: React.ReactNode}) {
      const isDraftMode = (await draftMode()).isEnabled
    
      return (
        <>
          {children}
          <SanityLive includeDrafts={isDraftMode} />
    +      {!isDraftMode && <RefreshOnReconnect />}
        </>
      )
    }

12.4.5

Patch Changes

  • 9446c27 Thanks @stipsan! - Fix a regression introduced in v12.4.0 (#3430) that lead to live preview sometimes failing to refresh and show draft content automatically

next-sanity@12.4.5

07 May 16:44
14ebc87

Choose a tag to compare

Patch Changes

  • 9446c27 Thanks @stipsan! - Fix a regression introduced in v12.4.0 (#3430) that lead to live preview sometimes failing to refresh and show draft content automatically