Pages
TanStack Start 页面路由、Legal/Changelog Git Content、布局组和自定义页面。
Tanplate 的页面系统建立在 TanStack Router 文件路由之上,并按职责拆分 UI、Server Route 与 Git Content。src/pages/_app 是不占 URL 段的公共布局组;它下面的页面共享 Header、Footer、Consent 与全局路由守卫。
核心能力
- 创建带公共应用壳的组件页面。
- 用 Git 管理 Blog、Changelog 和 Legal Markdown/MDX。
- 在构建期校验 frontmatter、内部链接、发布状态和安全渲染边界。
- 按 Manifest Capability Profile 将可选页面真正移出产物。
- 为公开页面统一生成 canonical、Open Graph、Twitter 与 JSON-LD。
页面结构
apps/web/
├── content/
│ ├── blog/ Blog Markdown/MDX
│ ├── changelog/ 版本更新 Markdown/MDX
│ └── legal/ 法律与隐私 Markdown/MDX
└── src/
├── pages/
│ ├── _app/ 带 Header/Footer 的页面
│ └── _fullscreen/ 不带公共壳的全屏页面
└── api/
└── -internal/ Server Route 背后的内部实现_app 是 pathless layout:它参与组件树,但不会出现在公开 URL 中。例如 src/pages/_app/legal/index.tsx 对应 /legal,而不是 /_app/legal。
页面私有渲染组件放在相邻 -components/。跨页面稳定复用的 UI 原语才进入 src/components;数据库、Provider 和 Worker binding 留在 src/api、-internal 与共享领域包中。
Legal 法律页面
法律内容存储在 apps/web/content/legal,由一组列表/动态详情路由统一渲染:
| 文件 | URL | 职责 |
|---|---|---|
src/pages/_app/legal/index.tsx | /legal | 已发布法律内容列表 |
src/pages/_app/legal/$slug.tsx | /legal/$slug | 按 slug 渲染详情,未知或未发布内容返回 404 |
与“Cookie、Privacy、Terms 各写一个 route”的结构不同,本项目只维护一个 $slug.tsx 动态路由。要提供三类法律页面,只需创建三个内容文件:
apps/web/content/legal/
├── cookie.md
├── privacy.md
└── terms.md当前仓库已经提供 privacy.md 作为可替换的起点。文件名必须与 slug 一致,目录必须与 kind 一致。
自定义法律内容
---
kind: legal
slug: privacy
title: Privacy policy
description: How we collect, use, retain, and protect personal data.
status: published
publishedAt: 2026-07-30
effectiveAt: 2026-07-30
---
<!--
[INPUT]: 当前产品的数据处理、Provider 与用户权利。
[OUTPUT]: `/legal/privacy` 的已发布隐私政策。
[POS]: 位于 Legal 内容目录,作为项目隐私政策。
[PROTOCOL]:
1. 一旦正文或 frontmatter 变化,必须同步更新本 Header。
2. 更新后必须检查所属 `.folder.md` 与引用本页的内部链接。
-->
## Introduction
Describe the data practices that apply to this deployment.法律 frontmatter 使用以下固定字段:
kind:必须是legal。slug:小写 kebab-case,且必须等于文件名。title/description:列表、SEO 与详情页使用的公开文案。status:draft或published。publishedAt:进入发布索引的日期。effectiveAt:页面展示的生效日期。
新增 Cookie 政策或服务条款时复制同一 Schema,分别使用 slug: cookie 和 slug: terms;不需要再创建 React route。
Changelog 更新日志
发布说明存储在 apps/web/content/changelog,同样由列表和动态详情路由渲染:
| 文件 | URL | 职责 |
|---|---|---|
src/pages/_app/changelog/index.tsx | /changelog | 按发布日期倒序展示已发布版本 |
src/pages/_app/changelog/$slug.tsx | /changelog/$slug | 渲染单个版本,未知或未发布内容返回 404 |
当前仓库包含 v0-4-content-foundation.md 示例。新增版本时只需添加新的 Markdown/MDX 文件:
---
kind: changelog
slug: v1-1-0
title: Product workflows
description: This release adds the next set of product workflows.
status: published
publishedAt: 2026-07-30
version: 1.1.0
---
<!--
[INPUT]: 1.1.0 已完成且已验证的产品变化。
[OUTPUT]: `/changelog/v1-1-0` 的已发布版本说明。
[POS]: 位于 Changelog 内容目录,作为 1.1.0 发布记录。
[PROTOCOL]:
1. 一旦正文或 frontmatter 变化,必须同步更新本 Header。
2. 更新后必须检查所属 `.folder.md`、Search、Feed 与 Sitemap。
-->
## Highlights
- Added the first workflow.
- Improved the second workflow.version 接受 1.1、1.1.0、v1.1.0,以及由小写字母、数字、点和连字符组成的可选后缀(例如 1.1.0-beta.1)。这是仓库的版本标签格式,不是完整 SemVer 校验。slug 仍需使用 kebab-case,因此版本文件建议使用 v1-1-0.md。
发布与 Capability Profile
Legal 和 Changelog 是 Content 的独立子能力。当前默认 Manifest 开启 Content/Blog,但关闭 Legal 与 Changelog:
{
"features": {
"content": {
"blog": true,
"changelog": false,
"enabled": true,
"legal": false
}
}
}要发布它们,必须同时保持 features.content.enabled: true,并将对应 legal / changelog 子开关设为 true,然后运行:
pnpm init:template
pnpm check:template
pnpm --filter web typecheck
pnpm --filter web build关闭子能力时,相关 route、Header/Footer 链接、预渲染路径和内容正文不会进入产物。只添加 Markdown、但不启用 Manifest,不会生成公开页面。
构建期内容合同
Vite 在启动或构建时读取 apps/web/content,再由 src/lib/markdown.ts 完成确定性校验:
- frontmatter 必须匹配严格 Schema,未知字段会失败。
status: draft或未来publishedAt不进入公开索引。- 已发布条目的 URL 不能重复。
- 内部链接必须指向当前 Profile 中真实发布的页面。
- MDX 禁止 import、export 和表达式,只允许固定的
Callout。
通过校验的条目注入 src/lib/pages.ts,供 Legal/Changelog 列表、详情、Search、Feed、Sitemap 和预渲染共用。运行时不读取 Git 文件,也不会把它们变成 D1 CMS。
创建组件页面
不适合 Markdown 的交互页面直接放在 src/pages/_app。例如:
/*
[INPUT]: APP_BRAND、站点 URL 与公开 SEO policy。
[OUTPUT]: `/about` 公开介绍页面。
[POS]: 位于 `_app`,作为带公共布局的介绍页。
[PROTOCOL]:
1. 一旦页面职责变化,必须同步更新 Header。
2. 更新后必须检查所属 `.folder.md`、路由树与 Sitemap。
*/
import { createFileRoute } from '@tanstack/react-router'
import { buildPublicSeo } from '@tanplate/ui'
import { APP_BRAND } from '@/config/app'
import { getSiteUrl } from '@/lib/urls'
export const Route = createFileRoute('/_app/about')({
component: AboutPage,
head: () =>
buildPublicSeo({
description: `About ${APP_BRAND.name}.`,
path: '/about',
siteName: APP_BRAND.name,
siteUrl: getSiteUrl(),
title: `About | ${APP_BRAND.name}`,
}),
})
function AboutPage() {
return (
<main className="mx-auto w-full max-w-4xl px-5 py-12 lg:px-8">
<h1 className="text-3xl font-semibold">About</h1>
</main>
)
}新增页面时:
- 选择
_app或_fullscreen,创建 file route 并补齐 Header。 - 公开页复用
buildPublicSeo;私有工具页使用 noindex policy。 - 页面私有组件放入相邻
-components/。 - 浏览器请求通过
src/services/api,HTTP transport 复用src/services/request。 - 服务端能力放在
src/api,内部实现放在src/api/-internal。 - 更新所属
.folder.md,生成 Route Tree,并运行 typecheck/build。
routeTree.gen.ts 是生成物,不能手工维护。
受保护页面
src/pages/__root.tsx 在每次页面进入前执行全局路由守卫。登录态页面应复用当前 src/routers 的 Session/guard 模块和既有 Capability Profile,不能在单个页面复制 Session fetch 或维护第二套中间件。
Projects 可以匿名公开只读;Favorites 只有 Projects 与 Authentication 同时开启时存在。Admin、User Assets、Developer API 和 AI 依赖 Authentication。