Skip to content

Commit

Permalink
Update Sanity example with Live Content API
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Dec 20, 2024
1 parent 032ea1f commit 39e4a2a
Show file tree
Hide file tree
Showing 39 changed files with 804 additions and 408 deletions.
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 @@ -39,4 +39,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 @@ -295,7 +295,7 @@ npx vercel link
- [Blog Starter](/examples/blog-starter)
- [WordPress](/examples/cms-wordpress)
[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%2Fsanity-io%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
[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
29 changes: 29 additions & 0 deletions examples/cms-sanity/app/(blog)/cors.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
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 "./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;
}
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 "./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<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
39 changes: 39 additions & 0 deletions examples/cms-sanity/app/(blog)/more-stories-layout-shift.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 39e4a2a

Please sign in to comment.