Skip to content

Commit a7c95fa

Browse files
feat: add seo tab & social previews (#80)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 01482f6 commit a7c95fa

File tree

11 files changed

+600
-3
lines changed

11 files changed

+600
-3
lines changed

.changeset/stupid-shoes-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools': minor
3+
---
4+
5+
add seo tab and improve UX of plugins tab

examples/react/basic/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,32 @@
55
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<meta name="theme-color" content="#000000" />
8+
<meta
9+
name="og:image"
10+
content="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800"
11+
/>
12+
<meta name="og:title" content="Basic Example - TanStack Devtools" />
13+
<meta
14+
name="og:description"
15+
content="A basic example of using TanStack Devtools with React and loading up the social previews"
16+
/>
17+
<meta name="og:url" content="https://example.com/basic" />
18+
19+
<meta
20+
name="twitter:image"
21+
content="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800"
22+
/>
23+
<meta name="twitter:title" content="Basic Example - TanStack Devtools" />
24+
<meta
25+
name="twitter:description"
26+
content="A basic example of using TanStack Devtools with React and loading up the social previews"
27+
/>
28+
<meta name="twitter:url" content="https://example.com/basic" />
829
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
930
<title>Basic Example - TanStack Devtools</title>
31+
<description
32+
>A basic example of using TanStack Devtools with React.</description
33+
>
1034
</head>
1135
<body>
1236
<noscript>You need to enable JavaScript to run this app.</noscript>

examples/react/start/src/components/Header.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export default function Header() {
77
<div className="px-2 font-bold">
88
<Link to="/">Home</Link>
99
</div>
10+
<div className="px-2 font-bold">
11+
<Link to="/about">About</Link>
12+
</div>
1013

1114
<div className="px-2 font-bold">
1215
<Link to="/demo/start/server-funcs">Start - Server Functions</Link>

examples/react/start/src/routeTree.gen.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { createServerRootRoute } from '@tanstack/react-start/server'
1212

1313
import { Route as rootRouteImport } from './routes/__root'
14+
import { Route as AboutRouteImport } from './routes/about'
1415
import { Route as IndexRouteImport } from './routes/index'
1516
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo.start.server-funcs'
1617
import { Route as DemoStartApiRequestRouteImport } from './routes/demo.start.api-request'
@@ -19,6 +20,11 @@ import { ServerRoute as ApiDemoNamesServerRouteImport } from './routes/api.demo-
1920

2021
const rootServerRouteImport = createServerRootRoute()
2122

23+
const AboutRoute = AboutRouteImport.update({
24+
id: '/about',
25+
path: '/about',
26+
getParentRoute: () => rootRouteImport,
27+
} as any)
2228
const IndexRoute = IndexRouteImport.update({
2329
id: '/',
2430
path: '/',
@@ -47,30 +53,43 @@ const ApiDemoNamesServerRoute = ApiDemoNamesServerRouteImport.update({
4753

4854
export interface FileRoutesByFullPath {
4955
'/': typeof IndexRoute
56+
'/about': typeof AboutRoute
5057
'/demo/start/api-request': typeof DemoStartApiRequestRoute
5158
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
5259
}
5360
export interface FileRoutesByTo {
5461
'/': typeof IndexRoute
62+
'/about': typeof AboutRoute
5563
'/demo/start/api-request': typeof DemoStartApiRequestRoute
5664
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
5765
}
5866
export interface FileRoutesById {
5967
__root__: typeof rootRouteImport
6068
'/': typeof IndexRoute
69+
'/about': typeof AboutRoute
6170
'/demo/start/api-request': typeof DemoStartApiRequestRoute
6271
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
6372
}
6473
export interface FileRouteTypes {
6574
fileRoutesByFullPath: FileRoutesByFullPath
66-
fullPaths: '/' | '/demo/start/api-request' | '/demo/start/server-funcs'
75+
fullPaths:
76+
| '/'
77+
| '/about'
78+
| '/demo/start/api-request'
79+
| '/demo/start/server-funcs'
6780
fileRoutesByTo: FileRoutesByTo
68-
to: '/' | '/demo/start/api-request' | '/demo/start/server-funcs'
69-
id: '__root__' | '/' | '/demo/start/api-request' | '/demo/start/server-funcs'
81+
to: '/' | '/about' | '/demo/start/api-request' | '/demo/start/server-funcs'
82+
id:
83+
| '__root__'
84+
| '/'
85+
| '/about'
86+
| '/demo/start/api-request'
87+
| '/demo/start/server-funcs'
7088
fileRoutesById: FileRoutesById
7189
}
7290
export interface RootRouteChildren {
7391
IndexRoute: typeof IndexRoute
92+
AboutRoute: typeof AboutRoute
7493
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute
7594
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute
7695
}
@@ -102,6 +121,13 @@ export interface RootServerRouteChildren {
102121

103122
declare module '@tanstack/react-router' {
104123
interface FileRoutesByPath {
124+
'/about': {
125+
id: '/about'
126+
path: '/about'
127+
fullPath: '/about'
128+
preLoaderRoute: typeof AboutRouteImport
129+
parentRoute: typeof rootRouteImport
130+
}
105131
'/': {
106132
id: '/'
107133
path: '/'
@@ -146,6 +172,7 @@ declare module '@tanstack/react-start/server' {
146172

147173
const rootRouteChildren: RootRouteChildren = {
148174
IndexRoute: IndexRoute,
175+
AboutRoute: AboutRoute,
149176
DemoStartApiRequestRoute: DemoStartApiRequestRoute,
150177
DemoStartServerFuncsRoute: DemoStartServerFuncsRoute,
151178
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/about')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return <div>Hello "/about"!</div>
9+
}

examples/react/start/src/routes/index.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@ import logo from '../logo.svg'
33

44
export const Route = createFileRoute('/')({
55
component: App,
6+
head() {
7+
return {
8+
meta: [
9+
{
10+
name: 'description',
11+
content: 'A basic example of using TanStack Devtools with React.',
12+
},
13+
{ name: 'og:title', content: 'Basic Example - TanStack Devtools' },
14+
{
15+
name: 'og:description',
16+
content: 'A basic example of using TanStack Devtools with React.',
17+
},
18+
{
19+
name: 'og:image',
20+
content:
21+
'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800',
22+
},
23+
{ name: 'og:url', content: 'https://example.com/basic' },
24+
{
25+
name: 'twitter:title',
26+
content: 'Basic Example - TanStack Devtools for twitter',
27+
},
28+
{
29+
name: 'twitter:description',
30+
content:
31+
'A basic example of using TanStack Devtools with React and loading up the social previews',
32+
},
33+
{
34+
name: 'twitter:image',
35+
content:
36+
'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800',
37+
},
38+
{ name: 'twitter:url', content: 'https://example.com/basic' },
39+
],
40+
}
41+
},
642
})
743

844
function App() {
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)