import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; import { cache } from "react"; import { defineQuery } from "next-sanity"; import { sanity } from "@/lib/sanity"; import { ArtistReleasesTab, type ArtistRelease } from "@/components/artist/ArtistReleasesTab"; import { ReleaseCover } from "@/components/release/ReleaseCover"; import { AnimatedText } from "@/components/AnimatedText"; import { TabsClient, type TabDef } from "@/components/TabsClient"; import { PortableText } from "@portabletext/react"; import { urlFor } from "@/lib/sanityImage"; import { portableTextComponents } from "@/lib/portableTextComponents"; import { ARTIST_PLACEHOLDER_SRC } from "@/lib/constants"; import { Breadcrumb } from "@/components/Breadcrumb"; export const dynamicParams = true; export const revalidate = 86400; type Work = { title?: string; slug?: string; description?: any; composerName?: string; composerSlug?: string; composerImage?: any; arrangerName?: string; releases?: ArtistRelease[]; }; const WORK_DETAIL_QUERY = defineQuery(` *[_type == "work" && slug.current == $slug][0]{ title, description, "slug": slug.current, "composerName": composer->name, "composerSlug": composer->slug.current, "composerImage": composer->image, "arrangerName": arranger->name, "releases": *[ _type == "release" && count(tracks[work->slug.current == $slug]) > 0 ] | order(releaseDate desc, catalogNo desc) { _id, name, albumArtist, catalogNo, "slug": slug.current, releaseDate, albumCover, genre, instrumentation } } `); const getWork = cache(async (slug: string) => { try { return await sanity.fetch(WORK_DETAIL_QUERY, { slug }); } catch (error) { console.error("Failed to fetch work:", error); return null; } }); const WORK_SLUGS_QUERY = defineQuery( `*[_type == "work" && defined(slug.current)]{ "slug": slug.current }`, ); export async function generateStaticParams() { const slugs = await sanity.fetch>(WORK_SLUGS_QUERY); return slugs.map((w) => ({ slug: w.slug })); } export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug: rawSlug } = await params; const slug = rawSlug.toLowerCase(); const work = await getWork(slug); if (!work) notFound(); const subtitle = work.arrangerName ? `${work.composerName} (arr. ${work.arrangerName})` : (work.composerName ?? ""); const description = `Explore releases featuring ${work.title} by ${subtitle} on TRPTK.`; const ogImage = work.composerImage ? urlFor(work.composerImage).width(1200).height(630).url() : undefined; return { title: `${work.title} ${subtitle ? ` • ${subtitle}` : ""}`, description, alternates: { canonical: `/work/${slug}` }, openGraph: { title: work.title, description: `${subtitle}${work.releases?.length ? ` - ${work.releases.length} release(s)` : ""}`, type: "music.album", ...(ogImage && { images: [{ url: ogImage, width: 1200, height: 630, alt: `${work.title} by ${subtitle}` }] }), }, twitter: { card: "summary_large_image", title: work.title, description, ...(ogImage && { images: [ogImage] }), }, }; } export default async function WorkPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; if (!slug) notFound(); const normalizedSlug = slug.toLowerCase(); if (slug !== normalizedSlug) { redirect(`/work/${normalizedSlug}`); } const work = await getWork(slug); if (!work) notFound(); const displayTitle = work.title ?? ""; const displaySubtitle = work.arrangerName ? `${work.composerName} (arr. ${work.arrangerName})` : (work.composerName ?? ""); const jsonLd = { "@context": "https://schema.org", "@type": "MusicComposition", name: work.title, url: `https://trptk.com/work/${slug}`, ...(work.composerName && { composer: { "@type": "Person", name: work.composerName }, }), ...(work.composerImage && { image: urlFor(work.composerImage).width(800).url(), }), }; return ( <>