rda-v3/src/lib/sanity.ts
Calum Muir 87182583f8 Wire home page content into Sanity CMS with hardcoded fallbacks
Adds homePage singleton document type (hero headline/mission, video section,
CTA cards, sponsors). Home page fetches from Sanity when configured and
falls back to the existing hardcoded values when not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 13:19:45 +00:00

141 lines
4.6 KiB
TypeScript

import { createClient } from '@sanity/client';
import { toHTML } from '@portabletext/to-html';
import type { NewsArticle } from './news';
// ── Client ────────────────────────────────────────────────────────────────────
export const sanityClient = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID ?? '',
dataset: import.meta.env.SANITY_DATASET ?? 'production',
apiVersion: '2024-01-01',
// useCdn in production; always false during dev so edits show immediately
useCdn: import.meta.env.PROD ?? false,
// Token only needed for draft previews; public published content works without one
token: import.meta.env.SANITY_API_TOKEN,
});
export function isSanityConfigured(): boolean {
return Boolean(import.meta.env.SANITY_PROJECT_ID);
}
// ── GROQ queries ──────────────────────────────────────────────────────────────
const POST_FIELDS = /* groq */ `
_id,
title,
"slug": slug.current,
date,
category,
summary,
"image": image.asset->url,
body
`;
const ALL_POSTS_QUERY = /* groq */ `*[_type == "post"] | order(date desc) { ${POST_FIELDS} }`;
const POST_BY_SLUG_QUERY = /* groq */ `*[_type == "post" && slug.current == $slug][0] { ${POST_FIELDS} }`;
// ── Raw Sanity shape ──────────────────────────────────────────────────────────
interface SanityPost {
_id: string;
title: string;
slug: string;
date: string;
category: string;
summary: string;
image?: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any[];
}
// ── Converters ────────────────────────────────────────────────────────────────
function toNewsArticle(post: SanityPost): NewsArticle {
const html = post.body
? toHTML(post.body, {
components: {
marks: {
link: ({ children, value }) => {
const target = value?.blank ? ' target="_blank" rel="noreferrer"' : '';
return `<a href="${value?.href ?? '#'}"${target}>${children}</a>`;
},
},
},
})
: '';
return {
title: post.title,
date: post.date ?? '',
summary: post.summary ?? '',
image: post.image ?? undefined,
slug: post.slug,
category: post.category ?? 'News',
body: '', // raw markdown not used when coming from Sanity
html,
};
}
// ── Home page ─────────────────────────────────────────────────────────────────
export interface SanityCtaCard {
label: string;
title: string;
description: string;
image?: string | null;
href: string;
}
export interface SanitySponsor {
name: string;
logo?: string | null;
url?: string | null;
}
export interface HomePageData {
heroHeadline: string;
heroMission: string;
videoHeading: string;
videoBody: string;
videoYoutubeId: string;
ctaCards: SanityCtaCard[];
sponsorHeading: string;
sponsors: SanitySponsor[];
}
const HOME_PAGE_QUERY = /* groq */ `*[_type == "homePage"][0]{
heroHeadline,
heroMission,
videoHeading,
videoBody,
videoYoutubeId,
ctaCards[]{
label,
title,
description,
"image": image.asset->url,
href,
},
sponsorHeading,
sponsors[]{
name,
"logo": logo.asset->url,
url,
}
}`;
export async function getSanityHomePage(): Promise<HomePageData | null> {
return sanityClient.fetch<HomePageData | null>(HOME_PAGE_QUERY);
}
// ── Public API ────────────────────────────────────────────────────────────────
export async function getSanityPosts(): Promise<NewsArticle[]> {
const posts = await sanityClient.fetch<SanityPost[]>(ALL_POSTS_QUERY);
return posts.map(toNewsArticle);
}
export async function getSanityPost(slug: string): Promise<NewsArticle | undefined> {
const post = await sanityClient.fetch<SanityPost | null>(POST_BY_SLUG_QUERY, { slug });
return post ? toNewsArticle(post) : undefined;
}