Skip to content

Commit 7432b11

Browse files
committed
feat(devtools): enhance SEO tab with canonical and robots meta tags
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.
1 parent 276b2ef commit 7432b11

4 files changed

Lines changed: 92 additions & 57 deletions

File tree

examples/react/basic/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
content="A basic example of using TanStack Devtools with React."
3434
/>
3535

36+
<link rel="canonical" href="http://localhost:3005/" />
37+
38+
<meta name="robots" content="index, follow" />
39+
3640
<description
3741
>A basic example of using TanStack Devtools with React.</description
3842
>

packages/devtools/src/tabs/seo-tab/canonical-url-preview.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { For } from 'solid-js'
1+
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'
@@ -26,15 +26,20 @@ function getCanonicalData(): CanonicalData {
2626
document.head.querySelectorAll<HTMLLinkElement>('link[rel]'),
2727
).filter((link) => link.rel.toLowerCase().split(/\s+/).includes('canonical'))
2828

29-
const canonicalRaw = canonicalLinks.map((link) => link.getAttribute('href') || '')
29+
const canonicalRaw = canonicalLinks.map(
30+
(link) => link.getAttribute('href') || '',
31+
)
3032
const canonicalResolved: Array<string> = []
3133
const issues: Array<Issue> = []
3234

3335
if (canonicalLinks.length === 0) {
3436
issues.push({ severity: 'error', message: 'No canonical link found.' })
3537
}
3638
if (canonicalLinks.length > 1) {
37-
issues.push({ severity: 'error', message: 'Multiple canonical links found.' })
39+
issues.push({
40+
severity: 'error',
41+
message: 'Multiple canonical links found.',
42+
})
3843
}
3944

4045
for (const raw of canonicalRaw) {
@@ -59,7 +64,10 @@ function getCanonicalData(): CanonicalData {
5964
})
6065
}
6166
} catch {
62-
issues.push({ severity: 'error', message: `Canonical URL is invalid: ${raw}` })
67+
issues.push({
68+
severity: 'error',
69+
message: `Canonical URL is invalid: ${raw}`,
70+
})
6371
}
6472
}
6573

@@ -91,7 +99,8 @@ function getCanonicalData(): CanonicalData {
9199
if (robots.length === 0) {
92100
issues.push({
93101
severity: 'info',
94-
message: 'No robots meta found. Default behavior is usually index/follow.',
102+
message:
103+
'No robots meta found. Default behavior is usually index/follow.',
95104
})
96105
}
97106

@@ -116,16 +125,9 @@ function getCanonicalData(): CanonicalData {
116125
}
117126
}
118127

119-
function getScore(issues: Array<Issue>): number {
120-
const errors = issues.filter((issue) => issue.severity === 'error').length
121-
const warnings = issues.filter((issue) => issue.severity === 'warning').length
122-
return Math.max(0, 100 - errors * 25 - warnings * 10)
123-
}
124-
125128
export function CanonicalUrlPreviewSection() {
126129
const styles = useStyles()
127130
const data = getCanonicalData()
128-
const score = getScore(data.issues)
129131

130132
return (
131133
<Section>
@@ -137,7 +139,6 @@ export function CanonicalUrlPreviewSection() {
137139
<div class={styles().serpPreviewBlock}>
138140
<div class={styles().serpPreviewLabel}>SEO status</div>
139141
<div style={{ display: 'flex', gap: '12px', 'flex-wrap': 'wrap' }}>
140-
<span>Score: {score}%</span>
141142
<span>Indexable: {data.indexable ? 'Yes' : 'No'}</span>
142143
<span>Follow: {data.follow ? 'Yes' : 'No'}</span>
143144
<span>Canonical tags: {data.canonicalRaw.length}</span>
@@ -151,28 +152,39 @@ export function CanonicalUrlPreviewSection() {
151152
</div>
152153
<div>
153154
<strong>Canonical:</strong>{' '}
154-
{data.canonicalResolved.join(', ') || data.canonicalRaw.join(', ') || 'None'}
155+
{data.canonicalResolved.join(', ') ||
156+
data.canonicalRaw.join(', ') ||
157+
'None'}
155158
</div>
156159
<div>
157160
<strong>Robots directives:</strong> {data.robots.join(', ') || 'None'}
158161
</div>
159-
<div style={{ 'margin-top': '6px', 'font-size': '12px', color: '#9ca3af' }}>
162+
<div
163+
style={{ 'margin-top': '6px', 'font-size': '12px', color: '#9ca3af' }}
164+
>
160165
X-Robots-Tag response headers are not available in this in-page view.
161166
</div>
162167
</div>
163168

164-
<div class={styles().serpPreviewBlock}>
165-
<div class={styles().serpPreviewLabel}>Issues</div>
166-
<ul class={styles().serpErrorList}>
167-
<For each={data.issues}>
168-
{(issue) => (
169-
<li style={{ color: seoSeverityColor(issue.severity), 'margin-top': '4px' }}>
170-
[{issue.severity}] {issue.message}
171-
</li>
172-
)}
173-
</For>
174-
</ul>
175-
</div>
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>
176188
</Section>
177189
)
178190
}

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,6 @@ function validateHeadings(headings: Array<HeadingItem>): Array<HeadingIssue> {
7575
}
7676
}
7777

78-
if (issues.length === 0) {
79-
issues.push({
80-
severity: 'info',
81-
message: 'Heading hierarchy looks healthy.',
82-
})
83-
}
84-
8578
return issues
8679
}
8780

@@ -129,18 +122,20 @@ export function HeadingStructurePreviewSection() {
129122
</Show>
130123
</div>
131124

132-
<div class={styles().serpPreviewBlock}>
133-
<div class={styles().serpPreviewLabel}>Structure issues</div>
134-
<ul class={styles().serpErrorList}>
135-
<For each={issues}>
136-
{(issue) => (
137-
<li style={{ color: seoSeverityColor(issue.severity), 'margin-top': '4px' }}>
138-
[{issue.severity}] {issue.message}
139-
</li>
140-
)}
141-
</For>
142-
</ul>
143-
</div>
125+
<Show when={issues.length > 0}>
126+
<div class={styles().serpPreviewBlock}>
127+
<div class={styles().serpPreviewLabel}>Structure issues</div>
128+
<ul class={styles().serpErrorList}>
129+
<For each={issues}>
130+
{(issue) => (
131+
<li style={{ color: seoSeverityColor(issue.severity), 'margin-top': '4px' }}>
132+
[{issue.severity}] {issue.message}
133+
</li>
134+
)}
135+
</For>
136+
</ul>
137+
</div>
138+
</Show>
144139
</Section>
145140
)
146141
}

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ function classifyLink(anchor: HTMLAnchorElement): LinkRow {
2828
const issues: Array<LinkIssue> = []
2929

3030
if (!text) {
31-
issues.push({ severity: 'error', message: 'Missing link text or accessible label.' })
31+
issues.push({
32+
severity: 'error',
33+
message: 'Missing link text or accessible label.',
34+
})
3235
}
3336

3437
if (/^\s*javascript:/i.test(href)) {
@@ -37,7 +40,6 @@ function classifyLink(anchor: HTMLAnchorElement): LinkRow {
3740
}
3841

3942
if (href.startsWith('#')) {
40-
issues.push({ severity: 'info', message: 'Hash link (in-page anchor).' })
4143
return { text, href, resolvedHref: null, kind: 'non-web', issues }
4244
}
4345

@@ -51,12 +53,18 @@ function classifyLink(anchor: HTMLAnchorElement): LinkRow {
5153

5254
const protocol = resolved.protocol.toLowerCase()
5355
if (protocol === 'mailto:' || protocol === 'tel:') {
54-
issues.push({ severity: 'info', message: `Non-web link protocol (${protocol}).` })
56+
issues.push({
57+
severity: 'info',
58+
message: `Non-web link protocol (${protocol}).`,
59+
})
5560
return { text, href, resolvedHref: resolved.href, kind: 'non-web', issues }
5661
}
5762

5863
if (protocol !== 'http:' && protocol !== 'https:') {
59-
issues.push({ severity: 'warning', message: `Unexpected protocol (${protocol}).` })
64+
issues.push({
65+
severity: 'warning',
66+
message: `Unexpected protocol (${protocol}).`,
67+
})
6068
return { text, href, resolvedHref: resolved.href, kind: 'non-web', issues }
6169
}
6270

@@ -92,7 +100,9 @@ function classifyLink(anchor: HTMLAnchorElement): LinkRow {
92100
}
93101

94102
function analyzeLinks(): Array<LinkRow> {
95-
const anchors = Array.from(document.body.querySelectorAll<HTMLAnchorElement>('a[href]'))
103+
const anchors = Array.from(
104+
document.body.querySelectorAll<HTMLAnchorElement>('a[href]'),
105+
)
96106
return anchors
97107
.filter(
98108
(anchor) =>
@@ -112,7 +122,10 @@ export function LinksPreviewSection() {
112122
acc[row.kind] += 1
113123
return acc
114124
},
115-
{ internal: 0, external: 0, 'non-web': 0, invalid: 0 } as Record<LinkKind, number>,
125+
{ internal: 0, external: 0, 'non-web': 0, invalid: 0 } as Record<
126+
LinkKind,
127+
number
128+
>,
116129
)
117130

118131
return (
@@ -130,22 +143,31 @@ export function LinksPreviewSection() {
130143
<span>External: {counts.external}</span>
131144
<span>Non-web: {counts['non-web']}</span>
132145
<span>Invalid: {counts.invalid}</span>
133-
<span>Issues: {issueCount}</span>
146+
<Show when={issueCount > 0}>
147+
<span>Issues: {issueCount}</span>
148+
</Show>
134149
</div>
135150
</div>
136151

137152
<Show
138153
when={links.length > 0}
139154
fallback={
140-
<div class={styles().seoMissingTagsSection}>No links found on this page.</div>
155+
<div class={styles().seoMissingTagsSection}>
156+
No links found on this page.
157+
</div>
141158
}
142159
>
143160
<div class={styles().serpPreviewBlock}>
144161
<div class={styles().serpPreviewLabel}>Links report</div>
145-
<ul class={styles().serpErrorList} style={{ 'list-style': 'none', padding: '0' }}>
162+
<ul
163+
class={styles().serpErrorList}
164+
style={{ 'list-style': 'none', padding: '0' }}
165+
>
146166
<For each={links}>
147167
{(row) => (
148-
<li style={{ 'margin-bottom': '12px', 'padding-bottom': '8px' }}>
168+
<li
169+
style={{ 'margin-bottom': '12px', 'padding-bottom': '8px' }}
170+
>
149171
<div>
150172
<strong>{row.text || '(no text)'}</strong> - {row.kind}
151173
</div>
@@ -156,7 +178,9 @@ export function LinksPreviewSection() {
156178
<ul class={styles().serpErrorList}>
157179
<For each={row.issues}>
158180
{(issue) => (
159-
<li style={{ color: seoSeverityColor(issue.severity) }}>
181+
<li
182+
style={{ color: seoSeverityColor(issue.severity) }}
183+
>
160184
[{issue.severity}] {issue.message}
161185
</li>
162186
)}

0 commit comments

Comments
 (0)