Skip to content

Commit e52378c

Browse files
committed
feat(devtools): introduce SEO overview section with comprehensive analysis
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.
1 parent 7432b11 commit e52378c

9 files changed

Lines changed: 526 additions & 126 deletions

File tree

packages/devtools/src/tabs/seo-tab/canonical-url-preview.tsx renamed to packages/devtools/src/tabs/seo-tab/canonical-url-data.ts

Lines changed: 10 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
1-
import { For, Show } from 'solid-js'
2-
import { Section, SectionDescription } from '@tanstack/devtools-ui'
3-
import { useStyles } from '../../styles/use-styles'
4-
import { seoSeverityColor, type SeoSeverity } from './seo-severity'
1+
import type { SeoSeverity } from './seo-severity'
52

6-
type Issue = {
3+
export type CanonicalPageIssue = {
74
severity: SeoSeverity
85
message: string
96
}
107

11-
type CanonicalData = {
8+
/**
9+
* Canonical URL, robots, and basic URL hygiene derived from the current
10+
* document head and `window.location`.
11+
*/
12+
export type CanonicalPageData = {
1213
currentUrl: string
1314
canonicalRaw: Array<string>
1415
canonicalResolved: Array<string>
1516
robots: Array<string>
1617
indexable: boolean
1718
follow: boolean
18-
issues: Array<Issue>
19+
issues: Array<CanonicalPageIssue>
1920
}
2021

21-
function getCanonicalData(): CanonicalData {
22+
export function getCanonicalPageData(): CanonicalPageData {
2223
const currentUrl = window.location.href
2324
const current = new URL(currentUrl)
2425

@@ -30,7 +31,7 @@ function getCanonicalData(): CanonicalData {
3031
(link) => link.getAttribute('href') || '',
3132
)
3233
const canonicalResolved: Array<string> = []
33-
const issues: Array<Issue> = []
34+
const issues: Array<CanonicalPageIssue> = []
3435

3536
if (canonicalLinks.length === 0) {
3637
issues.push({ severity: 'error', message: 'No canonical link found.' })
@@ -124,67 +125,3 @@ function getCanonicalData(): CanonicalData {
124125
issues,
125126
}
126127
}
127-
128-
export function CanonicalUrlPreviewSection() {
129-
const styles = useStyles()
130-
const data = getCanonicalData()
131-
132-
return (
133-
<Section>
134-
<SectionDescription>
135-
Checks canonical URL, robots directives, indexability/follow signals,
136-
and basic URL hygiene from the current page.
137-
</SectionDescription>
138-
139-
<div class={styles().serpPreviewBlock}>
140-
<div class={styles().serpPreviewLabel}>SEO status</div>
141-
<div style={{ display: 'flex', gap: '12px', 'flex-wrap': 'wrap' }}>
142-
<span>Indexable: {data.indexable ? 'Yes' : 'No'}</span>
143-
<span>Follow: {data.follow ? 'Yes' : 'No'}</span>
144-
<span>Canonical tags: {data.canonicalRaw.length}</span>
145-
</div>
146-
</div>
147-
148-
<div class={styles().serpPreviewBlock}>
149-
<div class={styles().serpPreviewLabel}>Signals</div>
150-
<div>
151-
<strong>Current URL:</strong> {data.currentUrl}
152-
</div>
153-
<div>
154-
<strong>Canonical:</strong>{' '}
155-
{data.canonicalResolved.join(', ') ||
156-
data.canonicalRaw.join(', ') ||
157-
'None'}
158-
</div>
159-
<div>
160-
<strong>Robots directives:</strong> {data.robots.join(', ') || 'None'}
161-
</div>
162-
<div
163-
style={{ 'margin-top': '6px', 'font-size': '12px', color: '#9ca3af' }}
164-
>
165-
X-Robots-Tag response headers are not available in this in-page view.
166-
</div>
167-
</div>
168-
169-
<Show when={data.issues.length > 0}>
170-
<div class={styles().serpPreviewBlock}>
171-
<div class={styles().serpPreviewLabel}>Issues</div>
172-
<ul class={styles().serpErrorList}>
173-
<For each={data.issues}>
174-
{(issue) => (
175-
<li
176-
style={{
177-
color: seoSeverityColor(issue.severity),
178-
'margin-top': '4px',
179-
}}
180-
>
181-
[{issue.severity}] {issue.message}
182-
</li>
183-
)}
184-
</For>
185-
</ul>
186-
</div>
187-
</Show>
188-
</Section>
189-
)
190-
}

packages/devtools/src/tabs/seo-tab/heading-structure-preview.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { For, Show } from 'solid-js'
22
import { Section, SectionDescription } from '@tanstack/devtools-ui'
33
import { useStyles } from '../../styles/use-styles'
44
import { seoSeverityColor, type SeoSeverity } from './seo-severity'
5+
import type { SeoSectionSummary } from './seo-section-summary'
56

67
type HeadingItem = {
78
id: string
@@ -78,6 +79,18 @@ function validateHeadings(headings: Array<HeadingItem>): Array<HeadingIssue> {
7879
return issues
7980
}
8081

82+
/**
83+
* Heading hierarchy issues and count for the SEO overview.
84+
*/
85+
export function getHeadingStructureSummary(): SeoSectionSummary {
86+
const headings = extractHeadings()
87+
const issues = validateHeadings(headings)
88+
return {
89+
issues,
90+
hint: `${headings.length} heading(s)`,
91+
}
92+
}
93+
8194
export function HeadingStructurePreviewSection() {
8295
const styles = useStyles()
8396
const headings = extractHeadings()

packages/devtools/src/tabs/seo-tab/index.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,25 @@ import { SerpPreviewSection } from './serp-preview'
66
import { JsonLdPreviewSection } from './json-ld-preview'
77
import { HeadingStructurePreviewSection } from './heading-structure-preview'
88
import { LinksPreviewSection } from './links-preview'
9-
import { CanonicalUrlPreviewSection } from './canonical-url-preview'
9+
import { SeoOverviewSection } from './seo-overview'
10+
import type { SeoDetailView } from './seo-section-summary'
1011

11-
type SeoSubView =
12-
| 'social-previews'
13-
| 'serp-preview'
14-
| 'json-ld-preview'
15-
| 'heading-structure'
16-
| 'links-preview'
17-
| 'canonical-url'
12+
type SeoSubView = 'overview' | SeoDetailView
1813

1914
export const SeoTab = () => {
20-
const [activeView, setActiveView] =
21-
createSignal<SeoSubView>('social-previews')
15+
const [activeView, setActiveView] = createSignal<SeoSubView>('overview')
2216
const styles = useStyles()
2317

2418
return (
2519
<MainPanel withPadding>
2620
<nav class={styles().seoSubNav} aria-label="SEO sections">
21+
<button
22+
type="button"
23+
class={`${styles().seoSubNavLabel} ${activeView() === 'overview' ? styles().seoSubNavLabelActive : ''}`}
24+
onClick={() => setActiveView('overview')}
25+
>
26+
SEO overview
27+
</button>
2728
<button
2829
type="button"
2930
class={`${styles().seoSubNavLabel} ${activeView() === 'social-previews' ? styles().seoSubNavLabelActive : ''}`}
@@ -59,15 +60,11 @@ export const SeoTab = () => {
5960
>
6061
Links Preview
6162
</button>
62-
<button
63-
type="button"
64-
class={`${styles().seoSubNavLabel} ${activeView() === 'canonical-url' ? styles().seoSubNavLabelActive : ''}`}
65-
onClick={() => setActiveView('canonical-url')}
66-
>
67-
Canonical & URL
68-
</button>
6963
</nav>
7064

65+
<Show when={activeView() === 'overview'}>
66+
<SeoOverviewSection goTo={(view) => setActiveView(view)} />
67+
</Show>
7168
<Show when={activeView() === 'social-previews'}>
7269
<SocialPreviewsSection />
7370
</Show>
@@ -83,9 +80,6 @@ export const SeoTab = () => {
8380
<Show when={activeView() === 'links-preview'}>
8481
<LinksPreviewSection />
8582
</Show>
86-
<Show when={activeView() === 'canonical-url'}>
87-
<CanonicalUrlPreviewSection />
88-
</Show>
8983
</MainPanel>
9084
)
9185
}

packages/devtools/src/tabs/seo-tab/json-ld-preview.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { For, Show } from 'solid-js'
22
import { Section, SectionDescription } from '@tanstack/devtools-ui'
33
import { useStyles } from '../../styles/use-styles'
44
import { seoSeverityColor, type SeoSeverity } from './seo-severity'
5+
import type { SeoSectionSummary } from './seo-section-summary'
56

67
type JsonLdValue = Record<string, unknown>
78

@@ -231,7 +232,7 @@ function getTypeSummary(value: unknown): Array<string> {
231232
return Array.from(typeSet)
232233
}
233234

234-
function analyzeJsonLdScripts(): Array<JsonLdEntry> {
235+
export function analyzeJsonLdScripts(): Array<JsonLdEntry> {
235236
const scripts = Array.from(
236237
document.querySelectorAll<HTMLScriptElement>('script[type="application/ld+json"]'),
237238
)
@@ -276,6 +277,29 @@ function analyzeJsonLdScripts(): Array<JsonLdEntry> {
276277
})
277278
}
278279

280+
/**
281+
* Flattens validation issues from all JSON-LD blocks for the SEO overview.
282+
*/
283+
export function getJsonLdPreviewSummary(): SeoSectionSummary {
284+
const entries = analyzeJsonLdScripts()
285+
if (entries.length === 0) {
286+
return {
287+
issues: [
288+
{
289+
severity: 'info',
290+
message: 'No JSON-LD scripts were detected on this page.',
291+
},
292+
],
293+
hint: 'No blocks',
294+
}
295+
}
296+
const issues = entries.flatMap((entry) => entry.issues)
297+
return {
298+
issues,
299+
hint: `${entries.length} block(s)`,
300+
}
301+
}
302+
279303
function getJsonLdScore(entries: Array<JsonLdEntry>): number {
280304
let errors = 0
281305
let warnings = 0

packages/devtools/src/tabs/seo-tab/links-preview.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { For, Show } from 'solid-js'
22
import { Section, SectionDescription } from '@tanstack/devtools-ui'
33
import { useStyles } from '../../styles/use-styles'
44
import { seoSeverityColor, type SeoSeverity } from './seo-severity'
5+
import type { SeoSectionSummary } from './seo-section-summary'
56

67
type LinkKind = 'internal' | 'external' | 'non-web' | 'invalid'
78

@@ -99,7 +100,9 @@ function classifyLink(anchor: HTMLAnchorElement): LinkRow {
99100
}
100101
}
101102

102-
function analyzeLinks(): Array<LinkRow> {
103+
const LINK_SUMMARY_ISSUE_CAP = 32
104+
105+
export function analyzeLinks(): Array<LinkRow> {
103106
const anchors = Array.from(
104107
document.body.querySelectorAll<HTMLAnchorElement>('a[href]'),
105108
)
@@ -112,6 +115,19 @@ function analyzeLinks(): Array<LinkRow> {
112115
.map(classifyLink)
113116
}
114117

118+
/**
119+
* Link-level issues (capped) and totals for the SEO overview.
120+
*/
121+
export function getLinksPreviewSummary(): SeoSectionSummary {
122+
const links = analyzeLinks()
123+
const allIssues = links.flatMap((row) => row.issues)
124+
return {
125+
issues: allIssues.slice(0, LINK_SUMMARY_ISSUE_CAP),
126+
issueCount: allIssues.length,
127+
hint: `${links.length} link(s)`,
128+
}
129+
}
130+
115131
export function LinksPreviewSection() {
116132
const styles = useStyles()
117133
const links = analyzeLinks()

0 commit comments

Comments
 (0)