├── tailwindcss.d.ts ├── public ├── favicon.ico ├── images │ ├── og-image.jpg │ ├── icons │ │ ├── favicon-48x48.png │ │ ├── apple-touch-icon.png │ │ ├── web-app-manifest-192x192.png │ │ └── web-app-manifest-512x512.png │ └── placeholder.svg ├── sanity │ └── preview │ │ ├── faqs.jpg │ │ ├── cta-1.jpg │ │ ├── hero-1.jpg │ │ ├── hero-2.jpg │ │ ├── all-posts.jpg │ │ ├── grid-card.jpg │ │ ├── grid-post.jpg │ │ ├── grid-row.jpg │ │ ├── split-row.jpg │ │ ├── carousel-1.jpg │ │ ├── carousel-2.jpg │ │ ├── logo-cloud-1.jpg │ │ ├── pricing-card.jpg │ │ ├── split-image.jpg │ │ ├── timeline-row.jpg │ │ ├── section-header.jpg │ │ ├── split-content.jpg │ │ ├── form-newsletter.jpg │ │ ├── split-cards-list.jpg │ │ └── split-info-list.jpg └── site.webmanifest ├── sample-data.tar.gz ├── types └── index.d.ts ├── postcss.config.mjs ├── sanity ├── lib │ ├── token.ts │ ├── client.ts │ ├── live.ts │ ├── image.ts │ ├── metadata.ts │ └── fetch.ts ├── queries │ ├── all-posts.ts │ ├── shared │ │ ├── image.ts │ │ ├── body.ts │ │ └── link.ts │ ├── navigation.ts │ ├── split │ │ ├── split-image.ts │ │ ├── split-cards-list.ts │ │ ├── split-info-list.ts │ │ ├── split-content.ts │ │ └── split-row.ts │ ├── forms │ │ └── newsletter.ts │ ├── logo-cloud │ │ └── logo-cloud-1.ts │ ├── faqs.ts │ ├── grid │ │ ├── pricing-card.ts │ │ ├── grid-card.ts │ │ ├── grid-post.ts │ │ └── grid-row.ts │ ├── carousel │ │ ├── carousel-1.ts │ │ └── carousel-2.ts │ ├── hero │ │ ├── hero-2.ts │ │ └── hero-1.ts │ ├── section-header.ts │ ├── timeline.ts │ ├── cta │ │ └── cta-1.ts │ ├── settings.ts │ ├── post.ts │ └── page.ts ├── schemas │ ├── blocks │ │ ├── shared │ │ │ ├── layout-variants.ts │ │ │ ├── section-padding.ts │ │ │ ├── button-variant.ts │ │ │ ├── color-variant.ts │ │ │ ├── link.ts │ │ │ └── block-content.ts │ │ ├── all-posts.ts │ │ ├── split │ │ │ ├── split-cards-list.ts │ │ │ ├── split-image.ts │ │ │ ├── split-card.ts │ │ │ ├── split-info-list.ts │ │ │ ├── split-info.ts │ │ │ ├── split-content.ts │ │ │ └── split-row.ts │ │ ├── timeline │ │ │ ├── timelines-1.ts │ │ │ └── timeline-row.ts │ │ ├── grid │ │ │ ├── grid-post.ts │ │ │ ├── grid-card.ts │ │ │ ├── pricing-card.ts │ │ │ └── grid-row.ts │ │ ├── hero │ │ │ ├── hero-2.ts │ │ │ └── hero-1.ts │ │ ├── faqs.ts │ │ ├── carousel │ │ │ ├── carousel-2.ts │ │ │ └── carousel-1.ts │ │ ├── logo-cloud │ │ │ └── logo-cloud-1.ts │ │ ├── forms │ │ │ └── newsletter.ts │ │ ├── section-header.ts │ │ └── cta │ │ │ └── cta-1.ts │ ├── documents │ │ ├── navigation.ts │ │ ├── category.ts │ │ ├── faq.ts │ │ ├── testimonial.ts │ │ ├── author.ts │ │ ├── settings.ts │ │ ├── post.ts │ │ └── page.ts │ └── previews │ │ └── youtube-preview.tsx ├── env.ts ├── presentation │ └── resolve.ts ├── structure.ts └── schema.ts ├── .github └── workflows │ └── validate.yml ├── app ├── api │ ├── draft-mode │ │ ├── disable │ │ │ └── route.ts │ │ └── enable │ │ │ └── route.ts │ └── newsletter │ │ └── route.ts ├── robots.ts ├── not-found.tsx ├── studio │ └── [[...tool]] │ │ └── page.tsx ├── (main) │ ├── page.tsx │ ├── layout.tsx │ ├── [slug] │ │ └── page.tsx │ └── blog │ │ └── [slug] │ │ └── page.tsx ├── sitemap.ts └── layout.tsx ├── components ├── theme-provider.tsx ├── post-date.tsx ├── ui │ ├── tag-line.tsx │ ├── label.tsx │ ├── missing-sanity-page.tsx │ ├── section-container.tsx │ ├── copy-button.tsx │ ├── sonner.tsx │ ├── star-rating.tsx │ ├── badge.tsx │ ├── avatar.tsx │ ├── input.tsx │ ├── breadcrumbs.tsx │ ├── card.tsx │ ├── post-card.tsx │ ├── button.tsx │ ├── accordion.tsx │ ├── breadcrumb.tsx │ └── form.tsx ├── disable-draft-mode.tsx ├── 404.tsx ├── blocks │ ├── split │ │ ├── split-info-list.tsx │ │ ├── split-image.tsx │ │ ├── split-cards-list.tsx │ │ ├── split-row.tsx │ │ ├── split-content.tsx │ │ ├── split-cards-item.tsx │ │ └── split-info-item.tsx │ ├── timeline │ │ ├── timeline-row.tsx │ │ └── timeline-1.tsx │ ├── faqs.tsx │ ├── all-posts.tsx │ ├── section-header.tsx │ ├── grid │ │ ├── grid-row.tsx │ │ ├── pricing-card.tsx │ │ ├── grid-card.tsx │ │ └── grid-post.tsx │ ├── hero │ │ ├── hero-2.tsx │ │ └── hero-1.tsx │ ├── index.tsx │ ├── cta │ │ └── cta-1.tsx │ ├── logo-cloud │ │ └── logo-cloud-1.tsx │ ├── carousel │ │ ├── carousel-2.tsx │ │ └── carousel-1.tsx │ ├── forms │ │ └── newsletter.tsx │ └── post-hero.tsx ├── header │ ├── index.tsx │ ├── desktop-nav.tsx │ └── mobile-nav.tsx ├── menu-toggle.tsx ├── logo.tsx └── footer.tsx ├── .env.local.example ├── sanity.cli.ts ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── lib └── utils.ts ├── package.json └── sanity.config.ts /tailwindcss.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tailwindcss/lib/util/flattenColorPalette"; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /sample-data.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/sample-data.tar.gz -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type BreadcrumbLink = { 2 | label: string; 3 | href: string; 4 | }; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/images/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/images/og-image.jpg -------------------------------------------------------------------------------- /public/sanity/preview/faqs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/faqs.jpg -------------------------------------------------------------------------------- /public/sanity/preview/cta-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/cta-1.jpg -------------------------------------------------------------------------------- /public/sanity/preview/hero-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/hero-1.jpg -------------------------------------------------------------------------------- /public/sanity/preview/hero-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/hero-2.jpg -------------------------------------------------------------------------------- /public/sanity/preview/all-posts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/all-posts.jpg -------------------------------------------------------------------------------- /public/sanity/preview/grid-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/grid-card.jpg -------------------------------------------------------------------------------- /public/sanity/preview/grid-post.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/grid-post.jpg -------------------------------------------------------------------------------- /public/sanity/preview/grid-row.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/grid-row.jpg -------------------------------------------------------------------------------- /public/sanity/preview/split-row.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/split-row.jpg -------------------------------------------------------------------------------- /public/images/icons/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/images/icons/favicon-48x48.png -------------------------------------------------------------------------------- /public/sanity/preview/carousel-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/carousel-1.jpg -------------------------------------------------------------------------------- /public/sanity/preview/carousel-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/carousel-2.jpg -------------------------------------------------------------------------------- /public/sanity/preview/logo-cloud-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/logo-cloud-1.jpg -------------------------------------------------------------------------------- /public/sanity/preview/pricing-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/pricing-card.jpg -------------------------------------------------------------------------------- /public/sanity/preview/split-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/split-image.jpg -------------------------------------------------------------------------------- /public/sanity/preview/timeline-row.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/timeline-row.jpg -------------------------------------------------------------------------------- /public/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/sanity/preview/section-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/section-header.jpg -------------------------------------------------------------------------------- /public/sanity/preview/split-content.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/split-content.jpg -------------------------------------------------------------------------------- /public/sanity/preview/form-newsletter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/form-newsletter.jpg -------------------------------------------------------------------------------- /public/sanity/preview/split-cards-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/split-cards-list.jpg -------------------------------------------------------------------------------- /public/sanity/preview/split-info-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/sanity/preview/split-info-list.jpg -------------------------------------------------------------------------------- /public/images/icons/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/images/icons/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/images/icons/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serge-0v/next-js-sanity-starter/HEAD/public/images/icons/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /sanity/lib/token.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | export const token = process.env.SANITY_API_READ_TOKEN; 4 | 5 | if (!token) { 6 | throw new Error("Missing SANITY_API_READ_TOKEN"); 7 | } 8 | -------------------------------------------------------------------------------- /sanity/queries/all-posts.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | 3 | // @sanity-typegen-ignore 4 | export const allPostsQuery = groq` 5 | _type == "all-posts" => { 6 | _type, 7 | _key, 8 | padding, 9 | colorVariant, 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /sanity/queries/shared/image.ts: -------------------------------------------------------------------------------- 1 | export const imageQuery = ` 2 | ..., 3 | asset->{ 4 | _id, 5 | url, 6 | mimeType, 7 | metadata { 8 | lqip, 9 | dimensions { 10 | width, 11 | height 12 | } 13 | } 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate Template 2 | on: push 3 | 4 | jobs: 5 | validate: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Validate Sanity Template 10 | uses: sanity-io/template-validator@v2 11 | -------------------------------------------------------------------------------- /sanity/queries/navigation.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { linkQuery } from "./shared/link"; 3 | 4 | export const NAVIGATION_QUERY = groq` 5 | *[_type == "navigation"]{ 6 | _type, 7 | _key, 8 | links[]{ 9 | ${linkQuery} 10 | } 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /app/api/draft-mode/disable/route.ts: -------------------------------------------------------------------------------- 1 | import { draftMode } from "next/headers"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(request: NextRequest) { 5 | (await draftMode()).disable(); 6 | return NextResponse.redirect(new URL("/", request.url)); 7 | } 8 | -------------------------------------------------------------------------------- /app/api/draft-mode/enable/route.ts: -------------------------------------------------------------------------------- 1 | import { defineEnableDraftMode } from "next-sanity/draft-mode"; 2 | import { client } from "@/sanity/lib/client"; 3 | import { token } from "@/sanity/lib/token"; 4 | 5 | export const { GET } = defineEnableDraftMode({ 6 | client: client.withConfig({ token }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: [ 6 | { 7 | userAgent: "*", 8 | allow: "/", 9 | }, 10 | ], 11 | sitemap: [`${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml`], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /sanity/queries/shared/body.ts: -------------------------------------------------------------------------------- 1 | import { linkQuery } from "./link"; 2 | import { imageQuery } from "./image"; 3 | 4 | export const bodyQuery = ` 5 | ..., 6 | markDefs[]{ 7 | ..., 8 | _type == "link" => { 9 | ${linkQuery} 10 | } 11 | }, 12 | _type == "image" => { 13 | ${imageQuery} 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /sanity/queries/split/split-image.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { imageQuery } from "../shared/image"; 3 | 4 | // @sanity-typegen-ignore 5 | export const splitImageQuery = groq` 6 | _type == "split-image" => { 7 | _type, 8 | _key, 9 | image{ 10 | ${imageQuery} 11 | }, 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /sanity/queries/forms/newsletter.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | 3 | // @sanity-typegen-ignore 4 | export const formNewsletterQuery = groq` 5 | _type == "form-newsletter" => { 6 | _type, 7 | _key, 8 | padding, 9 | colorVariant, 10 | stackAlign, 11 | consentText, 12 | buttonText, 13 | successMessage, 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SITE_URL=http://localhost:3000 2 | NEXT_PUBLIC_SITE_ENV=development 3 | RESEND_API_KEY= 4 | RESEND_AUDIENCE_ID= 5 | 6 | # SANITY CMS 7 | # use current date (YYYY-MM-DD) to target the latest API version 8 | NEXT_PUBLIC_SANITY_API_VERSION=2024-10-18 9 | NEXT_PUBLIC_SANITY_PROJECT_ID= 10 | NEXT_PUBLIC_SANITY_DATASET=development 11 | SANITY_API_READ_TOKEN= -------------------------------------------------------------------------------- /sanity/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "next-sanity"; 2 | 3 | import { apiVersion, dataset, projectId, useCdn } from "../env"; 4 | 5 | export const client = createClient({ 6 | projectId, 7 | dataset, 8 | apiVersion, 9 | useCdn, 10 | perspective: "published", 11 | stega: { 12 | studioUrl: process.env.NEXT_PUBLIC_SITE_URL + "/studio", 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /sanity/queries/logo-cloud/logo-cloud-1.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { imageQuery } from "../shared/image"; 3 | 4 | // @sanity-typegen-ignore 5 | export const logoCloud1Query = groq` 6 | _type == "logo-cloud-1" => { 7 | _type, 8 | _key, 9 | padding, 10 | colorVariant, 11 | title, 12 | images[]{ 13 | ${imageQuery} 14 | }, 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /sanity/queries/shared/link.ts: -------------------------------------------------------------------------------- 1 | export const linkQuery = ` 2 | _key, 3 | ..., 4 | "href": select( 5 | isExternal => href, 6 | defined(href) && !defined(internalLink) => href, 7 | @.internalLink->slug.current == "index" => "/", 8 | @.internalLink->_type == "post" => "/blog/" + @.internalLink->slug.current, 9 | "/" + @.internalLink->slug.current 10 | ) 11 | `; 12 | -------------------------------------------------------------------------------- /sanity/queries/faqs.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { bodyQuery } from "./shared/body"; 3 | 4 | // @sanity-typegen-ignore 5 | export const faqsQuery = groq` 6 | _type == "faqs" => { 7 | _type, 8 | _key, 9 | padding, 10 | colorVariant, 11 | faqs[]->{ 12 | _id, 13 | title, 14 | body[]{ 15 | ${bodyQuery} 16 | }, 17 | }, 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /sanity/queries/grid/pricing-card.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { linkQuery } from "../shared/link"; 3 | 4 | // @sanity-typegen-ignore 5 | export const pricingCardQuery = groq` 6 | _type == "pricing-card" => { 7 | _type, 8 | _key, 9 | title, 10 | tagLine, 11 | price, 12 | list[], 13 | excerpt, 14 | link{ 15 | ${linkQuery} 16 | }, 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /sanity/queries/split/split-cards-list.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { bodyQuery } from "../shared/body"; 3 | 4 | // @sanity-typegen-ignore 5 | export const splitCardsListQuery = groq` 6 | _type == "split-cards-list" => { 7 | _type, 8 | _key, 9 | list[]{ 10 | tagLine, 11 | title, 12 | body[]{ 13 | ${bodyQuery} 14 | }, 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file lets you run `$ sanity [command]` in this folder 3 | * Go to https://www.sanity.io/docs/cli to learn more. 4 | **/ 5 | import { defineCliConfig } from "sanity/cli"; 6 | 7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; 8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET; 9 | 10 | export default defineCliConfig({ api: { projectId, dataset } }); 11 | -------------------------------------------------------------------------------- /sanity/queries/carousel/carousel-1.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { imageQuery } from "../shared/image"; 3 | 4 | // @sanity-typegen-ignore 5 | export const carousel1Query = groq` 6 | _type == "carousel-1" => { 7 | _type, 8 | _key, 9 | padding, 10 | colorVariant, 11 | size, 12 | orientation, 13 | indicators, 14 | images[]{ 15 | ${imageQuery} 16 | }, 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /sanity/queries/hero/hero-2.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { linkQuery } from "../shared/link"; 3 | import { bodyQuery } from "../shared/body"; 4 | 5 | // @sanity-typegen-ignore 6 | export const hero2Query = groq` 7 | _type == "hero-2" => { 8 | _type, 9 | _key, 10 | tagLine, 11 | title, 12 | body[]{ 13 | ${bodyQuery} 14 | }, 15 | links[]{ 16 | ${linkQuery} 17 | }, 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /sanity/queries/grid/grid-card.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | import { imageQuery } from "../shared/image"; 3 | import { linkQuery } from "../shared/link"; 4 | 5 | // @sanity-typegen-ignore 6 | export const gridCardQuery = groq` 7 | _type == "grid-card" => { 8 | _type, 9 | _key, 10 | title, 11 | excerpt, 12 | image{ 13 | ${imageQuery} 14 | }, 15 | link{ 16 | ${linkQuery} 17 | }, 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/header"; 2 | import Footer from "@/components/footer"; 3 | import Custom404 from "@/components/404"; 4 | 5 | import type { Metadata } from "next"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Page not found", 9 | }; 10 | 11 | export default function NotFoundPage() { 12 | return ( 13 | <> 14 |
15 | 16 |