Skip to content

Feat SEO Section (Overview, Heading Structure, Links & JSON LD Preview)#416

Draft
abedshaaban wants to merge 46 commits intoTanStack:mainfrom
abedshaaban:feat/seo-overview
Draft

Feat SEO Section (Overview, Heading Structure, Links & JSON LD Preview)#416
abedshaaban wants to merge 46 commits intoTanStack:mainfrom
abedshaaban:feat/seo-overview

Conversation

@abedshaaban
Copy link
Copy Markdown
Contributor

@abedshaaban abedshaaban commented Apr 6, 2026

🎯 Changes

Introduced new tabs in the SEO section:

  • SEO Overview
  • Heading Structure
  • Links Preview
  • JSON-LD Preview

(Sorry for the low quality video but GitHub didn't let me upload the high quality one)

SEO-tab-pr.mp4

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Ship first SEO devtools plugin with React support: live SERP & social previews, JSON-LD inspection, heading-structure and links analysis, and an overall SEO overview score.
    • Added an SEO example app demonstrating plugin integration.
  • Bug Fixes

    • Improved SERP preview truncation using pixel-measurement for more accurate desktop/mobile rendering.
  • Improvements

    • More balanced health weighting and reactive refreshes on navigation/head changes.

abedshaaban and others added 30 commits April 1, 2026 01:02
…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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ee39095-95a0-43de-9966-36826053d63d

📥 Commits

Reviewing files that changed from the base of the PR and between 800eb0f and e6ba93f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • examples/react/seo/package.json
✅ Files skipped from review due to trivial changes (1)
  • examples/react/seo/package.json

📝 Walkthrough

Walkthrough

Adds a new @tanstack/devtools-seo package implementing multi-view SEO analysis and previews (overview, SERP, social, JSON‑LD, headings, links), Solid/React integration, example app, styles/tokens, and supporting utilities/hooks; also removes the static SEO tab from the main devtools tabs list and adds example HTML metadata.

Changes

Cohort / File(s) Summary
New package manifest & build config
packages/devtools-seo/package.json, packages/devtools-seo/tsconfig.json, packages/devtools-seo/vite.config.ts
Package metadata, exports (including ./react), TS and Vite/Vitest configuration for the new SEO devtools package.
Public API & core bootstrap
packages/devtools-seo/src/index.ts, packages/devtools-seo/src/core.tsx, packages/devtools-seo/src/react/index.ts
Public re-exports, core constructor (SeoDevtoolsCore) and React client entry that expose panel/plugin entrypoints.
React plugin & panels
packages/devtools-seo/src/react/SeoDevtools.tsx, packages/devtools-seo/src/react/plugin.ts
Creates React panel wrapper and two plugin instances (seoDevtoolsPlugin, seoDevtoolsNoOpPlugin) and exports panel components/types.
UI shell / Solid panel
packages/devtools-seo/src/solid-panel.tsx, packages/devtools-seo/src/seo-tab.tsx
Solid panel entry and SeoTab component implementing multi-view navigation and conditional rendering of subsection views.
Overview & subsection UIs
packages/devtools-seo/src/seo-overview.tsx, packages/devtools-seo/src/serp-preview.tsx, packages/devtools-seo/src/social-previews.tsx, packages/devtools-seo/src/json-ld-preview.tsx, packages/devtools-seo/src/heading-structure-preview.tsx, packages/devtools-seo/src/links-preview.tsx
New Solid UI sections for overview, SERP (pixel-measured truncation), social previews, JSON‑LD validation, heading structure, and links auditing; each exposes a summary function for aggregation.
Analysis types & scoring
packages/devtools-seo/src/seo-section-summary.ts, packages/devtools-seo/src/seo-severity.ts
Shared types, severity definitions, scoring functions, and aggregation utilities used across sections.
Utilities & hooks
packages/devtools-seo/src/devtools-dom-filter.ts, packages/devtools-seo/src/canonical-url-data.ts, packages/devtools-seo/src/hooks/use-location-changes.ts
DOM filtering for devtools owned nodes, canonical URL and robots parsing, and a hook to observe location/history changes.
Styling & tokens
packages/devtools-seo/src/use-seo-styles.ts, packages/devtools-seo/src/tokens.ts
Design tokens and theme-aware goober CSS styles used by the SEO UI.
Large new code artifacts (examples)
examples/react/seo/* (package.json, tsconfig.json, vite.config.ts, index.html, src/App.tsx, src/index.tsx)
New React example app demonstrating the SEO plugin and mounting TanStackDevtools with seoDevtoolsPlugin().
Devtools integration change
packages/devtools/src/tabs/index.tsx
Removed the static SEO tab object from the core tabs array (SEO now provided via plugin).
Basic example metadata
examples/react/basic/index.html
Added robots meta, canonical link, and an Organization JSON‑LD script to the basic example HTML.
Scripts & changelog
.changeset/puny-games-bow.md, package.json
New changeset entry for @tanstack/devtools-seo patch release and updated npm scripts to initialize Nx daemon and remove disabled marker before watch.
Other
packages/devtools/src/styles/use-styles.ts
Removed many previously exported SEO-related style keys from the core devtools styles factory (affected style keys listed in diff).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through head tags, scripts, and links,

counted headings, checked what every meta thinks.
Snippets trimmed by pixels, JSON‑LD in sight,
I stitched an overview dashboard late at night.
Hop, sniff, report — SEO blossoms take flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly summarizes the main change: introducing new SEO tabs (Overview, Heading Structure, Links Preview, JSON-LD Preview). It is specific, concise, and clearly communicates the primary feature addition.
Description check ✅ Passed The PR description covers the main changes (new SEO tabs) and includes a demo video. However, the Contributing Guide checklist item is unchecked, and only the test execution and changeset generation are confirmed. The description is brief but addresses the core change, though it could be more detailed about the technical implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 6, 2026

View your CI Pipeline Execution ↗ for commit e6ba93f

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 2m 30s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-09 20:40:31 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 6, 2026

More templates

@tanstack/devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools@416

@tanstack/devtools-a11y

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-a11y@416

@tanstack/devtools-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-client@416

@tanstack/devtools-seo

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-seo@416

@tanstack/devtools-ui

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-ui@416

@tanstack/devtools-utils

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-utils@416

@tanstack/devtools-vite

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-vite@416

@tanstack/devtools-event-bus

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-bus@416

@tanstack/devtools-event-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@416

@tanstack/preact-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/preact-devtools@416

@tanstack/react-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@416

@tanstack/solid-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/solid-devtools@416

@tanstack/vue-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/vue-devtools@416

commit: e6ba93f

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Keep the hidden mobile measurement width in sync with the rendered card.

Line 293 shrinks the mobile snippet to 328px, but serpMeasureHiddenMobile is still 340px later 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 | 🟠 Major

Refresh the SERP snapshot on client-side navigation.

getSerpFromHead() includes window.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: Reuse TANSTACK_DEVTOOLS for the self-filter.

This filter is what keeps the report from counting the devtools' own links, but it currently relies on a duplicated data-testid literal 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

📥 Commits

Reviewing files that changed from the base of the PR and between 22d60fc and 9e1ab03.

📒 Files selected for processing (15)
  • .changeset/puny-games-bow.md
  • examples/react/basic/index.html
  • package.json
  • packages/devtools/src/hooks/use-location-changes.ts
  • packages/devtools/src/styles/use-styles.ts
  • packages/devtools/src/tabs/seo-tab/canonical-url-data.ts
  • packages/devtools/src/tabs/seo-tab/heading-structure-preview.tsx
  • packages/devtools/src/tabs/seo-tab/index.tsx
  • packages/devtools/src/tabs/seo-tab/json-ld-preview.tsx
  • packages/devtools/src/tabs/seo-tab/links-preview.tsx
  • packages/devtools/src/tabs/seo-tab/seo-overview.tsx
  • packages/devtools/src/tabs/seo-tab/seo-section-summary.ts
  • packages/devtools/src/tabs/seo-tab/seo-severity.ts
  • packages/devtools/src/tabs/seo-tab/serp-preview.tsx
  • packages/devtools/src/tabs/seo-tab/social-previews.tsx

Comment on lines +36 to +37
<meta name="robots" content="index, follow" />
<link rel="canonical" href="http://localhost:3005" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +28 to +56
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +204 to +216
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(', ')}`,
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +452 to +470
/**
* 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
/**
* 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.

Comment on lines +26 to +37
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.',
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

@abedshaaban abedshaaban marked this pull request as draft April 6, 2026 15:41
abedshaaban and others added 11 commits April 8, 2026 21:10
…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
@abedshaaban abedshaaban marked this pull request as ready for review April 9, 2026 20:28
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (3)
packages/devtools-seo/src/json-ld-preview.tsx (2)

457-471: ⚠️ Potential issue | 🟠 Major

Use the shared section scorer here too.

This card uses 20/10/2 penalties, while packages/devtools-seo/src/seo-section-summary.ts uses 22/9/2 in sectionHealthScore(). 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 | 🟠 Major

Don't penalize keys outside this curated ruleset.

SUPPORTED_RULES is 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 | 🟠 Major

Don't report an “accessible label” error from a text-only heuristic.

This still only checks textContent, aria-label, and title, so links named via aria-labelledby or descendant img[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 in fontFamily.sans

fontFamily.sans repeats sans-serif twice; 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: Deduplicate neutral and gray scales to avoid drift

These 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 in seoTabContainer.

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 with StrictMode for consistency.

Other React examples in this repo (e.g., a11y-devtools, custom-devtools) wrap the render content with StrictMode. 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 between reports() and SOCIALS.

Using SOCIALS[i()] with non-null assertions is brittle; include network + accent directly 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 use TanStackDevtoolsTheme instead of a local theme union.

This keeps the type definition in sync with @tanstack/devtools-ui and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1ab03 and 800eb0f.

⛔ Files ignored due to path filters (2)
  • examples/react/seo/public/emblem-light.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (35)
  • .changeset/puny-games-bow.md
  • examples/react/basic/src/setup.tsx
  • examples/react/seo/index.html
  • examples/react/seo/package.json
  • examples/react/seo/src/App.tsx
  • examples/react/seo/src/index.tsx
  • examples/react/seo/tsconfig.json
  • examples/react/seo/vite.config.ts
  • package.json
  • packages/devtools-seo/package.json
  • packages/devtools-seo/src/canonical-url-data.ts
  • packages/devtools-seo/src/core.tsx
  • packages/devtools-seo/src/devtools-dom-filter.ts
  • packages/devtools-seo/src/heading-structure-preview.tsx
  • packages/devtools-seo/src/hooks/use-head-changes.ts
  • packages/devtools-seo/src/hooks/use-location-changes.ts
  • packages/devtools-seo/src/index.ts
  • packages/devtools-seo/src/json-ld-preview.tsx
  • packages/devtools-seo/src/links-preview.tsx
  • packages/devtools-seo/src/react/SeoDevtools.tsx
  • packages/devtools-seo/src/react/index.ts
  • packages/devtools-seo/src/react/plugin.ts
  • packages/devtools-seo/src/seo-overview.tsx
  • packages/devtools-seo/src/seo-section-summary.ts
  • packages/devtools-seo/src/seo-severity.ts
  • packages/devtools-seo/src/seo-tab.tsx
  • packages/devtools-seo/src/serp-preview.tsx
  • packages/devtools-seo/src/social-previews.tsx
  • packages/devtools-seo/src/solid-panel.tsx
  • packages/devtools-seo/src/tokens.ts
  • packages/devtools-seo/src/use-seo-styles.ts
  • packages/devtools-seo/tsconfig.json
  • packages/devtools-seo/vite.config.ts
  • packages/devtools/src/styles/use-styles.ts
  • packages/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

Comment on lines +175 to +177
<SectionDescription>
Visualizes heading structure (`h1`-`h6`) in DOM order and highlights
common hierarchy issues. This section scans once when opened.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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().

Comment on lines +135 to +157
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.',
},
]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 2

Repository: 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:


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.

Comment on lines +20 to +63
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +327 to +338
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +525 to +530
export function SerpPreviewSection() {
const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead())

useHeadChanges(() => {
setSerp(getSerpFromHead())
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +34 to +39
tags: [
{ key: 'twitter:title', prop: 'title' },
{ key: 'twitter:description', prop: 'description' },
{ key: 'twitter:image', prop: 'image' },
{ key: 'twitter:url', prop: 'url' },
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.

Comment on lines +274 to +275
xs: (_: string = 'rgb(0 0 0 / 0.1)') =>
`0 1px 2px 0 rgb(0 0 0 / 0.05)` as const,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@abedshaaban abedshaaban marked this pull request as draft April 9, 2026 20:50
@abedshaaban abedshaaban marked this pull request as draft April 9, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant