Feat SEO Section (Overview, Heading Structure, Links & JSON LD Preview)#416
Feat SEO Section (Overview, Heading Structure, Links & JSON LD Preview)#416abedshaaban wants to merge 46 commits intoTanStack:mainfrom
Conversation
…functionality This commit introduces a new README.md file for the SEO tab in the devtools package. It outlines the purpose of the SEO tab, including its major features such as Social Previews, SERP Previews, JSON-LD Previews, and more. Each section provides an overview of functionality, data sources, and how the previews are rendered, enhancing the documentation for better user understanding.
…structure, and links preview This commit introduces several new sections to the SEO tab in the devtools package, enhancing its functionality. The new features include: - **JSON-LD Preview**: Parses and validates JSON-LD scripts on the page, providing detailed feedback on required and recommended attributes. - **Heading Structure Preview**: Analyzes heading tags (`h1` to `h6`) for hierarchy and common issues, ensuring proper SEO practices. - **Links Preview**: Scans all links on the page, classifying them as internal, external, or invalid, and reports on accessibility and SEO-related issues. Additionally, the SEO tab navigation has been updated to include these new sections, improving user experience and accessibility of SEO insights.
This commit refactors the SEO tab components to standardize the handling of severity levels for issues. The `Severity` type has been replaced with `SeoSeverity`, and the `severityColor` function has been removed in favor of a centralized `seoSeverityColor` function. This change improves code consistency and maintainability across the `canonical-url-preview`, `heading-structure-preview`, `json-ld-preview`, and `links-preview` components, ensuring a unified approach to displaying issue severity in the SEO analysis features.
This commit adds a canonical link and robots meta tag to the basic example's HTML file, improving SEO capabilities. Additionally, it refactors the SEO tab components to utilize the `Show` component for conditional rendering of issues, enhancing the user experience by only displaying relevant information when applicable. This change streamlines the presentation of SEO analysis results across the canonical URL, heading structure, and links preview sections.
…lysis This commit adds a new SEO overview section to the devtools package, aggregating insights from various SEO components including canonical URLs, social previews, SERP previews, JSON-LD, heading structure, and links. It implements a health scoring system to provide a quick assessment of SEO status, highlighting issues and offering hints for improvement. Additionally, it refactors existing components to enhance data handling and presentation, improving the overall user experience in the SEO tab.
…reporting This commit introduces new styles for the SEO tab components, improving the visual presentation of SEO analysis results. It adds structured issue reporting for SEO elements, including headings, JSON-LD, and links, utilizing a consistent design for severity indicators. Additionally, it refactors existing components to enhance readability and maintainability, ensuring a cohesive user experience across the SEO tab.
This commit introduces new styles for the SEO tab components, including enhanced visual presentation for SEO analysis results. It refactors the handling of severity indicators across various sections, such as headings, JSON-LD, and links, utilizing a consistent design approach. Additionally, it improves the structure and readability of the code, ensuring a cohesive user experience throughout the SEO tab.
…ization This commit enhances the SEO tab by updating styles for the health score indicators, including a new design for the health track and fill elements. It refactors the health score rendering logic to utilize a more consistent approach across components, improving accessibility with ARIA attributes. Additionally, it introduces a sorting function for links in the report, ensuring a clearer display order based on link types. These changes aim to provide a more cohesive and visually appealing user experience in the SEO analysis features.
This commit enhances the LinksPreviewSection by introducing an accordion-style layout for displaying links, allowing users to expand and collapse groups of links categorized by type (internal, external, non-web, invalid). It adds new styles for the accordion components, improving the visual organization of link reports. Additionally, it refactors the existing link rendering logic to accommodate the new structure, enhancing user experience and accessibility in the SEO analysis features.
…on features This commit introduces new styles for the JSON-LD preview component, improving the visual presentation of structured data. It adds functionality for validating supported schema types and enhances the display of entity previews, including detailed rows for required and recommended fields. Additionally, it refactors the health scoring system to account for missing schema attributes, providing clearer insights into SEO performance. These changes aim to improve user experience and accessibility in the SEO tab.
…tures This commit introduces a comprehensive update to the SEO overview section, adding a scoring system for subsections based on issue severity. It includes new styles for the score ring visualization, improving the presentation of SEO health metrics. Additionally, it refactors the issue reporting logic to provide clearer insights into the status of SEO elements, enhancing user experience and accessibility in the SEO tab.
…links preview in SEO tab This commit enhances the SEO tab by introducing new navigation buttons for 'Heading Structure' and 'Links Preview', allowing users to easily switch between these views. It also updates the display logic to show the corresponding sections when selected, improving the overall user experience and accessibility of SEO insights. The SEO overview section has been adjusted to maintain a cohesive structure.
…and scrollbar customization This commit updates the styles for the seoSubNav component, adding responsive design features for smaller screens, including horizontal scrolling and custom scrollbar styles. It also ensures that the seoSubNavLabel maintains proper layout with flex properties, enhancing the overall user experience in the SEO tab.
…inks preview functionality This commit modifies the package.json to improve testing scripts by adding a command to clear the NX daemon and updating the size limit for the devtools package. Additionally, it refactors the JSON-LD and links preview components to enhance readability and maintainability, including changes to function declarations and formatting for better code clarity. These updates aim to improve the overall user experience and accessibility in the SEO tab.
… tab components This commit refactors the SEO tab components by cleaning up imports related to severity handling and ensuring consistent text handling by removing unnecessary nullish coalescing and optional chaining. These changes enhance code readability and maintainability across the heading structure, JSON-LD, and links preview components.
…ew component This commit refactors the classifyLink function in the links preview component by removing unnecessary checks for non-web links and the 'nofollow' issue reporting. It enhances the handling of relative paths and same-document fragments to align with browser behavior, improving code clarity and maintainability in the SEO tab.
…README This commit removes the unused 'seoOverviewFootnote' style and its corresponding JSX element from the SEO overview section. Additionally, it updates the README to streamline the description of checks included in the SEO tab, enhancing clarity and conciseness. These changes improve code maintainability and documentation accuracy.
This commit modifies the size limit for the devtools package in package.json, increasing the limit from 60 KB to 69 KB. This change reflects adjustments in the package's size requirements, ensuring accurate size tracking for future development.
… in SEO tab components This commit updates the SEO tab components by standardizing the capitalization of section titles and improving code formatting for better readability. Changes include updating button labels to 'SEO Overview' and 'Social Previews', as well as enhancing the structure of JSX elements for consistency. These adjustments aim to enhance the overall clarity and maintainability of the code.
This commit modifies the titles of the 'Links' and 'JSON-LD' sections in the SEO overview to 'Links Preview' and 'JSON-LD Preview', respectively. These changes aim to enhance clarity and consistency in the presentation of SEO insights, aligning with previous updates to standardize capitalization and improve formatting across the SEO tab components.
…ed data analysis This commit adds a new SEO tab in the devtools, featuring live head-driven social and SERP previews, structured data (JSON-LD) analysis, heading and link assessments, and an overview that scores and links to each section. This enhancement aims to provide users with comprehensive SEO insights and improve the overall functionality of the devtools.
…nonicalPageData This commit modifies the export statements for the CanonicalPageIssue and CanonicalPageData types in the SEO tab components, changing them from 'export type' to 'type'. This adjustment aims to streamline the code structure and improve consistency in type declarations across the module.
…link and improving robots handling This commit removes the canonical link from the basic example HTML file and updates the robots handling logic in the canonical URL data module. The changes include refining the conditions for indexability and follow directives, ensuring more accurate SEO assessments. Additionally, the links preview component is updated to enforce the inclusion of both 'noopener' and 'noreferrer' for external links with target='_blank'. These adjustments aim to improve the overall functionality and security of the SEO tab.
…tion This commit introduces a new hook, useLocationChanges, that allows components to react to changes in the browser's location. The hook sets up listeners for pushState, replaceState, and popstate events, enabling efficient updates when the URL changes. Additionally, it integrates with the SEO tab components to enhance responsiveness to location changes, improving user experience and functionality.
This commit refactors the links-preview component by consolidating import statements for better clarity and organization. The countBySeverity function and SeoSectionSummary type are now imported separately, enhancing code readability and maintainability.
This commit updates the JSON-LD analysis function to ensure that it handles cases where the script content is null or empty. By using optional chaining and providing a default empty string, the function now avoids potential errors and improves robustness in processing JSON-LD scripts.
…mponents This commit updates the json-ld-preview and links-preview components by removing optional chaining from the textContent property. This change ensures that the textContent is always trimmed, improving the handling of empty strings and enhancing the robustness of the SEO tab components.
… preview text handling This commit updates the max-width values for certain styles in the use-styles.ts file, increasing the desktop max-width to 620px and decreasing the mobile max-width to 328px. Additionally, it introduces new functions in serp-preview.tsx for measuring text width and truncating text based on width and line limits, improving the handling of SERP previews for better SEO representation.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds a new Changes
Sequence DiagramsequenceDiagram
participant User as User
participant App as App/React
participant DevTools as DevTools UI
participant SeoPlugin as SeoDevtoolsCore/Panel
participant Analyzers as Analysis Modules
participant DOM as Document/Head
User->>DevTools: Open SEO panel / click subsection
DevTools->>SeoPlugin: Mount SeoTab / setActiveView('overview')
SeoPlugin->>Analyzers: Trigger analyses (overview aggregation)
par Parallel scans
Analyzers->>DOM: Read head meta tags & title
Analyzers->>DOM: Read JSON-LD scripts
Analyzers->>DOM: Collect headings (h1-h6)
Analyzers->>DOM: Collect links and attributes
Analyzers->>DOM: Measure text widths (off-DOM canvas)
end
Analyzers->>Analyzers: Validate, compute issues & scores
Analyzers->>SeoPlugin: Return section summaries
SeoPlugin->>DevTools: Render overview or detail view
User->>App: Navigate (router)
App->>SeoPlugin: Location change event
SeoPlugin->>Analyzers: Re-run affected analyses
SeoPlugin->>DevTools: Update UI
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit e6ba93f
☁️ Nx Cloud last updated this comment at |
More templates
@tanstack/devtools
@tanstack/devtools-a11y
@tanstack/devtools-client
@tanstack/devtools-seo
@tanstack/devtools-ui
@tanstack/devtools-utils
@tanstack/devtools-vite
@tanstack/devtools-event-bus
@tanstack/devtools-event-client
@tanstack/preact-devtools
@tanstack/react-devtools
@tanstack/solid-devtools
@tanstack/vue-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/devtools/src/styles/use-styles.ts (1)
288-295:⚠️ Potential issue | 🟠 MajorKeep the hidden mobile measurement width in sync with the rendered card.
Line 293 shrinks the mobile snippet to
328px, butserpMeasureHiddenMobileis still340pxlater in this file. The truncation logic will now measure against the old layout, so the mobile preview can disagree with what users see.🧩 Suggested fix
- width: 340px; + width: 288px;Also applies to: 375-384
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools/src/styles/use-styles.ts` around lines 288 - 295, The mobile preview width constant is out of sync: update the hidden measurement used by serpMeasureHiddenMobile to match the rendered card width in serpSnippetMobile (change 340px to 328px or better unify both to a single shared constant), so the truncation logic measures against the actual max-width; locate serpSnippetMobile and serpMeasureHiddenMobile in use-styles.ts and make them derive from the same value (or replace the hard-coded 340px with 328px).packages/devtools/src/tabs/seo-tab/serp-preview.tsx (1)
525-530:⚠️ Potential issue | 🟠 MajorRefresh the SERP snapshot on client-side navigation.
getSerpFromHead()includeswindow.location.href, but this component only resamples on head mutations. Route changes that leave the head untouched will keep showing the previous URL until the section remounts.Proposed fix
import { useHeadChanges } from '../../hooks/use-head-changes' +import { useLocationChanges } from '../../hooks/use-location-changes' ... useHeadChanges(() => { setSerp(getSerpFromHead()) }) + + useLocationChanges(() => { + setSerp(getSerpFromHead()) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools/src/tabs/seo-tab/serp-preview.tsx` around lines 525 - 530, The component SerpPreviewSection only updates serp via useHeadChanges, so getSerpFromHead() (which reads window.location.href) is stale on client-side navigation; modify SerpPreviewSection to also listen for client navigation events and call setSerp(getSerpFromHead())—add listeners for 'popstate' and a custom 'locationchange' fired from patched history.pushState/history.replaceState (or listen for a router navigation event if available), ensure you register these listeners on mount and remove them on cleanup; keep useHeadChanges behavior intact and reference SerpPreviewSection, getSerpFromHead, useHeadChanges, and setSerp when implementing the fix.
🧹 Nitpick comments (1)
packages/devtools/src/tabs/seo-tab/links-preview.tsx (1)
109-113: ReuseTANSTACK_DEVTOOLSfor the self-filter.This filter is what keeps the report from counting the devtools' own links, but it currently relies on a duplicated
data-testidliteral and a fallback selector that is not wired to the current root element. Importing the shared constant here avoids that drift.Proposed refactor
+import { TANSTACK_DEVTOOLS } from '../../utils/storage' ... return anchors .filter( (anchor) => - !anchor.closest('[data-testid="tanstack_devtools"]') && - !anchor.closest('[data-devtools-root]'), + !anchor.closest(`[data-testid="${TANSTACK_DEVTOOLS}"]`), ) .map(classifyLink)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools/src/tabs/seo-tab/links-preview.tsx` around lines 109 - 113, Replace the hard-coded '[data-testid="tanstack_devtools"]' & the ad-hoc '[data-devtools-root]' fallback in the anchor filter with the shared TANSTACK_DEVTOOLS selector: import the TANSTACK_DEVTOOLS constant at the top of links-preview.tsx and use it in the filter that currently checks anchor.closest(...). Remove the duplicated literal and the fallback selector so the filter reads something like anchor.closest(TANSTACK_DEVTOOLS) to reliably exclude the devtools' own links.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/react/basic/index.html`:
- Around line 36-37: The canonical link in examples/react/basic/index.html
currently hard-codes "http://localhost:3005" which will mislead crawlers; update
the <link rel="canonical"> element (or remove it) so it does not point to
localhost—either replace the hard-coded URL with the deployed example's public
URL or remove the canonical tag entirely from the fixture; ensure the change is
applied to the <link rel="canonical"> element so the example no longer signals a
localhost origin.
In `@packages/devtools/src/hooks/use-location-changes.ts`:
- Around line 28-56: The teardown unconditionally restores
originalPushState/originalReplaceState which can clobber newer wrappers; instead
capture the wrapper functions when installing them (e.g. assign wrappedPushState
and wrappedReplaceState when you set window.history.pushState/replaceState) and,
in teardownLocationObservation, only restore the originals if the current
window.history.pushState === wrappedPushState (and similarly for replaceState);
leave the current values alone otherwise and still remove the event listeners
and clear teardownLocationObservation.
In `@packages/devtools/src/tabs/seo-tab/heading-structure-preview.tsx`:
- Around line 141-166: HeadingStructurePreviewSection currently computes
headings and issues once via extractHeadings() and validateHeadings(), causing
stale results after client-side navigation; subscribe to the location-change
signal (useLocationChanges()) and re-run the scan by moving headings/issues into
state or memo and recomputing them inside an effect or memo that depends on the
location-change value (and optionally a mutation observer trigger for body
changes). Specifically, call useLocationChanges() in
HeadingStructurePreviewSection, then in a useEffect or useMemo triggered by that
value, call extractHeadings() and validateHeadings(headings) and update local
state (or return memoized values) so the UI reflects the current route (and
consider adding a small debounce if needed for rapid mutations).
In `@packages/devtools/src/tabs/seo-tab/json-ld-preview.tsx`:
- Around line 204-216: The current code builds allowedSet from rules +
RESERVED_KEYS and treats any extra top-level keys in entity as a warning
(unknownKeys -> issues.push), which incorrectly penalizes valid but unlisted
schema properties; remove or disable this warning: delete the unknownKeys check
and the issues.push block (or gate it behind an explicit opt-in feature flag),
so that allowedSet/unknownKeys are not used to add warnings for arbitrary
top-level fields; reference symbols: allowedSet, rules, RESERVED_KEYS,
unknownKeys, entity, typeName, and issues.
- Around line 452-470: Replace the custom scoring logic in getJsonLdScore with a
call to the shared sectionHealthScore to ensure consistent weights; compute the
same errors/warnings/infos counts from entries (as currently done) then return
sectionHealthScore(errors, warnings, infos) (or the appropriate
sectionHealthScore signature used in the repo), and add the necessary import of
sectionHealthScore at the top of the file; remove the local penalty/math logic
so the score is derived solely from the shared sectionHealthScore implementation
used elsewhere.
In `@packages/devtools/src/tabs/seo-tab/links-preview.tsx`:
- Around line 26-37: The current heuristic sets text from anchor.textContent and
only aria-label/title attributes, which misclassifies accessible links that use
aria-labelledby or descendant img[alt]; update the logic around the text
variable (and the place that pushes into issues) to compute a more accurate
accessible name: first check aria-label, then resolve aria-labelledby by looking
up referenced element(s) and using their textContent, then check title, then
look for descendant elements that provide names (e.g., img[alt], svg title or
desc), and use the first non-empty result as the link name; if you prefer not to
implement the full computation, at minimum narrow the error message emitted to
only mention the specific signals you actually check (e.g., "Missing link text,
aria-label, aria-labelledby, title, or image alt") so the message matches the
implemented checks (referencing the variables anchor, text, and issues).
In `@packages/devtools/src/tabs/seo-tab/seo-section-summary.ts`:
- Around line 88-110: aggregateSeoHealth currently sets label based on counts
but the UI derives tier from the computed score; change label derivation in
aggregateSeoHealth (function aggregateSeoHealth) to use the computed score
instead of counts: after computing score, set label = score >= 80 ? 'Good' :
score >= 60 ? 'Fair' : 'Poor', then return { score, label, counts } so the label
aligns with seoHealthTier(score).
In `@packages/devtools/src/tabs/seo-tab/social-previews.tsx`:
- Around line 214-220: The component snapshots the styles signal into const s =
styles(), breaking reactivity; remove the snapshot and call the signal inline.
Replace usages of s (and the accent calculation) to use styles() directly —
e.g., compute accent with socialAccentClasses(styles(), props.accent) and use
className expressions like `${styles().seoPreviewCard} ${accent.card}` so theme
changes update without remounting. Ensure you keep the original useStyles()
binding and delete the `const s = styles()` line.
---
Outside diff comments:
In `@packages/devtools/src/styles/use-styles.ts`:
- Around line 288-295: The mobile preview width constant is out of sync: update
the hidden measurement used by serpMeasureHiddenMobile to match the rendered
card width in serpSnippetMobile (change 340px to 328px or better unify both to a
single shared constant), so the truncation logic measures against the actual
max-width; locate serpSnippetMobile and serpMeasureHiddenMobile in use-styles.ts
and make them derive from the same value (or replace the hard-coded 340px with
328px).
In `@packages/devtools/src/tabs/seo-tab/serp-preview.tsx`:
- Around line 525-530: The component SerpPreviewSection only updates serp via
useHeadChanges, so getSerpFromHead() (which reads window.location.href) is stale
on client-side navigation; modify SerpPreviewSection to also listen for client
navigation events and call setSerp(getSerpFromHead())—add listeners for
'popstate' and a custom 'locationchange' fired from patched
history.pushState/history.replaceState (or listen for a router navigation event
if available), ensure you register these listeners on mount and remove them on
cleanup; keep useHeadChanges behavior intact and reference SerpPreviewSection,
getSerpFromHead, useHeadChanges, and setSerp when implementing the fix.
---
Nitpick comments:
In `@packages/devtools/src/tabs/seo-tab/links-preview.tsx`:
- Around line 109-113: Replace the hard-coded
'[data-testid="tanstack_devtools"]' & the ad-hoc '[data-devtools-root]' fallback
in the anchor filter with the shared TANSTACK_DEVTOOLS selector: import the
TANSTACK_DEVTOOLS constant at the top of links-preview.tsx and use it in the
filter that currently checks anchor.closest(...). Remove the duplicated literal
and the fallback selector so the filter reads something like
anchor.closest(TANSTACK_DEVTOOLS) to reliably exclude the devtools' own links.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 60f7759d-3922-4791-9df2-30fd2914c864
📒 Files selected for processing (15)
.changeset/puny-games-bow.mdexamples/react/basic/index.htmlpackage.jsonpackages/devtools/src/hooks/use-location-changes.tspackages/devtools/src/styles/use-styles.tspackages/devtools/src/tabs/seo-tab/canonical-url-data.tspackages/devtools/src/tabs/seo-tab/heading-structure-preview.tsxpackages/devtools/src/tabs/seo-tab/index.tsxpackages/devtools/src/tabs/seo-tab/json-ld-preview.tsxpackages/devtools/src/tabs/seo-tab/links-preview.tsxpackages/devtools/src/tabs/seo-tab/seo-overview.tsxpackages/devtools/src/tabs/seo-tab/seo-section-summary.tspackages/devtools/src/tabs/seo-tab/seo-severity.tspackages/devtools/src/tabs/seo-tab/serp-preview.tsxpackages/devtools/src/tabs/seo-tab/social-previews.tsx
| <meta name="robots" content="index, follow" /> | ||
| <link rel="canonical" href="http://localhost:3005" /> |
There was a problem hiding this comment.
Don't ship a localhost canonical URL.
Line 37 hard-codes http://localhost:3005, so the published example will canonicalize to the wrong origin outside local dev. That gives crawlers — and this new SEO tab — a false signal. Use the deployed example URL here, or drop the canonical tag from this fixture.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/basic/index.html` around lines 36 - 37, The canonical link in
examples/react/basic/index.html currently hard-codes "http://localhost:3005"
which will mislead crawlers; update the <link rel="canonical"> element (or
remove it) so it does not point to localhost—either replace the hard-coded URL
with the deployed example's public URL or remove the canonical tag entirely from
the fixture; ensure the change is applied to the <link rel="canonical"> element
so the example no longer signals a localhost origin.
| const originalPushState = window.history.pushState | ||
| const originalReplaceState = window.history.replaceState | ||
|
|
||
| const handleLocationSignal = () => { | ||
| emitLocationChangeIfNeeded() | ||
| } | ||
|
|
||
| window.history.pushState = function (...args) { | ||
| originalPushState.apply(this, args) | ||
| dispatchLocationChangeEvent() | ||
| } | ||
|
|
||
| window.history.replaceState = function (...args) { | ||
| originalReplaceState.apply(this, args) | ||
| dispatchLocationChangeEvent() | ||
| } | ||
|
|
||
| window.addEventListener('popstate', handleLocationSignal) | ||
| window.addEventListener('hashchange', handleLocationSignal) | ||
| window.addEventListener(LOCATION_CHANGE_EVENT, handleLocationSignal) | ||
|
|
||
| teardownLocationObservation = () => { | ||
| window.history.pushState = originalPushState | ||
| window.history.replaceState = originalReplaceState | ||
| window.removeEventListener('popstate', handleLocationSignal) | ||
| window.removeEventListener('hashchange', handleLocationSignal) | ||
| window.removeEventListener(LOCATION_CHANGE_EVENT, handleLocationSignal) | ||
| teardownLocationObservation = undefined | ||
| } |
There was a problem hiding this comment.
Only restore history methods if this hook still owns them.
Lines 50-51 unconditionally put back the originals. If something else wraps history.pushState or history.replaceState after this observer starts, tearing down the last listener will silently remove that newer wrapper and can break routing or analytics hooks in the host app.
🛠️ Suggested fix
- window.history.pushState = function (...args) {
+ const pushStateWrapper: History['pushState'] = function (...args) {
originalPushState.apply(this, args)
dispatchLocationChangeEvent()
}
+ window.history.pushState = pushStateWrapper
- window.history.replaceState = function (...args) {
+ const replaceStateWrapper: History['replaceState'] = function (...args) {
originalReplaceState.apply(this, args)
dispatchLocationChangeEvent()
}
+ window.history.replaceState = replaceStateWrapper
...
teardownLocationObservation = () => {
- window.history.pushState = originalPushState
- window.history.replaceState = originalReplaceState
+ if (window.history.pushState === pushStateWrapper) {
+ window.history.pushState = originalPushState
+ }
+ if (window.history.replaceState === replaceStateWrapper) {
+ window.history.replaceState = originalReplaceState
+ }
window.removeEventListener('popstate', handleLocationSignal)
window.removeEventListener('hashchange', handleLocationSignal)
window.removeEventListener(LOCATION_CHANGE_EVENT, handleLocationSignal)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools/src/hooks/use-location-changes.ts` around lines 28 - 56,
The teardown unconditionally restores originalPushState/originalReplaceState
which can clobber newer wrappers; instead capture the wrapper functions when
installing them (e.g. assign wrappedPushState and wrappedReplaceState when you
set window.history.pushState/replaceState) and, in teardownLocationObservation,
only restore the originals if the current window.history.pushState ===
wrappedPushState (and similarly for replaceState); leave the current values
alone otherwise and still remove the event listeners and clear
teardownLocationObservation.
| const allowedSet = new Set([ | ||
| ...rules.required, | ||
| ...rules.recommended, | ||
| ...rules.optional, | ||
| ...RESERVED_KEYS, | ||
| ]) | ||
| const unknownKeys = Object.keys(entity).filter((key) => !allowedSet.has(key)) | ||
| if (unknownKeys.length > 0) { | ||
| issues.push({ | ||
| severity: 'warning', | ||
| message: `Possible invalid attributes for ${typeName}: ${unknownKeys.join(', ')}`, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Don't score unlisted schema properties as warnings.
SUPPORTED_RULES is a curated subset, not an exhaustive allowlist. With this block, any additional top-level field becomes a warning and lowers the health score, so valid snippets will look unhealthy just for using more properties.
Proposed fix
- const allowedSet = new Set([
- ...rules.required,
- ...rules.recommended,
- ...rules.optional,
- ...RESERVED_KEYS,
- ])
- const unknownKeys = Object.keys(entity).filter((key) => !allowedSet.has(key))
- if (unknownKeys.length > 0) {
- issues.push({
- severity: 'warning',
- message: `Possible invalid attributes for ${typeName}: ${unknownKeys.join(', ')}`,
- })
- }
+ // Only warn on unexpected keys once these per-type rules are exhaustive.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const allowedSet = new Set([ | |
| ...rules.required, | |
| ...rules.recommended, | |
| ...rules.optional, | |
| ...RESERVED_KEYS, | |
| ]) | |
| const unknownKeys = Object.keys(entity).filter((key) => !allowedSet.has(key)) | |
| if (unknownKeys.length > 0) { | |
| issues.push({ | |
| severity: 'warning', | |
| message: `Possible invalid attributes for ${typeName}: ${unknownKeys.join(', ')}`, | |
| }) | |
| } | |
| // Only warn on unexpected keys once these per-type rules are exhaustive. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools/src/tabs/seo-tab/json-ld-preview.tsx` around lines 204 -
216, The current code builds allowedSet from rules + RESERVED_KEYS and treats
any extra top-level keys in entity as a warning (unknownKeys -> issues.push),
which incorrectly penalizes valid but unlisted schema properties; remove or
disable this warning: delete the unknownKeys check and the issues.push block (or
gate it behind an explicit opt-in feature flag), so that allowedSet/unknownKeys
are not used to add warnings for arbitrary top-level fields; reference symbols:
allowedSet, rules, RESERVED_KEYS, unknownKeys, entity, typeName, and issues.
| /** | ||
| * JSON-LD health 0–100: errors and warnings dominate; each info issue applies a | ||
| * small penalty so optional-field gaps match how the SEO overview weights them. | ||
| */ | ||
| function getJsonLdScore(entries: Array<JsonLdEntry>): number { | ||
| let errors = 0 | ||
| let warnings = 0 | ||
| let infos = 0 | ||
|
|
||
| for (const entry of entries) { | ||
| for (const issue of entry.issues) { | ||
| if (issue.severity === 'error') errors += 1 | ||
| else if (issue.severity === 'warning') warnings += 1 | ||
| else infos += 1 | ||
| } | ||
| } | ||
|
|
||
| const penalty = Math.min(100, errors * 20 + warnings * 10 + infos * 2) | ||
| return Math.max(0, 100 - penalty) |
There was a problem hiding this comment.
Use the shared section scorer here.
This card uses 20/10/2 penalties while sectionHealthScore() uses 22/9/2. The same JSON-LD issues can therefore show one percentage here and a different one in the overview ring.
Proposed fix
+import { sectionHealthScore } from './seo-section-summary'
import type { SeoSectionSummary } from './seo-section-summary'
...
function getJsonLdScore(entries: Array<JsonLdEntry>): number {
- let errors = 0
- let warnings = 0
- let infos = 0
-
- for (const entry of entries) {
- for (const issue of entry.issues) {
- if (issue.severity === 'error') errors += 1
- else if (issue.severity === 'warning') warnings += 1
- else infos += 1
- }
- }
-
- const penalty = Math.min(100, errors * 20 + warnings * 10 + infos * 2)
- return Math.max(0, 100 - penalty)
+ return sectionHealthScore(entries.flatMap((entry) => entry.issues))
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * JSON-LD health 0–100: errors and warnings dominate; each info issue applies a | |
| * small penalty so optional-field gaps match how the SEO overview weights them. | |
| */ | |
| function getJsonLdScore(entries: Array<JsonLdEntry>): number { | |
| let errors = 0 | |
| let warnings = 0 | |
| let infos = 0 | |
| for (const entry of entries) { | |
| for (const issue of entry.issues) { | |
| if (issue.severity === 'error') errors += 1 | |
| else if (issue.severity === 'warning') warnings += 1 | |
| else infos += 1 | |
| } | |
| } | |
| const penalty = Math.min(100, errors * 20 + warnings * 10 + infos * 2) | |
| return Math.max(0, 100 - penalty) | |
| /** | |
| * JSON-LD health 0–100: errors and warnings dominate; each info issue applies a | |
| * small penalty so optional-field gaps match how the SEO overview weights them. | |
| */ | |
| function getJsonLdScore(entries: Array<JsonLdEntry>): number { | |
| return sectionHealthScore(entries.flatMap((entry) => entry.issues)) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools/src/tabs/seo-tab/json-ld-preview.tsx` around lines 452 -
470, Replace the custom scoring logic in getJsonLdScore with a call to the
shared sectionHealthScore to ensure consistent weights; compute the same
errors/warnings/infos counts from entries (as currently done) then return
sectionHealthScore(errors, warnings, infos) (or the appropriate
sectionHealthScore signature used in the repo), and add the necessary import of
sectionHealthScore at the top of the file; remove the local penalty/math logic
so the score is derived solely from the shared sectionHealthScore implementation
used elsewhere.
| const text = | ||
| anchor.textContent.trim() || | ||
| anchor.getAttribute('aria-label')?.trim() || | ||
| anchor.getAttribute('title')?.trim() || | ||
| '' | ||
| const issues: Array<LinkIssue> = [] | ||
|
|
||
| if (!text) { | ||
| issues.push({ | ||
| severity: 'error', | ||
| message: 'Missing link text or accessible label.', | ||
| }) |
There was a problem hiding this comment.
Don't treat textContent as the link's accessible name.
This will report common accessible patterns as errors, such as icon-only links labeled via aria-labelledby or descendant img[alt]. Either broaden the name heuristic or narrow the message so it only describes the signals you actually inspect.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools/src/tabs/seo-tab/links-preview.tsx` around lines 26 - 37,
The current heuristic sets text from anchor.textContent and only
aria-label/title attributes, which misclassifies accessible links that use
aria-labelledby or descendant img[alt]; update the logic around the text
variable (and the place that pushes into issues) to compute a more accurate
accessible name: first check aria-label, then resolve aria-labelledby by looking
up referenced element(s) and using their textContent, then check title, then
look for descendant elements that provide names (e.g., img[alt], svg title or
desc), and use the first non-empty result as the link name; if you prefer not to
implement the full computation, at minimum narrow the error message emitted to
only mention the specific signals you actually check (e.g., "Missing link text,
aria-label, aria-labelledby, title, or image alt") so the message matches the
implemented checks (referencing the variables anchor, text, and issues).
…emove deprecated components This commit introduces the @tanstack/devtools-seo package into the devtools, enhancing the SEO tab functionality. It updates the package.json files to include the new dependency and modifies the examples to utilize it. Additionally, it removes deprecated SEO-related components and their associated styles, streamlining the codebase and improving maintainability.
…tegration, update package configurations, and add example app
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (3)
packages/devtools-seo/src/json-ld-preview.tsx (2)
457-471:⚠️ Potential issue | 🟠 MajorUse the shared section scorer here too.
This card uses 20/10/2 penalties, while
packages/devtools-seo/src/seo-section-summary.tsuses 22/9/2 insectionHealthScore(). The same JSON-LD issues can therefore show one percentage here and a different one in the overview ring.♻️ Proposed fix
+import { sectionHealthScore } from './seo-section-summary' import type { SeoSectionSummary } from './seo-section-summary' @@ function getJsonLdScore(entries: Array<JsonLdEntry>): number { - let errors = 0 - let warnings = 0 - let infos = 0 - - for (const entry of entries) { - for (const issue of entry.issues) { - if (issue.severity === 'error') errors += 1 - else if (issue.severity === 'warning') warnings += 1 - else infos += 1 - } - } - - const penalty = Math.min(100, errors * 20 + warnings * 10 + infos * 2) - return Math.max(0, 100 - penalty) + return sectionHealthScore(entries.flatMap((entry) => entry.issues)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/json-ld-preview.tsx` around lines 457 - 471, getJsonLdScore duplicates scoring logic (20/10/2) that differs from the canonical scorer used in sectionHealthScore() (22/9/2); replace the bespoke logic in getJsonLdScore with a call to the shared section scorer used by seo-section-summary.ts (or extract the shared scoring function and import it) so both views compute the same percentage for the same JsonLdEntry issues; locate getJsonLdScore and either call sectionHealthScore (or the newly exported shared scorer) with the entry issues or refactor sectionHealthScore into a shared helper and use that helper in getJsonLdScore.
205-217:⚠️ Potential issue | 🟠 MajorDon't penalize keys outside this curated ruleset.
SUPPORTED_RULESis a subset, not an exhaustive schema allowlist. With this block, any extra top-level property becomes a warning and lowers the score, so valid markup looks unhealthy just because this tool doesn't know every schema field yet.♻️ Proposed fix
- const allowedSet = new Set([ - ...rules.required, - ...rules.recommended, - ...rules.optional, - ...RESERVED_KEYS, - ]) - const unknownKeys = Object.keys(entity).filter((key) => !allowedSet.has(key)) - if (unknownKeys.length > 0) { - issues.push({ - severity: 'warning', - message: `Possible invalid attributes for ${typeName}: ${unknownKeys.join(', ')}`, - }) - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/json-ld-preview.tsx` around lines 205 - 217, The current code constructs allowedSet and pushes a warning for any unknownKeys (derived from Object.keys(entity)), which penalizes valid but uncaptured schema fields; update the logic in json-ld-preview.tsx to stop treating unknown top-level properties as issues by removing or disabling the push to issues for unknownKeys (i.e., delete or guard the block that adds the warning), or alternatively narrow the check to only flag truly reserved/misplaced keys (use RESERVED_KEYS directly rather than allowedSet); refer to allowedSet, unknownKeys, entity and issues when locating the code to change.packages/devtools-seo/src/links-preview.tsx (1)
28-39:⚠️ Potential issue | 🟠 MajorDon't report an “accessible label” error from a text-only heuristic.
This still only checks
textContent,aria-label, andtitle, so links named viaaria-labelledbyor descendantimg[alt]will be reported as errors even though they have a valid accessible name. Either broaden the name lookup or narrow the message to the signals you actually inspect.♻️ Safe immediate fix
if (!text) { issues.push({ severity: 'error', - message: 'Missing link text or accessible label.', + message: 'Missing link text, aria-label, or title.', }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/links-preview.tsx` around lines 28 - 39, The current heuristic computes text from the local variables named text (using anchor.textContent, anchor.getAttribute('aria-label'), and anchor.getAttribute('title')) but then pushes a generic error about an "accessible label" which is misleading; update the issues.push message (and/or the error severity text) to only refer to the signals actually checked (e.g., "Missing link text, aria-label, or title") or alternatively expand the name lookup to include aria-labelledby and descendant img[alt] before deciding to push an error; specifically, modify the code around the text variable and the issues.push call in links-preview.tsx (referencing the text variable, anchor element, and LinkIssue type) so the message accurately reflects the checked attributes or add logic to resolve aria-labelledby and img[alt] into text first.
🧹 Nitpick comments (6)
packages/devtools-seo/src/tokens.ts (2)
208-208: Remove duplicate fallback infontFamily.sans
fontFamily.sansrepeatssans-seriftwice; keeping it once is cleaner.Suggested cleanup
- sans: 'ui-sans-serif, Inter, system-ui, sans-serif, sans-serif', + sans: 'ui-sans-serif, Inter, system-ui, sans-serif',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/tokens.ts` at line 208, fontFamily.sans currently lists the fallback "sans-serif" twice; update the definition (the fontFamily.sans entry) to remove the duplicated "sans-serif" so the value reads something like "ui-sans-serif, Inter, system-ui, sans-serif" with only one "sans-serif".
8-19: Deduplicateneutralandgrayscales to avoid driftThese two palettes are identical copies right now. Consider sharing one source object so future edits can’t diverge unintentionally.
Also applies to: 32-43
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/tokens.ts` around lines 8 - 19, The neutral and gray color scales in tokens.ts are duplicated and risk drifting; replace the duplicate by creating a single shared object (e.g., a single palette constant or export) and reference it from both places instead of repeating values so both neutral and gray (or whichever export names are required) point to the same source; update references to use the shared symbol (neutral/gray) and remove the redundant literal blocks to ensure future edits stay in sync.packages/devtools-seo/src/use-seo-styles.ts (1)
15-28: Duplicate CSS property inseoTabContainer.
overflow-y: auto;is declared twice (lines 21 and 27). The second declaration is redundant.🧹 Remove duplicate property
seoTabContainer: css` padding: 0; margin: 0 auto; background: ${t(colors.white, colors.darkGray[700])}; border-radius: 8px; box-shadow: none; overflow-y: auto; height: 100%; display: flex; flex-direction: column; gap: 0; width: 100%; - overflow-y: auto; `,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/use-seo-styles.ts` around lines 15 - 28, The css block for seoTabContainer contains a duplicate declaration of overflow-y: auto; — remove the redundant occurrence so seoTabContainer only declares overflow-y: auto once (keep the first or the most appropriate placement) within the css template literal to avoid duplication; update the css for seoTabContainer accordingly.examples/react/seo/src/index.tsx (1)
7-12: Consider wrapping withStrictModefor consistency.Other React examples in this repo (e.g.,
a11y-devtools,custom-devtools) wrap the render content withStrictMode. This helps catch potential issues during development.♻️ Add StrictMode wrapper
+import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { seoDevtoolsPlugin } from '@tanstack/devtools-seo/react' import { TanStackDevtools } from '@tanstack/react-devtools' import App from './App' createRoot(document.getElementById('root')!).render( - <> + <StrictMode> <App /> <TanStackDevtools plugins={[seoDevtoolsPlugin()]} /> - </>, + </StrictMode>, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/seo/src/index.tsx` around lines 7 - 12, The render call currently mounts <> <App /> <TanStackDevtools plugins={[seoDevtoolsPlugin()]} /> </> without React StrictMode; wrap the rendered JSX with StrictMode to match other examples. Update the code that calls createRoot(...).render(...) to render <StrictMode><App /><TanStackDevtools plugins={[seoDevtoolsPlugin()]} /></StrictMode>, and ensure StrictMode is imported (or use React.StrictMode) alongside createRoot; keep the same components (App, TanStackDevtools, seoDevtoolsPlugin) and plugin usage.packages/devtools-seo/src/social-previews.tsx (1)
259-267: Avoid index-coupling betweenreports()andSOCIALS.Using
SOCIALS[i()]with non-null assertions is brittle; includenetwork+accentdirectly in each report and render from report data only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/social-previews.tsx` around lines 259 - 267, The JSX currently couples reports() to the separate SOCIALS array by using SOCIALS[i()] and non-null assertions; instead add the network and accent properties into each report object (e.g., include report.network and report.accent when building reports()) and update the For renderer to pass those directly to <SocialPreview> (use meta={report.found} network={report.network} accent={report.accent}), removing the index lookup and the non-null assertions so rendering relies only on report data.packages/devtools-seo/src/solid-panel.tsx (1)
6-8: Import and useTanStackDevtoolsThemeinstead of a local theme union.This keeps the type definition in sync with
@tanstack/devtools-uiand ensures automatic alignment if supported themes expand in the future.♻️ Proposed typing change
import { ThemeContextProvider } from '@tanstack/devtools-ui' +import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui' import { SeoTab } from './seo-tab' type SeoPluginPanelProps = { - theme: 'light' | 'dark' + theme: TanStackDevtoolsTheme devtoolsOpen: boolean }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/devtools-seo/src/solid-panel.tsx` around lines 6 - 8, Replace the local union type for the theme prop with the exported TanStackDevtoolsTheme type from `@tanstack/devtools-ui`: import TanStackDevtoolsTheme and update the SeoPluginPanelProps type (and any references to the theme prop in the Solid component) to use TanStackDevtoolsTheme instead of 'light' | 'dark' so the prop's allowed values stay in sync with the devtools UI package.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/devtools-seo/src/heading-structure-preview.tsx`:
- Around line 175-177: The SectionDescription text is outdated: it says "This
section scans once when opened" but the component now rescans on location
changes (useLocationChanges()). Update the displayed description string in
heading-structure-preview.tsx (inside the SectionDescription component) to
reflect current behavior—e.g., mention that the panel rescans on location
changes or whenever the location updates—so the copy accurately matches the
scanning behavior implemented by useLocationChanges().
In `@packages/devtools-seo/src/json-ld-preview.tsx`:
- Around line 135-157: The validator currently rejects any non-string `@context`;
update validateContext to accept JSON‑LD permissible types: explicitly allow
null (context === null) and object (typeof context === 'object' && context !==
null) and handle arrays by verifying at least one element is a valid entry (a
schema.org string present in VALID_SCHEMA_CONTEXTS, an object, or null). Keep
the existing string branch for direct IRIs using VALID_SCHEMA_CONTEXTS, return
[] for accepted cases, and only return the error messages (invalid type or
invalid array contents) when none of these allowed forms are met; reference
validateContext, JsonLdValue, and VALID_SCHEMA_CONTEXTS to locate the changes.
In `@packages/devtools-seo/src/seo-tab.tsx`:
- Around line 20-63: The nav-based subview switcher should expose explicit tab
semantics: add role="tablist" to the <nav class={styles().seoSubNav}> and for
each button (the ones using class `${styles().seoSubNavLabel}` and
activeView()/setActiveView()) add role="tab" and set aria-selected={activeView()
=== '<view-name>'} (true for the active view, false otherwise); keep the
existing onClick handlers calling setActiveView(...) and ensure the active
styling logic remains tied to activeView() so assistive tech and keyboard users
can identify the selected tab.
In `@packages/devtools-seo/src/serp-preview.tsx`:
- Around line 525-530: SerpPreviewSection only updates on head mutations; also
subscribe to client-side navigation events so setSerp(getSerpFromHead()) runs on
route changes: in SerpPreviewSection add a listener for 'popstate' and a small
wrapper that patches history.pushState/history.replaceState to dispatch a custom
'locationchange' event, then listen for that event and call
setSerp(getSerpFromHead()); ensure you register these listeners alongside
useHeadChanges and remove/restore the listeners/patch on cleanup to avoid leaks.
- Around line 327-338: The title overflow is only computed for desktop
(titleOverflow) so mobile previews can be truncated without detection; add a
separate mobile title overflow check (e.g., titleOverflowMobile) and compute it
against the mobile constraints (use measureTextWidth or wrapTextByWidth with
MOBILE_TITLE_WIDTH_PX, TITLE_FONT and MOBILE_TITLE_MAX_LINES or
MOBILE_TITLE_WIDTH_PX) similar to how descriptionOverflowMobile is computed,
then include that property where the overflow object is constructed so mobile
title truncation is reported independently from desktop.
In `@packages/devtools-seo/src/social-previews.tsx`:
- Around line 34-39: Replace the unsupported twitter:url tag with og:url in the
tags definitions: update the tag object where key === 'twitter:url' to use key
'og:url' (keep prop 'url') and make the same replacement in the second
occurrence later in the file (the other tags array around lines 146-158); ensure
both tags arrays in social-previews.tsx are changed so Twitter/X cards use
og:url for the canonical URL.
In `@packages/devtools-seo/src/tokens.ts`:
- Around line 274-275: The shadow.xs function currently ignores its color
parameter and returns a hardcoded color; update the xs arrow function in
tokens.ts (the shadow.xs definition) to use the provided color parameter in the
returned template string (e.g., interpolate the color variable instead of the
fixed 'rgb(0 0 0 / 0.05)') while preserving the existing return type (as const)
and default value for the parameter.
---
Duplicate comments:
In `@packages/devtools-seo/src/json-ld-preview.tsx`:
- Around line 457-471: getJsonLdScore duplicates scoring logic (20/10/2) that
differs from the canonical scorer used in sectionHealthScore() (22/9/2); replace
the bespoke logic in getJsonLdScore with a call to the shared section scorer
used by seo-section-summary.ts (or extract the shared scoring function and
import it) so both views compute the same percentage for the same JsonLdEntry
issues; locate getJsonLdScore and either call sectionHealthScore (or the newly
exported shared scorer) with the entry issues or refactor sectionHealthScore
into a shared helper and use that helper in getJsonLdScore.
- Around line 205-217: The current code constructs allowedSet and pushes a
warning for any unknownKeys (derived from Object.keys(entity)), which penalizes
valid but uncaptured schema fields; update the logic in json-ld-preview.tsx to
stop treating unknown top-level properties as issues by removing or disabling
the push to issues for unknownKeys (i.e., delete or guard the block that adds
the warning), or alternatively narrow the check to only flag truly
reserved/misplaced keys (use RESERVED_KEYS directly rather than allowedSet);
refer to allowedSet, unknownKeys, entity and issues when locating the code to
change.
In `@packages/devtools-seo/src/links-preview.tsx`:
- Around line 28-39: The current heuristic computes text from the local
variables named text (using anchor.textContent,
anchor.getAttribute('aria-label'), and anchor.getAttribute('title')) but then
pushes a generic error about an "accessible label" which is misleading; update
the issues.push message (and/or the error severity text) to only refer to the
signals actually checked (e.g., "Missing link text, aria-label, or title") or
alternatively expand the name lookup to include aria-labelledby and descendant
img[alt] before deciding to push an error; specifically, modify the code around
the text variable and the issues.push call in links-preview.tsx (referencing the
text variable, anchor element, and LinkIssue type) so the message accurately
reflects the checked attributes or add logic to resolve aria-labelledby and
img[alt] into text first.
---
Nitpick comments:
In `@examples/react/seo/src/index.tsx`:
- Around line 7-12: The render call currently mounts <> <App />
<TanStackDevtools plugins={[seoDevtoolsPlugin()]} /> </> without React
StrictMode; wrap the rendered JSX with StrictMode to match other examples.
Update the code that calls createRoot(...).render(...) to render
<StrictMode><App /><TanStackDevtools plugins={[seoDevtoolsPlugin()]}
/></StrictMode>, and ensure StrictMode is imported (or use React.StrictMode)
alongside createRoot; keep the same components (App, TanStackDevtools,
seoDevtoolsPlugin) and plugin usage.
In `@packages/devtools-seo/src/social-previews.tsx`:
- Around line 259-267: The JSX currently couples reports() to the separate
SOCIALS array by using SOCIALS[i()] and non-null assertions; instead add the
network and accent properties into each report object (e.g., include
report.network and report.accent when building reports()) and update the For
renderer to pass those directly to <SocialPreview> (use meta={report.found}
network={report.network} accent={report.accent}), removing the index lookup and
the non-null assertions so rendering relies only on report data.
In `@packages/devtools-seo/src/solid-panel.tsx`:
- Around line 6-8: Replace the local union type for the theme prop with the
exported TanStackDevtoolsTheme type from `@tanstack/devtools-ui`: import
TanStackDevtoolsTheme and update the SeoPluginPanelProps type (and any
references to the theme prop in the Solid component) to use
TanStackDevtoolsTheme instead of 'light' | 'dark' so the prop's allowed values
stay in sync with the devtools UI package.
In `@packages/devtools-seo/src/tokens.ts`:
- Line 208: fontFamily.sans currently lists the fallback "sans-serif" twice;
update the definition (the fontFamily.sans entry) to remove the duplicated
"sans-serif" so the value reads something like "ui-sans-serif, Inter, system-ui,
sans-serif" with only one "sans-serif".
- Around line 8-19: The neutral and gray color scales in tokens.ts are
duplicated and risk drifting; replace the duplicate by creating a single shared
object (e.g., a single palette constant or export) and reference it from both
places instead of repeating values so both neutral and gray (or whichever export
names are required) point to the same source; update references to use the
shared symbol (neutral/gray) and remove the redundant literal blocks to ensure
future edits stay in sync.
In `@packages/devtools-seo/src/use-seo-styles.ts`:
- Around line 15-28: The css block for seoTabContainer contains a duplicate
declaration of overflow-y: auto; — remove the redundant occurrence so
seoTabContainer only declares overflow-y: auto once (keep the first or the most
appropriate placement) within the css template literal to avoid duplication;
update the css for seoTabContainer accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0f6bc8bd-4a95-4895-b832-efbe64854c54
⛔ Files ignored due to path filters (2)
examples/react/seo/public/emblem-light.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
.changeset/puny-games-bow.mdexamples/react/basic/src/setup.tsxexamples/react/seo/index.htmlexamples/react/seo/package.jsonexamples/react/seo/src/App.tsxexamples/react/seo/src/index.tsxexamples/react/seo/tsconfig.jsonexamples/react/seo/vite.config.tspackage.jsonpackages/devtools-seo/package.jsonpackages/devtools-seo/src/canonical-url-data.tspackages/devtools-seo/src/core.tsxpackages/devtools-seo/src/devtools-dom-filter.tspackages/devtools-seo/src/heading-structure-preview.tsxpackages/devtools-seo/src/hooks/use-head-changes.tspackages/devtools-seo/src/hooks/use-location-changes.tspackages/devtools-seo/src/index.tspackages/devtools-seo/src/json-ld-preview.tsxpackages/devtools-seo/src/links-preview.tsxpackages/devtools-seo/src/react/SeoDevtools.tsxpackages/devtools-seo/src/react/index.tspackages/devtools-seo/src/react/plugin.tspackages/devtools-seo/src/seo-overview.tsxpackages/devtools-seo/src/seo-section-summary.tspackages/devtools-seo/src/seo-severity.tspackages/devtools-seo/src/seo-tab.tsxpackages/devtools-seo/src/serp-preview.tsxpackages/devtools-seo/src/social-previews.tsxpackages/devtools-seo/src/solid-panel.tsxpackages/devtools-seo/src/tokens.tspackages/devtools-seo/src/use-seo-styles.tspackages/devtools-seo/tsconfig.jsonpackages/devtools-seo/vite.config.tspackages/devtools/src/styles/use-styles.tspackages/devtools/src/tabs/index.tsx
💤 Files with no reviewable changes (1)
- packages/devtools/src/styles/use-styles.ts
✅ Files skipped from review due to trivial changes (8)
- examples/react/seo/vite.config.ts
- packages/devtools-seo/tsconfig.json
- examples/react/seo/index.html
- examples/react/seo/tsconfig.json
- examples/react/basic/src/setup.tsx
- examples/react/seo/package.json
- packages/devtools-seo/src/index.ts
- packages/devtools-seo/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
- .changeset/puny-games-bow.md
| <SectionDescription> | ||
| Visualizes heading structure (`h1`-`h6`) in DOM order and highlights | ||
| common hierarchy issues. This section scans once when opened. |
There was a problem hiding this comment.
Update the description to match the current behavior.
The panel now rescans on useLocationChanges(), so “This section scans once when opened” is no longer true.
✏️ Proposed fix
<SectionDescription>
Visualizes heading structure (`h1`-`h6`) in DOM order and highlights
- common hierarchy issues. This section scans once when opened.
+ common hierarchy issues. This section updates when the current route changes.
</SectionDescription>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <SectionDescription> | |
| Visualizes heading structure (`h1`-`h6`) in DOM order and highlights | |
| common hierarchy issues. This section scans once when opened. | |
| <SectionDescription> | |
| Visualizes heading structure (`h1`-`h6`) in DOM order and highlights | |
| common hierarchy issues. This section updates when the current route changes. | |
| </SectionDescription> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/heading-structure-preview.tsx` around lines 175 -
177, The SectionDescription text is outdated: it says "This section scans once
when opened" but the component now rescans on location changes
(useLocationChanges()). Update the displayed description string in
heading-structure-preview.tsx (inside the SectionDescription component) to
reflect current behavior—e.g., mention that the panel rescans on location
changes or whenever the location updates—so the copy accurately matches the
scanning behavior implemented by useLocationChanges().
| function validateContext(entity: JsonLdValue): Array<ValidationIssue> { | ||
| const context = entity['@context'] | ||
| if (!context) { | ||
| return [{ severity: 'error', message: 'Missing @context attribute.' }] | ||
| } | ||
| if (typeof context === 'string') { | ||
| if (!VALID_SCHEMA_CONTEXTS.has(context)) { | ||
| return [ | ||
| { | ||
| severity: 'error', | ||
| message: `Invalid @context value "${context}". Expected schema.org context.`, | ||
| }, | ||
| ] | ||
| } | ||
| return [] | ||
| } | ||
| return [ | ||
| { | ||
| severity: 'error', | ||
| message: 'Invalid @context type. Expected a string schema.org URL.', | ||
| }, | ||
| ] | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/devtools-seo/src/json-ld-preview.tsx | sed -n '120,170p'Repository: TanStack/devtools
Length of output: 1732
🏁 Script executed:
# Check the VALID_SCHEMA_CONTEXTS constant definition
rg "VALID_SCHEMA_CONTEXTS" packages/devtools-seo/src/json-ld-preview.tsx -A 5 -B 2Repository: TanStack/devtools
Length of output: 446
🌐 Web query:
JSON-LD @context specification valid formats object array string
💡 Result:
In JSON-LD 1.1, the value of @context MUST be one of these forms:
- String: an IRI reference (typically a URL to a remote context)
- Object: a context definition
- Array: an array containing any mix of string IRIs, context-definition objects, and/or
null null(explicitly allowed)
This is stated in the JSON-LD 1.1 spec: if an object contains @context, its value MUST be null, an IRI reference, a context definition, or an array composed of any of these. [1] The JSON-LD 1.1 API spec also normalizes context processing by treating a non-array local context as a single-element array during processing. [2]
Sources: [1] (w3.org) [2] (w3.org)
Citations:
- 1: https://www.w3.org/TR/json-ld11/?utm_source=openai
- 2: https://www.w3.org/TR/json-ld11-api/?utm_source=openai
Allow valid non-string @context values in validation.
The validateContext() function rejects any @context that isn't a string. According to the JSON-LD 1.1 specification, @context can be a string (IRI), an object (context definition), an array (mixing strings, objects, and null), or null. Valid JSON-LD documents using object or array forms—such as "@context": ["https://schema.org", {...}]—are incorrectly marked as errors by this validator.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/json-ld-preview.tsx` around lines 135 - 157, The
validator currently rejects any non-string `@context`; update validateContext to
accept JSON‑LD permissible types: explicitly allow null (context === null) and
object (typeof context === 'object' && context !== null) and handle arrays by
verifying at least one element is a valid entry (a schema.org string present in
VALID_SCHEMA_CONTEXTS, an object, or null). Keep the existing string branch for
direct IRIs using VALID_SCHEMA_CONTEXTS, return [] for accepted cases, and only
return the error messages (invalid type or invalid array contents) when none of
these allowed forms are met; reference validateContext, JsonLdValue, and
VALID_SCHEMA_CONTEXTS to locate the changes.
| <nav class={styles().seoSubNav} aria-label="SEO sections"> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'overview' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('overview')} | ||
| > | ||
| SEO Overview | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'heading-structure' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('heading-structure')} | ||
| > | ||
| Heading Structure | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'links-preview' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('links-preview')} | ||
| > | ||
| Links Preview | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'social-previews' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('social-previews')} | ||
| > | ||
| Social Previews | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'serp-preview' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('serp-preview')} | ||
| > | ||
| SERP Preview | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'json-ld-preview' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('json-ld-preview')} | ||
| > | ||
| JSON-LD Preview | ||
| </button> | ||
| </nav> |
There was a problem hiding this comment.
Add explicit tab semantics for the subview switcher.
This behaves like a tabset; adding role="tablist" on the container and role="tab" + aria-selected on buttons will improve assistive-tech clarity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/seo-tab.tsx` around lines 20 - 63, The nav-based
subview switcher should expose explicit tab semantics: add role="tablist" to the
<nav class={styles().seoSubNav}> and for each button (the ones using class
`${styles().seoSubNavLabel}` and activeView()/setActiveView()) add role="tab"
and set aria-selected={activeView() === '<view-name>'} (true for the active
view, false otherwise); keep the existing onClick handlers calling
setActiveView(...) and ensure the active styling logic remains tied to
activeView() so assistive tech and keyboard users can identify the selected tab.
| overflow: { | ||
| titleOverflow: | ||
| measureTextWidth(titleText, TITLE_FONT) > DESKTOP_TITLE_MAX_WIDTH_PX, | ||
| descriptionOverflow: | ||
| desktopDescriptionLines.length > DESKTOP_DESCRIPTION_MAX_LINES || | ||
| desktopDescriptionLines.reduce( | ||
| (sum, line) => sum + measureTextWidth(line, DESCRIPTION_FONT), | ||
| 0, | ||
| ) > DESKTOP_DESCRIPTION_TOTAL_WIDTH_PX, | ||
| descriptionOverflowMobile: | ||
| wrapTextByWidth(descText, MOBILE_DESCRIPTION_WIDTH_PX, DESCRIPTION_FONT) | ||
| .length > MOBILE_DESCRIPTION_MAX_LINES, |
There was a problem hiding this comment.
Track mobile title overflow separately.
titleOverflow is only computed against the desktop width, but the same overflow state is reused for the mobile card. Titles that fit under 620px and still overflow the 328px mobile width will be truncated in the preview without any issue being reported.
♻️ Proposed fix
type SerpOverflow = {
- titleOverflow: boolean
+ titleOverflowDesktop: boolean
+ titleOverflowMobile: boolean
descriptionOverflow: boolean
descriptionOverflowMobile: boolean
}
@@
{
message:
- 'The title is wider than 600px and it may not be displayed in full length.',
- hasIssue: (_, overflow) => overflow.titleOverflow,
+ 'The title exceeds the desktop preview width and may be trimmed.',
+ hasIssue: (_, overflow) => overflow.titleOverflowDesktop,
},
@@
extraChecks: [
+ {
+ message: 'The title exceeds the mobile preview width and may be trimmed.',
+ hasIssue: (_, overflow) => overflow.titleOverflowMobile,
+ },
{
message:
'Description exceeds the 3-line limit for mobile view. Please shorten your text to fit within 3 lines.',
@@
overflow: {
- titleOverflow:
+ titleOverflowDesktop:
measureTextWidth(titleText, TITLE_FONT) > DESKTOP_TITLE_MAX_WIDTH_PX,
+ titleOverflowMobile:
+ measureTextWidth(titleText, TITLE_FONT) > MOBILE_TITLE_MAX_WIDTH_PX,
descriptionOverflow:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| overflow: { | |
| titleOverflow: | |
| measureTextWidth(titleText, TITLE_FONT) > DESKTOP_TITLE_MAX_WIDTH_PX, | |
| descriptionOverflow: | |
| desktopDescriptionLines.length > DESKTOP_DESCRIPTION_MAX_LINES || | |
| desktopDescriptionLines.reduce( | |
| (sum, line) => sum + measureTextWidth(line, DESCRIPTION_FONT), | |
| 0, | |
| ) > DESKTOP_DESCRIPTION_TOTAL_WIDTH_PX, | |
| descriptionOverflowMobile: | |
| wrapTextByWidth(descText, MOBILE_DESCRIPTION_WIDTH_PX, DESCRIPTION_FONT) | |
| .length > MOBILE_DESCRIPTION_MAX_LINES, | |
| overflow: { | |
| titleOverflowDesktop: | |
| measureTextWidth(titleText, TITLE_FONT) > DESKTOP_TITLE_MAX_WIDTH_PX, | |
| titleOverflowMobile: | |
| measureTextWidth(titleText, TITLE_FONT) > MOBILE_TITLE_MAX_WIDTH_PX, | |
| descriptionOverflow: | |
| desktopDescriptionLines.length > DESKTOP_DESCRIPTION_MAX_LINES || | |
| desktopDescriptionLines.reduce( | |
| (sum, line) => sum + measureTextWidth(line, DESCRIPTION_FONT), | |
| 0, | |
| ) > DESKTOP_DESCRIPTION_TOTAL_WIDTH_PX, | |
| descriptionOverflowMobile: | |
| wrapTextByWidth(descText, MOBILE_DESCRIPTION_WIDTH_PX, DESCRIPTION_FONT) | |
| .length > MOBILE_DESCRIPTION_MAX_LINES, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/serp-preview.tsx` around lines 327 - 338, The title
overflow is only computed for desktop (titleOverflow) so mobile previews can be
truncated without detection; add a separate mobile title overflow check (e.g.,
titleOverflowMobile) and compute it against the mobile constraints (use
measureTextWidth or wrapTextByWidth with MOBILE_TITLE_WIDTH_PX, TITLE_FONT and
MOBILE_TITLE_MAX_LINES or MOBILE_TITLE_WIDTH_PX) similar to how
descriptionOverflowMobile is computed, then include that property where the
overflow object is constructed so mobile title truncation is reported
independently from desktop.
| export function SerpPreviewSection() { | ||
| const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead()) | ||
|
|
||
| useHeadChanges(() => { | ||
| setSerp(getSerpFromHead()) | ||
| }) |
There was a problem hiding this comment.
Refresh this panel on client-side navigation too.
This section only reacts to head mutations, but it also renders window.location.href and uses the current location as fallback data. A route change that leaves <head> untouched will keep showing the previous page URL in the SERP preview.
♻️ Proposed fix
import { For, createMemo, createSignal } from 'solid-js'
import { useHeadChanges } from './hooks/use-head-changes'
+import { useLocationChanges } from './hooks/use-location-changes'
@@
useHeadChanges(() => {
setSerp(getSerpFromHead())
})
+
+ useLocationChanges(() => {
+ setSerp(getSerpFromHead())
+ })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/serp-preview.tsx` around lines 525 - 530,
SerpPreviewSection only updates on head mutations; also subscribe to client-side
navigation events so setSerp(getSerpFromHead()) runs on route changes: in
SerpPreviewSection add a listener for 'popstate' and a small wrapper that
patches history.pushState/history.replaceState to dispatch a custom
'locationchange' event, then listen for that event and call
setSerp(getSerpFromHead()); ensure you register these listeners alongside
useHeadChanges and remove/restore the listeners/patch on cleanup to avoid leaks.
| tags: [ | ||
| { key: 'twitter:title', prop: 'title' }, | ||
| { key: 'twitter:description', prop: 'description' }, | ||
| { key: 'twitter:image', prop: 'image' }, | ||
| { key: 'twitter:url', prop: 'url' }, | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
According to current official X Developer Cards docs, is twitter:urla supported/required card tag, or should URL come fromog:url / shared URL context?
💡 Result:
twitter:url is not a supported (or required) X Card meta tag in the current official Cards Markup Tag Reference—it’s not listed among the available twitter:* properties. [1]
For the URL associated with the card, X’s docs instead show using Open Graph canonical URL markup (i.e., og:url) alongside the shared URL/card crawl context (the URL included in the post). The “Getting started” example uses og:url (not twitter:url). [2]
So: don’t add twitter:url; use the shared URL itself and (optionally) og:url for the canonical URL. [1][2]
Replace twitter:url with og:url for Twitter/X Card metadata.
twitter:url is not a supported tag in the official X Cards Markup Tag Reference. Use og:url instead for the canonical URL, as shown in X's documentation.
Proposed fix
{
network: 'X/Twitter',
tags: [
{ key: 'twitter:title', prop: 'title' },
{ key: 'twitter:description', prop: 'description' },
{ key: 'twitter:image', prop: 'image' },
- { key: 'twitter:url', prop: 'url' },
+ { key: 'og:url', prop: 'url' },
],
accent: 'twitter',
},Also applies to: lines 146-158
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/social-previews.tsx` around lines 34 - 39, Replace
the unsupported twitter:url tag with og:url in the tags definitions: update the
tag object where key === 'twitter:url' to use key 'og:url' (keep prop 'url') and
make the same replacement in the second occurrence later in the file (the other
tags array around lines 146-158); ensure both tags arrays in social-previews.tsx
are changed so Twitter/X cards use og:url for the canonical URL.
| xs: (_: string = 'rgb(0 0 0 / 0.1)') => | ||
| `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const, |
There was a problem hiding this comment.
shadow.xs ignores its color argument
The function accepts a color parameter but always returns a hardcoded color, so caller-provided values never apply.
Suggested fix
- xs: (_: string = 'rgb(0 0 0 / 0.1)') =>
- `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const,
+ xs: (color: string = 'rgb(0 0 0 / 0.05)') =>
+ `0 1px 2px 0 ${color}` as const,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| xs: (_: string = 'rgb(0 0 0 / 0.1)') => | |
| `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const, | |
| xs: (color: string = 'rgb(0 0 0 / 0.05)') => | |
| `0 1px 2px 0 ${color}` as const, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/devtools-seo/src/tokens.ts` around lines 274 - 275, The shadow.xs
function currently ignores its color parameter and returns a hardcoded color;
update the xs arrow function in tokens.ts (the shadow.xs definition) to use the
provided color parameter in the returned template string (e.g., interpolate the
color variable instead of the fixed 'rgb(0 0 0 / 0.05)') while preserving the
existing return type (as const) and default value for the parameter.
🎯 Changes
Introduced new tabs in the SEO section:
(Sorry for the low quality video but GitHub didn't let me upload the high quality one)
SEO-tab-pr.mp4
✅ Checklist
pnpm test:pr.🚀 Release Impact
Summary by CodeRabbit
New Features
Bug Fixes
Improvements