Skip to content

Commit 6d9b016

Browse files
committed
feat(devtools): implement SEO devtools plugin with React and Solid integration, update package configurations, and add example app
1 parent c0a417d commit 6d9b016

13 files changed

Lines changed: 286 additions & 73 deletions

File tree

examples/react/seo/index.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<link rel="shortcut icon" href="/emblem-light.svg" />
7+
<link rel="apple-touch-icon" href="/emblem-light.svg" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1" />
9+
<meta name="theme-color" content="#000000" />
10+
11+
<title>SEO Devtools Example - TanStack Devtools</title>
12+
<meta
13+
name="description"
14+
content="A React example for the TanStack SEO devtools plugin."
15+
/>
16+
<meta property="og:title" content="SEO Devtools Example" />
17+
<meta
18+
property="og:description"
19+
content="Inspect page metadata, headings, links, and structured data."
20+
/>
21+
<meta property="og:url" content="https://example.com/seo" />
22+
<meta
23+
property="og:image"
24+
content="https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=1200"
25+
/>
26+
<meta name="twitter:card" content="summary_large_image" />
27+
<meta name="twitter:title" content="SEO Devtools Example" />
28+
<meta
29+
name="twitter:description"
30+
content="Inspect page metadata, headings, links, and structured data."
31+
/>
32+
<link rel="canonical" href="https://example.com/seo" />
33+
</head>
34+
<body>
35+
<script id="seo-json-ld" type="application/ld+json">
36+
{
37+
"@context": "https://schema.org",
38+
"@type": "WebSite",
39+
"name": "TanStack Devtools SEO Example",
40+
"url": "https://example.com/seo"
41+
}
42+
</script>
43+
<noscript>You need to enable JavaScript to run this app.</noscript>
44+
<div id="root"></div>
45+
<script type="module" src="/src/index.tsx"></script>
46+
</body>
47+
</html>

examples/react/seo/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@tanstack/react-devtools-seo-example",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3006",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/devtools-seo": "workspace:^",
13+
"@tanstack/react-devtools": "^0.10.1",
14+
"@tanstack/react-router": "^1.132.0",
15+
"react": "^19.2.0",
16+
"react-dom": "^19.2.0"
17+
},
18+
"devDependencies": {
19+
"@tanstack/devtools-vite": "0.6.0",
20+
"@types/react": "^19.2.0",
21+
"@types/react-dom": "^19.2.0",
22+
"@vitejs/plugin-react": "^6.0.1",
23+
"vite": "^8.0.0"
24+
},
25+
"browserslist": {
26+
"production": [
27+
">0.2%",
28+
"not dead",
29+
"not op_mini all"
30+
],
31+
"development": [
32+
"last 1 chrome version",
33+
"last 1 firefox version",
34+
"last 1 safari version"
35+
]
36+
}
37+
}

examples/react/seo/src/App.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
Link,
3+
Outlet,
4+
RouterProvider,
5+
createRootRoute,
6+
createRoute,
7+
createRouter,
8+
} from '@tanstack/react-router'
9+
10+
function AppShell() {
11+
return (
12+
<div>
13+
<nav
14+
style={{
15+
display: 'flex',
16+
gap: 12,
17+
padding: '16px 24px 0',
18+
}}
19+
>
20+
<Link to="/">Home</Link>
21+
<Link to="/about">About</Link>
22+
</nav>
23+
<Outlet />
24+
</div>
25+
)
26+
}
27+
28+
const rootRoute = createRootRoute({
29+
component: AppShell,
30+
})
31+
32+
const indexRoute = createRoute({
33+
getParentRoute: () => rootRoute,
34+
path: '/',
35+
component: () => {
36+
return <h1>Home</h1>
37+
},
38+
})
39+
40+
const aboutRoute = createRoute({
41+
getParentRoute: () => rootRoute,
42+
path: '/about',
43+
component: () => {
44+
return <h1>About</h1>
45+
},
46+
})
47+
48+
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
49+
50+
const router = createRouter({
51+
routeTree,
52+
})
53+
54+
export default function App() {
55+
return <RouterProvider router={router} />
56+
}

examples/react/seo/src/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import { seoDevtoolsPlugin } from '@tanstack/devtools-seo/react'
4+
import { TanStackDevtools } from '@tanstack/react-devtools'
5+
6+
import App from './App'
7+
8+
createRoot(document.getElementById('root')!).render(
9+
<StrictMode>
10+
<App />
11+
<TanStackDevtools plugins={[seoDevtoolsPlugin()]} />
12+
</StrictMode>,
13+
)

examples/react/seo/tsconfig.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
"moduleResolution": "Bundler",
8+
"allowImportingTsExtensions": true,
9+
"resolveJsonModule": true,
10+
"isolatedModules": true,
11+
"noEmit": true,
12+
"jsx": "react-jsx",
13+
"strict": true,
14+
"noUnusedLocals": true,
15+
"noUnusedParameters": true,
16+
"noFallthroughCasesInSwitch": true
17+
},
18+
"include": ["src", "vite.config.ts"]
19+
}

examples/react/seo/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import react from '@vitejs/plugin-react'
2+
import { defineConfig } from 'vite'
3+
import { devtools } from '@tanstack/devtools-vite'
4+
5+
export default defineConfig({
6+
plugins: [devtools(), react()],
7+
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"size-limit": [
4848
{
4949
"path": "packages/devtools/dist/index.js",
50-
"limit": "69 KB"
50+
"limit": "60 KB"
5151
},
5252
{
5353
"path": "packages/event-bus-client/dist/esm/plugin.js",

packages/devtools-seo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"build": "vite build"
5555
},
5656
"dependencies": {
57+
"@tanstack/devtools": "workspace:*",
5758
"@tanstack/devtools-ui": "workspace:*",
5859
"@tanstack/devtools-utils": "workspace:*",
5960
"goober": "^2.1.16",

packages/devtools-seo/src/core.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @jsxImportSource solid-js */
2+
3+
import { constructCoreClass } from '@tanstack/devtools-utils/solid'
4+
5+
const [SeoDevtoolsCore, SeoDevtoolsCoreNoOp] = constructCoreClass(
6+
() => import('./solid-panel'),
7+
)
8+
9+
export { SeoDevtoolsCore, SeoDevtoolsCoreNoOp }

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { For, Show } from 'solid-js'
1+
import { For, Show, createMemo, createSignal } from 'solid-js'
22
import { Section, SectionDescription } from '@tanstack/devtools-ui'
33
import { useSeoStyles } from './use-seo-styles'
44
import { pickSeverityClass } from './seo-severity'
5+
import { useLocationChanges } from './hooks/use-location-changes'
56
import type { SeoSeverity } from './seo-severity'
67
import type { SeoSectionSummary } from './seo-section-summary'
78

@@ -140,8 +141,18 @@ function headingTagClass(
140141

141142
export function HeadingStructurePreviewSection() {
142143
const styles = useSeoStyles()
143-
const headings = extractHeadings()
144-
const issues = validateHeadings(headings)
144+
const [tick, setTick] = createSignal(0)
145+
146+
useLocationChanges(() => {
147+
setTick((t) => t + 1)
148+
})
149+
150+
const headings = createMemo(() => {
151+
void tick()
152+
return extractHeadings()
153+
})
154+
155+
const issues = createMemo(() => validateHeadings(headings()))
145156
const s = styles()
146157

147158
const issueBulletClass = (sev: SeoSeverity) =>
@@ -169,19 +180,19 @@ export function HeadingStructurePreviewSection() {
169180
<div class={s.seoHeadingTreeHeaderRow}>
170181
<div class={s.serpPreviewLabelFlat}>Heading tree</div>
171182
<span class={s.seoHeadingTreeCount}>
172-
{headings.length} heading{headings.length === 1 ? '' : 's'}
183+
{headings().length} heading{headings().length === 1 ? '' : 's'}
173184
</span>
174185
</div>
175186
<Show
176-
when={headings.length > 0}
187+
when={headings().length > 0}
177188
fallback={
178189
<div class={s.seoMissingTagsSection}>
179190
No headings found on this page.
180191
</div>
181192
}
182193
>
183194
<ul class={s.seoHeadingTreeList}>
184-
<For each={headings}>
195+
<For each={headings()}>
185196
{(heading) => (
186197
<li
187198
class={`${s.seoHeadingTreeItem} ${headingIndentClass(s, heading.level)}`}
@@ -205,11 +216,11 @@ export function HeadingStructurePreviewSection() {
205216
</Show>
206217
</div>
207218

208-
<Show when={issues.length > 0}>
219+
<Show when={issues().length > 0}>
209220
<div class={s.serpPreviewBlock}>
210221
<div class={s.serpPreviewLabel}>Structure issues</div>
211222
<ul class={s.seoIssueList}>
212-
<For each={issues}>
223+
<For each={issues()}>
213224
{(issue) => (
214225
<li class={s.seoIssueRow}>
215226
<span class={issueBulletClass(issue.severity)}></span>

0 commit comments

Comments
 (0)