From 1be3ad0410aad4f58a60b7968fcb118240e9c403 Mon Sep 17 00:00:00 2001 From: Jeffrey Hales Date: Thu, 11 Jun 2026 13:16:36 -0700 Subject: [PATCH] refactor(homepage): drive homepage from structured frontmatter - replace the flat homepage sections array with a named home: block - validate homepage sections with typed Zod schemas - make HomePage.astro render the structured content file data - keep English and Spanish homepage copy easier to scan and edit --- www/src/components/HomePage.astro | 63 ++++++-- www/src/content.config.ts | 4 +- www/src/content/pages/en/index.md | 32 ++-- www/src/content/pages/es/index.md | 32 ++-- www/src/types/home-sections.ts | 233 +++++++++++++++++++----------- 5 files changed, 240 insertions(+), 124 deletions(-) diff --git a/www/src/components/HomePage.astro b/www/src/components/HomePage.astro index 8980bca..685f7ff 100644 --- a/www/src/components/HomePage.astro +++ b/www/src/components/HomePage.astro @@ -11,26 +11,61 @@ import HomeProcess from './home/HomeProcess.astro'; import HomeServicesIntro from './home/HomeServicesIntro.astro'; import HomeSkills from './home/HomeSkills.astro'; import HomeTestimonials from './home/HomeTestimonials.astro'; -import type { HomeSection } from '../types/home-sections'; +import type { HomePageContent, HomeSection } from '../types/home-sections'; interface Props { lang?: string } const { lang = 'en' } = Astro.props; const es = lang === 'es'; const entry = await getEntry('pages', es ? 'es/index' : 'en/index'); if (!entry) throw new Error(`[HomePage] Content entry not found for lang="${lang}". If running in dev, restart the dev server to rebuild the content layer cache.`); -const { title, description, canonical, sections = [] } = entry.data; +const { title, description, canonical } = entry.data; + +function fromHomeContent(home: HomePageContent): HomeSection[] { + return [ + { type: 'hero', ...home.hero }, + { type: 'services-intro', ...home.servicesIntro }, + { type: 'benefits', ...home.benefits }, + { type: 'skills', ...home.skills }, + { type: 'insurance', ...home.insurance }, + { type: 'esa', ...home.esa }, + { type: 'financial-help', ...home.financialHelp }, + { type: 'process', ...home.process }, + { type: 'director', ...home.director }, + { type: 'testimonials', ...home.testimonials }, + ]; +} + +const sections: HomeSection[] = entry.data.home + ? fromHomeContent(entry.data.home) + : ((entry.data.sections ?? []) as HomeSection[]); + +function renderSection(section: HomeSection) { + switch (section.type) { + case 'hero': + return ; + case 'services-intro': + return ; + case 'benefits': + return ; + case 'skills': + return ; + case 'insurance': + return ; + case 'esa': + return ; + case 'financial-help': + return ; + case 'process': + return ; + case 'director': + return ; + case 'testimonials': + return ; + } + + return null; +} --- - {(sections as HomeSection[]).map((section) => { - if (section.type === 'hero') return ; - if (section.type === 'services-intro') return ; - if (section.type === 'benefits') return ; - if (section.type === 'skills') return ; - if (section.type === 'insurance') return ; - if (section.type === 'esa') return ; - if (section.type === 'financial-help') return ; - if (section.type === 'process') return ; - if (section.type === 'director') return ; - if (section.type === 'testimonials') return ; - })} + {sections.map(renderSection)} diff --git a/www/src/content.config.ts b/www/src/content.config.ts index b9320f8..d412a82 100644 --- a/www/src/content.config.ts +++ b/www/src/content.config.ts @@ -1,5 +1,6 @@ import { defineCollection, z } from 'astro:content'; import { glob } from 'astro/loaders'; +import { homePageSchema, homeSectionsSchema } from './types/home-sections'; const language = z.enum(['en', 'ar', 'es']); const shared = { @@ -12,7 +13,8 @@ const shared = { lang: language, translationKey: z.string().optional(), draft: z.boolean().default(false), - sections: z.array(z.record(z.unknown())).optional() + home: homePageSchema.optional(), + sections: homeSectionsSchema.optional() }; const pages = defineCollection({ diff --git a/www/src/content/pages/en/index.md b/www/src/content/pages/en/index.md index 428e44a..d135185 100644 --- a/www/src/content/pages/en/index.md +++ b/www/src/content/pages/en/index.md @@ -6,8 +6,9 @@ canonical: "https://www.azinstitute4autism.com/" lang: "en" translationKey: "index" draft: false -sections: - - type: hero +home: + # Hero + hero: eyebrow: "Arizona's Leading Experts" heading: "Behavioral Health
& Special
Education" body: "Here at the Arizona Institute for Autism (AIA), we provide expert clinical care for children and teens who have an autism diagnosis. We currently serve families in Scottsdale, Gilbert, Mesa, Tempe, and Phoenix Metropolitan areas." @@ -17,7 +18,8 @@ sections: image: bcba-with-happy-toddler.webp imageAlt: BCBA therapist with happy toddler - - type: services-intro + # Services and commitments + servicesIntro: servicesHeading: ABA Therapy Services for Children and Teens Diagnosed with Autism Spectrum Disorder services: - Autism Advocacy Support @@ -42,7 +44,8 @@ sections: commitmentsImage: playing-boy.webp commitmentsImageAlt: Boy playing while learning - - type: benefits + # Benefits + benefits: heading: Benefits that go Beyond a Typical Integrated Therapy Provider subheading: The AIA Difference items: @@ -55,7 +58,8 @@ sections: videoImage: learner-journey.webp videoImageAlt: Learner journey for children with autism video - - type: skills + # Skills + skills: heading: Does Your Learner Struggle With These Skills? skills: - icon: icon-Social_Engagement.png @@ -71,7 +75,8 @@ sections: - icon: icon-Speach.png label: Speech - - type: insurance + # Insurance + insurance: heading: "Yes, We Take Insurance" body: "An all-in-one integrated service for your kiddo's special education needs, the Arizona Institute for Autism (AIA) offers and accepts most insurance plans that cover ABA therapy. These include BCBS AZ, Aetna, Optum, Tricare, United Healthcare AHCCCS, and UnitedHealth." logos: @@ -88,7 +93,8 @@ sections: - file: united-healthcare-logo-1.webp alt: United Healthcare - - type: esa + # ESA + esa: heading: Arizona Scholarship Account (ESA) body: >- Thousands of Arizona learners are currently eligible for the state funded ESA, @@ -100,14 +106,16 @@ sections: image: logo-az-dept-of-education.webp imageAlt: Arizona Department of Education - - type: financial-help + # Financial help + financialHelp: heading: Need Help Paying for Your Care? body: The Arizona Institute for Autism offers flexible financial assistance to the uninsured. Get the care you want or need and pay over time. cta: label: Request an Appointment href: /client-consultation - - type: process + # Process + process: heading: Our Process steps: - icon: learner-journey-step-1a.svg @@ -123,7 +131,8 @@ sections: - icon: learner-journey-step-6.svg label: Collaborate on a Care Plan - - type: director + # Director + director: heading: An Extension of Your Family quote: '"At AIA, We strive to provide excellent and compassionate care to all communities we serve. We could not accomplish that without excellent, passionate, and committed staff and families. We look forward to our collaboration with you as we continue to grow, serve and support our Learners in the pursuit of their individual potential."' photo: rula-diab.webp @@ -133,7 +142,8 @@ sections: signature: clinical-director-rula-diab.png signatureAlt: Clinical Director Rula Diab - - type: testimonials + # Testimonials + testimonials: heading: What Clients Are Saying items: - author: Claudia diff --git a/www/src/content/pages/es/index.md b/www/src/content/pages/es/index.md index 6797fad..0761fac 100644 --- a/www/src/content/pages/es/index.md +++ b/www/src/content/pages/es/index.md @@ -6,8 +6,9 @@ canonical: "https://www.azinstitute4autism.com/es" lang: "es" translationKey: "index" draft: false -sections: - - type: hero +home: + # Hero + hero: eyebrow: Los principales expertos de Arizona heading: "Salud Mental y
Educación Especial" body: "Aquí en el Instituto de Autismo de Arizona (AIA), brindamos atención clínica experta para niños y adolescentes con diagnóstico de autismo. Actualmente atendemos a familias en las áreas metropolitanas de Scottsdale, Gilbert, Mesa, Tempe y Phoenix." @@ -17,7 +18,8 @@ sections: image: bcba-with-happy-toddler.webp imageAlt: Terapeuta BCBA con niño feliz - - type: services-intro + # Servicios y compromisos + servicesIntro: servicesHeading: Servicios de terapia ABA para niños y adolescentes diagnosticados con TEA services: - Apoyo a la defensa del autismo @@ -42,7 +44,8 @@ sections: commitmentsImage: playing-boy.webp commitmentsImageAlt: Niño jugando mientras aprende - - type: benefits + # Beneficios + benefits: heading: Beneficios que van más allá de un proveedor de terapia integrada típico subheading: La Diferencia AIA items: @@ -55,7 +58,8 @@ sections: videoImage: learner-journey.webp videoImageAlt: Video del recorrido del estudiante - - type: skills + # Habilidades + skills: heading: ¿Su alumno tiene dificultades con estas habilidades? skills: - icon: icon-Social_Engagement.png @@ -71,7 +75,8 @@ sections: - icon: icon-Speach.png label: Discurso - - type: insurance + # Seguros + insurance: heading: "Sí, aceptamos seguros." body: "Un servicio integrado todo en uno para las necesidades de educación especial de su hijo, el Instituto de Autismo de Arizona (AIA) ofrece y acepta la mayoría de los planes de seguro que cubren la terapia ABA. Estos incluyen BCBS AZ, Aetna, Optum, Tricare, United Healthcare AHCCCS y UnitedHealth." logos: @@ -88,7 +93,8 @@ sections: - file: united-healthcare-logo-1.webp alt: United Healthcare - - type: esa + # ESA + esa: heading: Cuenta de Becas de Arizona (ESA) body: >- Miles de estudiantes de Arizona son actualmente elegibles para la ESA financiada @@ -101,14 +107,16 @@ sections: image: logo-az-dept-of-education.webp imageAlt: Departamento de Educación de Arizona - - type: financial-help + # Ayuda financiera + financialHelp: heading: ¿Necesita ayuda para pagar su atención? body: El Instituto de Autismo de Arizona ofrece asistencia financiera flexible a las personas sin seguro. Obtén la atención que deseas o necesitas y paga a plazos. cta: label: Solicitar cita href: /es/client-consultation - - type: process + # Proceso + process: heading: Nuestro proceso steps: - icon: learner-journey-step-1a.svg @@ -124,7 +132,8 @@ sections: - icon: learner-journey-step-6.svg label: Colaborar en un Plan de Cuidados - - type: director + # Directora + director: heading: Una extensión de tu familia quote: '"En AIA, nos esforzamos por brindar atención excelente y compasiva a todas las comunidades a las que servimos. No podríamos lograr eso sin un personal y familias excelentes, apasionados y comprometidos. Esperamos con ansias nuestra colaboración con usted a medida que continuamos creciendo, sirviendo y apoyando a nuestros estudiantes en la búsqueda de su potencial individual."' photo: rula-diab.webp @@ -134,7 +143,8 @@ sections: signature: clinical-director-rula-diab.png signatureAlt: Directora Clínica Rula Diab - - type: testimonials + # Testimonios + testimonials: heading: Lo que dicen los clientes items: - author: Claudia diff --git a/www/src/types/home-sections.ts b/www/src/types/home-sections.ts index 2484985..c5e285d 100644 --- a/www/src/types/home-sections.ts +++ b/www/src/types/home-sections.ts @@ -1,97 +1,156 @@ -export interface HeroSection { - type: 'hero'; - eyebrow: string; - heading: string; - body: string; - cta: { label: string; href: string }; - image: string; - imageAlt: string; -} +import { z } from 'astro:content'; -export interface ServicesIntroSection { - type: 'services-intro'; - servicesHeading: string; - services: string[]; - servicesImage: string; - servicesImageAlt: string; - commitmentsHeading: string; - commitments: string[]; - commitmentsCta: { label: string; href: string }; - commitmentsImage: string; - commitmentsImageAlt: string; -} +const ctaSchema = z.object({ + label: z.string(), + href: z.string(), +}); -export interface BenefitsSection { - type: 'benefits'; - heading: string; - subheading: string; - items: string[]; - videoUrl: string; - videoTitle: string; - videoImage: string; - videoImageAlt: string; -} +const heroSectionSchema = z.object({ + type: z.literal('hero'), + eyebrow: z.string(), + heading: z.string(), + body: z.string(), + cta: ctaSchema, + image: z.string(), + imageAlt: z.string(), +}); -export interface SkillsSection { - type: 'skills'; - heading: string; - skills: { icon: string; label: string }[]; -} +const servicesIntroSectionSchema = z.object({ + type: z.literal('services-intro'), + servicesHeading: z.string(), + services: z.array(z.string()), + servicesImage: z.string(), + servicesImageAlt: z.string(), + commitmentsHeading: z.string(), + commitments: z.array(z.string()), + commitmentsCta: ctaSchema, + commitmentsImage: z.string(), + commitmentsImageAlt: z.string(), +}); -export interface InsuranceSection { - type: 'insurance'; - heading: string; - body: string; - logos: { file: string; alt: string }[]; -} +const benefitsSectionSchema = z.object({ + type: z.literal('benefits'), + heading: z.string(), + subheading: z.string(), + items: z.array(z.string()), + videoUrl: z.string(), + videoTitle: z.string(), + videoImage: z.string(), + videoImageAlt: z.string(), +}); -export interface EsaSection { - type: 'esa'; - heading: string; - body: string; - image: string; - imageAlt: string; -} +const skillsSectionSchema = z.object({ + type: z.literal('skills'), + heading: z.string(), + skills: z.array(z.object({ + icon: z.string(), + label: z.string(), + })), +}); -export interface FinancialHelpSection { - type: 'financial-help'; - heading: string; - body: string; - cta: { label: string; href: string }; -} +const insuranceSectionSchema = z.object({ + type: z.literal('insurance'), + heading: z.string(), + body: z.string(), + logos: z.array(z.object({ + file: z.string(), + alt: z.string(), + })), +}); -export interface ProcessSection { - type: 'process'; - heading: string; - steps: { icon: string; label: string }[]; -} +const esaSectionSchema = z.object({ + type: z.literal('esa'), + heading: z.string(), + body: z.string(), + image: z.string(), + imageAlt: z.string(), +}); -export interface DirectorSection { - type: 'director'; - heading: string; - quote: string; - photo: string; - photoAlt: string; - name: string; - credentials: string; - signature: string; - signatureAlt: string; -} +const financialHelpSectionSchema = z.object({ + type: z.literal('financial-help'), + heading: z.string(), + body: z.string(), + cta: ctaSchema, +}); -export interface TestimonialsSection { - type: 'testimonials'; - heading: string; - items: { author: string; text: string }[]; -} +const processSectionSchema = z.object({ + type: z.literal('process'), + heading: z.string(), + steps: z.array(z.object({ + icon: z.string(), + label: z.string(), + })), +}); -export type HomeSection = - | HeroSection - | ServicesIntroSection - | BenefitsSection - | SkillsSection - | InsuranceSection - | EsaSection - | FinancialHelpSection - | ProcessSection - | DirectorSection - | TestimonialsSection; +const directorSectionSchema = z.object({ + type: z.literal('director'), + heading: z.string(), + quote: z.string(), + photo: z.string(), + photoAlt: z.string(), + name: z.string(), + credentials: z.string(), + signature: z.string(), + signatureAlt: z.string(), +}); + +const testimonialsSectionSchema = z.object({ + type: z.literal('testimonials'), + heading: z.string(), + items: z.array(z.object({ + author: z.string(), + text: z.string(), + })), +}); + +export const homeSectionSchema = z.discriminatedUnion('type', [ + heroSectionSchema, + servicesIntroSectionSchema, + benefitsSectionSchema, + skillsSectionSchema, + insuranceSectionSchema, + esaSectionSchema, + financialHelpSectionSchema, + processSectionSchema, + directorSectionSchema, + testimonialsSectionSchema, +]); + +export const homeSectionsSchema = z.array(homeSectionSchema); + +const homeHeroSchema = heroSectionSchema.omit({ type: true }); +const homeServicesIntroSchema = servicesIntroSectionSchema.omit({ type: true }); +const homeBenefitsSchema = benefitsSectionSchema.omit({ type: true }); +const homeSkillsSchema = skillsSectionSchema.omit({ type: true }); +const homeInsuranceSchema = insuranceSectionSchema.omit({ type: true }); +const homeEsaSchema = esaSectionSchema.omit({ type: true }); +const homeFinancialHelpSchema = financialHelpSectionSchema.omit({ type: true }); +const homeProcessSchema = processSectionSchema.omit({ type: true }); +const homeDirectorSchema = directorSectionSchema.omit({ type: true }); +const homeTestimonialsSchema = testimonialsSectionSchema.omit({ type: true }); + +export const homePageSchema = z.object({ + hero: homeHeroSchema, + servicesIntro: homeServicesIntroSchema, + benefits: homeBenefitsSchema, + skills: homeSkillsSchema, + insurance: homeInsuranceSchema, + esa: homeEsaSchema, + financialHelp: homeFinancialHelpSchema, + process: homeProcessSchema, + director: homeDirectorSchema, + testimonials: homeTestimonialsSchema, +}); + +export type HomeSection = z.infer; +export type HeroSection = z.infer; +export type ServicesIntroSection = z.infer; +export type BenefitsSection = z.infer; +export type SkillsSection = z.infer; +export type InsuranceSection = z.infer; +export type EsaSection = z.infer; +export type FinancialHelpSection = z.infer; +export type ProcessSection = z.infer; +export type DirectorSection = z.infer; +export type TestimonialsSection = z.infer; +export type HomePageContent = z.infer;