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:
parent
921e03e22e
commit
87182583f8
4 changed files with 183 additions and 39 deletions
|
|
@ -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[]> {
|
||||||
|
|
|
||||||
|
|
@ -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;">
|
||||||
|
{sponsors.map((s) => (
|
||||||
<div class="sponsor-logo">
|
<div class="sponsor-logo">
|
||||||
<img src="/sponsors/ffordes_logo.png" alt="Ffordes" loading="lazy" decoding="async" />
|
{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>
|
</div>
|
||||||
|
|
||||||
<!-- Corporate sponsorship package -->
|
<!-- Corporate sponsorship package -->
|
||||||
|
|
|
||||||
83
studio/schemaTypes/homePageType.ts
Normal file
83
studio/schemaTypes/homePageType.ts
Normal 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' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue