refactor(faq): make FaqSection self-contained; emit FAQPage JSON-LD#539
Open
rusikv wants to merge 20 commits into
Open
refactor(faq): make FaqSection self-contained; emit FAQPage JSON-LD#539rusikv wants to merge 20 commits into
rusikv wants to merge 20 commits into
Conversation
The site has no View Transitions/ClientRouter, so astro:page-load never fires. Each setup already inits on DOMContentLoaded (or immediately) and is idempotent, so the extra listener was dead registration.
…nk rewrite
MailerLite universal.js + ml('load') now load on first interaction via __deferOnInteraction instead of at parse; UtmTracker's document.links rewrite runs at requestIdleCallback instead of DOMContentLoaded. Moves third-party/main-thread work out of the load/LCP window.
Re-encode reporting_12-1.webp at q80 (same 2400x1512 dims) — the LCP element on the 4.2 release post. Visually identical for a UI screenshot.
…walk Hero highlight (3s) and gallery autoplay (5s) setInterval timers start only when their element scrolls into view (IntersectionObserver), so they never run off-screen; FAQ deep-link hash walk + scrollIntoView deferred to requestIdleCallback.
GTM already defers to first interaction via __deferOnInteraction; this raises the no-interaction fallback so tag-manager + injected pixels bootstrap after the LCP window for idle visitors. Trade-off: analytics for non-interacting visitors fires up to ~2.5s later.
InstallButton now imports a tiny boot module that wires one delegated click listener and dynamic-imports the dialog on first trigger click. The dialog module no longer self-initializes; openFor() reads the stored local base on first open. Drops the iot-hub model chunk (~77KB raw) from the eager graph on detail and solution-template pages: eager JS 96.6->16.5KB raw (29.9->7.5KB gz), verified against the built output.
The 3s fallback assumed GTM's old 2.5s; bump to 5.5s so the intended GTM-first ordering holds for non-interacting visitors.
Same path and format (webp q80 / png palette), dimensions unchanged, visually verified: 7.6MB -> 1.8MB total. Two files that carried PNG bytes under a .webp extension are now real WebP. The remaining >300KB files were measured and skipped (recompression gains <20% or negative).
Blog-post body images rendered as bare <img> with no dimensions and eager loading. New rehype plugin (scoped to src/content/blog/) injects intrinsic width/height read from public/ at build time, keeps the first body image eager (usual LCP element), and lazy-loads the rest with decoding=async. Verified in the built output: docs pages byte-identical, every post <=1 eager image.
The header mega-menu inlined 50+ icon SVGs (~30KB gz) ahead of <main> on every page. Icons now live in one cached /nav-sprite.svg referenced via <use>; currentColor theming and per-icon aspect ratios are preserved. Regenerate with pnpm generate:nav-sprite after editing icons.
DocImage and ImageGallery hardcoded loading=lazy on every image, including a page's first content image — its likely LCP element. A per-render Astro.locals flag now marks the first image eager + fetchpriority=high (dark-theme twins load eager without the priority boost); everything after stays lazy.
196KB -> 180KB at q80, same dimensions.
Each of ~500 FAQ rows inlined two identical tabler SVGs (chevron + copy-link). Both are now shared CSS mask definitions colored via currentColor — about 170KB of markup and ~2,000 DOM nodes off /pricing/ with identical rendering, including the copied state.
Same rationale as the iot-hub batch: no ClientRouter, the event never fires; both scripts already init on DOMContentLoaded or immediately.
The deferred callback almost always runs after DOMContentLoaded, so a DCL listener registered inside it never fired and the form never received its GTM id/class for first-touch visitors. Also make the 2500ms fallback explicit: the visible demo form deliberately embeds ahead of GTM's 5s tier.
Two rapid clicks racing the dynamic import both reached showModal(), which throws on an open dialog; open() now returns early. A rejected import is logged instead of silently swallowing the click. Also simplify: reading the local base at module scope is already first-click-lazy (the boot module's import() is the only consumer), and a document-level delegated listener needs no DOM-ready gate.
decodeURIComponent throws on a literal % in a filename and would fail the whole build; degrade to skipping dimensions like every other failure path. Plus comment/type tidying (dev-mode cache staleness note, drop a no-op NonNullable).
The generator now also writes a manifest (symbol id + viewBox) that NavIcon imports, so reference and geometry come from the same generation pass instead of NavIcon re-deriving them from raw sources. Build scripts run the generator (fast, 37 files) so a stale committed sprite can't ship; outputs carry do-not-edit markers. Sprite stays committed so dev works without a build.
- DeferredLoadTrigger now carries the authoritative idle-fallback schedule; GTM and GitHubButton comments point at it instead of cross-referencing each other. - FAQ hash deep-link and UTM link rewrite get idle timeouts (500ms/1000ms) so a busy main thread can't unboundedly delay a deep-link scroll or lose click attribution. - Drop the fetchpriority boost from first content images (eager stays): on text-first docs pages a below-fold image shouldn't compete with resources painting the fold. - Gallery autoplay: the never-cleared timer already makes start one-shot; drop the redundant flag and cross-reference the two visibility-gate copies. - FAQ mask data-URIs deduped into SCSS variables and pinned to CanvasText under forced-colors so the glyphs stay visible in high-contrast modes.
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Stacked on #536 (
perf/lighthouse-core-web-vitals) — merge that PR first. Until it merges, the diff below includes its commits; the actual change here is the single commit8baca79cd.What
Pricing/FaqSection.astrobecomes fully self-contained — markup, styles, behavior, and structured data in one place:#hashdeep-links. The duplicated copies are gone: ~150 lines of inline handlers on the pricing page plus the wholeiot-hub-faq.ts/FaqJsonLd.astropair.window.__tbFaqOpen(id)is the small public API the pricing page uses to open an item after activating the owning product/sub-tab (its FAQ blocks aredisplay:noneuntil then).jsonLdprop backed bysrc/util/faq-schema.ts(HTML stripped to plain text, entities decoded,<escaped so answer content can never terminate the script element). IoT Hub keeps its existing schema;/pricing/gains structured data for the first time — one block for the default-visible Public Cloud context (91 Q&A, ~28 KB raw).faq_copy_link/faq_node_interaction; IoT Hub stays silent — both exactly as before.A11y fixes (in the shared markup, so all consumers benefit)
<button>— it was aspan[role=button]nested inside the question<button>(invalid interactive-in-interactive) and keyboard-dead on pricing. Enter/Space now work everywhere and the control reveals on:focus-visible.h3wraps only the toggle, so the heading's accessible name is the question alone.aria-controlswired to new answer ids;aria-currentSSR'd on the active category tab.pe-pay-as-you-go-cancel-subscription× 2 — the second item was unreachable by deep link).Deliberate changes to flag
faq_node_interaction— the event now counts opens only, matching the accordion handler's documented semantics.display:noneuntil a pricing tab reveals them). Marking up all 8 would add ~194 KB to the page.Verification
astro check0 errors; ESLint clean.#hashdeep-link into a hidden pricing context,?faqSection=, tooltip (i)-icon jumps,data-open-calclinks, analytics event counts (pricing unchanged, IoT Hub zero), exactly one FAQPage block per route, and collision safety for the unrelated.faq-*implementations (partners/affiliate, Trendz) — all green.