feat(seo): separate display headings and improve social metadata
This commit is contained in:
17 files changed
+707
-12
No files matched your search
@@ -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} />}
|
||||
@@ -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>
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user