fix(docs): resolve hydration crash and broken navigation (#151)

* fix(docs): resolve hydration crash and broken navigation

- Moved server-only `source.getPage()` calls from client-side `head()` into server-side `loader()`.
- Passed extracted metadata (title, description, breadcrumbs) to `head()` via `loaderData`.
- Relocated structured data injection to React component tree to fix hydration mismatches.
- Removed blocking third-party scripts from root route.

* chore(docs): remove playwright dep and dead getOrganizationStructuredData code

- Remove playwright from devDependencies (only used for one-off verification)
- Remove unused getOrganizationStructuredData import from __root.tsx
- Remove dead getOrganizationStructuredData function from seo.ts (zero callers)

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
Rohith Gilla 2026-04-09 20:09:31 +05:30 committed by GitHub
parent 8ee81e199a
commit 667e9480bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 118 deletions

View file

@ -39,4 +39,4 @@
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4"
}
}
}

View file

@ -72,22 +72,6 @@ export function generateMetaTags({
return meta
}
export function getOrganizationStructuredData() {
return {
'@context': 'https://schema.org',
'@type': 'Organization',
name: DOCS_CONFIG.name,
url: DOCS_CONFIG.url,
logo: `${DOCS_CONFIG.url}/favicon.svg`,
description: DOCS_CONFIG.description,
sameAs: [
'https://datapeek.dev',
'https://github.com/Rohithgilla12/data-peek',
'https://x.com/gillarohith',
],
}
}
export function getTechArticleStructuredData({
title,
description,

View file

@ -7,47 +7,36 @@ import {
import * as React from "react";
import appCss from "@/styles/app.css?url";
import { RootProvider } from "fumadocs-ui/provider/tanstack";
import { generateMetaTags, DOCS_CONFIG, getOrganizationStructuredData } from "@/lib/seo";
import { generateMetaTags, DOCS_CONFIG } from "@/lib/seo";
import { Analytics } from "@vercel/analytics/react";
export const Route = createRootRoute({
head: () => ({
meta: generateMetaTags({
title: DOCS_CONFIG.title,
description: DOCS_CONFIG.description,
keywords: [
'data-peek documentation',
'PostgreSQL client docs',
'MySQL client docs',
'SQL client documentation',
'database client guide',
'SQL editor documentation',
head: () => {
return {
meta: generateMetaTags({
title: DOCS_CONFIG.title,
description: DOCS_CONFIG.description,
keywords: [
'data-peek documentation',
'PostgreSQL client docs',
'MySQL client docs',
'SQL client documentation',
'database client guide',
'SQL editor documentation',
],
}),
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
],
}),
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
],
scripts: [
{
src: "https://giveme.gilla.fun/script.js",
},
{
children: `(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "ukb6oie3zz");`,
},
{
src: "https://cdn.littlestats.click/embed/wq9151m57h17nmx",
},
{
src: "https://scripts.simpleanalyticscdn.com/latest.js",
async: true,
},
],
}),
scripts: [
{
src: "https://scripts.simpleanalyticscdn.com/latest.js",
async: true,
},
],
};
},
component: RootComponent,
});
@ -60,16 +49,10 @@ function RootComponent() {
}
function RootDocument({ children }: { children: React.ReactNode }) {
const orgStructuredData = getOrganizationStructuredData();
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgStructuredData) }}
/>
</head>
<body className="flex flex-col min-h-screen antialiased">
<RootProvider

View file

@ -21,37 +21,26 @@ import {
} from "@/lib/seo";
import { motion } from "framer-motion";
export const Route = createFileRoute("/docs/$")({
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split("/") ?? [];
const data = await loader({ data: slugs });
await clientLoader.preload(data.path);
return data;
},
head: ({ loaderData }) => {
if (!loaderData?.path) return {};
const loader = createServerFn({
method: "GET",
})
.inputValidator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => {
const page = source.getPage(slugs);
if (!page) throw notFound();
const page = source.getPage(loaderData.path.split("/").filter(Boolean));
if (!page) return {};
// Access frontmatter - fumadocs page structure
const pageData = (page as any).data || {};
const frontmatter = pageData.frontmatter || {
title: "Documentation",
description: DOCS_CONFIG.description,
};
const pagePath = `/docs/${loaderData.path}`;
const url = `${DOCS_CONFIG.url}${pagePath}`;
// Build breadcrumbs
const breadcrumbs = [
{ name: "Home", url: DOCS_CONFIG.url },
{ name: "Documentation", url: `${DOCS_CONFIG.url}/docs` },
];
const pathParts = loaderData.path.split("/").filter(Boolean);
const pathParts = page.path.split("/").filter(Boolean);
let currentPath = "";
pathParts.forEach((part, index) => {
currentPath += `/${part}`;
@ -65,8 +54,30 @@ export const Route = createFileRoute("/docs/$")({
}
});
const title = frontmatter.title || "Documentation";
const description = frontmatter.description || DOCS_CONFIG.description;
return {
tree: source.pageTree as object,
path: page.path,
title: frontmatter.title || "Documentation",
description: frontmatter.description || DOCS_CONFIG.description,
breadcrumbs,
};
});
export const Route = createFileRoute("/docs/$")({
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split("/") ?? [];
const data = await loader({ data: slugs });
await clientLoader.preload(data.path);
return data;
},
head: ({ loaderData }) => {
if (!loaderData?.path) return {};
const pagePath = `/docs/${loaderData.path}`;
const url = `${DOCS_CONFIG.url}${pagePath}`;
const title = loaderData.title;
const description = loaderData.description;
const meta = generateMetaTags({
title,
@ -84,46 +95,12 @@ export const Route = createFileRoute("/docs/$")({
type: "article",
});
const structuredData = [
getTechArticleStructuredData({
title,
description,
url,
}),
getBreadcrumbStructuredData(breadcrumbs),
];
// Add structured data as script tags
const structuredDataScripts = structuredData.map((data) => ({
children: `(function() {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.textContent = ${JSON.stringify(JSON.stringify(data))};
document.head.appendChild(script);
})();`,
}));
return {
meta,
scripts: structuredDataScripts,
};
},
});
const loader = createServerFn({
method: "GET",
})
.inputValidator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => {
const page = source.getPage(slugs);
if (!page) throw notFound();
return {
tree: source.pageTree as object,
path: page.path,
};
});
const clientLoader = browserCollections.docs.createClientLoader({
component({ toc, frontmatter, default: MDX }) {
return (
@ -156,10 +133,30 @@ function Page() {
[data.tree]
);
const url = `${DOCS_CONFIG.url}/docs${data.path}`;
const structuredData = [
getTechArticleStructuredData({
title: data.title,
description: data.description,
url,
}),
getBreadcrumbStructuredData(data.breadcrumbs),
];
return (
<DocsLayout {...baseOptions()} tree={tree}>
<Content />
</DocsLayout>
<>
{structuredData.map((sd, i) => (
<script
key={i}
type="application/ld+json"
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: JSON.stringify(sd) }}
/>
))}
<DocsLayout {...baseOptions()} tree={tree}>
<Content />
</DocsLayout>
</>
);
}

View file

@ -380,6 +380,9 @@ importers:
nitro:
specifier: 3.0.1-alpha.1
version: 3.0.1-alpha.1(@azure/identity@4.13.0)(@libsql/client@0.15.15)(better-sqlite3@12.5.0)(chokidar@4.0.3)(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7))(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.53.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6)(yaml@2.8.2))
playwright:
specifier: ^1.59.1
version: 1.59.1
tailwindcss:
specifier: ^4.1.16
version: 4.1.17
@ -6513,6 +6516,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -8295,6 +8303,16 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
playwright-core@1.59.1:
resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.59.1:
resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
engines: {node: '>=18'}
hasBin: true
plist@3.1.0:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
@ -16455,6 +16473,9 @@ snapshots:
fs.realpath@1.0.0: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@ -18642,6 +18663,14 @@ snapshots:
exsolve: 1.0.8
pathe: 2.0.3
playwright-core@1.59.1: {}
playwright@1.59.1:
dependencies:
playwright-core: 1.59.1
optionalDependencies:
fsevents: 2.3.2
plist@3.1.0:
dependencies:
'@xmldom/xmldom': 0.8.11