feat(seo): add validated article and FAQ structured data

This commit is contained in:
DeCentN2Madness committed 2026-10-08 17:38:06 -07:00
1 parent 4ed01d7d28
commit 4079156776
9 files changed
+773 -29

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ const readMore = { en: 'Read More', es: 'Leer Más', ar: 'اقرأ المزيد'
<article class="card blog-card">
{post.data.featuredImage && <a href={href}><img src={post.data.featuredImage} alt={post.data.alt || ''} loading="lazy" /></a>}
<div class="card-body">
<div class="blog-card-meta"><img src="/assets/images/rula-diab-avatar.jpg" alt="" /><p>Rula Diab, Clinical Director, M.Ed, BCBA, LBA<br /><time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString(post.data.lang, { day: 'numeric', month: 'long', year: 'numeric' })}</time></p></div>
<div class="blog-card-meta"><img src="/assets/images/rula-diab-avatar.jpg" alt="" /><p>Rula Diab, Clinical Director, M.Ed, BCBA, LBA<br /><time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString(post.data.lang, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}</time></p></div>
<h3><a href={href}>{post.data.title}</a></h3>
<p>{post.data.description}</p>
<div class="blog-card-footer" style="align-items:center;display:flex;justify-content:space-between;gap:var(--space-lg);margin-top:auto;">
+22 -19
View File
@@ -1,9 +1,6 @@
---
interface FAQItem {
question: string;
answer: string;
answerHtml?: string;
}
import { getPublication } from '../utils/publication';
import { faqItemsFromSourceHtml, faqPageSchema, serializeJsonLd, type FAQItem } from '../utils/structured-data';
interface Props {
label?: string;
@@ -12,6 +9,7 @@ interface Props {
canonical?: string;
variant?: 'blog' | 'source' | 'page';
sourceSelector?: string;
sourceHtml?: string;
searchSelector?: string;
faqIconColor?: string;
}
@@ -23,6 +21,7 @@ const {
canonical,
variant = 'blog',
sourceSelector,
sourceHtml,
searchSelector,
faqIconColor = '--color-accent-strong'
} = Astro.props;
@@ -34,19 +33,23 @@ const headingId = variant === 'page'
: label
? 'blog-faq-heading'
: 'source-faq-heading';
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
...(canonical ? { '@id': `${canonical}#faq` } : {}),
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer
}
}))
};
const publication = await getPublication(Astro.url.pathname);
if (canonical && canonical.replace(/\/$/, '') !== publication.canonical.replace(/\/$/, '')) {
throw new Error(`FAQ canonical disagrees with publication policy: ${publication.route}`);
}
if ((sourceSelector && items.length) || (sourceHtml !== undefined && !sourceSelector)) {
throw new Error('FAQ items and runtime source content must not be mixed');
}
const schemaItems = sourceSelector
? sourceHtml === undefined ? [] : faqItemsFromSourceHtml(sourceHtml)
: items;
const schema = faqPageSchema(schemaItems, publication);
// Locals belong to this render request, not a module-level build-wide registry.
const state = Astro.locals as { aiaFaqSchemaClaimed?: boolean };
if (schema) {
if (state.aiaFaqSchemaClaimed) throw new Error(`Multiple FAQ schemas: consolidate accordions on ${publication.route}`);
state.aiaFaqSchemaClaimed = true;
}
---
<section
class:list={[
@@ -83,7 +86,7 @@ const schema = {
</div>
</div>
</section>
{items.length > 0 && <script type="application/ld+json" is:inline set:html={JSON.stringify(schema)} />}
{schema && <script type="application/ld+json" is:inline set:html={serializeJsonLd(schema)} />}
<script>
const initFaqAccordions = () => {
+3 -3
View File
@@ -1,12 +1,11 @@
---
import Button from '../Button.astro';
import FAQAccordion from '../FAQAccordion.astro';
const sourceHtml = await Astro.slots.render('default');
---
<article class="faq-page">
<div id="faq-page-source" class="faq-source container source-page">
<slot />
</div>
<div id="faq-page-source" class="faq-source container source-page" set:html={sourceHtml} />
<section class="faq-search container-narrow">
<div class="hs-search-field hs-search-field--initialized">
@@ -36,6 +35,7 @@ import FAQAccordion from '../FAQAccordion.astro';
heading="Questions & Answers"
variant="page"
sourceSelector="#faq-page-source"
sourceHtml={sourceHtml}
searchSelector="#faq-search-input"
/>