diff --git a/app/(personal)/[slug]/page.tsx b/app/(personal)/[slug]/page.tsx index f99ac977..69127db2 100644 --- a/app/(personal)/[slug]/page.tsx +++ b/app/(personal)/[slug]/page.tsx @@ -1,24 +1,45 @@ import {CustomPortableText} from '@/components/CustomPortableText' import {Header} from '@/components/Header' -import {sanityFetch} from '@/sanity/lib/live' -import {pagesBySlugQuery, slugsByTypeQuery} from '@/sanity/lib/queries' +import { + isPreviewMode, + sanityFetch, + sanityFetchMetadata, + sanityFetchStaticParams, +} from '@/sanity/lib/live' +import {slugsByTypeQuery} from '@/sanity/lib/queries' import type {Metadata, ResolvingMetadata} from 'next' -import {toPlainText, type PortableTextBlock} from 'next-sanity' -import {draftMode} from 'next/headers' +import {defineQuery, toPlainText, type PortableTextBlock} from 'next-sanity' import {notFound} from 'next/navigation' +import {Suspense} from 'react' type Props = { params: Promise<{slug: string}> } +export async function generateStaticParams() { + return sanityFetchStaticParams({ + query: slugsByTypeQuery, + params: {type: 'page'}, + }) +} + +const pagesBySlugQuery = defineQuery(` + *[_type == "page" && slug.current == $slug][0] { + _id, + _type, + body, + overview, + title, + "slug": slug.current, + } +`) export async function generateMetadata( {params}: Props, parent: ResolvingMetadata, ): Promise { - const {data: page} = await sanityFetch({ + const {data: page} = await sanityFetchMetadata({ query: pagesBySlugQuery, - params, - stega: false, + params: await params, }) return { @@ -27,50 +48,61 @@ export async function generateMetadata( } } -export async function generateStaticParams() { - const {data} = await sanityFetch({ - query: slugsByTypeQuery, - params: {type: 'page'}, - stega: false, - perspective: 'published', - }) - return data +export default function PageSlugRoute({params}: Props) { + return ( +
+
+ + } + > + + +
+
+
+ ) } -export default async function PageSlugRoute({params}: Props) { - const {data} = await sanityFetch({query: pagesBySlugQuery, params}) +async function PageSlugRouteContent({params}: Props) { + const {data} = await sanityFetch({query: pagesBySlugQuery, params: await params}) - // Only show the 404 page if we're in production, when in draft mode we might be about to create a page on this slug, and live reload won't work on the 404 route - if (!data?._id && !(await draftMode()).isEnabled) { + // Only show the 404 page if we're in production, when in preview mode we might be about to create a page on this slug, and live reload won't work on the 404 route + if (!data?._id && !(await isPreviewMode())) { notFound() } const {body, overview, title} = data ?? {} return ( -
-
- {/* Header */} -
+ {/* Header */} +
+ + {/* Body */} + {body && ( + - - {/* Body */} - {body && ( - - )} -
-
-
+ )} + ) } diff --git a/app/(personal)/layout.tsx b/app/(personal)/layout.tsx index 9502de59..6e111216 100644 --- a/app/(personal)/layout.tsx +++ b/app/(personal)/layout.tsx @@ -1,12 +1,10 @@ import '@/styles/index.css' -import {CustomPortableText} from '@/components/CustomPortableText' -import {Navbar} from '@/components/Navbar' +import {FallbackNavbar, Navbar} from '@/components/Navbar' import IntroTemplate from '@/intro-template' -import {sanityFetch, SanityLive} from '@/sanity/lib/live' -import {homePageQuery, settingsQuery} from '@/sanity/lib/queries' +import {isPreviewMode, sanityFetchMetadata, SanityLive} from '@/sanity/lib/live' import {urlForOpenGraphImage} from '@/sanity/lib/utils' import type {Metadata, Viewport} from 'next' -import {toPlainText, type PortableTextBlock} from 'next-sanity' +import {defineQuery} from 'next-sanity' import {VisualEditing} from 'next-sanity/visual-editing' import {draftMode} from 'next/headers' import {Suspense} from 'react' @@ -14,25 +12,37 @@ import {Toaster} from 'sonner' import {handleError} from './client-functions' import {DraftModeToast} from './DraftModeToast' import {SpeedInsights} from '@vercel/speed-insights/next' +import {DEFAULT_TITLE} from '../constants' +import {Footer} from '@/components/Footer' export async function generateMetadata(): Promise { - const [{data: settings}, {data: homePage}] = await Promise.all([ - sanityFetch({query: settingsQuery, stega: false}), - sanityFetch({query: homePageQuery, stega: false}), - ]) + // Only select exactly the fields we need + const layoutMetadataQuery = defineQuery(`{ + "settings": *[_type == "settings"][0]{ogImage}, + "home": *[_type == "home"][0]{ + title, + // Render portabletext to string + "description": pt::text(overview) + } + }`) + const {data, tags} = await sanityFetchMetadata({ + query: layoutMetadataQuery, + params: {defaultTitle: DEFAULT_TITLE}, + }) const ogImage = urlForOpenGraphImage( // @ts-expect-error - @TODO update @sanity/image-url types so it's compatible - settings?.ogImage, + data?.ogImage, ) return { - title: homePage?.title + title: data?.home?.title ? { - template: `%s | ${homePage.title}`, - default: homePage.title || 'Personal website', + template: `%s | ${data.home.title}`, + default: data.home.title, } - : undefined, - description: homePage?.overview ? toPlainText(homePage.overview) : undefined, + : DEFAULT_TITLE, + // @TODO truncate to max length of 155 characters + description: data?.home?.description?.trim().split('\n').join(' ').split(' ').join(' '), openGraph: { images: ogImage ? [ogImage] : [], }, @@ -43,36 +53,23 @@ export const viewport: Viewport = { themeColor: '#000', } -export default async function IndexRoute({children}: {children: React.ReactNode}) { - const {data} = await sanityFetch({query: settingsQuery}) +export default async function PersonalWebsiteLayout({children}: {children: React.ReactNode}) { return ( <>
- + }> + +
{children}
-
- {data?.footer && ( - - )} -
+
+ {(await isPreviewMode()) && } - {(await draftMode()).isEnabled && ( - <> - - - - )} + {(await draftMode()).isEnabled && } ) diff --git a/app/(personal)/page.tsx b/app/(personal)/page.tsx index 8e15047f..2a2dddd2 100644 --- a/app/(personal)/page.tsx +++ b/app/(personal)/page.tsx @@ -1,10 +1,45 @@ -import {HomePage} from '@/components/HomePage' +import {Header} from '@/components/Header' +import {OptimisticSortOrder} from '@/components/OptimisticSortOrder' +import {ProjectListItem} from '@/components/ProjectListItem' import {studioUrl} from '@/sanity/lib/api' import {sanityFetch} from '@/sanity/lib/live' -import {homePageQuery} from '@/sanity/lib/queries' +import {resolveHref} from '@/sanity/lib/utils' +import {createDataAttribute, defineQuery} from 'next-sanity' import Link from 'next/link' +import {Suspense} from 'react' -export default async function IndexRoute() { +const homePageQuery = defineQuery(` + *[_type == "home"][0]{ + _id, + _type, + overview, + showcaseProjects[]{ + _key, + ...@->{ + _id, + _type, + coverImage, + overview, + "slug": slug.current, + tags, + title, + } + }, + title, + } +`) + +export default function HomePage() { + return ( +
+ }> + + +
+ ) +} + +async function HomePageContent() { const {data} = await sanityFetch({query: homePageQuery}) if (!data) { @@ -19,5 +54,54 @@ export default async function IndexRoute() { ) } - return + // Default to an empty object to allow previews on non-existent documents + const {overview = [], showcaseProjects = [], title = ''} = data ?? {} + + const dataAttribute = + data?._id && data?._type + ? createDataAttribute({ + baseUrl: studioUrl, + id: data._id, + type: data._type, + }) + : null + + return ( + <> + {/* Header */} + {title && ( +
+ )} + {/* Showcase projects */} +
+ + {showcaseProjects && + showcaseProjects.length > 0 && + showcaseProjects.map((project) => { + const href = resolveHref(project?._type, project?.slug) + if (!href) { + return null + } + return ( + + + + ) + })} + +
+ + ) } diff --git a/app/(personal)/projects/[slug]/page.tsx b/app/(personal)/projects/[slug]/page.tsx index ae1bc2a8..057db4c1 100644 --- a/app/(personal)/projects/[slug]/page.tsx +++ b/app/(personal)/projects/[slug]/page.tsx @@ -2,7 +2,7 @@ import {CustomPortableText} from '@/components/CustomPortableText' import {Header} from '@/components/Header' import ImageBox from '@/components/ImageBox' import {studioUrl} from '@/sanity/lib/api' -import {sanityFetch} from '@/sanity/lib/live' +import {sanityFetch, sanityFetchMetadata, sanityFetchStaticParams} from '@/sanity/lib/live' import {projectBySlugQuery, slugsByTypeQuery} from '@/sanity/lib/queries' import {urlForOpenGraphImage} from '@/sanity/lib/utils' import type {Metadata, ResolvingMetadata} from 'next' @@ -10,6 +10,7 @@ import {createDataAttribute, toPlainText} from 'next-sanity' import {draftMode} from 'next/headers' import Link from 'next/link' import {notFound} from 'next/navigation' +import {Suspense} from 'react' type Props = { params: Promise<{slug: string}> @@ -19,10 +20,9 @@ export async function generateMetadata( {params}: Props, parent: ResolvingMetadata, ): Promise { - const {data: project} = await sanityFetch({ + const {data: project} = await sanityFetchMetadata({ query: projectBySlugQuery, - params, - stega: false, + params: await params, }) const ogImage = urlForOpenGraphImage( // @ts-expect-error - @TODO update @sanity/image-url types so it's compatible @@ -41,17 +41,38 @@ export async function generateMetadata( } export async function generateStaticParams() { - const {data} = await sanityFetch({ + return await sanityFetchStaticParams({ query: slugsByTypeQuery, params: {type: 'project'}, - stega: false, - perspective: 'published', }) - return data } -export default async function ProjectSlugRoute({params}: Props) { - const {data} = await sanityFetch({query: projectBySlugQuery, params}) +export default function ProjectSlugRoute({params}: Props) { + return ( +
+
+ + } + > + + +
+
+
+ ) +} + +async function ProjectSlugRouteContent({params}: Props) { + const {data} = await sanityFetch({query: projectBySlugQuery, params: await params}) // Only show the 404 page if we're in production, when in draft mode we might be about to create a project on this slug, and live reload won't work on the 404 route if (!data?._id && !(await draftMode()).isEnabled) { @@ -74,86 +95,83 @@ export default async function ProjectSlugRoute({params}: Props) { const endYear = duration?.end ? new Date(duration?.end).getFullYear() : 'Now' return ( -
-
- {/* Header */} -
+ {/* Header */} +
+ +
+ {/* Image */} + -
- {/* Image */} - - -
- {/* Duration */} - {!!(startYear && endYear) && ( -
-
Duration
-
- {startYear} - {' - '} - {endYear} -
+
+ {/* Duration */} + {!!(startYear && endYear) && ( +
+
Duration
+
+ {startYear} + {' - '} + {endYear}
- )} +
+ )} - {/* Client */} - {client && ( -
-
Client
-
{client}
-
- )} - - {/* Site */} - {site && ( -
-
Site
- {site && ( - - {site} - - )} -
- )} + {/* Client */} + {client && ( +
+
Client
+
{client}
+
+ )} - {/* Tags */} + {/* Site */} + {site && (
-
Tags
-
- {tags?.map((tag, key) => ( -
- #{tag} -
- ))} -
+
Site
+ {site && ( + + {site} + + )} +
+ )} + + {/* Tags */} +
+
Tags
+
+ {tags?.map((tag, key) => ( +
+ #{tag} +
+ ))}
- - {/* Description */} - {description && ( - - )}
-
-
+ + {/* Description */} + {description && ( + + )} + ) } diff --git a/app/constants.ts b/app/constants.ts new file mode 100644 index 00000000..9dc3f3fd --- /dev/null +++ b/app/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_TITLE = 'Personal website' diff --git a/app/studio/[[...index]]/page.tsx b/app/studio/[[...index]]/page.tsx index 80d4adfe..da7007e4 100644 --- a/app/studio/[[...index]]/page.tsx +++ b/app/studio/[[...index]]/page.tsx @@ -10,8 +10,6 @@ import config from '@/sanity.config' import {NextStudio} from 'next-sanity/studio' -export const dynamic = 'force-static' - export {metadata, viewport} from 'next-sanity/studio' export default function StudioPage() { diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 00000000..10d29a0a --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,37 @@ +import {sanityFetch} from '@/sanity/lib/live' +import {defineQuery, type PortableTextBlock} from 'next-sanity' +import {Suspense} from 'react' +import {CustomPortableText} from './CustomPortableText' + +const footerQuery = defineQuery(` + *[_type == "settings"][0]{ + _id, + _type, + footer, + } +`) +export function Footer() { + console.log('Footer') + return ( +
+ + + +
+ ) +} + +async function FooterContent() { + console.log('FooterContent') + const {data} = await sanityFetch({query: footerQuery}) + if (!data?.footer) return null + return ( + + ) +} diff --git a/components/Header.tsx b/components/Header.tsx index 137027cd..052d836a 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -8,14 +8,17 @@ interface HeaderProps { centered?: boolean description?: null | any[] title?: string | null + loading?: boolean } export function Header(props: HeaderProps) { - const {id, type, path, title, description, centered = false} = props + const {id, type, path, title, description, centered = false, loading} = props if (!description && !title) { return null } return ( -
+
{/* Title */} {title &&
{title}
} {/* Description */} diff --git a/components/HomePage.tsx b/components/HomePage.tsx deleted file mode 100644 index 10c6a633..00000000 --- a/components/HomePage.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import {Header} from '@/components/Header' -import {OptimisticSortOrder} from '@/components/OptimisticSortOrder' -import {ProjectListItem} from '@/components/ProjectListItem' -import type {HomePageQueryResult} from '@/sanity.types' -import {studioUrl} from '@/sanity/lib/api' -import {resolveHref} from '@/sanity/lib/utils' -import {createDataAttribute} from 'next-sanity' -import Link from 'next/link' - -export interface HomePageProps { - data: HomePageQueryResult | null -} - -export async function HomePage({data}: HomePageProps) { - // Default to an empty object to allow previews on non-existent documents - const {overview = [], showcaseProjects = [], title = ''} = data ?? {} - - const dataAttribute = - data?._id && data?._type - ? createDataAttribute({ - baseUrl: studioUrl, - id: data._id, - type: data._type, - }) - : null - - return ( -
- {/* Header */} - {title && ( -
- )} - {/* Showcase projects */} -
- - {showcaseProjects && - showcaseProjects.length > 0 && - showcaseProjects.map((project) => { - const href = resolveHref(project?._type, project?.slug) - if (!href) { - return null - } - return ( - - - - ) - })} - -
-
- ) -} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index f556ff9b..2b127ebf 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,15 +1,26 @@ import {OptimisticSortOrder} from '@/components/OptimisticSortOrder' -import type {SettingsQueryResult} from '@/sanity.types' import {studioUrl} from '@/sanity/lib/api' +import {sanityFetch} from '@/sanity/lib/live' import {resolveHref} from '@/sanity/lib/utils' -import {createDataAttribute, stegaClean} from 'next-sanity' +import {createDataAttribute, defineQuery} from 'next-sanity' import Link from 'next/link' -interface NavbarProps { - data: SettingsQueryResult -} -export function Navbar(props: NavbarProps) { - const {data} = props +const navbarQuery = defineQuery(` + *[_type == "settings"][0]{ + _id, + _type, + menuItems[]{ + _key, + ...@->{ + _type, + "slug": slug.current, + title + } + } + }`) +export async function Navbar() { + // Disable stega since `data-sanity` attributes are used here for marking clickable elements + const {data} = await sanityFetch({query: navbarQuery, stega: false}) const dataAttribute = data?._id && data?._type ? createDataAttribute({ @@ -41,7 +52,7 @@ export function Navbar(props: NavbarProps) { ])} href={href} > - {stegaClean(menuItem.title)} + {menuItem.title} ) })} @@ -49,3 +60,13 @@ export function Navbar(props: NavbarProps) {
) } + +export function FallbackNavbar() { + return ( +
+ + Loading… + +
+ ) +} diff --git a/components/ProjectListItem.tsx b/components/ProjectListItem.tsx index 887a3c74..c7fb637b 100644 --- a/components/ProjectListItem.tsx +++ b/components/ProjectListItem.tsx @@ -27,7 +27,6 @@ export function ProjectListItem(props: ProjectProps) { } function TextBox({project}: {project: ShowcaseProject}) { - console.log(project) return (
diff --git a/next.config.ts b/next.config.ts index b742fe44..a03e0ec1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,19 +1,11 @@ import {NextConfig} from 'next' const config: NextConfig = { + cacheComponents: true, reactCompiler: true, - images: { - remotePatterns: [{hostname: 'cdn.sanity.io'}], - }, - typescript: { - // Set this to false if you want production builds to abort if there's type errors - ignoreBuildErrors: process.env.VERCEL_ENV === 'production', - }, - logging: { - fetches: { - fullUrl: true, - }, - }, + images: {remotePatterns: [{hostname: 'cdn.sanity.io'}]}, + // Set this to false if you want production builds to abort if there's type errors + typescript: {ignoreBuildErrors: process.env.VERCEL_ENV === 'production'}, env: { // Matches the behavior of `sanity dev` which sets styled-components to use the fastest way of inserting CSS rules in both dev and production. It's default behavior is to disable it in dev mode. SC_DISABLE_SPEEDY: 'false', diff --git a/package-lock.json b/package-lock.json index 945ad6fb..4842acf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "classnames": "2.5.1", "date-fns": "4.1.0", "next": "16.0.0", - "next-sanity": "11.6.0", + "next-sanity": "11.6.2", "react": "19.2.0", "react-dom": "19.2.0", "react-live-transitions": "0.2.0", @@ -349,7 +349,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2104,7 +2103,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -2196,7 +2194,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2241,7 +2238,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2275,7 +2271,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2364,7 +2359,6 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -3046,7 +3040,6 @@ "integrity": "sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", @@ -4463,7 +4456,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -4982,7 +4974,6 @@ "resolved": "https://registry.npmjs.org/@portabletext/sanity-bridge/-/sanity-bridge-1.1.15.tgz", "integrity": "sha512-+WOYC+Hcfk89rtzgL3SS/gMfP8yHWQL65YzjppJjhQ+V19KiNRwsff3BPDV3lwwMRb8y3xcm8WMqULoIRM//WA==", "license": "MIT", - "peer": true, "dependencies": { "@portabletext/schema": "^1.2.0", "get-random-values-esm": "^1.0.2", @@ -5437,7 +5428,6 @@ "resolved": "https://registry.npmjs.org/@sanity/client/-/client-7.12.0.tgz", "integrity": "sha512-RIQ4JXOpLuc3UkUK34xxYA+n9z6m0RCEbsdloKEbS0areaHymM3144IkzeZGFcIokN1YlNenHfnBHNsMjf3TaQ==", "license": "MIT", - "peer": true, "dependencies": { "@sanity/eventsource": "^5.0.2", "get-it": "^8.6.9", @@ -5479,7 +5469,6 @@ "resolved": "https://registry.npmjs.org/@sanity/color/-/color-3.0.6.tgz", "integrity": "sha512-2TjYEvOftD0v7ukx3Csdh9QIu44P2z7NDJtlC3qITJRYV36J7R6Vfd3trVhFnN77/7CZrGjqngrtohv8VqO5nw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" } @@ -6407,7 +6396,6 @@ "resolved": "https://registry.npmjs.org/@sanity/schema/-/schema-4.11.0.tgz", "integrity": "sha512-YpROqwl7nzpow5F+b6skZDlDtcql2l7wKxd9U0MzpvU+jrGScuRqnMS9tuE8lY3RonuAog5nni3bAuuEVp0sXA==", "license": "MIT", - "peer": true, "dependencies": { "@sanity/descriptors": "^1.1.1", "@sanity/generate-help-url": "^3.0.0", @@ -6595,7 +6583,6 @@ "resolved": "https://registry.npmjs.org/@sanity/types/-/types-4.11.0.tgz", "integrity": "sha512-VfheyQJN3fS57fmibQWxMAy+dn3QGkRTHoN/v3nek8BwbhDsvhzq3O0DQoYYTLaN+QHXdFOX9tDEwqfdgK02nQ==", "license": "MIT", - "peer": true, "dependencies": { "@sanity/client": "^7.12.0", "@sanity/media-library-types": "^1.0.1" @@ -7018,7 +7005,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -7040,7 +7026,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7268,7 +7253,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7612,7 +7596,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -7775,7 +7758,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -9041,7 +9023,6 @@ "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9116,7 +9097,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10686,7 +10666,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -11373,7 +11352,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^2.0.1", "cssstyle": "^4.0.1", @@ -12248,7 +12226,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.0.tgz", "integrity": "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.0.0", "@swc/helpers": "0.5.15", @@ -12297,9 +12274,9 @@ } }, "node_modules/next-sanity": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/next-sanity/-/next-sanity-11.6.0.tgz", - "integrity": "sha512-nlD269BGAJwiR5gJCBIEj8FWaPMLKsNPkqPYJN9PAuD4nPnwPEmBJANqvedOR2jC7hFOwVHCtOSgy0OVEA/P6w==", + "version": "11.6.2", + "resolved": "https://registry.npmjs.org/next-sanity/-/next-sanity-11.6.2.tgz", + "integrity": "sha512-yDMLlcUI9GdIGGW/yJC7i3YwfCJv96zHI2Aj1IcwlibAOvFyGDTGXH4cp266I0VMNO8qoUPuAzV3mf0ivu1HRA==", "license": "MIT", "dependencies": { "@portabletext/react": "^4.0.3", @@ -13168,7 +13145,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13348,7 +13324,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13660,7 +13635,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13691,7 +13665,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13770,8 +13743,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-live-transitions": { "version": "0.2.0", @@ -14392,7 +14364,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -14449,7 +14420,6 @@ "resolved": "https://registry.npmjs.org/sanity/-/sanity-4.11.0.tgz", "integrity": "sha512-BMud9nyvOk8uWamHdkRnWXay9fqhrdfrr8CCkdtcCcQLkLsE+iJOWMwI9jOE8nFE+j5C8g8W5ylqKntZzgop4Q==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^6.0.1", @@ -14878,7 +14848,6 @@ "resolved": "https://registry.npmjs.org/slate/-/slate-0.118.1.tgz", "integrity": "sha512-6H1DNgnSwAFhq/pIgf+tLvjNzH912M5XrKKhP9Frmbds2zFXdSJ6L/uFNyVKxQIkPzGWPD0m+wdDfmEuGFH5Tg==", "license": "MIT", - "peer": true, "dependencies": { "immer": "^10.0.3", "tiny-warning": "^1.0.3" @@ -15233,7 +15202,6 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -15399,7 +15367,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15657,7 +15624,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15859,7 +15825,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16148,7 +16113,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -16206,7 +16170,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16317,7 +16280,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16586,7 +16548,6 @@ "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.23.0.tgz", "integrity": "sha512-jo126xWXkU6ySQ91n51+H2xcgnMuZcCQpQoD3FQ79d32a6RQvryRh8rrDHnH4WDdN/yg5xNjlIRol9ispMvzeg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" @@ -16719,7 +16680,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 4a1ebf58..0c85ff84 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "classnames": "2.5.1", "date-fns": "4.1.0", "next": "16.0.0", - "next-sanity": "11.6.0", + "next-sanity": "11.6.2", "react": "19.2.0", "react-dom": "19.2.0", "react-live-transitions": "0.2.0", diff --git a/sanity-typegen.json b/sanity-typegen.json new file mode 100644 index 00000000..e6a10116 --- /dev/null +++ b/sanity-typegen.json @@ -0,0 +1,3 @@ +{ + "path": "./{app,components,sanity}/**/*.{ts,tsx}" +} diff --git a/sanity.config.ts b/sanity.config.ts index 75ad2cef..7ae41eea 100644 --- a/sanity.config.ts +++ b/sanity.config.ts @@ -22,6 +22,12 @@ import {structureTool} from 'sanity/structure' const title = process.env.NEXT_PUBLIC_SANITY_PROJECT_TITLE || 'Next.js Personal Website with Sanity.io' +let needsDraftMode = true +if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') { + // next-sanity needs to support env var patterns, it currently only supports draft mode + // needsDraftMode = false +} + export default defineConfig({ basePath: studioUrl, projectId: projectId || '', @@ -48,7 +54,7 @@ export default defineConfig({ }), presentationTool({ resolve, - previewUrl: {previewMode: {enable: '/api/draft-mode/enable'}}, + previewUrl: needsDraftMode ? {previewMode: {enable: '/api/draft-mode/enable'}} : '/', }), // Configures the global "new document" button, and document actions, to suit the Settings document singleton singletonPlugin([home.name, settings.name]), diff --git a/sanity.types.ts b/sanity.types.ts index da66fc15..05c2c28b 100644 --- a/sanity.types.ts +++ b/sanity.types.ts @@ -423,7 +423,93 @@ export type AllSanitySchemaTypes = | Slug | SanityAssetSourceData export declare const internalGroqTypeReferenceTo: unique symbol -// Source: ./sanity/lib/queries.ts +// Source: ./app/(personal)/[slug]/page.tsx +// Variable: pagesBySlugQuery +// Query: *[_type == "page" && slug.current == $slug][0] { _id, _type, body, overview, title, "slug": slug.current, } +export type PagesBySlugQueryResult = { + _id: string + _type: 'page' + body: Array< + | ({ + _key: string + } & Timeline) + | { + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + } + | { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + media?: unknown + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + caption?: string + alt?: string + _type: 'image' + _key: string + } + > | null + overview: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: never + markDefs?: null + level?: number + _type: 'block' + _key: string + }> | null + title: string | null + slug: string | null +} | null + +// Source: ./app/(personal)/layout.tsx +// Variable: layoutMetadataQuery +// Query: { "settings": *[_type == "settings"][0]{ogImage}, "home": *[_type == "home"][0]{ title, // Render portabletext to string "description": pt::text(overview) } } +export type LayoutMetadataQueryResult = { + settings: { + ogImage: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + media?: unknown + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + _type: 'image' + } | null + } | null + home: { + title: string | null + description: string + } | null +} + +// Source: ./app/(personal)/page.tsx // Variable: homePageQuery // Query: *[_type == "home"][0]{ _id, _type, overview, showcaseProjects[]{ _key, ...@->{ _id, _type, coverImage, overview, "slug": slug.current, tags, title, } }, title, } export type HomePageQueryResult = { @@ -483,66 +569,62 @@ export type HomePageQueryResult = { }> | null title: string | null } | null -// Variable: pagesBySlugQuery -// Query: *[_type == "page" && slug.current == $slug][0] { _id, _type, body, overview, title, "slug": slug.current, } -export type PagesBySlugQueryResult = { + +// Source: ./components/Footer.tsx +// Variable: footerQuery +// Query: *[_type == "settings"][0]{ _id, _type, footer, } +export type FooterQueryResult = { _id: string - _type: 'page' - body: Array< - | ({ - _key: string - } & Timeline) - | { - children?: Array<{ - marks?: Array - text?: string - _type: 'span' - _key: string - }> - style?: 'normal' - listItem?: 'bullet' | 'number' - markDefs?: Array<{ - href?: string - _type: 'link' - _key: string - }> - level?: number - _type: 'block' - _key: string - } - | { - asset?: { - _ref: string - _type: 'reference' - _weak?: boolean - [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' - } - media?: unknown - hotspot?: SanityImageHotspot - crop?: SanityImageCrop - caption?: string - alt?: string - _type: 'image' - _key: string - } - > | null - overview: Array<{ + _type: 'settings' + footer: Array<{ children?: Array<{ marks?: Array text?: string _type: 'span' _key: string }> - style?: 'normal' - listItem?: never - markDefs?: null + style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> level?: number _type: 'block' _key: string }> | null - title: string | null - slug: string | null } | null + +// Source: ./components/Navbar.tsx +// Variable: navbarQuery +// Query: *[_type == "settings"][0]{ _id, _type, menuItems[]{ _key, ...@->{ _type, "slug": slug.current, title } } } +export type NavbarQueryResult = { + _id: string + _type: 'settings' + menuItems: Array< + | { + _key: null + _type: 'home' + slug: null + title: string | null + } + | { + _key: null + _type: 'page' + slug: string | null + title: string | null + } + | { + _key: null + _type: 'project' + slug: string | null + title: string | null + } + > | null +} | null + +// Source: ./sanity/lib/queries.ts // Variable: projectBySlugQuery // Query: *[_type == "project" && slug.current == $slug][0] { _id, _type, client, coverImage, description, duration, overview, site, "slug": slug.current, tags, title, } export type ProjectBySlugQueryResult = { @@ -685,8 +767,11 @@ export type SlugsByTypeQueryResult = Array<{ import '@sanity/client' declare module '@sanity/client' { interface SanityQueries { - '\n *[_type == "home"][0]{\n _id,\n _type,\n overview,\n showcaseProjects[]{\n _key,\n ...@->{\n _id,\n _type,\n coverImage,\n overview,\n "slug": slug.current,\n tags,\n title,\n }\n },\n title,\n }\n': HomePageQueryResult '\n *[_type == "page" && slug.current == $slug][0] {\n _id,\n _type,\n body,\n overview,\n title,\n "slug": slug.current,\n }\n': PagesBySlugQueryResult + '{\n "settings": *[_type == "settings"][0]{ogImage},\n "home": *[_type == "home"][0]{\n title,\n // Render portabletext to string\n "description": pt::text(overview)\n }\n }': LayoutMetadataQueryResult + '\n *[_type == "home"][0]{\n _id,\n _type,\n overview,\n showcaseProjects[]{\n _key,\n ...@->{\n _id,\n _type,\n coverImage,\n overview,\n "slug": slug.current,\n tags,\n title,\n }\n },\n title,\n }\n': HomePageQueryResult + '\n *[_type == "settings"][0]{\n _id,\n _type,\n footer,\n }\n': FooterQueryResult + '\n *[_type == "settings"][0]{\n _id,\n _type,\n menuItems[]{\n _key,\n ...@->{\n _type,\n "slug": slug.current,\n title\n }\n }\n }': NavbarQueryResult '\n *[_type == "project" && slug.current == $slug][0] {\n _id,\n _type,\n client,\n coverImage,\n description,\n duration,\n overview,\n site,\n "slug": slug.current,\n tags,\n title,\n }\n': ProjectBySlugQueryResult '\n *[_type == "settings"][0]{\n _id,\n _type,\n footer,\n menuItems[]{\n _key,\n ...@->{\n _type,\n "slug": slug.current,\n title\n }\n },\n ogImage,\n }\n': SettingsQueryResult '\n *[_type == $type && defined(slug.current)]{"slug": slug.current}\n': SlugsByTypeQueryResult diff --git a/sanity/lib/live.ts b/sanity/lib/live.ts index 254da11b..de52c5e3 100644 --- a/sanity/lib/live.ts +++ b/sanity/lib/live.ts @@ -1,9 +1,61 @@ -import {defineLive} from 'next-sanity/live' +import {defineLive, type SanityFetchOptions} from 'next-sanity/experimental/live' import {client} from './client' import {token} from './token' +import {draftMode} from 'next/headers' -export const {SanityLive, sanityFetch} = defineLive({ +const {sanityFetch: _sanityFetch, SanityLive} = defineLive({ client, serverToken: token, browserToken: token, }) +export {SanityLive} + +/** + * When Next.js Draft Mode is enabled it'll disable cache layers, + * this reduces the performance of draft content preview. + * Thus, on a Vercel preview deployment we'll rely on deployment protection to prevent unauthorized access, + * and otherwise query draft content and leverage the remote cache. + */ +export async function isPreviewMode(): Promise { + // next-sanity doesn't support this yet + // if (process.env.VERCEL_ENV === 'preview') return true + const {isEnabled} = await draftMode() + // @TODO also check for VERCEL_ENV when deciding if we're in preview mode + return isEnabled +} + +/** + * When fetching data in a server component the right `perspective` and `stega` options are automatically set based on the environment. + */ +export const sanityFetch = async ({ + query, + params, + stega, +}: Pick, 'query' | 'params' | 'stega'>) => { + const isPreviewModeEnabled = await isPreviewMode() + const perspective = isPreviewModeEnabled ? 'drafts' : 'published' + return _sanityFetch({query, params, perspective, stega: stega ?? isPreviewModeEnabled}) +} + +/** + * When using `generateMetadata`, `generateViewport` and similar, we never need stega in strings as those are used for detecting where to render overlays + * when in visual editing. + */ +export const sanityFetchMetadata = async ({ + query, + params, +}: Pick, 'query' | 'params'>) => { + const perspective = (await isPreviewMode()) ? 'drafts' : 'published' + return _sanityFetch({query, params, perspective, stega: false}) +} + +/** + * When using `generateStaticParams` we are always querying published content, and it should never contain stega in strings + */ +export const sanityFetchStaticParams = async ({ + query, + params, +}: Pick, 'query' | 'params'>) => { + const {data} = await _sanityFetch({query, params, perspective: 'published', stega: false}) + return data +} diff --git a/sanity/lib/queries.ts b/sanity/lib/queries.ts index a27f4cac..ae19a93b 100644 --- a/sanity/lib/queries.ts +++ b/sanity/lib/queries.ts @@ -1,37 +1,5 @@ import {defineQuery} from 'next-sanity' -export const homePageQuery = defineQuery(` - *[_type == "home"][0]{ - _id, - _type, - overview, - showcaseProjects[]{ - _key, - ...@->{ - _id, - _type, - coverImage, - overview, - "slug": slug.current, - tags, - title, - } - }, - title, - } -`) - -export const pagesBySlugQuery = defineQuery(` - *[_type == "page" && slug.current == $slug][0] { - _id, - _type, - body, - overview, - title, - "slug": slug.current, - } -`) - export const projectBySlugQuery = defineQuery(` *[_type == "project" && slug.current == $slug][0] { _id,