ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

<!-- src/blog/hello-world.md -->

2026/9/16 16:28:55 拓冰建站 浏览量
<!-- src/blog/hello-world.md --> 【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/routertitle: Hello World published: 2024-01-15 authors:Jane Doe description: My first blog postHero ImageWelcome to my blog! This is my first post.Getting StartedHeres some content withboldanditalictext.console.log(Hello, world!)published 使用 z.string().date() 校验确保必须是合法日期字符串authors 使用数组类型对应 frontmatter 中的列表语法。 ### 3.5 在路由中消费集合 构建时生成 content-collections 模块直接导入即可获得类型完整的文章数组。列表页按发布日期倒序排列并渲染为链接列表 tsx // src/routes/blog.index.tsx import { createFileRoute } from tanstack/react-router import { allPosts } from content-collections export const Route createFileRoute(/blog/)({ component: BlogIndex, }) function BlogIndex() { // Posts are sorted by published date const sortedPosts allPosts.sort( (a, b) new Date(b.published).getTime() - new Date(a.published).getTime(), ) return ( div h1Blog/h1 ul {sortedPosts.map((post) ( li key{post.slug} Link to/blog/$slug params{{ slug: post.slug }} h2{post.title}/h2 p{post.excerpt}/p span{post.published}/span /Link /li ))} /ul /div ) } ### 3.6 渲染单篇文章详情 详情页通过 $slug 动态段匹配在 loader 中查找对应文章未命中时抛出 notFound()交由 TanStack Router 内置的 404 机制处理——notFound/isNotFound 由 tanstack/router-core 导出并在 packages/router-core/src/not-found.ts 中实现路由层会拦截这类错误并渲染 Not Found 匹配项参见 [router-core 导出](https://link.gitcode.com/i/fc474d5546ecc79127116799d6be78ed) tsx // src/routes/blog.$slug.tsx import { createFileRoute, notFound } from tanstack/react-router import { allPosts } from content-collections import { Markdown } from ~/components/Markdown export const Route createFileRoute(/blog/$slug)({ loader: ({ params }) { const post allPosts.find((p) p.slug params.slug) if (!post) { throw notFound() } return post }, component: BlogPost, }) function BlogPost() { const post Route.useLoaderData() return ( article header h1{post.title}/h1 p By {post.authors.join(, )} on {post.published} /p /header Markdown content{post.content} classNameprose / /article ) } 注意 Markdown 使用了 Tailwind 风格的 prose 类名若未使用 Tailwind Typography 插件可自行编写文章排版样式。 --- ## 四、方式二从远程源动态获取 Markdown 当内容存放在仓库外部如 GitHub 仓库、需要实时更新时可借助 TanStack Start 的 **Server Function** 动态抓取并渲染。createServerFn 是 TanStack Start 的核心原语由 tanstack/react-start 直接导出见 [react-start 公共 API](https://link.gitcode.com/i/eb6a8bb15667880fbc3268c54a68bc39)其底层实现来自 tanstack/start-client-core它保证逻辑只在服务端执行同时维持跨网络边界的类型安全。 ### 4.1 创建服务端抓取工具 tsx // src/utils/docs.server.ts import { createServerFn } from tanstack/react-start import matter from gray-matter type FetchDocsParams { repo: string // e.g., tanstack/router branch: string // e.g., main filePath: string // e.g., docs/guide/getting-started.md } export const fetchDocs createServerFn({ method: GET }) .validator((params: FetchDocsParams) params) .handler(async ({ data: { repo, branch, filePath } }) { const url https://raw.githubusercontent.com/${repo}/${branch}/${filePath} const response await fetch(url, { headers: { // Add GitHub token for private repos or higher rate limits // Authorization: token ${process.env.GITHUB_TOKEN}, }, }) if (!response.ok) { throw new Error(Failed to fetch: ${response.status}) } const rawContent await response.text() const { data: frontmatter, content } matter(rawContent) return { frontmatter, content, filePath, } }) 要点说明 - createServerFn({ method: GET }) 声明 HTTP 方法GET 为默认值适合可缓存的读取操作 - .validator() 在请求进入 handler 前校验参数保证类型安全 - 拉取后立即用 gray-matter 剥离 frontmatter返回结构化的 { frontmatter, content, filePath } - 私有仓库或需要更高速率限制时可在 headers 中注入 Authorization: token ${process.env.GITHUB_TOKEN}。 ### 4.2 为生产环境添加缓存头 在 handler 中通过 context.response 设置缓存头让 CDN 层缓存文档内容 tsx export const fetchDocs createServerFn({ method: GET }) .validator((params: FetchDocsParams) params) .handler(async ({ data: { repo, branch, filePath }, context }) { // Set cache headers for CDN caching context.response.headers.set( Cache-Control, public, max-age0, must-revalidate, ) context.response.headers.set( CDN-Cache-Control, max-age300, stale-while-revalidate300, ) // ... fetch logic }) 这里采用了浏览器缓存与 CDN 缓存分层的经典策略Cache-Control 对浏览器要求每次重新验证must-revalidate而 CDN-Cache-Control 允许 CDN 缓存 300 秒并支持 stale-while-revalidate300 的过期后后台刷新兼顾内容新鲜度与边缘性能。 ### 4.3 在路由中使用动态文档 tsx // src/routes/docs.$path.tsx import { createFileRoute } from tanstack/react-router import { fetchDocs } from ~/utils/docs.server import { Markdown } from ~/components/Markdown export const Route createFileRoute(/docs/$path)({ loader: async ({ params }) { return fetchDocs({ data: { repo: your-org/your-repo, branch: main, filePath: docs/${params.path}.md, }, }) }, component: DocsPage, }) function DocsPage() { const { frontmatter, content } Route.useLoaderData() return ( article h1{frontmatter.title}/h1 Markdown content{content} classNameprose / /article ) } $path 参数直接映射为 GitHub 仓库内的文件路径天然支持多级文档目录如 /docs/api/router → docs/api/router.md。 ### 4.4 拉取目录结构构建导航 若要基于 GitHub 目录动态生成侧边导航可调用 GitHub Contents API 并过滤 Markdown 文件 tsx // src/utils/docs.server.ts type GitHubContent { name: string path: string type: file | dir } export const fetchRepoContents createServerFn({ method: GET }) .validator((params: { repo: string; branch: string; path: string }) params) .handler(async ({ data: { repo, branch, path } }) { const url https://api.github.com/repos/${repo}/contents/${path}?ref${branch} const response await fetch(url, { headers: { Accept: application/vnd.github.v3json, // Authorization: token ${process.env.GITHUB_TOKEN}, }, }) if (!response.ok) { throw new Error(Failed to fetch contents: ${response.status}) } const contents: ArrayGitHubContent await response.json() return contents .filter((item) item.type file item.name.endsWith(.md)) .map((item) ({ name: item.name.replace(.md, ), path: item.path, })) }) 该函数返回按字母序过滤后的 { name, path } 列表可直接喂给导航组件渲染Accept: application/vnd.github.v3json 头可确保收到结构化 JSON。 --- ## 五、使用 Shiki 添加语法高亮 在客户端组件内直接跑完整高亮管线开销较大更推荐的做法是先用 Shiki 在服务端或构建期把代码块转成带主题样式的 HTML再交给 Markdown 组件渲染。 定义独立的高亮工具函数 tsx // src/utils/markdown.ts import { codeToHtml } from shiki // Process code blocks after parsing export async function highlightCode( code: string, language: string, ): Promisestring { return codeToHtml(code, { lang: language, themes: { light: github-light, dark: tokyo-night, }, }) } themes 同时声明亮色与暗色主题Shiki 会生成双主题 CSS 变量配合 prefers-color-scheme 自动切换。 随后在 Markdown 组件的 replace 函数中拦截 pre 元素提取语言与源码并替换为自定义 CodeBlock tsx // In your Markdown components replace function if (domNode.name pre) { const codeElement domNode.children.find( (child) child instanceof Element child.name code, ) if (codeElement) { const className codeElement.attribs.class || const language className.replace(language-, ) || text const code getText(codeElement) return CodeBlock code{code} language{language} / } }【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考