Skip to content
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
112 changes: 72 additions & 40 deletions app/(personal)/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
const {data: page} = await sanityFetch({
const {data: page} = await sanityFetchMetadata({
query: pagesBySlugQuery,
params,
stega: false,
params: await params,
})

return {
Expand All @@ -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 (
<div>
<div className="mb-14">
<Suspense
fallback={
<Header
id={null}
type={null}
path={['overview']}
title="Loading…"
description={null}
loading
/>
}
>
<PageSlugRouteContent params={params} />
</Suspense>
</div>
<div className="absolute left-0 w-screen border-t" />
</div>
)
}

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 (
<div>
<div className="mb-14">
{/* Header */}
<Header
<>
{/* Header */}
<Header
id={data?._id || null}
type={data?._type || null}
path={['overview']}
title={title || (data?._id ? 'Untitled' : '404 Page Not Found')}
description={overview}
/>

{/* Body */}
{body && (
<CustomPortableText
id={data?._id || null}
type={data?._type || null}
path={['overview']}
title={title || (data?._id ? 'Untitled' : '404 Page Not Found')}
description={overview}
path={['body']}
paragraphClasses="font-serif max-w-3xl text-gray-600 text-xl"
value={body as unknown as PortableTextBlock[]}
/>

{/* Body */}
{body && (
<CustomPortableText
id={data?._id || null}
type={data?._type || null}
path={['body']}
paragraphClasses="font-serif max-w-3xl text-gray-600 text-xl"
value={body as unknown as PortableTextBlock[]}
/>
)}
</div>
<div className="absolute left-0 w-screen border-t" />
</div>
)}
</>
)
}
67 changes: 32 additions & 35 deletions app/(personal)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,48 @@
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'
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<Metadata> {
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] : [],
},
Expand All @@ -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 (
<>
<div className="flex min-h-screen flex-col bg-white text-black">
<Navbar data={data} />
<Suspense fallback={<FallbackNavbar />}>
<Navbar />
</Suspense>
<div className="mt-20 flex-grow px-4 md:px-16 lg:px-32">{children}</div>
<footer className="bottom-0 w-full bg-white py-12 text-center md:py-20">
{data?.footer && (
<CustomPortableText
id={data._id}
type={data._type}
path={['footer']}
paragraphClasses="text-md md:text-xl"
value={data.footer as unknown as PortableTextBlock[]}
/>
)}
</footer>
<Footer />
<Suspense>
<IntroTemplate />
</Suspense>
</div>
<Toaster />
{(await isPreviewMode()) && <VisualEditing />}
<SanityLive onError={handleError} />
{(await draftMode()).isEnabled && (
<>
<DraftModeToast />
<VisualEditing />
</>
)}
{(await draftMode()).isEnabled && <DraftModeToast />}
<SpeedInsights />
</>
)
Expand Down
92 changes: 88 additions & 4 deletions app/(personal)/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-20">
<Suspense fallback={<div className="w-full h-screen" />}>
<HomePageContent />
</Suspense>
</div>
)
}

async function HomePageContent() {
const {data} = await sanityFetch({query: homePageQuery})

if (!data) {
Expand All @@ -19,5 +54,54 @@ export default async function IndexRoute() {
)
}

return <HomePage data={data} />
// 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 && (
<Header
id={data?._id || null}
type={data?._type || null}
path={['overview']}
centered
title={title}
description={overview}
/>
)}
{/* Showcase projects */}
<div className="mx-auto max-w-[100rem] rounded-md border">
<OptimisticSortOrder id={data?._id} path={'showcaseProjects'}>
{showcaseProjects &&
showcaseProjects.length > 0 &&
showcaseProjects.map((project) => {
const href = resolveHref(project?._type, project?.slug)
if (!href) {
return null
}
return (
<Link
className="flex flex-col gap-x-5 p-2 transition odd:border-b odd:border-t hover:bg-gray-50/50 xl:flex-row odd:xl:flex-row-reverse"
key={project._key}
href={href}
data-sanity={dataAttribute?.(['showcaseProjects', {_key: project._key}])}
>
<ProjectListItem project={project as any} />
</Link>
)
})}
</OptimisticSortOrder>
</div>
</>
)
}
Loading