From 8baca79cdcfac39ef4c137f2c3b32f62ae05c2b3 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 8 Jul 2026 11:59:36 +0300 Subject: [PATCH 1/2] refactor(faq): make FaqSection self-contained; emit FAQPage JSON-LD Move the generic FAQ behavior (accordion, category tabs, load more, copy-link, hash deep-link) into a script co-located with the component; delete the duplicated copies (pricing inline handlers, iot-hub-faq.ts). The pricing page keeps only its page-specific parts and opens items through the new window.__tbFaqOpen API after activating the owning product tab. - New util builds schema.org FAQPage JSON (HTML stripped to plain text, entities decoded, `<` escaped so answer content can never terminate the script element); FaqSection emits it via a jsonLd prop. IoT Hub pages keep their existing schema (FaqJsonLd.astro absorbed); /pricing/ gains one block for the default-visible Public Cloud context. - A11y: copy-link is a real button (was span[role=button] nested inside the question button - invalid nesting, and keyboard-dead on pricing); the heading wraps only the toggle so its accessible name is the question alone; aria-controls wired to new answer ids; aria-current SSR'd on active category tabs; focus-visible styles on both buttons. - Analytics via an opt-in prop: pricing keeps faq_copy_link / faq_node_interaction, iot-hub stays silent (both as before). One deliberate change: repeat jumps to an already-open item no longer re-push faq_node_interaction (opens only, matching the accordion handler's documented semantics). - Fix a duplicate DOM id in the self-managed FAQ data (the second item was unreachable by deep link and blocked aria-controls). --- src/components/IotHub/CategoryFaq.astro | 11 +- src/components/IotHub/FaqJsonLd.astro | 32 --- src/components/IotHub/iot-hub-faq.ts | 127 ----------- src/components/Pricing/FaqSection.astro | 279 +++++++++++++++++++++--- src/data/pricing/faq/tb-self-managed.ts | 4 +- src/pages/pricing/index.astro | 161 ++++---------- src/util/faq-schema.ts | 49 +++++ 7 files changed, 342 insertions(+), 321 deletions(-) delete mode 100644 src/components/IotHub/FaqJsonLd.astro delete mode 100644 src/components/IotHub/iot-hub-faq.ts create mode 100644 src/util/faq-schema.ts diff --git a/src/components/IotHub/CategoryFaq.astro b/src/components/IotHub/CategoryFaq.astro index 670afe7cc7..2aa2cdaee3 100644 --- a/src/components/IotHub/CategoryFaq.astro +++ b/src/components/IotHub/CategoryFaq.astro @@ -1,6 +1,5 @@ --- import FaqSection from '@components/Pricing/FaqSection.astro'; -import FaqJsonLd from '@components/IotHub/FaqJsonLd.astro'; import { getCategoryFaq } from '@data/iot-hub/sections'; import { IOT_HUB_STRINGS } from '@models/iot-hub'; @@ -14,10 +13,9 @@ const groups = getCategoryFaq(categorySlug); { groups && ( -
+

{IOT_HUB_STRINGS.faqHeading}

- - +
) } @@ -38,8 +36,3 @@ const groups = getCategoryFaq(categorySlug); @include iot-hub-section-title; } - - diff --git a/src/components/IotHub/FaqJsonLd.astro b/src/components/IotHub/FaqJsonLd.astro deleted file mode 100644 index c72c620e24..0000000000 --- a/src/components/IotHub/FaqJsonLd.astro +++ /dev/null @@ -1,32 +0,0 @@ ---- -import type { FaqGroup } from '@models/iot-hub-sections'; - -interface Props { - groups: FaqGroup[]; -} - -const { groups } = Astro.props; - -const stripHtml = (s: string) => - s - .replace(/<[^>]+>/g, '') - .replace(/\s+/g, ' ') - .trim(); - -const jsonLd = { - '@context': 'https://schema.org', - '@type': 'FAQPage', - mainEntity: groups.flatMap((group) => - group.items.map((item) => ({ - '@type': 'Question', - name: item.question, - acceptedAnswer: { - '@type': 'Answer', - text: stripHtml(item.answer), - }, - })) - ), -}; ---- - - +