diff --git a/app/projects/query.tsx b/app/projects/query.tsx
index 05e33b0b..290a5ff0 100644
--- a/app/projects/query.tsx
+++ b/app/projects/query.tsx
@@ -1,37 +1,83 @@
+import { FaDiscord, FaGithub } from 'react-icons/fa'
+import reactLogo from '~/images/react-logo.svg'
+import solidLogo from '~/images/solid-logo.svg'
+import vueLogo from '~/images/vue-logo.svg'
+import svelteLogo from '~/images/svelte-logo.svg'
+import angularLogo from '~/images/angular-logo.svg'
+import { useDocsConfig } from '~/utils/config'
+import { Link } from '@remix-run/react'
+import type { ConfigSchema, MenuItem } from '~/utils/config'
+
+export const repo = 'tanstack/query'
+
+export const latestBranch = 'main'
+export const latestVersion = 'v5'
+export const availableVersions = ['v5', 'v4', 'v3']
+
export const gradientText =
'inline-block text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-amber-500'
-export const repo = 'tanstack/query'
+export const frameworks = {
+ react: { label: 'React', logo: reactLogo, value: 'react' },
+ solid: { label: 'Solid', logo: solidLogo, value: 'solid' },
+ vue: { label: 'Vue', logo: vueLogo, value: 'vue' },
+ svelte: { label: 'Svelte', logo: svelteLogo, value: 'svelte' },
+ angular: { label: 'Angular', logo: angularLogo, value: 'angular' },
+} as const
-const latestBranch = 'main'
+export type Framework = keyof typeof frameworks
-export const latestVersion = 'v5'
+export const createLogo = (version?: string) => (
+ <>
+
+ TanStack
+
+
+ Query{' '}
+
+ {version === 'latest' ? latestVersion : version}
+
+
+ >
+)
-export const availableVersions = [
- {
- name: 'v5',
- branch: latestBranch,
- },
- {
- name: 'v4',
- branch: 'v4',
- },
- {
- name: 'v3',
- branch: 'v3',
- },
-] as const
+export const localMenu: MenuItem = {
+ label: 'Menu',
+ children: [
+ {
+ label: 'Home',
+ to: '..',
+ },
+ {
+ label: (
+
+ GitHub
+
+ ),
+ to: `https://github.com/${repo}`,
+ },
+ {
+ label: (
+
+ Discord
+
+ ),
+ to: 'https://tlinz.com/discord',
+ },
+ ],
+}
export function getBranch(argVersion?: string) {
const version = argVersion || latestVersion
- if (version === 'latest') {
- return latestBranch
- }
-
- return (
- availableVersions.find((v) => v.name === version)?.branch ?? latestBranch
- )
+ return ['latest', latestVersion].includes(version) ? latestBranch : version
}
-export type Framework = 'angular' | 'react' | 'svelte' | 'vue' | 'solid'
+export const useQueryDocsConfig = (config: ConfigSchema) => {
+ return useDocsConfig({
+ config,
+ frameworks,
+ localMenu,
+ availableVersions,
+ })
+}
diff --git a/app/routes/query.$.tsx b/app/routes/query.$.tsx
index 023f68f4..506a1fc7 100644
--- a/app/routes/query.$.tsx
+++ b/app/routes/query.$.tsx
@@ -4,7 +4,7 @@ import type { LoaderFunctionArgs } from '@remix-run/node'
export const loader = (context: LoaderFunctionArgs) => {
handleRedirectsFromV3(context)
- return redirect(`/query/latest`, 301)
+ return redirect(`/query/latest`)
}
function handleRedirectsFromV3(context: LoaderFunctionArgs) {
@@ -105,8 +105,7 @@ function handleRedirectsFromV3(context: LoaderFunctionArgs) {
reactQueryv3List.forEach((item) => {
if (url.pathname.startsWith(`/query/v3/${item.from}`)) {
throw redirect(
- `/query/latest/${item.to}?from=reactQueryV3&original=https://tanstack.com/query/v3/${item.to}`,
- 301
+ `/query/latest/${item.to}?from=reactQueryV3&original=https://tanstack.com/query/v3/${item.to}`
)
}
})
diff --git a/app/routes/query.$version.docs.$.tsx b/app/routes/query.$version.docs.$.tsx
new file mode 100644
index 00000000..b738e1a3
--- /dev/null
+++ b/app/routes/query.$version.docs.$.tsx
@@ -0,0 +1,44 @@
+import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node'
+import { repo, getBranch } from '~/projects/query'
+import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary'
+import { seo } from '~/utils/seo'
+import { useLoaderData, useParams } from '@remix-run/react'
+import { loadDocs } from '~/utils/docs'
+import { Doc } from '~/components/Doc'
+
+export const loader = async (context: LoaderFunctionArgs) => {
+ const { '*': docsPath, version } = context.params
+ const { url } = context.request
+
+ return loadDocs({
+ repo,
+ branch: getBranch(version),
+ docPath: `docs/${docsPath}`,
+ currentPath: url,
+ redirectPath: url.replace(/\/docs.*/, '/docs/framework/react/overview'),
+ })
+}
+
+export const meta: MetaFunction = ({ data }) => {
+ return seo({
+ title: `${data?.title} | TanStack Query Docs`,
+ description: data?.description,
+ })
+}
+
+export const ErrorBoundary = DefaultErrorBoundary
+
+export default function RouteDocs() {
+ const { title, content, filePath } = useLoaderData()
+ const { version } = useParams()
+ const branch = getBranch(version)
+ return (
+
+ )
+}
diff --git a/app/routes/query.$version.docs.$framework.$.tsx b/app/routes/query.$version.docs.$framework.$.tsx
deleted file mode 100644
index 7065bdf9..00000000
--- a/app/routes/query.$version.docs.$framework.$.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node'
-import { json, redirect } from '@remix-run/node'
-import { extractFrontMatter, fetchRepoFile } from '~/utils/documents.server'
-import { repo, getBranch } from '~/projects/query'
-import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary'
-import { seo } from '~/utils/seo'
-import removeMarkdown from 'remove-markdown'
-import { useLoaderData, useParams } from '@remix-run/react'
-import { Doc } from '~/components/Doc'
-
-export const loader = async (context: LoaderFunctionArgs) => {
- const { '*': docsPath, framework, version } = context.params
- const url = new URL(context.request.url)
-
- /*
- `v3` has only React docs. See:
- https://github.com/TanStack/query/blob/v3/docs/config.json#L9
-
- The redirect we throw here for all frameworks that do not have
- docs for TanStack Query v3 fixes this bug:
- https://github.com/TanStack/tanstack.com/issues/85
- */
- if (version === 'v3' && framework && framework !== 'react') {
- throw redirect(`/query/v3/docs/react/${docsPath}`)
- }
-
- // When first path part after docs does not contain framework name, add `react`
- if (
- !context.request.url.includes('/docs/angular') &&
- !context.request.url.includes('/docs/react') &&
- !context.request.url.includes('/docs/solid') &&
- !context.request.url.includes('/docs/vue') &&
- !context.request.url.includes('/docs/svelte')
- ) {
- throw redirect(context.request.url.replace(/\/docs\//, '/docs/react/'))
- }
-
- // Redirect old `adapters` pages
- const adaptersRedirects = [
- { from: 'docs/react/adapters/vue-query', to: 'docs/vue/overview' },
- { from: 'docs/react/adapters/solid-query', to: 'docs/solid/overview' },
- { from: 'docs/react/adapters/svelte-query', to: 'docs/svelte/overview' },
- ]
-
- adaptersRedirects.forEach((item) => {
- if (url.pathname.startsWith(`/query/v4/${item.from}`)) {
- throw redirect(`/query/latest/${item.to}`, 301)
- }
- })
-
- if (!docsPath) {
- throw new Error('Invalid docs path')
- }
-
- const filePath = `docs/${framework}/${docsPath}.md`
-
- const branch = getBranch(version)
- const file = await fetchRepoFile(repo, branch, filePath)
-
- if (!file) {
- throw redirect(
- context.request.url.replace(/\/docs.*/, `/docs/${framework}`)
- )
- }
-
- const frontMatter = extractFrontMatter(file)
- const description = removeMarkdown(frontMatter.excerpt ?? '')
-
- return json(
- {
- title: frontMatter.data?.title,
- description,
- filePath,
- content: frontMatter.content,
- },
- {
- headers: {
- 'Cache-Control': 's-maxage=1, stale-while-revalidate=300',
- },
- }
- )
-}
-
-export const meta: MetaFunction = ({ data }) => {
- return seo({
- title: `${data?.title} | TanStack Query Docs`,
- description: data?.description,
- })
-}
-
-export const ErrorBoundary = DefaultErrorBoundary
-
-export default function RouteDocs() {
- const { title, content, filePath } = useLoaderData()
- const { version } = useParams()
- const branch = getBranch(version)
-
- return (
-
- )
-}
diff --git a/app/routes/query.$version.docs.$framework._index.tsx b/app/routes/query.$version.docs.$framework._index.tsx
deleted file mode 100644
index b2e67ee3..00000000
--- a/app/routes/query.$version.docs.$framework._index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { LoaderFunctionArgs } from '@remix-run/node'
-import { redirect } from '@remix-run/node'
-
-export const loader = (context: LoaderFunctionArgs) => {
- // When first path part after docs contain framework name, just redirect to overview
- if (
- context.request.url.includes('/docs/react') ||
- context.request.url.includes('/docs/solid') ||
- context.request.url.includes('/docs/vue') ||
- context.request.url.includes('/docs/svelte')
- ) {
- throw redirect(context.request.url + '/overview')
- }
- // Otherwise it's an old react doc path, so add `react` after `docs`
- throw redirect(context.request.url.replace(/\/docs\//, '/docs/react/'))
-}
diff --git a/app/routes/query.$version.docs.$framework.tsx b/app/routes/query.$version.docs.$framework.tsx
deleted file mode 100644
index 69bf336e..00000000
--- a/app/routes/query.$version.docs.$framework.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import * as React from 'react'
-import { FaDiscord, FaGithub } from 'react-icons/fa'
-import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node'
-import {
- Link,
- json,
- useLoaderData,
- useMatches,
- useNavigate,
-} from '@remix-run/react'
-import { seo } from '~/utils/seo'
-import type { DocsConfig } from '~/components/DocsLayout'
-import { DocsLayout } from '~/components/DocsLayout'
-import { QueryGGBanner } from '~/components/QueryGGBanner'
-import {
- availableVersions,
- getBranch,
- gradientText,
- latestVersion,
- repo,
-} from '~/projects/query'
-import reactLogo from '~/images/react-logo.svg'
-import solidLogo from '~/images/solid-logo.svg'
-import vueLogo from '~/images/vue-logo.svg'
-import svelteLogo from '~/images/svelte-logo.svg'
-import angularLogo from '~/images/angular-logo.svg'
-import type { AvailableOptions } from '~/components/Select'
-import { generatePath } from '~/utils/utils'
-import { getTanstackDocsConfig, type MenuItem } from '~/utils/config'
-
-export const loader = async (context: LoaderFunctionArgs) => {
- const { version, framework } = context.params
- const branch = getBranch(version)
- const tanstackDocsConfig = await getTanstackDocsConfig(repo, branch)
-
- return json({
- tanstackDocsConfig,
- framework,
- version,
- })
-}
-
-const frameworks = {
- react: { label: 'React', logo: reactLogo, value: 'react' },
- solid: { label: 'Solid', logo: solidLogo, value: 'solid' },
- vue: { label: 'Vue', logo: vueLogo, value: 'vue' },
- svelte: { label: 'Svelte', logo: svelteLogo, value: 'svelte' },
- angular: { label: 'Angular', logo: angularLogo, value: 'angular' },
-}
-
-const logo = (version?: string) => (
- <>
-
- TanStack
-
-
- Query{' '}
-
- {version === 'latest' ? latestVersion : version}
-
-
- >
-)
-
-const localMenu: MenuItem = {
- label: 'Menu',
- children: [
- {
- label: 'Home',
- to: '..',
- },
- {
- label: (
-
- GitHub
-
- ),
- to: `https://github.com/${repo}`,
- },
- {
- label: (
-
- Discord
-
- ),
- to: 'https://tlinz.com/discord',
- },
- ],
-}
-
-export const meta: MetaFunction = () => {
- return seo({
- title:
- 'TanStack Query Docs | React Query, Solid Query, Svelte Query, Vue Query',
- })
-}
-
-export default function RouteFrameworkParam() {
- const matches = useMatches()
- const match = matches[matches.length - 1]
- const navigate = useNavigate()
- const { tanstackDocsConfig, version, framework } =
- useLoaderData()
-
- let config = tanstackDocsConfig
-
- const docsConfig = React.useMemo(() => {
- const frameworkMenu = config.frameworkMenus?.find(
- (d) => d.framework === framework
- )
- if (!frameworkMenu) return null
- return {
- ...config,
- menu: [localMenu, ...(frameworkMenu?.menuItems || [])],
- } as DocsConfig
- }, [framework, config])
-
- const frameworkConfig = React.useMemo(() => {
- if (!config.frameworkMenus) {
- return undefined
- }
-
- const availableFrameworks = config.frameworkMenus.reduce(
- (acc: AvailableOptions, menuEntry) => {
- acc[menuEntry.framework as string] =
- frameworks[menuEntry.framework as keyof typeof frameworks]
- return acc
- },
- { react: frameworks['react'] }
- )
-
- return {
- label: 'Framework',
- selected: framework,
- available: availableFrameworks,
- onSelect: (option: { label: string; value: string }) => {
- const url = generatePath(match.id, {
- ...match.params,
- framework: option.value,
- })
- navigate(url)
- },
- }
- }, [framework, match, navigate, config.frameworkMenus])
-
- const versionConfig = React.useMemo(() => {
- const available = availableVersions.reduce(
- (acc: AvailableOptions, version) => {
- acc[version.name] = {
- label: version.name,
- value: version.name,
- }
- return acc
- },
- {
- latest: {
- label: 'Latest',
- value: 'latest',
- },
- }
- )
-
- return {
- label: 'Version',
- selected: version,
- available,
- onSelect: (option: { label: string; value: string }) => {
- const url = generatePath(match.id, {
- ...match.params,
- version: option.value,
- })
- navigate(url)
- },
- }
- }, [version, match, navigate])
-
- return (
- <>
-
-
- >
- )
-}
diff --git a/app/routes/query.$version.docs._index.tsx b/app/routes/query.$version.docs._index.tsx
index c24e00ba..87944c92 100644
--- a/app/routes/query.$version.docs._index.tsx
+++ b/app/routes/query.$version.docs._index.tsx
@@ -3,6 +3,6 @@ import { redirect } from '@remix-run/node'
export const loader = (context: LoaderFunctionArgs) => {
throw redirect(
- context.request.url.replace(/\/docs.*/, '/docs/react/overview')
+ context.request.url.replace(/\/docs.*/, '/docs/framework/react/overview')
)
}
diff --git a/app/routes/query.$version.docs.framework.$framework.$.tsx b/app/routes/query.$version.docs.framework.$framework.$.tsx
new file mode 100644
index 00000000..648a7b91
--- /dev/null
+++ b/app/routes/query.$version.docs.framework.$framework.$.tsx
@@ -0,0 +1,45 @@
+import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node'
+import { repo, getBranch } from '~/projects/query'
+import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary'
+import { seo } from '~/utils/seo'
+import { useLoaderData, useParams } from '@remix-run/react'
+import { Doc } from '~/components/Doc'
+import { loadDocs } from '~/utils/docs'
+
+export const loader = async (context: LoaderFunctionArgs) => {
+ const { '*': docsPath, framework, version } = context.params
+ const { url } = context.request
+
+ return loadDocs({
+ repo,
+ branch: getBranch(version),
+ docPath: `docs/framework/${framework}/${docsPath}`,
+ currentPath: url,
+ redirectPath: url.replace(/\/docs.*/, '/docs/framework/react/overview'),
+ })
+}
+
+export const meta: MetaFunction = ({ data }) => {
+ return seo({
+ title: `${data?.title} | TanStack Query Docs`,
+ description: data?.description,
+ })
+}
+
+export const ErrorBoundary = DefaultErrorBoundary
+
+export default function RouteDocs() {
+ const { title, content, filePath } = useLoaderData()
+ const { version } = useParams()
+ const branch = getBranch(version)
+
+ return (
+
+ )
+}
diff --git a/app/routes/query.$version.docs.$framework.examples._index.tsx b/app/routes/query.$version.docs.framework.$framework._index.tsx
similarity index 60%
rename from app/routes/query.$version.docs.$framework.examples._index.tsx
rename to app/routes/query.$version.docs.framework.$framework._index.tsx
index 0fdcd8cc..87944c92 100644
--- a/app/routes/query.$version.docs.$framework.examples._index.tsx
+++ b/app/routes/query.$version.docs.framework.$framework._index.tsx
@@ -1,10 +1,8 @@
-import { redirect } from '@remix-run/node'
import type { LoaderFunctionArgs } from '@remix-run/node'
+import { redirect } from '@remix-run/node'
export const loader = (context: LoaderFunctionArgs) => {
- const { framework } = context.params
-
throw redirect(
- context.request.url.replace('/examples/', `/examples/${framework}/basic`)
+ context.request.url.replace(/\/docs.*/, '/docs/framework/react/overview')
)
}
diff --git a/app/routes/query.$version.docs.$framework.examples.$.tsx b/app/routes/query.$version.docs.framework.$framework.examples.$.tsx
similarity index 81%
rename from app/routes/query.$version.docs.$framework.examples.$.tsx
rename to app/routes/query.$version.docs.framework.$framework.examples.$.tsx
index 72b729c8..180f5b7f 100644
--- a/app/routes/query.$version.docs.$framework.examples.$.tsx
+++ b/app/routes/query.$version.docs.framework.$framework.examples.$.tsx
@@ -9,30 +9,28 @@ import { capitalize, slugToTitle } from '~/utils/utils'
import { FaExternalLinkAlt } from 'react-icons/fa'
export const loader = async (context: LoaderFunctionArgs) => {
- const { '*': examplePath } = context.params
- const [kind, _name] = (examplePath ?? '').split('/')
- const [name, search] = _name.split('?')
+ const { framework, '*': name } = context.params
- return json({ kind, name, search: search ?? '' })
+ return json({ framework, name })
}
export const meta: MetaFunction = ({ data }) => {
return seo({
- title: `${capitalize(data.kind)} Query ${slugToTitle(
+ title: `${capitalize(data.framework)} Query ${slugToTitle(
data.name
)} Example | TanStack Query Docs`,
description: `An example showing how to implement ${slugToTitle(
data.name
- )} in ${capitalize(data.kind)} Query`,
+ )} in ${capitalize(data.framework)} Query`,
})
}
export default function RouteExamples() {
- const { kind, name, search } = useLoaderData()
+ const { framework, name } = useLoaderData()
const { version } = useParams()
const branch = getBranch(version)
- const examplePath = branch === 'v3' ? name : [kind, name].join('/')
+ const examplePath = [framework, name].join('/')
const [isDark, setIsDark] = React.useState(true)
@@ -41,10 +39,10 @@ export default function RouteExamples() {
}, [])
const githubUrl = `https://github.com/${repo}/tree/${branch}/examples/${examplePath}`
- const stackBlitzUrl = `https://stackblitz.com/github/${repo}/tree/${branch}/examples/${examplePath}?${search}embed=1&theme=${
+ const stackBlitzUrl = `https://stackblitz.com/github/${repo}/tree/${branch}/examples/${examplePath}?embed=1&theme=${
isDark ? 'dark' : 'light'
}`
- const codesandboxUrl = `https://codesandbox.io/s/github/${repo}/tree/${branch}/examples/${examplePath}?${search}embed=1&theme=${
+ const codesandboxUrl = `https://codesandbox.io/s/github/${repo}/tree/${branch}/examples/${examplePath}?embed=1&theme=${
isDark ? 'dark' : 'light'
}`
@@ -53,7 +51,7 @@ export default function RouteExamples() {
- {capitalize(kind)} Example: {slugToTitle(name)}
+ {capitalize(framework)} Example: {slugToTitle(name)}
{
+ throw redirect(context.request.url.replace('/examples/', `/examples/basic`))
+}
diff --git a/app/routes/query.$version.docs.tsx b/app/routes/query.$version.docs.tsx
index 633ee1a2..955f8468 100644
--- a/app/routes/query.$version.docs.tsx
+++ b/app/routes/query.$version.docs.tsx
@@ -1,5 +1,52 @@
-import { Outlet } from '@remix-run/react'
+import * as React from 'react'
+import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node'
+import { json, useLoaderData } from '@remix-run/react'
+import { seo } from '~/utils/seo'
+import { DocsLayout } from '~/components/DocsLayout'
+import { QueryGGBanner } from '~/components/QueryGGBanner'
+import {
+ getBranch,
+ createLogo,
+ repo,
+ useQueryDocsConfig,
+} from '~/projects/query'
+import { getTanstackDocsConfig } from '~/utils/config'
-export default function RouteDocsParam() {
- return
+export const loader = async (context: LoaderFunctionArgs) => {
+ const { version } = context.params
+ const branch = getBranch(version)
+ const tanstackDocsConfig = await getTanstackDocsConfig(repo, branch)
+
+ return json({
+ tanstackDocsConfig,
+ version,
+ })
+}
+
+export const meta: MetaFunction = () => {
+ return seo({
+ title:
+ 'TanStack Query Docs | React Query, Solid Query, Svelte Query, Vue Query',
+ })
+}
+
+export default function RouteFrameworkParam() {
+ const { tanstackDocsConfig, version } = useLoaderData()
+ let config = useQueryDocsConfig(tanstackDocsConfig)
+
+ return (
+ <>
+
+
+ >
+ )
}
diff --git a/app/routes/query.$version.tsx b/app/routes/query.$version.tsx
index 08bc691a..c4ab044d 100644
--- a/app/routes/query.$version.tsx
+++ b/app/routes/query.$version.tsx
@@ -2,6 +2,7 @@ import { Link, Outlet, useLocation, useSearchParams } from '@remix-run/react'
import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary'
import { latestVersion } from '~/projects/query'
import { RedirectVersionBanner } from '~/components/RedirectVersionBanner'
+import { useClientOnlyRender } from '~/utils/useClientOnlyRender'
export const ErrorBoundary = DefaultErrorBoundary
@@ -14,6 +15,10 @@ export default function RouteVersionParam() {
const version = location.pathname.match(/\/query\/v(\d)/)?.[1] || '999'
+ if (!useClientOnlyRender()) {
+ return null
+ }
+
return (
<>
{showV3Redirect ? (