配置 talizen.config.ts
面向 AI 智能体的站点配置规范:字段分组与求值时机、html/body 标签属性、head/bodyEnd 注入代码、按请求的 (ctx) => 值。
当 AI 编程智能体需要配置站点级行为(依赖、路由、metadata、文档外壳、注入代码)时,请遵循本规范。talizen.config.ts 是站点级配置的唯一入口,以下规则是实现约束,不是可选的界面建议。
它管什么,不管什么
判据只有一句:有自己 URL 的产物是文件,没有 URL、作用于所有页面的是配置。
配置
talizen.config.ts 里的字段没有独立 URL,它们是每个页面 HTML 里的站点级部分。
端点文件
/robots.ts → /robots.txt、/sitemap.ts → /sitemap.xml、/llms.ts → /llms.txt。各自产出一个独立响应体,所以是文件。
页面文件
/pages/about.tsx → /about。页面自己的 metadata / generateMetadata 只作用于这一个页面。
没有 layout.tsx
平台不存在这个约定文件。写了不会报错,会被静默忽略。文档外壳用下面的 html / body / head / bodyEnd 表达。
字段与求值时机
配置文件必须 export default 一个普通对象,不要导入包(import type 除外),不要使用 defineConfig。
字段分两组,界线是「平台是否在请求之前就需要它」:
- 必须是静态值:
importMap、i18n、redirects。打包与建路由都发生在任何请求之前。 - 可以写成
(ctx) => 值:metadata、html、body、head、bodyEnd、viewport。它们只塑造最终 HTML,因此可以按请求求值。
硬性要求:把
importMap、i18n或redirects写成函数会在站点加载时报错。这三个字段在请求之前就要被读取,函数会被整段忽略 —— 页面看起来正常,配置等于没生效,所以平台选择直接报错而不是静默降级。
标准配置
import type { TalizenConfig, TalizenConfigContext } from 'talizen'
export default {
// 必须静态:打包与建路由发生在请求之前
importMap: {
imports: { 'framer-motion': 'https://esm.talizen.com/framer-motion@12' },
},
i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en'],
},
redirects: [
{ source: '/old', destination: '/new', permanent: true },
],
// 文档输出:可以是值,也可以是 (ctx) => 值
metadata: (ctx: TalizenConfigContext) => ({
title: {
template: ctx.locale === 'en' ? '%s | Acme' : '%s | Acme',
default: 'Acme',
},
description: ctx.locale === 'en' ? 'English description' : '中文描述',
}),
html: { className: 'dark scroll-smooth' },
body: { className: 'antialiased bg-neutral-950 text-neutral-100' },
head: (ctx: TalizenConfigContext) =>
ctx.host.endsWith('.cn')
? '<script async src="https://hm.baidu.com/hm.js?xxx"></script>'
: '<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXX"></script>',
bodyEnd: '<script src="/widget.js" defer></script>',
} satisfies TalizenConfig
用 satisfies TalizenConfig 而不是 as:前者在编译期就能拦住"把静态字段写成函数"这类错误,后者会把错误压掉。
html 与 body:文档标签属性
html 与 body 是标签属性对象,直接落在服务端渲染出的 <html> / <body> 上。
className与class等价,写哪个都可以。- 不要写
lang:平台按当前语言自动填充。只有在需要覆盖平台推导时才显式给出。 - 属性名与值都会被 HTML 转义。
- 这是暗色模式类名的正确位置:写在配置里的属性直接进服务端 HTML,没有客户端脚本造成的闪烁。
硬性要求:共享的头部导航、页脚等需要交互的 UI 不属于这里。文档外壳是静态声明,给不了水合;把它们写成组件由页面
import。
head 与 bodyEnd:注入代码
两者都是 HTML 字符串:head 注入 </head> 之前,bodyEnd 注入 </body> 之前。
它们取代 customCode.head / customCode.body,关键差别是可以按请求求值 —— 例如按域名给不同的统计脚本,这是静态 customCode 表达不了的。
- 注入顺序为:平台标签 →
customCode→head/bodyEnd。因此新写法可以覆盖旧写法。 - 第三方脚本请自行写
async或defer,否则head里的同步脚本会拖慢首屏渲染。 - 不要在这里重复
metadata已经能表达的标签(title、description、Open Graph),否则会输出两份。
按请求求值的 ctx
写成函数的字段会在每次渲染时被调用,入参只有以下字段:
interface TalizenConfigContext {
locale: string // 当前语言,单语言站点为空字符串
locales?: string[] // i18n.locales
defaultLocale?: string // 内容基准语言
routingDefaultLocale?: string // 当前域名的无前缀默认语言
host: string // 请求域名,用于按域名分支
path: string // 去掉语言前缀的请求路径
}
硬性要求:ctx 刻意不提供 cookies 与 CMS 访问。读 cookie 会让页面按 cookie 分桶缓存;配置层取数据则无法参与缓存失效。不要在配置字段里取数据,保持它们同步且简单。函数抛错时请求会失败,而不是静默丢掉站点级配置。
metadata 分层
站点级 metadata 由配置提供,页面 metadata 叠在它之上:
title.template写在配置里,会套用页面给出的字面标题。title.default原样输出,不套用 template —— 它是"页面没给标题时用这个"。- 页面
title: { absolute: '…' }绕过 template。 - 需要按语言给站点 metadata 时,把
metadata写成函数并用ctx.locale分支。metadata._i18n仍然可用但已废弃,新代码不要用。
实现检查清单
export default一个普通对象,不导入包(import type除外),不使用defineConfig。importMap、i18n、redirects为静态值。- 只在确实需要按
locale或host分支时才把字段写成函数;不需要就直接写值。 html/body不写lang。- 第三方脚本带
async或defer。 - 用
satisfies TalizenConfig让类型在编译期兜住。 - 推送后在真实预览 URL 上查看页面源码,确认
<html>/<body>属性、<title>、注入脚本的位置都符合预期;多语言站点逐个语言前缀验证一遍。
禁止的实现
- 不要创建
layout.tsx:平台没有这个约定,写了会被静默忽略。 - 不要把
importMap、i18n、redirects写成函数。 - 不要在配置字段里取数据(CMS、fetch、读 cookie)。
- 不要在
head/bodyEnd里手写metadata已能表达的 SEO 标签。 - 不要为了新功能继续用
customCode:它是静态的,无法按语言或域名分支。 - 不要把共享的交互式头尾放进文档外壳;它们是页面
import的组件。 - 不要把
viewport写在metadata里,它是独立的顶层字段。
