diff --git a/app/(site)/[locale]/[...slug]/page.tsx b/app/(site)/[locale]/[...slug]/page.tsx index b32c0d92f..c6e62f811 100644 --- a/app/(site)/[locale]/[...slug]/page.tsx +++ b/app/(site)/[locale]/[...slug]/page.tsx @@ -1,18 +1,19 @@ import CategoryPage from '@/components/pages/category-page'; +import CategoryPagePreview from '@/components/pages/category-page-preview'; import ProductPage from '@/components/pages/product-page'; +import ProductPagePreview from '@/components/pages/product-page-preview'; import SearchPage from '@/components/pages/search-page'; import SearchPagePreview from '@/components/pages/search-page-preview'; import SinglePage from '@/components/pages/single-page'; import SinglePagePreview from '@/components/pages/single-page-preview'; -import PreviewProvider from '@/components/preview-provider'; import getQueryFromSlug from '@/helpers/get-query-from-slug'; -import { getCachedClient } from 'lib/sanity/sanity.client'; +import { categoryQuery, pageQuery, productQuery, searchPageQuery } from '@/lib/sanity/queries'; +import { getCategory, getPage, getProduct, getSearch } from '@/lib/sanity/sanity.fetch'; import type { Metadata } from 'next'; +import { LiveQuery } from 'next-sanity/preview/live-query'; import { draftMode } from 'next/headers'; import { notFound } from 'next/navigation'; -export const dynamic = 'force-dynamic'; - export async function generateMetadata({ params }: { @@ -20,18 +21,28 @@ export async function generateMetadata({ }): Promise { const { slug, locale } = params; - const { query = '', queryParams } = getQueryFromSlug(slug, locale); + const { queryParams, docType } = getQueryFromSlug(slug, locale); - const page = await getCachedClient()(query, queryParams); + let page; + + if (docType === 'page') { + page = await getPage(queryParams.slug, queryParams.locale); + } else if (docType === 'product') { + page = await getProduct(queryParams.slug, queryParams.locale); + } else if (docType === 'category') { + page = await getCategory(queryParams.slug, queryParams.locale); + } else if (docType === 'search') { + page = await getSearch(queryParams.slug, queryParams.locale); + } if (!page) return notFound(); return { title: `${page.seo?.title || page.title}`, - description: page.seo?.description || page.bodySummary, + description: page.seo?.description, openGraph: { - publishedTime: page.createdAt, - modifiedTime: page.updatedAt, + // publishedTime: page.createdAt, + // modifiedTime: page.updatedAt, type: 'article' } }; @@ -39,49 +50,70 @@ export async function generateMetadata({ interface PageParams { params: { - locale: string; slug: string[]; + locale: string; }; } export default async function Page({ params }: PageParams) { - const preview = draftMode().isEnabled ? { token: process.env.SANITY_API_READ_TOKEN } : undefined; - const { slug, locale } = params; - const { query = '', queryParams, docType } = getQueryFromSlug(slug, locale); + const { queryParams, docType } = getQueryFromSlug(slug, locale); - let pageData; + let data; if (docType === 'page') { - pageData = await getCachedClient()(query, queryParams); + data = await getPage(queryParams.slug, queryParams.locale); } else if (docType === 'product') { - pageData = await getCachedClient()(query, queryParams); + data = await getProduct(queryParams.slug, queryParams.locale); } else if (docType === 'category') { - pageData = await getCachedClient()(query, queryParams); + data = await getCategory(queryParams.slug, queryParams.locale); } else if (docType === 'search') { - pageData = await getCachedClient()(query, queryParams); - } else { - return; + data = await getSearch(queryParams.slug, queryParams.locale); } - if (!pageData) return notFound(); + let PagePreview; - if (preview && preview.token) { - return ( - - {docType === 'page' && } - {docType === 'search' && } - - ); + if (docType === 'page') { + PagePreview = SinglePagePreview; + } else if (docType === 'product') { + PagePreview = ProductPagePreview; + } else if (docType === 'category') { + PagePreview = CategoryPagePreview; + } else if (docType === 'search') { + PagePreview = SearchPagePreview; + } + + let query = ''; + + if (docType === 'page') { + query = pageQuery; + } else if (docType === 'product') { + query = productQuery; + } else if (docType === 'category') { + query = categoryQuery; + } else if (docType === 'search') { + query = searchPageQuery; + } + + if (!query && !PagePreview && !data && !draftMode().isEnabled) { + notFound(); } return ( - <> - {docType === 'page' && } - {docType === 'product' && } - {docType === 'category' && } - {docType === 'search' && } - + + <> + {docType === 'page' && } + {docType === 'product' && } + {docType === 'category' && } + {docType === 'search' && } + + ); } diff --git a/app/(site)/[locale]/layout.tsx b/app/(site)/[locale]/layout.tsx index 0347a72d8..4a6eaa2db 100644 --- a/app/(site)/[locale]/layout.tsx +++ b/app/(site)/[locale]/layout.tsx @@ -1,8 +1,11 @@ +import PreviewProvider from '@/components/preview/preview-provider'; +import { token } from '@/lib/sanity/sanity.fetch'; import { Analytics } from '@vercel/analytics/react'; import Footer from 'components/layout/footer/footer'; import Header from 'components/layout/header/header'; import { NextIntlClientProvider } from 'next-intl'; import { Inter } from 'next/font/google'; +import { draftMode } from 'next/headers'; import { notFound } from 'next/navigation'; import { ReactNode, Suspense } from 'react'; import { supportedLanguages } from '../../../i18n-config'; @@ -54,7 +57,9 @@ export default async function LocaleLayout({ children, params: { locale } }: Loc notFound(); } - return ( + const isDraftMode = draftMode().isEnabled; + + const layout = ( @@ -70,4 +75,10 @@ export default async function LocaleLayout({ children, params: { locale } }: Loc ); + + if (isDraftMode) { + return {layout}; + } + + return layout; } diff --git a/app/(site)/[locale]/page.tsx b/app/(site)/[locale]/page.tsx index 7eda94c08..542665fb5 100644 --- a/app/(site)/[locale]/page.tsx +++ b/app/(site)/[locale]/page.tsx @@ -1,27 +1,26 @@ import HomePage from '@/components/pages/home-page'; import HomePagePreview from '@/components/pages/home-page-preview'; -import PreviewProvider from '@/components/preview-provider'; -import { homePageQuery } from 'lib/sanity/queries'; -import { getCachedClient } from 'lib/sanity/sanity.client'; +import { homePageQuery } from '@/lib/sanity/queries'; +import { getHomePage } from '@/lib/sanity/sanity.fetch'; import { Metadata } from 'next'; +import { LiveQuery } from 'next-sanity/preview/live-query'; import { draftMode } from 'next/headers'; import { notFound } from 'next/navigation'; export const runtime = 'edge'; -export const dynamic = 'force-dynamic'; export async function generateMetadata({ params }: { - params: { slug: string; locale: string }; + params: { locale: string }; }): Promise { - const homePage = await getCachedClient()(homePageQuery, params); + const homePage = await getHomePage(params.locale); if (!homePage) return notFound(); return { - title: homePage.seo.title || homePage.title, - description: homePage.seo.description || homePage.description + title: homePage?.seo?.title || homePage.title, + description: homePage?.seo?.description }; } interface HomePageParams { @@ -31,21 +30,21 @@ interface HomePageParams { } export default async function IndexPage({ params }: HomePageParams) { - const preview = draftMode().isEnabled ? { token: process.env.SANITY_API_READ_TOKEN } : undefined; + const data = await getHomePage(params.locale); - const data = await getCachedClient(preview)(homePageQuery, params); - - if (!data) return notFound(); - - if (preview && preview.token) { - return ( - - - - ); + if (!data && !draftMode().isEnabled) { + notFound(); } return ( - + + + ); } diff --git a/app/api/preview/route.ts b/app/api/preview/route.ts index 5f1dce999..8a2cb8507 100644 --- a/app/api/preview/route.ts +++ b/app/api/preview/route.ts @@ -1,4 +1,8 @@ +import { previewSecretId } from '@/lib/sanity/sanity.api' +import { client } from '@/lib/sanity/sanity.client' +import { token } from '@/lib/sanity/sanity.fetch' import { draftMode } from 'next/headers' +import { isValidSecret } from 'sanity-plugin-iframe-pane/is-valid-secret' export async function GET(request: Request) { const { searchParams } = new URL(request.url) @@ -7,10 +11,26 @@ export async function GET(request: Request) { const type = searchParams.get('type') const locale = searchParams.get('locale') - // Check the secret and next parameters - // This secret should only be known to this route handler and the CMS - if (secret !== process.env.SANITY_API_READ_TOKEN) { - return new Response('Invalid token', { status: 401 }) + if (!token) { + throw new Error( + 'The `SANITY_API_READ_TOKEN` environment variable is required.', + ) + } + + if (!secret) { + return new Response('Invalid secret', { status: 401 }) + } + + const authenticatedClient = client.withConfig({ token }) + + const validSecret = await isValidSecret( + authenticatedClient, + previewSecretId, + secret, + ) + + if (!validSecret) { + return new Response('Invalid secret', { status: 401 }) } draftMode().enable() diff --git a/app/api/revalidate/sanity/route.ts b/app/api/revalidate/sanity/route.ts new file mode 100644 index 000000000..91743a2d6 --- /dev/null +++ b/app/api/revalidate/sanity/route.ts @@ -0,0 +1,64 @@ +/** + * This code is responsible for revalidating queries as the dataset is updated. + * + * It is set up to receive a validated GROQ-powered Webhook from Sanity.io: + * https://www.sanity.io/docs/webhooks + * + * 1. Go to the API section of your Sanity project on sanity.io/manage or run `npx sanity hook create` + * 2. Click "Create webhook" + * 3. Set the URL to https://YOUR_NEXTJS_SITE_URL/api/revalidate + * 4. Dataset: Choose desired dataset or leave at default "all datasets" + * 5. Trigger on: "Create", "Update", and "Delete" + * 6. Filter: Leave empty + * 7. Projection: {_type, "slug": slug.current} + * 8. Status: Enable webhook + * 9. HTTP method: POST + * 10. HTTP Headers: Leave empty + * 11. API version: v2021-03-25 + * 12. Include drafts: No + * 13. Secret: Set to the same value as SANITY_REVALIDATE_SECRET (create a random secret if you haven't yet, for example by running `Math.random().toString(36).slice(2)` in your console) + * 14. Save the cofiguration + * 15. Add the secret to Vercel: `npx vercel env add SANITY_REVALIDATE_SECRET` + * 16. Redeploy with `npx vercel --prod` to apply the new environment variable + */ + +import { revalidateSecret } from '@/lib/sanity/sanity.api' +import { parseBody } from 'next-sanity/webhook' +import { revalidateTag } from 'next/cache' +import { NextResponse, type NextRequest } from 'next/server' + +export async function POST(req: NextRequest) { + try { + const { body, isValidSignature } = await parseBody<{ + _type: string + slug?: string | undefined + language: string | undefined + }>(req, revalidateSecret) + if (!isValidSignature) { + const message = 'Invalid signature' + return new Response(message, { status: 401 }) + } + + if (!body?._type) { + return new Response('Bad Request', { status: 400 }) + } + + revalidateTag(body._type) + + if (body.slug) { + revalidateTag(`${body._type}:${body.slug}`) + } else { + revalidateTag(`${body._type}`) + } + + return NextResponse.json({ + status: 200, + revalidated: true, + now: Date.now(), + body, + }) + } catch (err: any) { + console.error(err) + return new Response(err.message, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 5434063cf..dcae2abe7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -36,39 +36,12 @@ :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 3.9%; - --radius: 0.5rem; } - * { - @apply border-border; - } - ::-moz-selection { /* Code for Firefox */ color: #ffffff; @@ -82,7 +55,7 @@ html, body { - @apply h-full bg-white font-sans text-high-contrast; + @apply h-full bg-background text-foreground; box-sizing: border-box; touch-action: manipulation; @@ -91,9 +64,6 @@ -moz-osx-font-smoothing: grayscale; overscroll-behavior-x: none; } - body { - @apply bg-background text-foreground; - } } @layer components { diff --git a/components/layout/footer/footer.tsx b/components/layout/footer/footer.tsx index 27155e3f1..7fede0a5a 100644 --- a/components/layout/footer/footer.tsx +++ b/components/layout/footer/footer.tsx @@ -1,6 +1,5 @@ -import Text from '@/components/ui/text'; -import { footerMenusQuery } from '@/lib/sanity/queries'; -import { getCachedClient } from '@/lib/sanity/sanity.client'; +import Text from '@/components/ui/text/text'; +import { getFooterMenus } from '@/lib/sanity/sanity.fetch'; import LocaleSwitcher from 'components/ui/locale-switcher/locale-switcher'; import Logo from 'components/ui/logo/logo'; import Link from 'next/link'; @@ -15,7 +14,7 @@ export default async function Footer({ locale }: FooterProps) { locale: locale }; - const footerMenus = await getCachedClient()(footerMenusQuery, params); + const footerMenus = await getFooterMenus(params.locale); return (