trptk/app/[slug]/page.tsx
2026-02-24 17:14:07 +01:00

135 lines
4 KiB
TypeScript

import { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { cache } from "react";
import { defineQuery } from "next-sanity";
import { sanity } from "@/lib/sanity";
import { AnimatedText } from "@/components/AnimatedText";
import { ReleaseCover } from "@/components/release/ReleaseCover";
import { StreamingLinks } from "@/components/release/StreamingLinks";
import { urlFor } from "@/lib/sanityImage";
import { ARTIST_PLACEHOLDER_SRC } from "@/lib/constants";
import { buildLinks, type Release } from "@/lib/release";
export const dynamicParams = true;
export const revalidate = 86400;
const RELEASE_BY_CATALOG_NO_QUERY = defineQuery(`
*[_type == "release" && lower(catalogNo) == $slug][0]{
name,
albumArtist,
label,
catalogNo,
albumCover,
officialUrl,
spotifyUrl,
appleMusicUrl,
deezerUrl,
amazonMusicUrl,
tidalUrl,
qobuzUrl,
}
`);
const getRelease = cache(async (slug: string) => {
try {
const release = await sanity.fetch<Release>(RELEASE_BY_CATALOG_NO_QUERY, { slug });
return release;
} catch (error) {
console.error("Failed to fetch release:", error);
return null;
}
});
const RELEASE_CATALOG_SLUGS_QUERY = defineQuery(
`*[_type == "release" && defined(catalogNo)][]{ "slug": lower(catalogNo) }`,
);
export async function generateStaticParams() {
const releases = await sanity.fetch<{ slug: string }[]>(RELEASE_CATALOG_SLUGS_QUERY);
return releases.map(({ slug }) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
if (!slug) return { title: "TRPTK" };
const release = await getRelease(slug.toLowerCase());
if (!release) return { title: "TRPTK" };
const description = `Listen to ${release.name}${release.albumArtist ? ` by ${release.albumArtist}` : ""} on all major streaming platforms.`;
const ogImage = release.albumCover
? urlFor(release.albumCover).width(1200).height(1200).url()
: undefined;
return {
title: `${release.albumArtist ? `${release.albumArtist}` : ""}${release.name}`,
description,
openGraph: {
title: release.name,
description: `${release.albumArtist || "TRPTK"}${release.label ? ` - ${release.label}` : ""}`,
type: "music.album",
...(ogImage && { images: [{ url: ogImage, width: 1200, height: 1200, alt: `${release.name} by ${release.albumArtist}` }] }),
},
twitter: {
card: "summary_large_image",
title: release.name,
description,
...(ogImage && { images: [ogImage] }),
},
};
}
export default async function ReleasePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
if (!slug) notFound();
const normalizedSlug = slug.toLowerCase();
if (slug !== normalizedSlug) {
redirect(`/${normalizedSlug}`);
}
const release = await getRelease(slug);
if (!release) notFound();
const links = buildLinks(release);
const title = release.name ?? slug;
const albumArtist = release.albumArtist ?? "";
return (
<div className="flex h-dvh w-full flex-col">
<main className="mx-auto my-auto w-full max-w-90 px-6 py-12 text-center font-silka text-sm text-lighttext transition-all duration-1000 ease-in-out md:px-8 md:py-16 dark:text-darktext">
<header>
<ReleaseCover
src={release.albumCover ? urlFor(release.albumCover).url() : ARTIST_PLACEHOLDER_SRC}
alt={`Album cover image for ${title} by ${albumArtist}`}
/>
<div className="my-10">
<AnimatedText
text={title}
as="h1"
className="mb-2 font-argesta text-2xl break-words text-lighttext dark:text-white"
/>
<AnimatedText
text={albumArtist}
as="h2"
className="text-sm break-words text-lightsec dark:text-darksec"
delay={0.25}
/>
</div>
</header>
<StreamingLinks releaseName={release.name} links={links} />
</main>
</div>
);
}