Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Sanity example with Live Content API #74186

Draft
wants to merge 1 commit into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/cms-sanity/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion examples/cms-sanity/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ next-env.d.ts

# Env files created by scripts for working locally
.env
.env.local
.env.local
.env.local.*
!.env.local.example
2 changes: 1 addition & 1 deletion examples/cms-sanity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 0 additions & 52 deletions examples/cms-sanity/app/(blog)/alert-banner.tsx

This file was deleted.

3 changes: 1 addition & 2 deletions examples/cms-sanity/app/(blog)/avatar.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
47 changes: 47 additions & 0 deletions examples/cms-sanity/app/(blog)/client-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"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,
});
}
}

export function onPendingHeroPostLayoutShift(resume: () => void) {
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" }),
);
resume();
},
},
});
}
11 changes: 7 additions & 4 deletions examples/cms-sanity/app/(blog)/cover-image.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,8 +24,11 @@ export default function CoverImage(props: CoverImageProps) {
);

return (
<div className="shadow-md transition-shadow duration-200 group-hover:shadow-lg sm:mx-0">
<motion.div
layout
className="shadow-md transition-shadow duration-200 group-hover:shadow-lg sm:mx-0"
>
{image}
</div>
</motion.div>
);
}
55 changes: 55 additions & 0 deletions examples/cms-sanity/app/(blog)/draft-mode-toast.tsx
Original file line number Diff line number Diff line change
@@ -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 "./server-functions";

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;
}
46 changes: 46 additions & 0 deletions examples/cms-sanity/app/(blog)/hero-layout-shift.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
70 changes: 29 additions & 41 deletions examples/cms-sanity/app/(blog)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 "./client-functions";
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<Metadata> {
const settings = await sanityFetch({
const { data: settings } = await sanityFetch({
query: settingsQuery,
// Metadata should never contain stega
stega: false,
Expand Down Expand Up @@ -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 (
<html lang="en" className={`${inter.variable} bg-white text-black`}>
<body>
<section className="min-h-screen">
{isDraftMode && <AlertBanner />}
<main>{children}</main>
<main>
<AnimatePresence mode="wait">{children}</AnimatePresence>
</main>
<footer className="bg-accent-1 border-accent-2 border-t">
<div className="container mx-auto px-5">
{footer.length > 0 ? (
<PortableText
className="prose-sm text-pretty bottom-0 w-full max-w-none bg-white py-12 text-center md:py-20"
value={footer as PortableTextBlock[]}
/>
) : (
<div className="flex flex-col items-center py-28 lg:flex-row">
<h3 className="mb-10 text-center text-4xl font-bold leading-tight tracking-tighter lg:mb-0 lg:w-1/2 lg:pr-4 lg:text-left lg:text-5xl">
Built with Next.js.
</h3>
<div className="flex flex-col items-center justify-center lg:w-1/2 lg:flex-row lg:pl-4">
<a
href="https://nextjs.org/docs"
className="mx-3 mb-6 border border-black bg-black py-3 px-12 font-bold text-white transition-colors duration-200 hover:bg-white hover:text-black lg:mb-0 lg:px-8"
>
Read Documentation
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples/cms-sanity"
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
)}
<PortableText
className="prose-sm bottom-0 w-full max-w-none text-pretty bg-white py-6 text-center md:py-12"
value={(footer as PortableTextBlock[]) || demo.footer}
/>
</div>
</footer>
</section>
{isDraftMode && <VisualEditing />}
<Toaster />
{isDraftMode && (
<>
<DraftModeToast />
<VisualEditing />
</>
)}
<SanityLive onError={handleError} />
<SpeedInsights />
<Analytics />
</body>
</html>
);
Expand Down
Loading
Loading