Merge main to release#493
Merged
Merged
Conversation
Replace the single static promo strip with a crossfading carousel: - Two announcement slides crossfade (CSS opacity) with ‹ › manual controls and a 6s auto-rotate; slides whose target is the current page are filtered out so the banner never self-promotes. - Jump-free layout: both slides share one auto-sized grid cell (grid-area 1/1) with a fixed-width right-aligned badge slot, so the badge and message stay at a fixed x across the crossfade. - Graceful degradation: minmax(0,max-content) track + message ellipsis so a too-long slide shrinks instead of clipping on narrow desktop. - A11y: visually-hidden role="status" aria-live region announces the active slide on manual nav (silent during autoplay to avoid a chatty live region); reduced-motion disables autoplay; ground-truth hover/focus pause state so autoplay can't get stuck off. - Mobile keeps the wrapping, full-width single-slide layout.
* [WIP]: New Trendz landing page * [WIP]: trendz landing page: first screen, contact modal * [WIP]: feat(trendz): add HowIotImprovesBusinessSection + contact form fixes - New accordion section with 6 items: desktop shows sticky right panel with image/text fade transitions; mobile/tablet shows inline panel as a separate rounded #f4f8fe box below the chips box (16px gap between) - Accordion scroll-jump fix: compensate viewport shift when a new item opens and the previously active one collapses above it - ProjectScreenFrame: add adjustable frontInset/middleInset/backInset props; use overflow:hidden + padding-top on screen-wrap to contain decorative border overflow in the inline panel context - Responsive typography: section padding 32px/100px (xs/sm+), title h2→h3 at xs, accordion title h3→h5-medium at xs, body body-s→body-xs at xs; add trendz-h5-medium mixin (20px/500/28px/0.249px) - TrendzContactModal: add missing client_id and fpr hidden inputs so existing JS tracking logic can actually submit them; randomize honeypot field name on every modal open * [WIP]: fix(trendz): accordion scroll behaviour on mobile and desktop - Mobile: on item open, smooth-scroll the item to just below the fixed header; offset reads --header-height CSS custom property (80px) + 8px gap so the header never clips the accordion title - Desktop: activate only, no scroll side-effect — avoids the unwanted page jump when selecting lower accordion items * [WIP]: feat(trendz): add MetricExplorerSection + accordion semantic and style fixes - New MetricExplorerSection: two-column layout (row ≥1280px, column below), YouTube embed via YouTubeVideo component (youtube-nocookie.com) - Accordion h3 semantics: accordion titles promoted from <span> to <h3> wrapping the <button>; add font:inherit on button to pick up parent typography; add trendz-h3-regular mixin (400 weight) - Accordion title states: non-active → h3-regular + secondary color, active → h3-medium + primary color - MetricExplorerSection typography: title h2/h3 per breakpoint, body switched to body-s, button changed to solid filled accent with body-m-medium (18px/500) * [WIP]: feat(trendz): add DeploymentOptionsSection with 3-card layout * [WIP]: feat(trendz): add TopFeaturesSection and DeploymentOptionsSection * Updated botom CTA Section * [WIP]: feat(trendz): add FAQSection with 3-category accordion layout * [WIP]: feat(trendz): add FAQSection with tabbed accordion layout * [WIP]: trendz landing page: layout fixes and button polish - MetricExplorerSection: equal flex columns (1:1), align-items flex-start, gap 32px - HowIotImprovesBusinessSection: equal columns (1:1), section-title bottom margin 100px (sm+) / 32px (xs) - HeroSection: buttons use trendz-body-m-medium, consistent padding/height/border-radius at all breakpoints - BottomCTASection: button padding 8px 84px, card border-radius 24px * [WIP]: fix(dev): changed hero video; added border and shadow to hero video * feat(trendz): use webp for "How IoT Improves Business" section images Render the six section mockups (KPIs, forecasting, predictive maintenance, anomaly detection, find outliers, utilization) as 2x webp served from public/products/trendz, and point HowIotImprovesBusinessSection at them. Drop the old per-section PNG sources from src/assets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Trendz landing page: updated links to open in a new tab * fix(trendz): make TopFeatures "Read more" links descriptive to pass link-label lint * Updated formatting * fix(trendz): restore "Read more" label with scoped visually-hidden context --------- Co-authored-by: Roman Kostenko <romen.kost.2332@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, a11y fixes + site-wide astro:page-load cleanup (#484) * fix(use-cases): restore dashboard carousels lost in migration The old Jekyll site showed an image carousel on 13 use-case pages; only 8 were migrated. Restore the missing 5 — smart-metering, water-metering, smart-retail, air-quality-monitoring, and scada-energy-management — by copying the original webp screenshots and adding the overview carousel block to each data file. Alt/title texts, image order, and dimensions match the legacy carousel data verbatim, except scada-energy-management slide 3, whose declared 1286x660 box was 1.59% off the real asset's aspect ratio (2604x1358) and is corrected to the aspect-exact 1302x679. The smart-retail and air-quality screenshots were exported near-lossless on the legacy site; they are re-encoded at webp quality 80 (matching their siblings), cutting those 10 files from 2.4 MB to 1.0 MB. The remaining 13 were already optimally encoded. * feat(carousel): redesign nav chrome with brand indigo accents Replace the legacy owl-carousel-era chrome (thin hand-drawn SVG chevrons, 72x4 dash pagination) on the shared Landing carousel: - Arrows: 44px circular buttons with astro-icon tabler chevrons; lavender-tinted disc at rest, solid accent with contrast icon on hover, focus-visible ring. - Dots: 8px round dots; the active one morphs into a 24px pill. Uniform 28x24px touch targets replace the mobile-only override. - Colors ride on --carousel-accent: brand indigo in light theme, lavender in dark, via var(--color-brand-indigo, var(--color-product-cloud)) so it picks up the brand token once the brand unification lands while rendering identically today. - New .carousel-stage wrapper anchors the arrow overlay to the slide area (excluding dots, and the caption in the simple variant) so buttons center on the image instead of sitting low. - Home-page usecase variant: side padding 50px -> 64px to clear the round buttons. * fix(blog): replace raw YouTube iframes broken by referrer policy The site serves referrer-policy: same-origin (Cloudflare managed transform), so browsers send no Referer to YouTube's embed player, which now requires one — raw iframes fail with player error 153. Swap the two raw <iframe> embeds for the YouTubeVideo component, whose referrerpolicy="strict-origin-when-cross-origin" attribute overrides the document policy (plus nocookie domain, lazy loading, and an accessible title). These were the only raw YT embeds left. * fix(carousel): replace hand-rolled autoplay with embla plugin, respect reduced motion The custom setInterval autoplay could start a second timer without clearing the first (drag a slide, then mouse out: pointerUp and mouseleave each call startAutoplay), leaking an interval and advancing slides at double speed. It also resumed autoplay on pointerUp even while the cursor was still over the carousel, contradicting the hover-pause behavior. Swap it for the embla-carousel-autoplay plugin already used by AdvantagesCarousel (delay from the existing autoplayInterval prop, stopOnInteraction: false, stopOnMouseEnter: true), and skip autoplay entirely when the user has prefers-reduced-motion set (WCAG 2.2.2). * feat(carousel): ARIA carousel semantics + drop random ids Add the APG carousel pattern markup to the shared Landing carousel: role=region with aria-roledescription=carousel and an aria-label (new ariaLabel prop, set at both call sites), role=group slides labelled 'N of M', and aria-current tracking on the active dot. Also drop the Math.random() viewport id: the script finds the viewport with a class selector inside its own wrapper, so the id only made build output non-reproducible. * refactor(carousel): extract shared Carousel module with slot mode Move Landing/Carousel.astro to src/components/Carousel/ and generalize it so other embla carousels can ride it: - carousel-init.ts: client logic extracted from the inline script — lazy embla import, IntersectionObserver init, reduced-motion autoplay gate. Adds a carousel:select CustomEvent for master-detail consumers and JS-built dots (from scrollSnapList) for slot mode. Initial dot/button state is applied via direct calls since embla defers 'init' to a macrotask. - Slot mode + CarouselSlide.astro for arbitrary slide markup; slides are sized through --carousel-slide-basis/--carousel-slide-gutter custom props because slot content is outside the component's style scope. - perView={3} ladder (3/2/1 across lg/md) using loop-seam-safe margin-right gutters instead of flex gap. - arrowPlacement='header' renders the nav buttons in a title row above the viewport instead of the floating overlay. - CarouselItem href/title become optional (drops the '#' sentinel); homeCarousel.ts imports the interface instead of re-declaring it. - ariaLabel is now a required prop. Delete the UseCaseCarousel wrapper — use-cases/[slug].astro calls Carousel directly with the same mapping (title || alt caption fallback and section margins preserved). Zero behavior change for the homepage and use-case page carousels. * refactor(advantages): ride the shared Carousel component AdvantagesCarousel keeps only the card markup (in CarouselSlide slots) and delegates all carousel behavior to Carousel with arrowPlacement='header', perView={3}, autoplay. Intentional changes vs the standalone implementation: - autoplay is now suppressed under prefers-reduced-motion - nav arrows adopt the shared indigo .carousel-button chrome - gains role=region/slide ARIA semantics and lazy IO init - drops the Math.random() viewport ids and the duplicated embla script * refactor(blog): migrate featured carousel onto shared Carousel Replace the blog's hand-rolled embla wiring with the shared Carousel component in slot mode. The editorial card markup and the bordered pill-dots chrome stay page-local; only the engine moves. - Add a dotCount prop to Carousel so slot-mode dots render at SSR (no layout shift waiting for JS) instead of being built after init. - Card slides keep their full-viewport flush layout: gap is zeroed on the container directly, since the wrapper self-declares --carousel-gap and a parent-level custom-prop override would be shadowed. - Slide 0 keeps loading=eager/fetchpriority=high for LCP. - Drop the page's static embla import + initCarousel; the shared component lazy-loads embla via IntersectionObserver. Intended behavior changes vs the old implementation: - autoplay no longer dies permanently on first interaction (stopOnInteraction false + stopOnMouseEnter) and is suppressed under prefers-reduced-motion - card text is no longer selectable during drag (user-select: none), matching every other carousel on the site * fix(blog): full-bleed featured carousel on mobile On phones the featured card was inset by the blog container gutter on both sides, which looked cramped. Break the carousel section out of the container's mobile padding (var(--spacing-4); the container also drops its border below 1024px) so the card spans edge-to-edge, and drop the card's rounded corners + side borders so the flush edge reads as intentional. Negative margins rather than 100vw/transform, to avoid clashing with the transform-based section entrance animation. * refactor(carousel): drop dead 'default' variant The 'default' items-variant had no consumers (homepage uses 'usecase', use-cases use 'simple', blog/advantages use slot mode) and rendered markup identical to 'simple'. Collapse it: remove the variant value and its dead render branch + CSS, and stop stamping a meaningless carousel-<variant> class on slot-mode wrappers (they get carousel-slotted). Also clarify the dotCount doc comment. * fix(a11y): respect reduced-motion and hide duplicates on marquees The three CSS marquees (logo strip, company hero photos, visualization widgets strip) each duplicate their content for a seamless loop but animated unconditionally and announced the duplicate copy to screen readers. For each: add a prefers-reduced-motion guard that holds the strip still, and aria-hidden the duplicated half (the visualization duplicate also gets alt=""). HeroPhotoCarousel's drag handler no longer re-arms the auto-scroll under reduced motion. * fix(carousel): stop autoplay on touch interaction; guard double-init Review follow-ups: - Autoplay used stopOnInteraction:false + stopOnMouseEnter:true, but mouseenter never fires on touch, so a swipe didn't pause autoplay and it kept yanking the slide away. Gate stopOnInteraction on (pointer: coarse): touch stops on swipe, mouse keeps hover-pause + resume-after-drag. - Move the data-carousel-init check inside the IntersectionObserver callback so a second observeCarousels() run (e.g. astro:page-load + DOMContentLoaded) can't double-init the same wrapper. * fix(a11y): SSR the slide accessible name in slot mode CarouselSlide now takes index/total and renders the "N of M" aria-label server-side, matching items-mode slides. carousel-init.ts keeps setting it as a JS fallback only when the props are omitted. Blog featured and advantages carousels pass index/total so their slides are named without relying on JS. * fix(use-cases): real captions for water-metering and SCADA carousels The simple-variant carousel renders title as a visible caption, so the placeholder alt text ("water metering 1"…) was showing on-screen. Give each water-metering and SCADA energy-management dashboard image a real title (and descriptive alt for the water-metering placeholders), written from the actual dashboard contents. * fix(use-cases): caption SCADA-drilling carousel; drop alt-as-caption fallback Review follow-up (#484): the SCADA oil-&-gas drilling carousel images had no title, so the simple variant fell back to rendering their screen-reader alt text as a visible caption. Give the three images real captions (written from the dashboards), and pass carouselImages straight through instead of `title: img.title || img.alt` — CarouselImage already satisfies CarouselItem, so a title-less image now shows no caption rather than leaking alt prose. * refactor(carousel): rename perView prop to layout 'single' | 'multi' `perView: 1 | 3` read awkwardly (3 selected a 3/2/1 responsive ladder, 2 was impossible). `layout: 'single' | 'multi'` maps to one observable behavior each. Pure rename — no behavior change. * refactor(carousel): make Carousel a content-agnostic slot-only utility Strip all slide-content knowledge out of Carousel.astro so it is a pure embla engine + chrome (arrows, dots, header, autoplay, layout). The component no longer ships an items/variant API or any slide CSS. - Extract the homepage text+image+overlay slide into Landing/DashboardSlide (now owns the CarouselItem type). - Extract the use-case image+caption slide into UseCase/DashboardImageSlide. - Drop the items-mode render block and the CarouselItem export. - Rename dotCount -> slideCount (SSR dot count). - Replace the implicit carousel-slotted bleed with an explicit `bleed` prop. Blog and advantages carousels were already slot-based; they opt into `bleed` for their bordered cards. No visual change on any surface (check/lint/dev-parity/adversarial parity review all clean). * chore: remove dead astro:page-load listeners site-wide The Astro View Transitions router is not enabled anywhere on this site (no <ClientRouter/>, no transition:* directives, no experimental flags; Starlight 0.39.2 does not inject it), so astro:page-load never fires. Every document.addEventListener('astro:page-load', ...) registration was therefore dead code. Remove all 23 registrations across 26 files and trim the now-inaccurate 'fires on every view-transition' comments. Each component keeps its real init path (DOMContentLoaded listener, immediate init() call, or readyState guard) and all idempotency guards, so behavior is unchanged today. Verified: astro check 0 errors/warnings, eslint clean, zero astro:page-load references remain. * refactor(carousel): address review — robust init, data-layer type, required fields, alias + doc polish - Carousel init now uses the readyState guard (matches DashboardStructure/ FeedbackPrompt/ImageGallery) so it runs even if the bundled module executes after DOMContentLoaded has fired. - Move CarouselItem to the data layer (src/data/homeCarousel.ts); DashboardSlide imports it via @data/homeCarousel — fixes the inverted data->component dependency. - Make title/description/linkLabel/href required on CarouselItem (every home item supplies them; a slide without them is unusable). - Document that slideCount must equal the slide count, and CarouselSlide index/total as optional (carousel-init.ts fills the N-of-M label client-side). - Normalize this PR's new carousel imports (+ blog YouTubeVideo) to the @components alias for consistency across the new files. astro check 0 errors/warnings, eslint clean.
…oken review links (#494) - Drop `white-space: break-spaces` from `.em-subtitle` so card copy wraps naturally instead of rendering source indentation/newlines as whitespace - Repoint the Asset tracking / SCADA / Cold chain review "read more" links off the empty `/industries/* → clients-feedback?category=:category` redirect to populated targets (smart-energy, smart-city, senseing case study)
Deploying staging-thingsboard-io with
|
| Latest commit: |
a1adc82
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3547cff1.staging-thingsboard-io.pages.dev |
* Updated Loriot Integration * Updated Loriot Integration * Potential fix for pull request finding * Apply suggestions from code review
* White-labeling Login updated guide * Potential fix for pull request finding * updated text
* updated AI Credits block * updated AI Credits block * Potential fix for pull request finding ---------
* Fix adaptive margins, paddings, buttons * fix(trendz-faq): prevent category switch from reflowing button column Desktop panels used grid-row: 1 / -1, which caused all button rows to inflate whenever the active panel was taller than the sum of button heights. Fixed by making panels position:absolute within the grid area (removing them from track sizing) and syncing the layout min-height to the tallest panel via JS so content below is never overlapped. * fix(trendz landing page images): moved newly added images from 'public/products/trendz/' to 'src/assets/images/landings/trendz/' * fix: replace trendz landing page hero video with a link to a video * fix(trendz): prevent Starlight's :where(a):hover from bleeding into CTA buttons CTA button hover states only set background, leaving no competing color declaration for Starlight's zero-specificity :where(a):hover rule to lose against. Added explicit color to each hover block in HowIotImprovesBusinessSection and BottomCTASection. * refactor(trendz): promote recurring hardcoded colors to design tokens Added --color-trendz-surface (#f4f8fe), --color-trendz-surface-accent (#d9e9fc), and --color-trendz-accent-light (#3e9af8) to _trendz.scss. Replaced all scattered hardcoded occurrences across HeroSection, TopFeaturesSection, HowIotImprovesBusinessSection, DeploymentOptionsSection, and FAQSection. Existing #2a7dec usages were folded into the already-correct --color-trendz-accent token. * fix(trendz-faq): track live panel height and prevent row stretch on expand syncMinHeight now measures the active panel's current scrollHeight instead of a one-time snapshot, so expanding Q&A items correctly grows the layout. align-content: start prevents the grid from distributing the extra min-height space into the button rows, which was causing the left column to stretch. * refactor(trendz): replace undefined Figma token names with real trendz tokens --text-text-primary, --text-text-secondary, --text-text-white, and --web-BG were never defined, so their var() fallback literals always fired and the custom-property indirection did nothing. Replaced all occurrences across FAQSection, HowIotImprovesBusinessSection, TopFeaturesSection, DeploymentOptionsSection, and MetricExplorerSection with the existing --color-trendz-text-primary / --color-trendz-text-secondary tokens and plain #fff where applicable. * fix(trendz-modal): add focus management and focus trap for keyboard/screen-reader users On open, store the trigger element and move focus to the first form field. On close, return focus to the opener. Tab and Shift+Tab cycle within the dialog's focusable elements, excluding the off-screen honeypot field. * fix(trendz-top-features): make card expand/collapse keyboard and screen-reader accessible Replaced the div.tf-card-header click target with an h3 > button matching the HowIot accordion pattern. Button carries aria-expanded and aria-controls pointing to the body panel, so screen readers announce state correctly. JS now keeps aria-expanded in sync on toggle and on breakpoint reset. * fix(trendz): video poster, preload, IntersectionObserver pause, and aria-hidden on inactive panels * refactor(project-screen-frame): replace geometry props with named variants * fix(trendz-hero-section): removed unsued assets and code * refactor(trendz): scope landing CSS vars to page, keep _trendz.scss mixin-only Drop the global @use 'trendz' from global.scss so the html#trendz design tokens are no longer emitted site-wide. Move the token CSS vars into the Trendz landing page's own global <style> block; _trendz.scss is now a pure Sass partial providing only breakpoint/typography mixins to the sections. * **Address PR review comments on the Trendz landing page** - `HowIotImprovesBusinessSection`: replace `/src/assets/...` string literals with asset pipeline imports so images resolve correctly in the production build - `ProjectScreenFrame`: extract a `base` geometry preset and spread it into `hero`/`compact` to eliminate variant duplication; move color defaults (`--psf-accent/muted/fill`) to the component stylesheet so they participate in the CSS cascade - `index.astro` (Trendz): override `--psf-*` color vars at the page level via Trendz tokens; remove the now-unused `--color-trendz-accent-light` intermediate token - `FAQSection`: wrap the `resize` listener in `cancelAnimationFrame` + `requestAnimationFrame` to batch forced layout reads to one per frame; add a comment tying `LG_BP` to `$trendz-bp-lg` - `_trendz.scss`: add `trendz-btn-narrow-padding` mixin; replace three repeated `@include trendz-down(xs) { padding-left/right: 16px }` blocks in `HeroSection` and `BottomCTASection` - `formspree.ts`: centralize Formspree endpoint URLs; wire `TrendzContactModal` to `FORMSPREE_CONTACT * fix(trendz): override Starlight's :where(a):hover color on landing page buttons` Starlight's global `:where(a):hover { color: #2a7dec }` has zero specificity and was bleeding into Trendz landing buttons whose hover rules didn't explicitly set `color`. Added `color` to the hover blocks in `MetricExplorerSection` and `DeploymentOptionsSection` to prevent the override. * fix(trendz): let page-level --psf-* vars inherit into ProjectScreenFrame Removed --psf-accent/muted/fill declarations from .project-screen-frame and moved defaults to inline fallbacks (var(--psf-muted, #d1e9ff)). A direct class declaration always beats an inherited value regardless of specificity, so the page-level html overrides were silently losing to the component's own defaults. * fix(trendz): restore html#trendz scoping for page-level CSS tokens * fix: wire FeedbackPrompt to FORMSPREE_FEEDBACK constant Removes the local hardcoded endpoint and imports from ~/data/formspree, making the module the actual single source of truth it was meant to be. * refactor(ProjectScreenFrame): fold fill into Tokens, remove variantFill map Eliminates the separate variantFill lookup by adding an optional fill field to Tokens so all variant-driven visuals flow through one structure. * docs(_trendz.scss): update header comment from "Tokens" to "Breakpoints" --------- Co-authored-by: smykytenk0 <mykytenk00s@gmail.com> Co-authored-by: Vladyslav_Prykhodko <vprykhodko@thingsboard.io>
Breakpoints - Remove the Trendz-specific breakpoint vars ($trendz-bp-*) and the trendz-up()/trendz-down()/trendz-btn-narrow-padding mixins from _trendz.scss; the file now @forward/@use the shared `variables`. - Remap all section components onto the platform media-up()/media-down() mixins (sm->sm, md->lg, lg->xl); 1280px boundary stays exact. Fixes - TopFeatures: getColCount() 2-col threshold 960 -> 1024 to match the CSS grid's media-up(lg); fixes a JS/CSS desync in the 960-1023px band. - HeroSection: remove ~55 lines of dead CSS (unused #upper-icon-*/ #down-icon-* rules + unreferenced fromBottomLeft/fromRight keyframes); dedupe .btn-accent/.btn-outline via a shared hero-btn-base mixin. - HowIot: pass intrinsic image width/height to ProjectScreenFrame (reserves aspect ratio -> no CLS); drop aria-live from the desktop panel to stop screen-reader churn over the stacked blocks. - FAQ: rename LG_BP -> XL_BP (it's the 1280px/xl boundary). - BottomCTA: remove empty frontmatter fence.
Mid-crossfade Safari paints both slides' messages at near-full opacity, producing a garbled doubled-text overlap (Chrome/Firefox blend the two group-opacity layers into a smooth dissolve). Scope a fade-through retiming to WebKit via @supports (background: -webkit-named-image(i)) — an Apple-only CSS function absent from Blink/Gecko — so the outgoing slide fully fades out before the incoming fades in and the two messages are never both visible. Chrome/Firefox keep the original crossfade. Reduced-motion rule widened to also match .is-active so it still wins.
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.
Description
Type of change
src/content/docs/**)src/content/_includes/**)src/components/**,src/styles/**)src/pages/**,src/data/**)src/data/redirects.ts)releaseskill)Affected products
Related issues
Checklist
pnpm checkpasses (Astro / TypeScript)pnpm lint:eslintpassespnpm lint:slugcheckpasses (required if pages were added/renamed/moved across languages)pnpm lint:linkcheckpasses locally — required to merge; run it before requesting review (usepnpm lint:linkcheck:nobuildif you already ran a build)src/data/redirects.ts, andpnpm generate:redirectswas runsrc/data/versions.ts