Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion content-collections.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineCollection, defineConfig } from '@content-collections/core'
import { extractFrontMatter } from '~/utils/documents.server'
import { normalizeRedirectFrom } from '~/utils/documents.server'

const posts = defineCollection({
name: 'posts',
Expand All @@ -11,16 +11,20 @@ const posts = defineCollection({
draft: z.boolean().optional(),
excerpt: z.string(),
authors: z.string().array(),
redirect_from: z.string().array().optional(),
}),
transform: ({ content, ...post }) => {
// Extract header image (first image after frontmatter)
const headerImageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/)
const headerImage = headerImageMatch ? headerImageMatch[2] : undefined
const redirectFrom = normalizeRedirectFrom(post.redirect_from)

return {
...post,
slug: post._meta.path,
headerImage,
redirect_from: redirectFrom,
redirectFrom,
content,
}
},
Expand Down
243 changes: 243 additions & 0 deletions docs-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Maintainer Documentation Guide

This page covers the markdown features supported in TanStack docs and the preferred workflow for future redirects.

## Redirects

For new page moves or consolidations, keep the canonical page and list old URLs in frontmatter with `redirect_from`.

```yaml
---
title: Overview
redirect_from:
- /framework/react/overview
- /framework/solid/overview
---
```

In the example above, old framework-specific URLs will redirect to the shared `/overview` page without needing to add a new entry to the central redirect files in the `tanstack.com` repo.

## Supported markdown

Docs support normal GitHub-flavored markdown, including:

- headings
- links
- lists
- tables
- fenced code blocks
- images
- blockquotes
- task lists

## Callouts

GitHub-style alerts are supported. For a customized title, for example to replace `Note` with something else, you can use the syntax `> [!TYPE] Title`:

```md
> [!NOTE] Custom title
> Use `redirect_from` on the canonical page when docs are moved.

> [!TIP]
> Prefer absolute paths like `/framework/react/overview`.
```

## Code Blocks

Fenced code blocks are supported with language identifiers for syntax highlighting. You can also add metadata like `title="..."` for file tabs.

````md
```tsx title="app.tsx"
export function App() {
return <div>Hello</div>
}
;``
```
````

## Tabs component

The tabs component lets you group content into tabbed sections. It supports multiple variants, including file tabs and package manager tabs.

### File tabs

Use `variant="files"` when the block should render code examples as file tabs. It scans consecutive code blocks, extracts language, title, and content, and uses that to build `file-tab` data. Titles come from code-block metadata such as `title="..."` or will default to the language name if not provided.

````md
<!-- ::start:tabs variant="files" -->

```tsx file="app.tsx"
export function App() {
return <div>Hello</div>
}
```

```css file="styles.css"
body {
color: tomato;
}
```

<!-- ::end:tabs -->
````

### What matters

- use fenced code blocks
- add `title="..."` if you want meaningful file tab labels
- language comes from the code fence language
- code text is extracted from the `<code>` node content

### Package manager tabs

Package-manager tabs are a special `tabs` variant. The parser reads framework lines like `react: ...` or `solid: ...`, groups packages, and later generates package-manager-specific commands.

There are various supported package manager formats, including npm, yarn, pnpm, and bun.

If you're looking to support a multi-line command, you can add multiple instances of the same framework. For example:

```md
<!-- ::start:tabs variant="package-manager" -->

react: <package-1>
react: <package-2>

<!-- ::end:tabs -->
```

This will become:

```bash
npm i <package-1>
npm i <package-2>
```

#### Supported modes

- `install` (default)
- `dev-install`
- `local-install`
- `create`
- `custom` (for custom command templates)

##### Install (default)

```md
<!-- ::start:tabs variant="package-manager" mode="install" -->

react: <package>
solid: <package>

<!-- ::end:tabs -->
```

becomes

```bash
npm i <package>
```

##### Dev install

```md
<!-- ::start:tabs variant="package-manager" mode="dev-install" -->

react: <package>
solid: <package>

<!-- ::end:tabs -->
```

becomes

```bash
npm i -D <package>
```

##### Local install

```md
<!-- ::start:tabs variant="package-manager" mode="local-install" -->

react: <package>
solid: <package>

<!-- ::end:tabs -->
```

becomes

```bash
npx <package> --workspace=./path/to/workspace
```

##### Create

```md
<!-- ::start:tabs variant="package-manager" mode="create" -->

react: <package>
solid: <package>

<!-- ::end:tabs -->
```

becomes

```bash
npm create <package>
```

##### Custom

```md
<!-- ::start:tabs variant="package-manager" mode="custom" -->

react: <command> <package>
solid: <command> <package>

<!-- ::end:tabs -->
```

becomes

```bash
npm <command> <package>
```

## Framework component

Framework blocks let one markdown source contain React, Solid, or other framework-specific content. Internally, the transformer looks for h1 headings inside the framework block and treats each `# Heading` as a framework section boundary. It then stores framework metadata and rewrites the block into separate framework panels.

> **Note**: This should only be used when the majority of the content is the same. If the content is mostly different, it's better to create separate markdown files for each framework and use redirects to point to the canonical page.

````md
<!-- ::start:framework -->

# React

Use the React adapter here.

```tsx
// React code
```

# Solid

Use the Solid adapter here.

```tsx
// Solid Code
```

<!-- ::end:framework -->
````

Each top-level `#` heading becomes a framework panel. Nested headings inside a framework section are preserved for the table of contents.

## Editing notes

- Keep redirects on the surviving page, not on the page being removed.
- Use absolute paths in `redirect_from`.
- Avoid duplicate `redirect_from` values across pages.
- Existing central redirect files still handle older redirects; use frontmatter for new ones going forward.
25 changes: 22 additions & 3 deletions src/routes/$libraryId/$version.docs.$.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { seo } from '~/utils/seo'
import { Doc } from '~/components/Doc'
import { loadDocs } from '~/utils/docs'
import { loadDocs, resolveDocsRedirect } from '~/utils/docs'
import { findLibrary, getBranch, getLibrary } from '~/libraries'
import { DocContainer } from '~/components/DocContainer'
import type { ConfigSchema } from '~/utils/config'
Expand All @@ -10,6 +10,7 @@ import {
useMatch,
isNotFound,
createFileRoute,
redirect,
} from '@tanstack/react-router'

export const Route = createFileRoute('/$libraryId/$version/docs/$')({
Expand All @@ -22,19 +23,37 @@ export const Route = createFileRoute('/$libraryId/$version/docs/$')({
throw notFound()
}

const branch = getBranch(library, version)
const docsRoot = library.docsRoot || 'docs'

const redirectPath = await resolveDocsRedirect({
repo: library.repo,
branch,
docsRoot,
docsPaths: docsPath ? [docsPath] : [],
})

if (redirectPath !== null) {
throw redirect({
href: `/${libraryId}/${version}/docs${redirectPath ? `/${redirectPath}` : ''}`,
})
}

try {
return await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsPath: `${library.docsRoot || 'docs'}/${docsPath}`,
branch,
docsPath: `${docsRoot}/${docsPath ?? ''}`,
})
} catch (error) {
const isNotFoundError =
isNotFound(error) ||
(error && typeof error === 'object' && 'isNotFound' in error)

if (isNotFoundError) {
throw notFound()
}

throw error
}
},
Expand Down
26 changes: 21 additions & 5 deletions src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@tanstack/react-router'
import { seo } from '~/utils/seo'
import { Doc } from '~/components/Doc'
import { loadDocs } from '~/utils/docs'
import { loadDocs, resolveDocsRedirect } from '~/utils/docs'
import { getBranch, getLibrary } from '~/libraries'
import { capitalize } from '~/utils/utils'
import { DocContainer } from '~/components/DocContainer'
Expand All @@ -21,14 +21,29 @@ export const Route = createFileRoute(
const { _splat: docsPath, framework, version, libraryId } = ctx.params

const library = getLibrary(libraryId)
const branch = getBranch(library, version)
const docsRoot = library.docsRoot || 'docs'

const redirectPath = await resolveDocsRedirect({
repo: library.repo,
branch,
docsRoot,
docsPaths: docsPath
? [`framework/${framework}/${docsPath}`, `${framework}/${docsPath}`]
: [],
})

if (redirectPath !== null) {
throw redirect({
href: `/${libraryId}/${version}/docs${redirectPath ? `/${redirectPath}` : ''}`,
})
}

try {
return await loadDocs({
repo: library.repo,
branch: getBranch(library, version),
docsPath: `${
library.docsRoot || 'docs'
}/framework/${framework}/${docsPath}`,
branch,
docsPath: `${docsRoot}/framework/${framework}/${docsPath ?? ''}`,
})
} catch (error) {
// If doc not found, redirect to framework docs root instead of showing 404
Expand All @@ -37,6 +52,7 @@ export const Route = createFileRoute(
const isNotFoundError =
isNotFound(error) ||
(error && typeof error === 'object' && 'isNotFound' in error)

if (isNotFoundError) {
throw redirect({
to: '/$libraryId/$version/docs/framework/$framework',
Expand Down
Loading
Loading