Site configurationConfigure talizen.config.ts

Configure talizen.config.ts

Implementation spec for AI agents: field groups and evaluation timing, html/body tag attributes, head/bodyEnd injected code, and per-request (ctx) => value fields.

Follow this specification when an AI coding agent configures site-level behaviour: dependencies, routing, metadata, the document shell and injected code. talizen.config.ts is the single entry point for site-level configuration, and the rules below are implementation constraints, not optional UI suggestions.

What belongs here, and what does not

One rule decides it: anything with its own URL is a file; anything without a URL that applies to every page is configuration.

Configuration

Fields in talizen.config.ts have no URL of their own. They are the site-level part of every page's HTML.

Endpoint files

/robots.ts/robots.txt, /sitemap.ts/sitemap.xml, /llms.ts/llms.txt. Each produces one standalone response body, so each is a file.

Page files

/pages/about.tsx/about. A page's own metadata / generateMetadata applies to that page only.

No layout.tsx

The platform has no such convention. Creating one does not error — it is silently ignored. Express the document shell with html / body / head / bodyEnd below.

Fields and when they are evaluated

The config file must export default a plain object. Do not import packages (except import type) and do not use defineConfig.

Fields fall into two groups, split by whether the platform needs them before a request exists:

  • Static values only: importMap, i18n, redirects. Bundling and route building both happen ahead of any request.
  • May be (ctx) => value: metadata, html, body, head, bodyEnd, viewport. These only shape the rendered HTML, so they can be evaluated per request.

Hard requirement: writing importMap, i18n or redirects as a function fails at site load. Those three are read before a request exists, so a function there would be ignored entirely — the page would look fine while the config had no effect. The platform errors instead of degrading silently.

Canonical configuration

import type { TalizenConfig, TalizenConfigContext } from 'talizen'

export default {
  // Static: bundling and route building happen before a request
  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 },
  ],

  // Document output: a value, or (ctx) => value
  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

Use satisfies TalizenConfig rather than as: the former catches mistakes such as a static-only field written as a function while you type, the latter suppresses them.

html and body: document tag attributes

html and body are attribute objects applied directly to the server-rendered <html> / <body>.

  • className and class are equivalent; either works.
  • Do not set lang. The platform fills it from the current locale. Set it only to override that inference.
  • Attribute names and values are HTML-escaped.
  • This is the correct home for a dark-mode class: attributes declared in config land in the server HTML, so there is no flash from a client-side script.

Hard requirement: shared interactive UI such as a header nav or footer does not belong here. The document shell is a static declaration and cannot hydrate; keep that UI in components that pages import.

head and bodyEnd: injected code

Both are HTML strings: head is injected before </head>, bodyEnd before </body>.

They supersede customCode.head / customCode.body, and the difference that matters is that they can be evaluated per request — for example a different analytics script per domain, which static customCode cannot express.

  • Injection order is: platform tags → customCodehead / bodyEnd. The newer fields can therefore override the older ones.
  • Mark third-party scripts async or defer yourself; a synchronous script in head delays first render.
  • Do not repeat tags that metadata already expresses (title, description, Open Graph) or the page emits two of them.

The per-request ctx

Fields written as functions are called on every render and receive only these fields:

interface TalizenConfigContext {
  locale: string                 // current locale; empty string on single-language sites
  locales?: string[]             // i18n.locales
  defaultLocale?: string         // content baseline locale
  routingDefaultLocale?: string  // the unprefixed default locale of this host
  host: string                   // request host, for per-domain branching
  path: string                   // request path with the locale prefix removed
}

Hard requirement: the ctx deliberately offers no cookies and no CMS access. Reading a cookie would make the page cache per cookie, and fetching data at the config layer cannot participate in cache invalidation. Do not fetch data in config fields; keep them synchronous and simple. If a function throws, the request fails rather than silently dropping the site-level config.

Metadata layering

Site-level metadata comes from config; page metadata layers on top.

  • A title.template in config wraps the literal title a page provides.
  • title.default is emitted as-is and is not wrapped by the template — it means "use this when the page gives no title".
  • A page's title: { absolute: '…' } bypasses the template.
  • To vary site metadata by language, write metadata as a function and branch on ctx.locale. The metadata._i18n field still works but is deprecated; do not use it in new code.

Implementation checklist

  • export default a plain object; no package imports except import type; no defineConfig.
  • importMap, i18n and redirects are static values.
  • Write a field as a function only when it genuinely branches on locale or host; otherwise write the value directly.
  • html / body do not set lang.
  • Third-party scripts carry async or defer.
  • Use satisfies TalizenConfig so the types catch mistakes at authoring time.
  • After pushing, view source on the real preview URL and confirm the <html> / <body> attributes, the <title> and the position of injected scripts; on multilingual sites verify each locale prefix.

Forbidden implementations

  • Do not create layout.tsx: the platform has no such convention and it is silently ignored.
  • Do not write importMap, i18n or redirects as functions.
  • Do not fetch data in config fields (CMS, fetch, reading cookies).
  • Do not hand-write SEO tags in head / bodyEnd that metadata already expresses.
  • Do not keep using customCode for new work: it is static and cannot branch on locale or host.
  • Do not put shared interactive chrome in the document shell; those are components that pages import.
  • Do not nest viewport inside metadata; it is its own top-level field.

Render diagnostics