diff --git a/README.md b/README.md
index eccf1043da..e8b928f2fb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,19 @@
-

+
+
+
+
+
diff --git a/media/header_form.png b/media/header_form.png
deleted file mode 100644
index 82a4503c97..0000000000
Binary files a/media/header_form.png and /dev/null differ
diff --git a/packages/angular-form/README.md b/packages/angular-form/README.md
index 946e70fbbf..23eb7f167f 100644
--- a/packages/angular-form/README.md
+++ b/packages/angular-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Directives for managing form state in Angular
diff --git a/packages/form-devtools/README.md b/packages/form-devtools/README.md
index 76befe0607..4ff18fa061 100644
--- a/packages/form-devtools/README.md
+++ b/packages/form-devtools/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Devtools for the TanStack form library.
diff --git a/packages/lit-form/README.md b/packages/lit-form/README.md
index ca75c21c78..cbe9688a28 100644
--- a/packages/lit-form/README.md
+++ b/packages/lit-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Controllers for managing form state in Lit
diff --git a/packages/preact-form/README.md b/packages/preact-form/README.md
index b20213713b..6fa21737c4 100644
--- a/packages/preact-form/README.md
+++ b/packages/preact-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Hooks for managing form state in Preact
diff --git a/packages/react-form-devtools/README.md b/packages/react-form-devtools/README.md
index 1866876e9e..ee06f30af1 100644
--- a/packages/react-form-devtools/README.md
+++ b/packages/react-form-devtools/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Hooks for managing form state in React
diff --git a/packages/react-form-nextjs/README.md b/packages/react-form-nextjs/README.md
index 1866876e9e..ee06f30af1 100644
--- a/packages/react-form-nextjs/README.md
+++ b/packages/react-form-nextjs/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Hooks for managing form state in React
diff --git a/packages/react-form-start/README.md b/packages/react-form-start/README.md
index 1866876e9e..ee06f30af1 100644
--- a/packages/react-form-start/README.md
+++ b/packages/react-form-start/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Hooks for managing form state in React
diff --git a/packages/react-form/README.md b/packages/react-form/README.md
index 1866876e9e..ee06f30af1 100644
--- a/packages/react-form/README.md
+++ b/packages/react-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Hooks for managing form state in React
diff --git a/packages/solid-form/README.md b/packages/solid-form/README.md
index 5db77cbe66..f531c9070a 100644
--- a/packages/solid-form/README.md
+++ b/packages/solid-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Signals for managing form state in Solid
diff --git a/packages/vue-form/README.md b/packages/vue-form/README.md
index a09cdedcac..cd945eebf2 100644
--- a/packages/vue-form/README.md
+++ b/packages/vue-form/README.md
@@ -1,6 +1,20 @@
-
+
+
+
+
+
Compositions for managing form state in Vue
diff --git a/scripts/verify-links.ts b/scripts/verify-links.ts
index 8a1c40ddc5..de044e29a8 100644
--- a/scripts/verify-links.ts
+++ b/scripts/verify-links.ts
@@ -9,6 +9,7 @@ const errors: Array<{
link: string
resolvedPath: string
reason: string
+ nav?: string
}> = []
function isRelativeLink(link: string) {
@@ -27,6 +28,29 @@ function stripExtension(p: string): string {
return p.replace(`${extname(p)}`, '')
}
+/**
+ * Map a resolved `/docs` path to the file or directory that actually serves it,
+ * and report whether that target exists.
+ */
+function resolveDocTarget(absPath: string): { path: string; exists: boolean } {
+ // Examples live outside /docs: /docs/framework/{framework}/examples/{name}
+ // is served from /examples/{framework}/{name}
+ if (absPath.includes('/examples/')) {
+ const examplePath = absPath.replace(
+ /\/docs\/framework\/([^/]+)\/examples\//,
+ '/examples/$1/',
+ )
+ return {
+ path: examplePath,
+ exists: existsSync(examplePath) && statSync(examplePath).isDirectory(),
+ }
+ }
+
+ // Everything else is a markdown page
+ const mdPath = absPath.endsWith('.md') ? absPath : `${absPath}.md`
+ return { path: mdPath, exists: existsSync(mdPath) }
+}
+
function relativeLinkExists(link: string, file: string): boolean {
// Remove hash if present
const linkWithoutHash = link.split('#')[0]
@@ -39,7 +63,7 @@ function relativeLinkExists(link: string, file: string): boolean {
// Resolve the path relative to the markdown file's directory
// Nav up a level to simulate how links are resolved on the web
- let absPath = resolve(filePath, '..', linkPath)
+ const absPath = resolve(filePath, '..', linkPath)
// Ensure the resolved path is within /docs
const docsRoot = resolve('docs')
@@ -53,32 +77,13 @@ function relativeLinkExists(link: string, file: string): boolean {
return false
}
- // Check if this is an example path
- const isExample = absPath.includes('/examples/')
-
- let exists = false
-
- if (isExample) {
- // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
- absPath = absPath.replace(
- /\/docs\/framework\/([^/]+)\/examples\//,
- '/examples/$1/',
- )
- // For examples, we want to check if the directory exists
- exists = existsSync(absPath) && statSync(absPath).isDirectory()
- } else {
- // For non-examples, we want to check if the .md file exists
- if (!absPath.endsWith('.md')) {
- absPath = `${absPath}.md`
- }
- exists = existsSync(absPath)
- }
+ const { path: resolvedPath, exists } = resolveDocTarget(absPath)
if (!exists) {
errors.push({
link,
file,
- resolvedPath: absPath,
+ resolvedPath,
reason: 'Not found',
})
}
@@ -108,12 +113,69 @@ async function verifyMarkdownLinks() {
})
}
}
+}
+
+interface ConfigNode {
+ label?: string
+ to?: string
+ children?: Array
+ frameworks?: Array
+}
+
+/**
+ * Every `to` in docs/config.json becomes a sidebar link on tanstack.com, so an
+ * entry pointing at a page that no longer exists renders as a 404. These are
+ * invisible to the markdown scan above, which only reads links written inside
+ * .md files.
+ */
+function verifyConfigLinks() {
+ const configPath = 'docs/config.json'
+ const config = JSON.parse(readFileSync(configPath, 'utf-8')) as {
+ sections?: Array
+ }
+
+ const docsRoot = resolve('docs')
+ let checked = 0
+
+ function walk(node: ConfigNode, breadcrumb: Array) {
+ const trail = node.label ? [...breadcrumb, node.label] : breadcrumb
+
+ if (node.to) {
+ checked++
+ const { path: resolvedPath, exists } = resolveDocTarget(
+ resolve(docsRoot, node.to),
+ )
+
+ if (!exists) {
+ errors.push({
+ file: configPath,
+ link: node.to,
+ resolvedPath,
+ reason: 'Not found',
+ nav: trail.join(' > '),
+ })
+ }
+ }
+
+ node.children?.forEach((child) => walk(child, trail))
+ node.frameworks?.forEach((framework) => walk(framework, trail))
+ }
+
+ const sections = config.sections ?? []
+ sections.forEach((section) => walk(section, []))
+
+ console.log(`Found ${checked} nav entries in ${configPath}\n`)
+}
+
+async function verifyLinks() {
+ await verifyMarkdownLinks()
+ verifyConfigLinks()
if (errors.length > 0) {
console.log(`\n❌ Found ${errors.length} broken links:`)
errors.forEach((err) => {
console.log(
- `${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
+ `${err.file}${err.nav ? `\n nav: ${err.nav}` : ''}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
)
})
process.exit(1)
@@ -122,4 +184,7 @@ async function verifyMarkdownLinks() {
}
}
-verifyMarkdownLinks().catch(console.error)
+verifyLinks().catch((error) => {
+ console.error(error)
+ process.exitCode = 1
+})