From 0aef93e8863575c21f6fd57e45ad39296c95f051 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:07:42 +0200 Subject: [PATCH 01/14] refactor(devtools): remove SeoTab component and related functionality This commit deletes the SeoTab component from the devtools package, which included social media preview functionality and meta tag analysis. The removal streamlines the codebase by eliminating unused features. --- packages/devtools/src/tabs/seo-tab/index.tsx | 10 ++ .../social-preview.tsx} | 125 +++++++++--------- 2 files changed, 72 insertions(+), 63 deletions(-) create mode 100644 packages/devtools/src/tabs/seo-tab/index.tsx rename packages/devtools/src/tabs/{seo-tab.tsx => seo-tab/social-preview.tsx} (71%) diff --git a/packages/devtools/src/tabs/seo-tab/index.tsx b/packages/devtools/src/tabs/seo-tab/index.tsx new file mode 100644 index 00000000..2a57d09b --- /dev/null +++ b/packages/devtools/src/tabs/seo-tab/index.tsx @@ -0,0 +1,10 @@ +import { MainPanel } from '@tanstack/devtools-ui' +import { SocialPreviewSection } from './social-preview' + +export const SeoTab = () => { + return ( + + + + ) +} diff --git a/packages/devtools/src/tabs/seo-tab.tsx b/packages/devtools/src/tabs/seo-tab/social-preview.tsx similarity index 71% rename from packages/devtools/src/tabs/seo-tab.tsx rename to packages/devtools/src/tabs/seo-tab/social-preview.tsx index 3aec384d..d73b061b 100644 --- a/packages/devtools/src/tabs/seo-tab.tsx +++ b/packages/devtools/src/tabs/seo-tab/social-preview.tsx @@ -1,27 +1,13 @@ -import { For, createSignal } from 'solid-js' +import { createSignal, For } from 'solid-js' +import { useStyles } from '../../styles/use-styles' +import { useHeadChanges } from '../../hooks/use-head-changes' import { - MainPanel, Section, SectionDescription, SectionIcon, SectionTitle, } from '@tanstack/devtools-ui' import { SocialBubble } from '@tanstack/devtools-ui/icons' -import { useStyles } from '../styles/use-styles' -import { useHeadChanges } from '../hooks/use-head-changes' - -type SocialMeta = { - title?: string - description?: string - image?: string - url?: string -} - -type SocialReport = { - network: string - found: Partial - missing: Array -} const SOCIALS = [ { @@ -96,6 +82,20 @@ const SOCIALS = [ }, // Add more networks as needed ] + +type SocialMeta = { + title?: string + description?: string + image?: string + url?: string +} + +type SocialReport = { + network: string + found: Partial + missing: Array +} + function SocialPreview(props: { meta: SocialMeta color: string @@ -145,7 +145,8 @@ function SocialPreview(props: { ) } -export const SeoTab = () => { + +export function SocialPreviewSection() { const [reports, setReports] = createSignal>(analyzeHead()) const styles = useStyles() @@ -182,51 +183,49 @@ export const SeoTab = () => { }) return ( - -
- - - - - Social previews - - - See how your current page will look when shared on popular social - networks. The tool checks for essential meta tags and highlights any - that are missing. - -
- - {(report, i) => { - const social = SOCIALS[i()] - return ( -
- - {report.missing.length > 0 ? ( - <> -
- Missing tags for {social?.network}: +
+ + + + + Social previews + + + See how your current page will look when shared on popular social + networks. The tool checks for essential meta tags and highlights any + that are missing. + +
+ + {(report, i) => { + const social = SOCIALS[i()] + return ( +
+ + {report.missing.length > 0 ? ( + <> +
+ Missing tags for {social?.network}: -
    - - {(tag) => ( -
  • {tag}
  • - )} -
    -
-
- - ) : null} -
- ) - }} -
-
-
- +
    + + {(tag) => ( +
  • {tag}
  • + )} +
    +
+
+ + ) : null} +
+ ) + }} +
+
+
) } From 6263ab7b096539e3abcc605aa39df1d57709f23f Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:08:17 +0200 Subject: [PATCH 02/14] refactor(devtools): rename SocialPreviewSection to SocialPreviewsSection This commit updates the naming of the SocialPreviewSection component to SocialPreviewsSection for consistency and clarity in the SEO tab of the devtools package. The change improves code readability and aligns with the pluralization of the component's purpose. --- packages/devtools/src/tabs/seo-tab/index.tsx | 4 ++-- packages/devtools/src/tabs/seo-tab/social-preview.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devtools/src/tabs/seo-tab/index.tsx b/packages/devtools/src/tabs/seo-tab/index.tsx index 2a57d09b..e4388ec3 100644 --- a/packages/devtools/src/tabs/seo-tab/index.tsx +++ b/packages/devtools/src/tabs/seo-tab/index.tsx @@ -1,10 +1,10 @@ import { MainPanel } from '@tanstack/devtools-ui' -import { SocialPreviewSection } from './social-preview' +import { SocialPreviewsSection } from './social-preview' export const SeoTab = () => { return ( - + ) } diff --git a/packages/devtools/src/tabs/seo-tab/social-preview.tsx b/packages/devtools/src/tabs/seo-tab/social-preview.tsx index d73b061b..9c8655b7 100644 --- a/packages/devtools/src/tabs/seo-tab/social-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/social-preview.tsx @@ -146,7 +146,7 @@ function SocialPreview(props: { ) } -export function SocialPreviewSection() { +export function SocialPreviewsSection() { const [reports, setReports] = createSignal>(analyzeHead()) const styles = useStyles() From a2f1d8103c7464864d9a0112f2b067056130a1e3 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:08:45 +0200 Subject: [PATCH 03/14] refactor(devtools): rename social-preview file to social-previews and update import path This commit renames the social-preview.tsx file to social-previews.tsx for consistency with the component naming convention. The import path in the SeoTab component is also updated accordingly, enhancing code clarity and maintainability. --- packages/devtools/src/tabs/seo-tab/index.tsx | 2 +- .../tabs/seo-tab/{social-preview.tsx => social-previews.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/devtools/src/tabs/seo-tab/{social-preview.tsx => social-previews.tsx} (100%) diff --git a/packages/devtools/src/tabs/seo-tab/index.tsx b/packages/devtools/src/tabs/seo-tab/index.tsx index e4388ec3..6da37145 100644 --- a/packages/devtools/src/tabs/seo-tab/index.tsx +++ b/packages/devtools/src/tabs/seo-tab/index.tsx @@ -1,5 +1,5 @@ import { MainPanel } from '@tanstack/devtools-ui' -import { SocialPreviewsSection } from './social-preview' +import { SocialPreviewsSection } from './social-previews' export const SeoTab = () => { return ( diff --git a/packages/devtools/src/tabs/seo-tab/social-preview.tsx b/packages/devtools/src/tabs/seo-tab/social-previews.tsx similarity index 100% rename from packages/devtools/src/tabs/seo-tab/social-preview.tsx rename to packages/devtools/src/tabs/seo-tab/social-previews.tsx From 56823702d2ce55705600947b34202508c1b82506 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:21:24 +0200 Subject: [PATCH 04/14] feat(devtools): add SEO tab navigation and enhance SocialPreviewsSection This commit introduces a navigation bar in the SeoTab component, allowing users to switch between 'Social Previews' and 'SERP Preview' sections. It also updates the SocialPreviewsSection to conditionally render its title based on a new prop, improving flexibility in display options. New styles for the navigation elements are added to enhance the user interface. --- packages/devtools/src/styles/use-styles.ts | 26 +++++++++++++ packages/devtools/src/tabs/seo-tab/index.tsx | 31 ++++++++++++++- .../src/tabs/seo-tab/serp-preview.tsx | 38 +++++++++++++++++++ .../src/tabs/seo-tab/social-previews.tsx | 16 ++++---- 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 packages/devtools/src/tabs/seo-tab/serp-preview.tsx diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index 36feac1f..c20e48ec 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -119,6 +119,32 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { margin-bottom: 2rem; border-radius: 0.75rem; `, + seoSubNav: css` + display: flex; + flex-direction: row; + gap: 0; + margin-bottom: 1rem; + border-bottom: 1px solid ${t(colors.gray[200], colors.gray[800])}; + `, + seoSubNavLabel: css` + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + color: ${t(colors.gray[600], colors.gray[400])}; + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + cursor: pointer; + font-family: inherit; + &:hover { + color: ${t(colors.gray[800], colors.gray[200])}; + } + `, + seoSubNavLabelActive: css` + color: ${t(colors.gray[900], colors.gray[100])}; + border-bottom-color: ${t(colors.gray[900], colors.gray[100])}; + `, seoPreviewSection: css` display: flex; flex-direction: row; diff --git a/packages/devtools/src/tabs/seo-tab/index.tsx b/packages/devtools/src/tabs/seo-tab/index.tsx index 6da37145..71b6170d 100644 --- a/packages/devtools/src/tabs/seo-tab/index.tsx +++ b/packages/devtools/src/tabs/seo-tab/index.tsx @@ -1,10 +1,39 @@ +import { Show, createSignal } from 'solid-js' import { MainPanel } from '@tanstack/devtools-ui' +import { useStyles } from '../../styles/use-styles' import { SocialPreviewsSection } from './social-previews' +import { SerpPreviewSection } from './serp-preview' + +type SeoSubView = 'social-previews' | 'serp-preview' export const SeoTab = () => { + const [activeView, setActiveView] = createSignal('social-previews') + const styles = useStyles() + return ( - + + + + + + + ) } diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx new file mode 100644 index 00000000..2261e525 --- /dev/null +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -0,0 +1,38 @@ +import { + Section, + SectionDescription, + SectionIcon, + SectionTitle, +} from '@tanstack/devtools-ui' +import { PageSearch } from '@tanstack/devtools-ui/icons' + +const DUMMY_SERP = { + title: 'Example Page Title - Your Site Name', + description: + 'This is a short meta description that shows how your page might appear in Google search results. Keep it under 160 characters.', + url: 'https://example.com/page-path', +} + +export function SerpPreviewSection(props: { noTitle?: boolean } = {}) { + return ( +
+ {!props.noTitle && ( + + + + + SERP Preview + + )} + + See how your title tag and meta description to see your website's SERP + snippet preview in Google search results. + +
+

{DUMMY_SERP.title}

+

{DUMMY_SERP.description}

+

{DUMMY_SERP.url}

+
+
+ ) +} diff --git a/packages/devtools/src/tabs/seo-tab/social-previews.tsx b/packages/devtools/src/tabs/seo-tab/social-previews.tsx index 9c8655b7..af851d55 100644 --- a/packages/devtools/src/tabs/seo-tab/social-previews.tsx +++ b/packages/devtools/src/tabs/seo-tab/social-previews.tsx @@ -146,7 +146,7 @@ function SocialPreview(props: { ) } -export function SocialPreviewsSection() { +export function SocialPreviewsSection(props: { noTitle?: boolean } = {}) { const [reports, setReports] = createSignal>(analyzeHead()) const styles = useStyles() @@ -184,12 +184,14 @@ export function SocialPreviewsSection() { return (
- - - - - Social previews - + {!props.noTitle && ( + + + + + Social previews + + )} See how your current page will look when shared on popular social networks. The tool checks for essential meta tags and highlights any From 98fa3c9f1fdfbb76635368883adb2ee1ef8489bb Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:26:51 +0200 Subject: [PATCH 05/14] feat(devtools): enhance SERP and Social Previews with new styles and data handling This commit introduces new styles for the SERP snippet and updates the SERP and Social Previews sections to dynamically display data from the current page's metadata. The SocialPreviewsSection and SerpPreviewSection components are refactored to improve clarity and functionality, removing unnecessary props and enhancing the user interface with better styling. --- packages/devtools/src/styles/use-styles.ts | 36 ++++++ packages/devtools/src/tabs/seo-tab/index.tsx | 8 +- .../src/tabs/seo-tab/serp-preview.tsx | 105 +++++++++++++----- .../src/tabs/seo-tab/social-previews.tsx | 18 +-- 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index c20e48ec..ace4c405 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -231,6 +231,42 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { padding: 0 10px 8px 10px; font-size: 0.875rem; `, + serpSnippet: css` + border: 1px solid ${t(colors.gray[200], colors.gray[800])}; + border-radius: 8px; + padding: 1rem 1.25rem; + background: ${t(colors.white, colors.darkGray[900])}; + max-width: 600px; + font-family: ${fontFamily.sans}; + `, + serpSnippetUrlRow: css` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + font-size: 0.875rem; + color: ${t(colors.gray[600], colors.gray[400])}; + `, + serpSnippetFavicon: css` + width: 16px; + height: 16px; + border-radius: 2px; + flex-shrink: 0; + object-fit: contain; + `, + serpSnippetTitle: css` + font-size: 1.25rem; + font-weight: 400; + color: ${t('#1a0dab', '#8ab4f8')}; + margin: 0 0 4px 0; + line-height: 1.3; + `, + serpSnippetDesc: css` + font-size: 0.875rem; + color: ${t(colors.gray[700], colors.gray[300])}; + margin: 0; + line-height: 1.5; + `, devtoolsPanelContainer: ( panelLocation: TanStackDevtoolsConfig['panelLocation'], isDetached: boolean, diff --git a/packages/devtools/src/tabs/seo-tab/index.tsx b/packages/devtools/src/tabs/seo-tab/index.tsx index 71b6170d..c00a97e9 100644 --- a/packages/devtools/src/tabs/seo-tab/index.tsx +++ b/packages/devtools/src/tabs/seo-tab/index.tsx @@ -7,7 +7,8 @@ import { SerpPreviewSection } from './serp-preview' type SeoSubView = 'social-previews' | 'serp-preview' export const SeoTab = () => { - const [activeView, setActiveView] = createSignal('social-previews') + const [activeView, setActiveView] = + createSignal('social-previews') const styles = useStyles() return ( @@ -28,11 +29,12 @@ export const SeoTab = () => { SERP Preview + - + - + ) diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index 2261e525..fc677fc9 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -1,37 +1,86 @@ -import { - Section, - SectionDescription, - SectionIcon, - SectionTitle, -} from '@tanstack/devtools-ui' -import { PageSearch } from '@tanstack/devtools-ui/icons' - -const DUMMY_SERP = { - title: 'Example Page Title - Your Site Name', - description: - 'This is a short meta description that shows how your page might appear in Google search results. Keep it under 160 characters.', - url: 'https://example.com/page-path', +import { createSignal } from 'solid-js' +import { useStyles } from '../../styles/use-styles' +import { useHeadChanges } from '../../hooks/use-head-changes' +import { Section, SectionDescription } from '@tanstack/devtools-ui' + +type SerpData = { + title: string + description: string + siteName: string + favicon: string | null + url: string } -export function SerpPreviewSection(props: { noTitle?: boolean } = {}) { +function getSerpFromHead(): SerpData { + const title = document.title || '' + const url = typeof window !== 'undefined' ? window.location.href : '' + + const metaTags = Array.from(document.head.querySelectorAll('meta')) + const descriptionMeta = metaTags.find( + (m) => m.getAttribute('name')?.toLowerCase() === 'description', + ) + const description = + descriptionMeta?.getAttribute('content')?.trim() || '' + + const siteNameMeta = metaTags.find( + (m) => m.getAttribute('property') === 'og:site_name', + ) + const siteName = + siteNameMeta?.getAttribute('content')?.trim() || + (typeof window !== 'undefined' + ? window.location.hostname.replace(/^www\./, '') + : '') + + const linkTags = Array.from(document.head.querySelectorAll('link')) + const iconLink = linkTags.find( + (l) => + l.getAttribute('rel')?.toLowerCase().split(/\s+/).includes('icon'), + ) + let favicon: string | null = iconLink?.getAttribute('href') || null + if (favicon && typeof window !== 'undefined') { + try { + favicon = new URL(favicon, url).href + } catch { + favicon = null + } + } + + return { title, description, siteName, favicon, url } +} + +export function SerpPreviewSection() { + const [serp, setSerp] = createSignal(getSerpFromHead()) + const styles = useStyles() + + useHeadChanges(() => { + setSerp(getSerpFromHead()) + }) + + const data = serp() + return (
- {!props.noTitle && ( - - - - - SERP Preview - - )} - See how your title tag and meta description to see your website's SERP - snippet preview in Google search results. + See how your title tag and meta description may look in Google search + results. Data is read from the current page. -
-

{DUMMY_SERP.title}

-

{DUMMY_SERP.description}

-

{DUMMY_SERP.url}

+
+
+ {data.favicon ? ( + + ) : null} + {data.siteName || data.url} +
+
+ {data.title || 'No title'} +
+
+ {data.description || 'No meta description.'} +
) diff --git a/packages/devtools/src/tabs/seo-tab/social-previews.tsx b/packages/devtools/src/tabs/seo-tab/social-previews.tsx index af851d55..ebe3ed6a 100644 --- a/packages/devtools/src/tabs/seo-tab/social-previews.tsx +++ b/packages/devtools/src/tabs/seo-tab/social-previews.tsx @@ -1,13 +1,7 @@ import { createSignal, For } from 'solid-js' import { useStyles } from '../../styles/use-styles' import { useHeadChanges } from '../../hooks/use-head-changes' -import { - Section, - SectionDescription, - SectionIcon, - SectionTitle, -} from '@tanstack/devtools-ui' -import { SocialBubble } from '@tanstack/devtools-ui/icons' +import { Section, SectionDescription } from '@tanstack/devtools-ui' const SOCIALS = [ { @@ -146,7 +140,7 @@ function SocialPreview(props: { ) } -export function SocialPreviewsSection(props: { noTitle?: boolean } = {}) { +export function SocialPreviewsSection() { const [reports, setReports] = createSignal>(analyzeHead()) const styles = useStyles() @@ -184,14 +178,6 @@ export function SocialPreviewsSection(props: { noTitle?: boolean } = {}) { return (
- {!props.noTitle && ( - - - - - Social previews - - )} See how your current page will look when shared on popular social networks. The tool checks for essential meta tags and highlights any From 6c45bce93258601b79475f8856a6441379bfd4f4 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:34:40 +0200 Subject: [PATCH 06/14] refactor(devtools): update SERP snippet styles and structure This commit refactors the SERP snippet styles by renaming the `serpSnippetUrlRow` to `serpSnippetTopRow` and introduces new styles for site name and URL. The structure of the SERP preview is enhanced to better organize the display of site information, improving the overall layout and visual consistency. --- packages/devtools/src/styles/use-styles.ts | 32 ++++++++++++++----- .../src/tabs/seo-tab/serp-preview.tsx | 9 ++++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index ace4c405..46f9d797 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -239,21 +239,37 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { max-width: 600px; font-family: ${fontFamily.sans}; `, - serpSnippetUrlRow: css` + serpSnippetTopRow: css` display: flex; align-items: center; - gap: 6px; - margin-bottom: 4px; - font-size: 0.875rem; - color: ${t(colors.gray[600], colors.gray[400])}; + gap: 12px; + margin-bottom: 8px; `, serpSnippetFavicon: css` - width: 16px; - height: 16px; - border-radius: 2px; + width: 28px; + height: 28px; + border-radius: 50%; flex-shrink: 0; object-fit: contain; `, + serpSnippetSiteColumn: css` + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; + `, + serpSnippetSiteName: css` + font-size: 0.875rem; + color: ${t(colors.gray[900], colors.gray[100])}; + line-height: 1.4; + margin: 0; + `, + serpSnippetSiteUrl: css` + font-size: 0.75rem; + color: ${t(colors.gray[500], colors.gray[500])}; + line-height: 1.4; + margin: 0; + `, serpSnippetTitle: css` font-size: 1.25rem; font-weight: 400; diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index fc677fc9..57b1a518 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -65,7 +65,7 @@ export function SerpPreviewSection() { results. Data is read from the current page.
-
+
{data.favicon ? ( ) : null} - {data.siteName || data.url} +
+ + {data.siteName || data.url} + + {data.url} +
{data.title || 'No title'} From f9046d172454c42cd65c06acb4f213b7aea2d087 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:44:52 +0200 Subject: [PATCH 07/14] feat(devtools): enhance SERP preview with truncation logic and overflow reporting This commit adds functionality to the SERP preview section, implementing text truncation for the title and description based on specified width limits. It introduces new state management for overflow detection and displays warnings when the title or description exceeds the defined character limits. Additionally, new styles are added to support the hidden measurement elements for accurate text sizing. --- packages/devtools/src/styles/use-styles.ts | 17 ++++ .../src/tabs/seo-tab/serp-preview.tsx | 86 ++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index 46f9d797..5a9c15da 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -283,6 +283,23 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { margin: 0; line-height: 1.5; `, + serpMeasureHidden: css` + position: absolute; + left: -9999px; + top: 0; + white-space: nowrap; + visibility: hidden; + pointer-events: none; + `, + serpReportSection: css` + margin-top: 1rem; + font-size: 0.875rem; + color: ${t(colors.gray[700], colors.gray[300])}; + `, + serpReportItem: css` + margin-top: 0.5rem; + color: ${t(colors.yellow[700], colors.yellow[400])}; + `, devtoolsPanelContainer: ( panelLocation: TanStackDevtoolsConfig['panelLocation'], isDetached: boolean, diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index 57b1a518..d1147967 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -1,8 +1,26 @@ -import { createSignal } from 'solid-js' +import { createEffect, createSignal } from 'solid-js' import { useStyles } from '../../styles/use-styles' import { useHeadChanges } from '../../hooks/use-head-changes' import { Section, SectionDescription } from '@tanstack/devtools-ui' +const TITLE_MAX_WIDTH_PX = 600 +const DESCRIPTION_MAX_WIDTH_PX = 960 +const ELLIPSIS = '...' + +function truncateToWidth( + el: HTMLDivElement, + text: string, + maxPx: number, +): string { + el.textContent = text + if (el.offsetWidth <= maxPx) return text + for (let i = text.length - 1; i >= 0; i--) { + el.textContent = text.slice(0, i) + ELLIPSIS + if (el.offsetWidth <= maxPx) return text.slice(0, i) + ELLIPSIS + } + return ELLIPSIS +} + type SerpData = { title: string description: string @@ -50,12 +68,48 @@ function getSerpFromHead(): SerpData { export function SerpPreviewSection() { const [serp, setSerp] = createSignal(getSerpFromHead()) + const [titleOverflow, setTitleOverflow] = createSignal(false) + const [descriptionOverflow, setDescriptionOverflow] = createSignal(false) + const [displayTitle, setDisplayTitle] = createSignal('') + const [displayDescription, setDisplayDescription] = createSignal('') + const [titleMeasureEl, setTitleMeasureEl] = createSignal< + HTMLDivElement | undefined + >(undefined) + const [descMeasureEl, setDescMeasureEl] = createSignal< + HTMLDivElement | undefined + >(undefined) const styles = useStyles() useHeadChanges(() => { setSerp(getSerpFromHead()) }) + createEffect(() => { + const titleEl = titleMeasureEl() + const descEl = descMeasureEl() + const data = serp() + if (!titleEl || !descEl) return + + const titleText = data.title || 'No title' + const descText = data.description || 'No meta description.' + + const truncatedTitle = truncateToWidth( + titleEl, + titleText, + TITLE_MAX_WIDTH_PX, + ) + setDisplayTitle(truncatedTitle) + setTitleOverflow(truncatedTitle !== titleText) + + const truncatedDesc = truncateToWidth( + descEl, + descText, + DESCRIPTION_MAX_WIDTH_PX, + ) + setDisplayDescription(truncatedDesc) + setDescriptionOverflow(truncatedDesc !== descText) + }) + const data = serp() return ( @@ -81,12 +135,38 @@ export function SerpPreviewSection() {
- {data.title || 'No title'} + {displayTitle() || data.title || 'No title'}
+
) } From f0534696d78a9e3991cb166daecd0cbaa78dd4ed Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 20:55:48 +0200 Subject: [PATCH 08/14] feat(devtools): add SERP overflow reporting and improve styles This commit enhances the SERP preview section by introducing a new overflow reporting mechanism for the title and description. It adds a list of issues when the title or description exceeds specified limits, improving user feedback. Additionally, new styles are implemented for better visual presentation of error messages in the SERP preview. --- packages/devtools/src/styles/use-styles.ts | 10 ++- .../src/tabs/seo-tab/serp-preview.tsx | 67 +++++++++++++------ 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index 5a9c15da..dc3e9d70 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -296,9 +296,15 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { font-size: 0.875rem; color: ${t(colors.gray[700], colors.gray[300])}; `, + serpErrorList: css` + margin: 4px 0 0 0; + padding-left: 1.25rem; + list-style-type: disc; + `, serpReportItem: css` - margin-top: 0.5rem; - color: ${t(colors.yellow[700], colors.yellow[400])}; + margin-top: 0.25rem; + color: ${t(colors.red[700], colors.red[400])}; + font-size: 0.875rem; `, devtoolsPanelContainer: ( panelLocation: TanStackDevtoolsConfig['panelLocation'], diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index d1147967..02e7add1 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal } from 'solid-js' +import { createEffect, createMemo, createSignal, For } from 'solid-js' import { useStyles } from '../../styles/use-styles' import { useHeadChanges } from '../../hooks/use-head-changes' import { Section, SectionDescription } from '@tanstack/devtools-ui' @@ -37,8 +37,7 @@ function getSerpFromHead(): SerpData { const descriptionMeta = metaTags.find( (m) => m.getAttribute('name')?.toLowerCase() === 'description', ) - const description = - descriptionMeta?.getAttribute('content')?.trim() || '' + const description = descriptionMeta?.getAttribute('content')?.trim() || '' const siteNameMeta = metaTags.find( (m) => m.getAttribute('property') === 'og:site_name', @@ -50,9 +49,8 @@ function getSerpFromHead(): SerpData { : '') const linkTags = Array.from(document.head.querySelectorAll('link')) - const iconLink = linkTags.find( - (l) => - l.getAttribute('rel')?.toLowerCase().split(/\s+/).includes('icon'), + const iconLink = linkTags.find((l) => + l.getAttribute('rel')?.toLowerCase().split(/\s+/).includes('icon'), ) let favicon: string | null = iconLink?.getAttribute('href') || null if (favicon && typeof window !== 'undefined') { @@ -66,6 +64,32 @@ function getSerpFromHead(): SerpData { return { title, description, siteName, favicon, url } } +type SerpOverflow = { + titleOverflow: boolean + descriptionOverflow: boolean +} + +function getSerpReports(data: SerpData, overflow: SerpOverflow): string[] { + const issues: string[] = [] + if (!data.title?.trim()) { + issues.push('No title tag set on the page.') + } + if (!data.description?.trim()) { + issues.push('No meta description set on the page.') + } + if (overflow.titleOverflow) { + issues.push( + 'The title is wider than 600px and it may not be displayed in full length.', + ) + } + if (overflow.descriptionOverflow) { + issues.push( + 'The meta description may get trimmed at ~960 pixels on desktop and at ~680px on mobile. Keep it below ~158 characters.', + ) + } + return issues +} + export function SerpPreviewSection() { const [serp, setSerp] = createSignal(getSerpFromHead()) const [titleOverflow, setTitleOverflow] = createSignal(false) @@ -110,6 +134,13 @@ export function SerpPreviewSection() { setDescriptionOverflow(truncatedDesc !== descText) }) + const reports = createMemo(() => + getSerpReports(serp(), { + titleOverflow: titleOverflow(), + descriptionOverflow: descriptionOverflow(), + }), + ) + const data = serp() return ( @@ -151,22 +182,16 @@ export function SerpPreviewSection() { aria-hidden="true" /> - {(titleOverflow() || descriptionOverflow()) && ( -
- {titleOverflow() && ( -
- The title is wider than 600px and it may not be displayed in full - length. -
- )} - {descriptionOverflow() && ( -
- The meta description may get trimmed at ~960 pixels on desktop - and at ~680px on mobile. Keep it below ~158 characters. -
- )} + {reports().length > 0 ? ( +
+ SERP preview issues: +
    + + {(issue) =>
  • {issue}
  • } +
    +
- )} + ) : null}
) } From e9caf21d3419d8c33e6323331e9e4dbb4b0eaabc Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 21:09:31 +0200 Subject: [PATCH 09/14] feat(devtools): add meta description and default favicon styles This commit enhances the SEO capabilities of the basic example by adding a meta description tag for improved search engine visibility. Additionally, it introduces new styles for a default favicon, ensuring a consistent visual representation when no favicon is provided. This improves user feedback in the SERP preview section. --- examples/react/basic/index.html | 5 +++++ packages/devtools/src/styles/use-styles.ts | 16 ++++++++++++++++ .../devtools/src/tabs/seo-tab/serp-preview.tsx | 7 ++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/examples/react/basic/index.html b/examples/react/basic/index.html index 1c96ec87..b63b73f6 100644 --- a/examples/react/basic/index.html +++ b/examples/react/basic/index.html @@ -28,6 +28,11 @@ Basic Example - TanStack Devtools + + A basic example of using TanStack Devtools with React. diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index dc3e9d70..144aa1f1 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -251,6 +251,22 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { border-radius: 50%; flex-shrink: 0; object-fit: contain; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + `, + serpSnippetDefaultFavicon: css` + width: 28px; + height: 28px; + background-color: ${t(colors.gray[200], colors.gray[800])}; + border-radius: 50%; + flex-shrink: 0; + object-fit: contain; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; `, serpSnippetSiteColumn: css` display: flex; diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index 02e7add1..68a082c6 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -77,6 +77,9 @@ function getSerpReports(data: SerpData, overflow: SerpOverflow): string[] { if (!data.description?.trim()) { issues.push('No meta description set on the page.') } + if (!data.favicon) { + issues.push('No favicon or icon set on the page.') + } if (overflow.titleOverflow) { issues.push( 'The title is wider than 600px and it may not be displayed in full length.', @@ -157,7 +160,9 @@ export function SerpPreviewSection() { alt="" class={styles().serpSnippetFavicon} /> - ) : null} + ) : ( +
+ )}
{data.siteName || data.url} From 700961720ecc03175fd728e834592627b69732e4 Mon Sep 17 00:00:00 2001 From: Abed Al Ghani Shaaban Date: Sat, 7 Mar 2026 21:15:09 +0200 Subject: [PATCH 10/14] feat(devtools): enhance SERP preview layout with new styles This commit introduces new styles for the SERP preview section, including a dedicated block for the preview and a label for better organization. The layout improvements enhance the visual presentation of the desktop preview, ensuring a clearer display of the site name, URL, title, and description. --- packages/devtools/src/styles/use-styles.ts | 9 +++ .../src/tabs/seo-tab/serp-preview.tsx | 67 ++++++++++--------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index 144aa1f1..a849a276 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -231,6 +231,15 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { padding: 0 10px 8px 10px; font-size: 0.875rem; `, + serpPreviewBlock: css` + margin-bottom: 1.5rem; + `, + serpPreviewLabel: css` + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: ${t(colors.gray[700], colors.gray[300])}; + `, serpSnippet: css` border: 1px solid ${t(colors.gray[200], colors.gray[800])}; border-radius: 8px; diff --git a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx index 68a082c6..ed03aa83 100644 --- a/packages/devtools/src/tabs/seo-tab/serp-preview.tsx +++ b/packages/devtools/src/tabs/seo-tab/serp-preview.tsx @@ -152,40 +152,43 @@ export function SerpPreviewSection() { See how your title tag and meta description may look in Google search results. Data is read from the current page. -
-
- {data.favicon ? ( - - ) : ( -
- )} -
- - {data.siteName || data.url} - - {data.url} +
+
Desktop preview
+
+
+ {data.favicon ? ( + + ) : ( +
+ )} +
+ + {data.siteName || data.url} + + {data.url} +
+
+ {displayTitle() || data.title || 'No title'} +
+