Skip to content

Commit d7df03b

Browse files
committed
feat(devtools): integrate SEO tab with new devtools-seo package and remove 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.
1 parent 9e1ab03 commit d7df03b

25 files changed

Lines changed: 1878 additions & 1271 deletions

examples/react/basic/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@tanstack/devtools-client": "0.0.6",
1313
"@tanstack/devtools-event-client": "0.4.3",
14+
"@tanstack/devtools-seo": "workspace:*",
1415
"@tanstack/react-devtools": "^0.10.1",
1516
"@tanstack/react-form": "^1.23.7",
1617
"@tanstack/react-query": "^5.90.1",
@@ -22,6 +23,7 @@
2223
"zod": "^4.3.5"
2324
},
2425
"devDependencies": {
26+
"@tanstack/devtools-a11y": "workspace:^",
2527
"@tanstack/devtools-ui": "0.5.1",
2628
"@tanstack/devtools-vite": "0.6.0",
2729
"@tanstack/react-form-devtools": "^0.1.7",

examples/react/basic/src/setup.tsx

Lines changed: 41 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
1-
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
2-
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
3-
4-
import {
5-
Link,
6-
Outlet,
7-
RouterProvider,
8-
createRootRoute,
9-
createRoute,
10-
createRouter,
11-
} from '@tanstack/react-router'
12-
import { TanStackDevtools } from '@tanstack/react-devtools'
13-
import { PackageJsonPanel } from './package-json-panel'
14-
1+
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
2+
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
3+
import { Link, Outlet, RouterProvider, createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
4+
import { TanStackDevtools } from '@tanstack/react-devtools';
5+
import { PackageJsonPanel } from './package-json-panel';
6+
import { createA11yPlugin } from "@tanstack/devtools-a11y";
157
const rootRoute = createRootRoute({
16-
component: () => (
17-
<>
8+
component: () => <>
189
<div className="p-2 flex gap-2">
1910
<Link to="/" className="[&.active]:font-bold">
2011
Home
@@ -26,67 +17,52 @@ const rootRoute = createRootRoute({
2617
<hr />
2718
<Outlet />
2819
</>
29-
),
30-
})
20+
});
3121
const indexRoute = createRoute({
3222
getParentRoute: () => rootRoute,
3323
path: '/',
3424
component: function Index() {
35-
return (
36-
<div className="p-2">
25+
return <div className="p-2">
3726
<h3>Welcome Home!</h3>
38-
</div>
39-
)
40-
},
41-
})
27+
</div>;
28+
}
29+
});
4230
function About() {
43-
return (
44-
<div className="p-2">
31+
return <div className="p-2">
4532
<h3>Hello from About!</h3>
46-
</div>
47-
)
33+
</div>;
4834
}
4935
const aboutRoute = createRoute({
5036
getParentRoute: () => rootRoute,
5137
path: '/about',
52-
component: About,
53-
})
54-
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
38+
component: About
39+
});
40+
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
5541
const router = createRouter({
56-
routeTree,
57-
})
42+
routeTree
43+
});
5844
export default function DevtoolsExample() {
59-
return (
60-
<>
61-
<TanStackDevtools
62-
eventBusConfig={{
63-
connectToServerBus: true,
64-
}}
65-
plugins={[
66-
{
67-
name: 'TanStack Query',
68-
render: <ReactQueryDevtoolsPanel />,
69-
},
70-
{
71-
name: 'TanStack Router',
72-
render: <TanStackRouterDevtoolsPanel router={router} />,
73-
},
74-
{
75-
name: 'TanStack Router',
76-
render: <TanStackRouterDevtoolsPanel router={router} />,
77-
},
78-
{
79-
name: 'Package.json',
80-
render: () => <PackageJsonPanel />,
81-
},
45+
return <>
46+
<TanStackDevtools eventBusConfig={{
47+
connectToServerBus: true
48+
}} plugins={[{
49+
name: 'TanStack Query',
50+
render: <ReactQueryDevtoolsPanel />
51+
}, {
52+
name: 'TanStack Router',
53+
render: <TanStackRouterDevtoolsPanel router={router} />
54+
}, {
55+
name: 'TanStack Router',
56+
render: <TanStackRouterDevtoolsPanel router={router} />
57+
}, {
58+
name: 'Package.json',
59+
render: () => <PackageJsonPanel />
60+
}
8261

83-
/* {
84-
name: "The actual app",
85-
render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
86-
} */
87-
]}
88-
/>
62+
/* {
63+
name: "The actual app",
64+
render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
65+
} */, createA11yPlugin()]} />
8966
<RouterProvider router={router} />
90-
</>
91-
)
92-
}
67+
</>;
68+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
"test:eslint": "nx affected --target=test:eslint --exclude=examples/**",
3232
"test:knip": "knip",
3333
"test:lib": "nx affected --targets=test:lib --exclude=examples/**",
34-
"test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib",
34+
"test:lib:dev": "pnpm test:lib && node -e \"require('node:fs').rmSync('.nx/workspace-data/d/disabled',{force:true})\" && NX_DAEMON=true nx daemon --start && NX_DAEMON=true nx watch --all -- pnpm test:lib",
3535
"test:pr": "nx affected --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build",
3636
"test:sherif": "sherif",
3737
"test:types": "nx affected --targets=test:types --exclude=examples/**",
38-
"watch": "pnpm run build:all && nx watch --all -- pnpm run build:all"
38+
"watch": "pnpm run build:all && node -e \"require('node:fs').rmSync('.nx/workspace-data/d/disabled',{force:true})\" && NX_DAEMON=true nx daemon --start && NX_DAEMON=true nx watch --all -- pnpm run build:all"
3939
},
4040
"nx": {
4141
"includedScripts": [
@@ -85,6 +85,7 @@
8585
"overrides": {
8686
"@tanstack/devtools": "workspace:*",
8787
"@tanstack/devtools-a11y": "workspace:*",
88+
"@tanstack/devtools-seo": "workspace:*",
8889
"@tanstack/react-devtools": "workspace:*",
8990
"@tanstack/preact-devtools": "workspace:*",
9091
"@tanstack/solid-devtools": "workspace:*",

packages/devtools-seo/package.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@tanstack/devtools-seo",
3+
"version": "0.1.0",
4+
"description": "SEO overview panel for TanStack Devtools",
5+
"author": "TanStack",
6+
"license": "MIT",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/TanStack/devtools.git",
10+
"directory": "packages/devtools-seo"
11+
},
12+
"homepage": "https://tanstack.com/devtools",
13+
"funding": {
14+
"type": "github",
15+
"url": "https://github.com/sponsors/tannerlinsley"
16+
},
17+
"keywords": [
18+
"devtools",
19+
"seo",
20+
"solid-js"
21+
],
22+
"type": "module",
23+
"types": "dist/index.d.ts",
24+
"module": "dist/index.js",
25+
"exports": {
26+
".": {
27+
"types": "./dist/index.d.ts",
28+
"import": "./dist/index.js"
29+
},
30+
"./package.json": "./package.json"
31+
},
32+
"sideEffects": false,
33+
"engines": {
34+
"node": ">=18"
35+
},
36+
"files": [
37+
"dist/",
38+
"src"
39+
],
40+
"scripts": {
41+
"clean": "premove ./build ./dist",
42+
"lint:fix": "eslint ./src --fix",
43+
"test:eslint": "eslint ./src",
44+
"test:types": "tsc",
45+
"test:build": "publint --strict",
46+
"build": "tsup"
47+
},
48+
"dependencies": {
49+
"@tanstack/devtools-ui": "workspace:*",
50+
"goober": "^2.1.16",
51+
"solid-js": "^1.9.9"
52+
},
53+
"peerDependencies": {
54+
"solid-js": ">=1.9.7"
55+
},
56+
"devDependencies": {
57+
"tsup": "^8.5.0",
58+
"tsup-preset-solid": "^2.2.0",
59+
"vite-plugin-solid": "^2.11.11"
60+
}
61+
}
File renamed without changes.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { For, Show } from 'solid-js'
22
import { Section, SectionDescription } from '@tanstack/devtools-ui'
3-
import { useStyles } from '../../styles/use-styles'
3+
import { useSeoStyles } from './use-seo-styles'
44
import { pickSeverityClass } from './seo-severity'
55
import type { SeoSeverity } from './seo-severity'
66
import type { SeoSectionSummary } from './seo-section-summary'
@@ -98,7 +98,7 @@ export function getHeadingStructureSummary(): SeoSectionSummary {
9898
}
9999

100100
function headingIndentClass(
101-
s: ReturnType<ReturnType<typeof useStyles>>,
101+
s: ReturnType<ReturnType<typeof useSeoStyles>>,
102102
level: HeadingItem['level'],
103103
): string {
104104
switch (level) {
@@ -118,7 +118,7 @@ function headingIndentClass(
118118
}
119119

120120
function headingTagClass(
121-
s: ReturnType<ReturnType<typeof useStyles>>,
121+
s: ReturnType<ReturnType<typeof useSeoStyles>>,
122122
level: HeadingItem['level'],
123123
): string {
124124
const base = s.seoHeadingTag
@@ -139,7 +139,7 @@ function headingTagClass(
139139
}
140140

141141
export function HeadingStructurePreviewSection() {
142-
const styles = useStyles()
142+
const styles = useSeoStyles()
143143
const headings = extractHeadings()
144144
const issues = validateHeadings(headings)
145145
const s = styles()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { onCleanup, onMount } from 'solid-js'
2+
3+
type HeadChange =
4+
| { kind: 'added'; node: Node }
5+
| { kind: 'removed'; node: Node }
6+
| {
7+
kind: 'attr'
8+
target: Element
9+
name: string | null
10+
oldValue: string | null
11+
}
12+
| { kind: 'title'; title: string }
13+
14+
type UseHeadChangesOptions = {
15+
/**
16+
* Observe attribute changes on elements inside <head>
17+
* Default: true
18+
*/
19+
attributes?: boolean
20+
/**
21+
* Observe added/removed nodes in <head>
22+
* Default: true
23+
*/
24+
childList?: boolean
25+
/**
26+
* Observe descendants of <head>
27+
* Default: true
28+
*/
29+
subtree?: boolean
30+
/**
31+
* Also observe <title> changes explicitly
32+
* Default: true
33+
*/
34+
observeTitle?: boolean
35+
}
36+
37+
export function useHeadChanges(
38+
onChange: (change: HeadChange, raw?: MutationRecord) => void,
39+
opts: UseHeadChangesOptions = {},
40+
) {
41+
const {
42+
attributes = true,
43+
childList = true,
44+
subtree = true,
45+
observeTitle = true,
46+
} = opts
47+
48+
onMount(() => {
49+
const headObserver = new MutationObserver((mutations) => {
50+
for (const m of mutations) {
51+
if (m.type === 'childList') {
52+
m.addedNodes.forEach((node) => onChange({ kind: 'added', node }, m))
53+
m.removedNodes.forEach((node) =>
54+
onChange({ kind: 'removed', node }, m),
55+
)
56+
} else if (m.type === 'attributes') {
57+
const el = m.target as Element
58+
onChange(
59+
{
60+
kind: 'attr',
61+
target: el,
62+
name: m.attributeName,
63+
oldValue: m.oldValue ?? null,
64+
},
65+
m,
66+
)
67+
} else {
68+
// If someone mutates a Text node inside <title>, surface it as a title change.
69+
const isInTitle =
70+
m.target.parentNode &&
71+
(m.target.parentNode as Element).tagName.toLowerCase() === 'title'
72+
if (isInTitle) onChange({ kind: 'title', title: document.title }, m)
73+
}
74+
}
75+
})
76+
77+
headObserver.observe(document.head, {
78+
childList,
79+
attributes,
80+
subtree,
81+
attributeOldValue: attributes,
82+
characterData: true, // helps catch <title> text node edits
83+
characterDataOldValue: false,
84+
})
85+
86+
// Extra explicit observer for <title>, since `document.title = "..."`
87+
// may not always bubble as a head mutation in all setups.
88+
let titleObserver: MutationObserver | undefined
89+
if (observeTitle) {
90+
const titleEl =
91+
document.head.querySelector('title') ||
92+
// create a <title> if missing so future changes are observable
93+
document.head.appendChild(document.createElement('title'))
94+
95+
titleObserver = new MutationObserver(() => {
96+
onChange({ kind: 'title', title: document.title })
97+
})
98+
titleObserver.observe(titleEl, {
99+
childList: true,
100+
characterData: true,
101+
subtree: true,
102+
})
103+
}
104+
105+
onCleanup(() => {
106+
headObserver.disconnect()
107+
titleObserver?.disconnect()
108+
})
109+
})
110+
}

0 commit comments

Comments
 (0)