feat(seo): separate display headings and improve social metadata

This commit is contained in:
DeCentN2Madness committed 2026-10-08 14:50:54 -07:00
1 parent c720917627
commit 4ed01d7d28
17 files changed
+707 -12

No files matched your search

+9 -4
View File
@@ -1,12 +1,14 @@
---
import { allowIndexing } from '../utils/publication';
import { hreflangLinksFor, robotsFor, type TranslatedPublication } from '../utils/publication-policy';
import { socialImageUrl } from '../utils/seo';
import site from '../data/site.json';
interface Props {
title: string;
description?: string;
publication: TranslatedPublication;
image?: string;
lang?: string;
imageAlt?: string;
type?: 'website' | 'article';
}
const {
@@ -14,11 +16,12 @@ const {
description = '',
publication,
image,
lang = 'en',
imageAlt,
type = 'website'
} = Astro.props;
const { canonical } = publication;
const absoluteImage = image ? new URL(image, Astro.site ?? Astro.url).href : undefined;
const absoluteImage = socialImageUrl(image, site.url);
const imageDescription = absoluteImage && imageAlt?.trim() ? imageAlt : undefined;
---
<title>{title}</title>
<meta name="description" content={description} />
@@ -30,7 +33,9 @@ const absoluteImage = image ? new URL(image, Astro.site ?? Astro.url).href : und
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
{absoluteImage && <meta property="og:image" content={absoluteImage} />}
{imageDescription && <meta property="og:image:alt" content={imageDescription} />}
<meta name="twitter:card" content={absoluteImage ? 'summary_large_image' : 'summary'} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta property="og:locale" content={lang} />
{absoluteImage && <meta name="twitter:image" content={absoluteImage} />}
{imageDescription && <meta name="twitter:image:alt" content={imageDescription} />}
+3 -3
View File
@@ -1,14 +1,14 @@
---
import Button from '../Button.astro';
import type { HeroSection } from '../../types/home-sections';
interface Props { section: HeroSection }
const { section } = Astro.props;
interface Props { section: HeroSection; displayH1?: string }
const { section, displayH1 } = Astro.props;
---
<section class="home-hero">
<div class="container home-hero-grid">
<div>
<p class="eyebrow">{section.eyebrow}</p>
<h1 set:html={section.heading} />
{displayH1 === undefined ? <h1 set:html={section.heading} /> : <h1>{displayH1}</h1>}
<p>{section.body}</p>
<Button href={section.cta.href}>{section.cta.label}</Button>
</div>
+1 -1
View File
@@ -40,7 +40,7 @@ const sections: HomeSection[] = entry.data.home
{sections.map((section) => {
switch (section.type) {
case 'hero':
return <HomeHero section={section} />;
return <HomeHero section={section} displayH1={entry.data.displayH1} />;
case 'services-intro':
return <HomeServicesIntro section={section} />;
case 'benefits':
+3
View File
@@ -9,6 +9,9 @@ const translationKey = z.string().min(1).refine((value) => value === value.trim(
}).optional();
const shared = {
title: z.string(),
displayH1: z.string().refine((value) => value.trim().length > 0, {
message: 'displayH1 must not be empty or whitespace-only'
}).optional(),
description: z.string().default(''),
slug: z.string(),
canonical: z.url().optional(),
@@ -1,5 +1,6 @@
---
title: "Unlock Independence for Children with Autism Through ABA Self‑Advocacy"
displayH1: "Guide to Teaching Self‑Advocacy in ABA"
description: "Empower your child with individualized ABA therapy at Arizona Institute for Autism. Book a free consultation and kickstart personalized autism care today!"
slug: "autism-self-advocacy-skills-aba"
canonical: "https://www.azinstitute4autism.com/library/autism-self-advocacy-skills-aba"
@@ -1,5 +1,6 @@
---
title: "Desbloqueando la independencia para niños con autismo a través de la autodefensa ABA"
displayH1: "Guía para enseñar la autodefensa en el ABA"
description: "Empodere a su hijo con terapia ABA individualizada en el Instituto de Autismo de Arizona. ¡Reserva una consulta gratuita y comienza hoy mismo la atención personalizada para el autismo!"
slug: "autism-self-advocacy-skills-aba"
canonical: "https://www.azinstitute4autism.com/es/library/autism-self-advocacy-skills-aba"
+5 -1
View File
@@ -8,6 +8,10 @@ import '../styles/global.css';
import '../styles/rtl.css';
const { title, description, image, lang = 'en', type = 'website' } = Astro.props;
const publication = await getPublication(Astro.url.pathname);
// Reuse alt text only for the image it describes, not a fallback banner image.
const imageAlt = image && image === publication.entry.data.featuredImage
? publication.entry.data.alt
: undefined;
const dir = lang === 'ar' ? 'rtl' : 'ltr';
const organizationSchema = {
'@context': 'https://schema.org',
@@ -32,7 +36,7 @@ const organizationSchema = {
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" href="/assets/images/favicon.svg" />
<Seo title={title} description={description} publication={publication} image={image} lang={lang} type={type} />
<Seo title={title} description={description} publication={publication} image={image} imageAlt={imageAlt} type={type} />
<slot name="head" />
<script type="application/ld+json" is:inline set:html={JSON.stringify(organizationSchema)} />
</head>
+3 -1
View File
@@ -1,6 +1,7 @@
---
import BaseLayout from './BaseLayout.astro';
import PageHero from '../components/PageHero.astro';
import { getPublication } from '../utils/publication';
interface Props {
title: string;
@@ -10,9 +11,10 @@ interface Props {
}
const { title, description, intro = [], lang = 'en' } = Astro.props;
const { entry } = await getPublication(Astro.url.pathname);
---
<BaseLayout title={title} description={description} lang={lang}>
<PageHero title={title} image="/assets/images/hero-library-index.webp" eyebrow="Welcome to" constrain />
<PageHero title={entry.data.displayH1 ?? title} image="/assets/images/hero-library-index.webp" eyebrow="Welcome to" constrain />
<section class="library-intro">
<div class="container">
<h2>Library</h2>
+1 -1
View File
@@ -7,7 +7,7 @@ const { entry } = Astro.props;
<BaseLayout title={entry.data.title} description={entry.data.description} image={entry.data.featuredImage} lang={entry.data.lang} type="article">
<article class="blog-post">
<header class="article-header container">
<h1>{entry.data.title}</h1>
<h1>{entry.data.displayH1 ?? entry.data.title}</h1>
<div class="article-byline">
<img src="/assets/images/rula-diab-avatar.jpg" alt="" />
<p><strong>Rula Diab, Clinical Director, M.Ed, BCBA, LBA</strong><br /><time datetime={entry.data.date.toISOString()}>{entry.data.date.toLocaleDateString(entry.data.lang, { day: '2-digit', month: 'long', year: 'numeric' })}</time></p>
+1 -1
View File
@@ -57,7 +57,7 @@ const heroImage = pageHeroImages[entry.data.slug];
---
<BaseLayout title={entry.data.title} description={entry.data.description} image={entry.data.featuredImage || heroImage} lang={entry.data.lang}>
{entry.data.slug === 'contact' && <Fragment slot="head"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" /></Fragment>}
<PageHero title={bannerTitle} subtitle={entry.data.slug === 'faqs' ? 'FAQs' : undefined} image={heroImage} constrain />
<PageHero title={entry.data.displayH1 ?? bannerTitle} subtitle={entry.data.slug === 'faqs' ? 'FAQs' : undefined} image={heroImage} constrain />
{entry.data.slug === 'team' && entry.data.lang === 'en'
? <TeamPage />
: entry.data.slug === 'about' && entry.data.lang === 'en'
+9
View File
@@ -0,0 +1,9 @@
/** Resolve social assets against the public site, never a preview host or canonical. */
export function socialImageUrl(image: string | undefined, site: string): string | undefined {
if (!image?.trim()) return undefined;
const url = new URL(image, new URL('/', site));
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
throw new Error(`Invalid social image URL: ${image}`);
}
return url.href;
}