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>
This commit is contained in:
Calum Muir 2026-06-07 13:19:45 +00:00
parent 921e03e22e
commit 87182583f8
4 changed files with 183 additions and 39 deletions

View file

@ -76,6 +76,58 @@ function toNewsArticle(post: SanityPost): NewsArticle {
}; };
} }
// ── 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 ──────────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────────
export async function getSanityPosts(): Promise<NewsArticle[]> { export async function getSanityPosts(): Promise<NewsArticle[]> {

View file

@ -3,6 +3,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
import SiteHeader from "../components/SiteHeader.astro"; import SiteHeader from "../components/SiteHeader.astro";
import SiteFooter from "../components/SiteFooter.astro"; import SiteFooter from "../components/SiteFooter.astro";
import { navigationItems } from "../lib/navigation"; import { navigationItems } from "../lib/navigation";
import { isSanityConfigured, getSanityHomePage } from "../lib/sanity";
const pageSeo = { const pageSeo = {
title: "Highland Group RDA", title: "Highland Group RDA",
@ -13,32 +14,34 @@ const pageSeo = {
canonicalPath: "/", canonicalPath: "/",
}; };
const ctaCards = [ // ── Fallback content (used when Sanity is not configured or document is empty) ─
{ const DEFAULTS = {
label: "Fundraising", heroHeadline: "Enriching lives through horses in the Highlands.",
title: "Sponsor a pony", heroMission: "Our mission is to bring about meaningful and positive changes in the health and well-being of people with physical or mental disabilities through activities with our horses and ponies.",
sub: "Your support keeps our ponies healthy and our sessions running — from hoof trimming to hay bales.", videoHeading: "What is Riding for the Disabled?",
img: "/alineofponies-1920w.webp", videoBody: "Across the Highlands, we provide riding sessions for people with a wide range of physical and mental disabilities — connecting them with our ponies in a way that's therapeutic, joyful, and life-changing.",
objectPos: "50% 50%", videoYoutubeId: "IX2DGd6m8BU",
href: "/support-us", sponsorHeading: "We are grateful for the generous support of our sponsors.",
}, ctaCards: [
{ { label: "Fundraising", title: "Sponsor a pony", description: "Your support keeps our ponies healthy and our sessions running — from hoof trimming to hay bales.", image: "/alineofponies-1920w.webp", href: "/support-us" },
label: "Get involved", { label: "Get involved", title: "Apply to volunteer", description: "No horse experience needed. Sidestep walkers, groomers, fundraisers — all roles make a real difference.", image: "/Vols5-2880w-1024x768.avif", href: "/volunteer-application" },
title: "Apply to volunteer", { label: "Riding sessions",title: "Apply to participate", description: "We welcome riders and carriage drivers with a wide range of physical and mental disabilities.", image: "/Samantha-and-Connolly-1280.webp", href: "/participant-application" },
sub: "No horse experience needed. Sidestep walkers, groomers, fundraisers — all roles make a real difference.", ],
img: "/Vols5-2880w-1024x768.avif", sponsors: [
objectPos: "50% 30%", { name: "Ffordes", logo: "/sponsors/ffordes_logo.png", url: null },
href: "/volunteer-application", ],
}, };
{
label: "Riding sessions", const cms = isSanityConfigured() ? (await getSanityHomePage()) : null;
title: "Apply to participate",
sub: "We welcome riders and carriage drivers with a wide range of physical and mental disabilities.", const heroHeadline = cms?.heroHeadline || DEFAULTS.heroHeadline;
img: "/Samantha-and-Connolly-1280.webp", const heroMission = cms?.heroMission || DEFAULTS.heroMission;
objectPos: "50% 38%", const videoHeading = cms?.videoHeading || DEFAULTS.videoHeading;
href: "/participant-application", const videoBody = cms?.videoBody || DEFAULTS.videoBody;
}, const videoYoutubeId = cms?.videoYoutubeId || DEFAULTS.videoYoutubeId;
]; const sponsorHeading = cms?.sponsorHeading || DEFAULTS.sponsorHeading;
const ctaCards = cms?.ctaCards?.length ? cms.ctaCards : DEFAULTS.ctaCards;
const sponsors = cms?.sponsors?.length ? cms.sponsors : DEFAULTS.sponsors;
--- ---
<BaseLayout {...pageSeo}> <BaseLayout {...pageSeo}>
@ -242,7 +245,7 @@ const ctaCards = [
</p> </p>
<div style="margin-top: 14px; width: 48px; height: 2px; background: #7DA371;" /> <div style="margin-top: 14px; width: 48px; height: 2px; background: #7DA371;" />
<h1 style="margin-top: 18px; font-family: 'Merriweather', serif; font-weight: 900; font-size: clamp(34px, 5vw, 62px); color: #F6F1E8; line-height: 1.12;"> <h1 style="margin-top: 18px; font-family: 'Merriweather', serif; font-weight: 900; font-size: clamp(34px, 5vw, 62px); color: #F6F1E8; line-height: 1.12;">
Enriching lives through horses in the Highlands. {heroHeadline}
</h1> </h1>
</div> </div>
@ -253,7 +256,7 @@ const ctaCards = [
> >
<div style="width: 32px; height: 3px; background: #7DA371; margin-bottom: 14px;" /> <div style="width: 32px; height: 3px; background: #7DA371; margin-bottom: 14px;" />
<blockquote style="font-family: 'Merriweather', serif; font-weight: 700; font-style: italic; font-size: clamp(15px, 1.5vw, 18px); color: #F6F1E8; line-height: 1.55; margin: 0;"> <blockquote style="font-family: 'Merriweather', serif; font-weight: 700; font-style: italic; font-size: clamp(15px, 1.5vw, 18px); color: #F6F1E8; line-height: 1.55; margin: 0;">
Our mission is to bring about meaningful and positive changes in the health and well-being of people with physical or mental disabilities through activities with our horses and ponies. {heroMission}
</blockquote> </blockquote>
</div> </div>
</div> </div>
@ -289,14 +292,14 @@ const ctaCards = [
Our Story Our Story
</p> </p>
<h2 style="font-family: 'Merriweather', serif; font-weight: 700; font-size: clamp(24px, 3vw, 38px); color: #F6F1E8; margin-bottom: 20px; line-height: 1.2; max-width: 600px;"> <h2 style="font-family: 'Merriweather', serif; font-weight: 700; font-size: clamp(24px, 3vw, 38px); color: #F6F1E8; margin-bottom: 20px; line-height: 1.2; max-width: 600px;">
What is Riding for the Disabled? {videoHeading}
</h2> </h2>
<p style="font-family: 'Public Sans', sans-serif; font-weight: 600; font-size: 16px; color: rgba(246,241,232,0.75); line-height: 1.65; max-width: 680px; margin-bottom: 40px;"> <p style="font-family: 'Public Sans', sans-serif; font-weight: 600; font-size: 16px; color: rgba(246,241,232,0.75); line-height: 1.65; max-width: 680px; margin-bottom: 40px;">
Across the Highlands, we provide riding sessions for people with a wide range of physical and mental disabilities — connecting them with our ponies in a way that's therapeutic, joyful, and life-changing. {videoBody}
</p> </p>
<div class="video-embed" style="max-width: 860px; margin: 0 auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5);"> <div class="video-embed" style="max-width: 860px; margin: 0 auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5);">
<iframe <iframe
src="https://www.youtube.com/embed/IX2DGd6m8BU?rel=0&modestbranding=1" src={`https://www.youtube.com/embed/${videoYoutubeId}?rel=0&modestbranding=1`}
title="What is Riding for the Disabled Association (RDA)?" title="What is Riding for the Disabled Association (RDA)?"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen allowfullscreen
@ -329,9 +332,9 @@ const ctaCards = [
<a class="cta-card" href={card.href}> <a class="cta-card" href={card.href}>
<img <img
class="cta-card-img" class="cta-card-img"
src={card.img} src={card.image ?? ''}
alt={card.title} alt={card.title}
style={`object-position: ${card.objectPos};`} style="object-position: 50% 35%;"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> />
@ -344,7 +347,7 @@ const ctaCards = [
{card.title} {card.title}
</h3> </h3>
<p class="cta-sub" style="font-family: 'Public Sans', sans-serif; font-weight: 600; font-size: 14px; color: rgba(255,255,255,0.8); line-height: 1.6; max-width: 260px;"> <p class="cta-sub" style="font-family: 'Public Sans', sans-serif; font-weight: 600; font-size: 14px; color: rgba(255,255,255,0.8); line-height: 1.6; max-width: 260px;">
{card.sub} {card.description}
</p> </p>
<div class="cta-more" style="margin-top: 16px; display: flex; align-items: center; gap: 8px;"> <div class="cta-more" style="margin-top: 16px; display: flex; align-items: center; gap: 8px;">
<span style="font-family: 'Public Sans', sans-serif; font-weight: 800; font-size: 13px; color: white; letter-spacing: 0.08em; text-transform: uppercase;"> <span style="font-family: 'Public Sans', sans-serif; font-weight: 800; font-size: 13px; color: white; letter-spacing: 0.08em; text-transform: uppercase;">
@ -367,14 +370,19 @@ const ctaCards = [
</p> </p>
<div style="width: 36px; height: 2px; background: #7DA371; margin: 0 auto 32px;" /> <div style="width: 36px; height: 2px; background: #7DA371; margin: 0 auto 32px;" />
<p style="font-family: 'Merriweather', serif; font-weight: 700; font-size: clamp(20px, 2.5vw, 30px); color: #F6F1E8; text-align: center; margin-bottom: 48px; line-height: 1.2;"> <p style="font-family: 'Merriweather', serif; font-weight: 700; font-size: clamp(20px, 2.5vw, 30px); color: #F6F1E8; text-align: center; margin-bottom: 48px; line-height: 1.2;">
We are grateful for the generous support of our sponsors. {sponsorHeading}
</p> </p>
<!-- Current sponsors --> <!-- Current sponsors -->
<div style="display: flex; flex-wrap: wrap; gap: 40px; justify-content: center; align-items: center; margin-bottom: 64px;"> <div style="display: flex; flex-wrap: wrap; gap: 40px; justify-content: center; align-items: center; margin-bottom: 64px;">
<div class="sponsor-logo"> {sponsors.map((s) => (
<img src="/sponsors/ffordes_logo.png" alt="Ffordes" loading="lazy" decoding="async" /> <div class="sponsor-logo">
</div> {s.url
? <a href={s.url} target="_blank" rel="noreferrer"><img src={s.logo ?? ''} alt={s.name} loading="lazy" decoding="async" /></a>
: <img src={s.logo ?? ''} alt={s.name} loading="lazy" decoding="async" />
}
</div>
))}
</div> </div>
<!-- Corporate sponsorship package --> <!-- Corporate sponsorship package -->

View file

@ -0,0 +1,83 @@
import { defineType, defineField, defineArrayMember } from 'sanity';
export const homePageType = defineType({
name: 'homePage',
title: 'Home Page',
type: 'document',
// Singleton — only one document of this type should exist
__experimental_actions: ['update', 'publish', 'unpublish'],
fields: [
defineField({
name: 'heroHeadline',
title: 'Hero headline',
type: 'string',
}),
defineField({
name: 'heroMission',
title: 'Mission statement (hero blockquote)',
type: 'text',
rows: 3,
}),
defineField({
name: 'videoHeading',
title: 'Video section heading',
type: 'string',
}),
defineField({
name: 'videoBody',
title: 'Video section body text',
type: 'text',
rows: 3,
}),
defineField({
name: 'videoYoutubeId',
title: 'YouTube video ID',
type: 'string',
description: 'Just the ID, e.g. IX2DGd6m8BU — not the full URL',
}),
defineField({
name: 'ctaCards',
title: 'CTA cards (editorial panel)',
type: 'array',
of: [
defineArrayMember({
type: 'object',
name: 'ctaCard',
fields: [
defineField({ name: 'label', title: 'Eyebrow label', type: 'string' }),
defineField({ name: 'title', title: 'Card title', type: 'string' }),
defineField({ name: 'description', title: 'Description', type: 'text', rows: 2 }),
defineField({ name: 'image', title: 'Background image', type: 'image', options: { hotspot: true } }),
defineField({ name: 'href', title: 'Link path', type: 'string' }),
],
preview: { select: { title: 'title', subtitle: 'label' } },
}),
],
}),
defineField({
name: 'sponsorHeading',
title: 'Sponsors section heading',
type: 'string',
}),
defineField({
name: 'sponsors',
title: 'Sponsors',
type: 'array',
of: [
defineArrayMember({
type: 'object',
name: 'sponsor',
fields: [
defineField({ name: 'name', title: 'Name', type: 'string' }),
defineField({ name: 'logo', title: 'Logo', type: 'image' }),
defineField({ name: 'url', title: 'Website URL', type: 'url' }),
],
preview: { select: { title: 'name' } },
}),
],
}),
],
preview: {
prepare: () => ({ title: 'Home Page' }),
},
});

View file

@ -1,3 +1,4 @@
import { postType } from './postType'; import { postType } from './postType';
import { homePageType } from './homePageType';
export const schemaTypes = [postType]; export const schemaTypes = [postType, homePageType];