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 ────────────────────────────────────────────────────────────────
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 SiteFooter from "../components/SiteFooter.astro";
import { navigationItems } from "../lib/navigation";
import { isSanityConfigured, getSanityHomePage } from "../lib/sanity";
const pageSeo = {
title: "Highland Group RDA",
@ -13,32 +14,34 @@ const pageSeo = {
canonicalPath: "/",
};
const ctaCards = [
{
label: "Fundraising",
title: "Sponsor a pony",
sub: "Your support keeps our ponies healthy and our sessions running — from hoof trimming to hay bales.",
img: "/alineofponies-1920w.webp",
objectPos: "50% 50%",
href: "/support-us",
},
{
label: "Get involved",
title: "Apply to volunteer",
sub: "No horse experience needed. Sidestep walkers, groomers, fundraisers — all roles make a real difference.",
img: "/Vols5-2880w-1024x768.avif",
objectPos: "50% 30%",
href: "/volunteer-application",
},
{
label: "Riding sessions",
title: "Apply to participate",
sub: "We welcome riders and carriage drivers with a wide range of physical and mental disabilities.",
img: "/Samantha-and-Connolly-1280.webp",
objectPos: "50% 38%",
href: "/participant-application",
},
];
// ── Fallback content (used when Sanity is not configured or document is empty) ─
const DEFAULTS = {
heroHeadline: "Enriching lives through horses in the Highlands.",
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.",
videoHeading: "What is Riding for the Disabled?",
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.",
videoYoutubeId: "IX2DGd6m8BU",
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", 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" },
{ 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" },
],
sponsors: [
{ name: "Ffordes", logo: "/sponsors/ffordes_logo.png", url: null },
],
};
const cms = isSanityConfigured() ? (await getSanityHomePage()) : null;
const heroHeadline = cms?.heroHeadline || DEFAULTS.heroHeadline;
const heroMission = cms?.heroMission || DEFAULTS.heroMission;
const videoHeading = cms?.videoHeading || DEFAULTS.videoHeading;
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}>
@ -242,7 +245,7 @@ const ctaCards = [
</p>
<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;">
Enriching lives through horses in the Highlands.
{heroHeadline}
</h1>
</div>
@ -253,7 +256,7 @@ const ctaCards = [
>
<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;">
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>
</div>
</div>
@ -289,14 +292,14 @@ const ctaCards = [
Our Story
</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;">
What is Riding for the Disabled?
{videoHeading}
</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;">
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>
<div class="video-embed" style="max-width: 860px; margin: 0 auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5);">
<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)?"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
@ -329,9 +332,9 @@ const ctaCards = [
<a class="cta-card" href={card.href}>
<img
class="cta-card-img"
src={card.img}
src={card.image ?? ''}
alt={card.title}
style={`object-position: ${card.objectPos};`}
style="object-position: 50% 35%;"
loading="lazy"
decoding="async"
/>
@ -344,7 +347,7 @@ const ctaCards = [
{card.title}
</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;">
{card.sub}
{card.description}
</p>
<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;">
@ -367,14 +370,19 @@ const ctaCards = [
</p>
<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;">
We are grateful for the generous support of our sponsors.
{sponsorHeading}
</p>
<!-- Current sponsors -->
<div style="display: flex; flex-wrap: wrap; gap: 40px; justify-content: center; align-items: center; margin-bottom: 64px;">
<div class="sponsor-logo">
<img src="/sponsors/ffordes_logo.png" alt="Ffordes" loading="lazy" decoding="async" />
</div>
{sponsors.map((s) => (
<div class="sponsor-logo">
{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>
<!-- 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 { homePageType } from './homePageType';
export const schemaTypes = [postType];
export const schemaTypes = [postType, homePageType];