> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Source code

> The website source model: pages, components, reusable modules, singleton files, edit leases, and append-only version history.

Website source code is a set of versioned pages, components, reusable modules, and site-wide singletons that Cactal's framework validates, builds, and serves.

## File types

Every source file has a `type`. Pages, components, and modules are named; the rest are singletons with at most one file of each type per website.

| Type                | Named | Purpose                                                                                     |
| ------------------- | ----- | ------------------------------------------------------------------------------------------- |
| `page`              | Yes   | One routable page, exported with `definePage`                                               |
| `component`         | Yes   | A reusable React component imported via the `@components` alias                             |
| `module`            | Yes   | Shared constants, helpers, hooks, SDK setup, and other project code imported via `@modules` |
| `global_css`        | No    | Site-wide CSS, seeded with the Tailwind import on new websites                              |
| `not_found`         | No    | The page rendered for unmatched routes                                                      |
| `website_config`    | No    | Site-wide metadata, redirects, and response headers, exported with `defineWebsiteConfig`    |
| `custom_head_start` | No    | Raw HTML injected at the start of `<head>`                                                  |
| `custom_head_end`   | No    | Raw HTML injected at the end of `<head>`                                                    |
| `custom_body_start` | No    | Raw HTML injected at the start of `<body>`                                                  |
| `custom_body_end`   | No    | Raw HTML injected at the end of `<body>`                                                    |

The four custom-code slots exist for third-party scripts and tags — analytics snippets, meta tags, chat widgets — that must land in exact positions in the served HTML.

Favicons are current website settings, not source files. Set one through `PUT /v1/websites/{websiteId}/favicon`; it applies immediately across published, draft, and version hosts without a source lease or publish. An unset or unavailable favicon uses the Cactal mark, while explicit site routes and redirects for `/favicon.ico` keep precedence. See [Upload assets](/docs/guides/upload-assets).

Page, component, and module names are path-like strings, 1–255 characters, with no leading `/`, no `.`/`..` segments, and none of `< > : " | ? * \ #`. Singleton type names (`global_css` and the rest) are reserved and cannot be used as named source files. Every named file is a React module whatever its name, so names ending in an asset extension such as `.svg`, `.png`, `.css`, or `.json` are refused: there is no static file tree. Upload images and other files to the [asset library](/docs/concepts/assets) and reference them by URL, or write an SVG inline as JSX.

## Site-wide configuration

Source-backed settings follow the website's head and published versions:

| Setting                                        | Source                                                                         | Production changes when        |
| ---------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------ |
| Site metadata, redirects, and response headers | `website_config`                                                               | The head version is published  |
| Global CSS and Tailwind CSS                    | `global_css`                                                                   | The head version is published  |
| Custom 404 page                                | `not_found`                                                                    | The head version is published  |
| Raw document integrations                      | `custom_head_start`, `custom_head_end`, `custom_body_start`, `custom_body_end` | The head version is published  |
| Website name and slug                          | Website resource                                                               | The resource is updated        |
| Favicon                                        | Website favicon setting                                                        | The setting is updated         |
| Custom and primary domains                     | Domain resources                                                               | DNS and domain state are ready |

`defineWebsiteConfig` supports three fields:

```tsx website_config theme={null}
import { defineWebsiteConfig } from '@website'

export default defineWebsiteConfig({
	metadata: {
		title: 'Acme Journal',
		titleTemplate: '%s | Acme Journal'
	},
	redirects: [
		{ from: '/news', to: '/blog', status: 301 }
	],
	headers: {
		'Referrer-Policy': 'strict-origin-when-cross-origin'
	}
})
```

All three fields must be statically extractable. The config must use a direct default export, an inline top-level object, and no top-level spreads or computed properties. Metadata, redirect entries, and response headers have their own literal-shape validation.

* [Configure SEO metadata](/docs/guides/configure-seo-metadata) documents the complete metadata schema and template language.
* [Configure routes and redirects](/docs/guides/configure-routes-and-redirects) documents route patterns, redirect precedence, status codes, parameters, and validation.
* [Configure response headers](/docs/guides/configure-response-headers) documents the header allowlist, platform defaults, and validation.
* [Style a website](/docs/guides/style-a-website) documents raw CSS, Tailwind CSS, theme tokens, and class discovery.
* [Customize site-wide files](/docs/guides/customize-site-wide-files) documents the custom 404 page, raw HTML slots, and exact document order.

## Pages

A page is a TypeScript module whose default export is `definePage` from the generated `@website` SDK. The `route` literal is the page's URL path; `render` returns its JSX.

```tsx home theme={null}
import { definePage } from '@website'

export default definePage({
  route: '/',
  render: () => (
    <main>
      <h1>Welcome to Acme</h1>
    </main>
  )
})
```

Two structured endpoints edit page source without you rewriting the file: `PATCH /v1/websiteSourceCode/pages/route` changes the `route` literal, and `PATCH /v1/websiteSourceCode/metadata` updates a page's metadata when `page` is sent. Otherwise it updates the site-wide defaults in `website_config`, creating the file if it does not exist.

Page metadata can use constrained `{{ ... }}` templates to resolve page CMS queries, route parameters, and query parameters without executing arbitrary metadata code. See [Configure SEO metadata](/docs/guides/configure-seo-metadata) for the full metadata schema and template language.

See [Build pages with CMS data](/docs/guides/build-pages-with-cms-data) for the complete `definePage` contract, generated CMS query builder, request-parameter binding, render props, and result shapes. See [Configure routes and redirects](/docs/guides/configure-routes-and-redirects) for route normalization, matching priority, ambiguity, and reserved paths.

## Imports and dependencies

Page, component, and module files use ordinary TypeScript `import` statements. Seven kinds of specifier resolve:

| Specifier             | Example                                          | Resolves to                                |
| --------------------- | ------------------------------------------------ | ------------------------------------------ |
| `@website`            | `import { definePage } from '@website'`          | The generated site SDK                     |
| `@components/<name>`  | `import { Hero } from '@components/Hero'`        | Another component file                     |
| `@pages/<name>`       | `import Home from '@pages/home'`                 | Another page file                          |
| `@modules/<name>`     | `import { siteName } from '@modules/lib/config'` | A reusable module file                     |
| `@modules`            | `import modules from '@modules'`                 | The registry of reusable module namespaces |
| `react`               | `import { useState } from 'react'`               | The runtime's React                        |
| `<package>@<version>` | `import { motion } from 'motion/react@12.42.2'`  | A pinned npm package                       |

Relative paths such as `./card` or `../lib/format` do not resolve, and neither do asset imports such as `./logo.svg`. Reach shared code through the `@modules` alias, components through `@components`, and pages through `@pages`. For an SVG, either write the `<svg>` inline in JSX (use `fill="currentColor"` so it follows the text color) or upload it as an asset and use its CDN `url` in an `<img>`.

### Reusable modules

Modules are regular TypeScript or TSX source files. They do not need a default export or a React component. Use them for public configuration, constants, formatting and data helpers, hooks, SDK initialization, and other shared project code.

```tsx modules/lib/config theme={null}
export const siteName = 'Acme Journal'
export const publicApiUrl = 'https://api.example.com'
```

```tsx modules/lib/format theme={null}
import { siteName } from '@modules/lib/config'

export function formatTitle(title: string) {
	return `${title} | ${siteName}`
}
```

Pages and components import the registered name:

```tsx pages/home theme={null}
import { definePage } from '@website'
import { formatTitle } from '@modules/lib/format'

export default definePage({
	route: '/',
	render: () => <h1>{formatTitle('Home')}</h1>
})
```

Module imports match component imports: use an extensionless name, an explicit `.tsx` extension, or a nested name. The root `@modules` registry maps each registered name to that module's namespace, so named and default exports stay available:

```tsx theme={null}
import modules from '@modules'

modules['lib/config'].siteName
modules['lib/config'].default
```

<Warning>
  Website source code is built and delivered to the browser, so every value in a module is public. Keep API secrets and private keys out of site code; only publishable values such as public SDK keys belong in modules.
</Warning>

### npm packages

Import a public npm package by pinning an exact version directly in the specifier. Cactal reads your imports and installs the packages for you, so there is no `package.json` or manifest to maintain.

```tsx components/PriceChart theme={null}
import { motion } from 'motion/react@12.42.2'
import { format } from 'date-fns@4.1.0'
import { clsx } from 'clsx@2.1.1'
```

Subpaths such as `motion/react` and scoped packages such as `@scope/pkg@1.2.3` work the same way, with the version at the very end of the specifier. The framework enforces a few rules so builds stay reproducible:

| Rule                    | Detail                                                                                                                                                                                  |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Exact versions only     | Every import ends with `@<major>.<minor>.<patch>`. Ranges like `^1.2.0`, tags like `latest`, and `npm:` or URL specifiers are rejected.                                                 |
| One version per package | A package resolves to a single version across the whole site. Two different versions of the same package fail the check.                                                                |
| Pin every import        | Each import of a package carries its version. A bare `import 'pkg'` alongside a pinned import of the same package is rejected.                                                          |
| Static imports only     | Dependencies come from top-level `import` and `export ... from` statements. Dynamic `import()` is not analyzed.                                                                         |
| Browser packages only   | A package must resolve to a browser ESM build. Node built-ins such as `fs` and `path` cannot be imported, and `react` and `react-dom` are supplied by the runtime and cannot be pinned. |

Packages with bundled TypeScript declarations use those declarations automatically. When a package has no declarations, Cactal installs its DefinitelyTyped `@types` package if one is published. If neither source exists, the import is still usable but untyped, so its values are `any`. There is no way to add a `@types` package or a `.d.ts` file yourself, and none is needed.

Every page and component module is evaluated once during the build to collect its routes and queries, and again on each request to render the page. That happens in a JavaScript runtime with the standard Web globals a browser package expects at import time, including `AbortController`, `Event`, `EventTarget`, `URL`, `structuredClone`, `TextEncoder`, and `crypto`.

Two things are absent, and a package that reaches for either at module scope will fail to load:

| Missing   | What it means for your code                                                                                                                                                                                                                                                                                   |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No DOM    | `window`, `document`, and `fetch` do not exist. Move anything that needs them into an effect or an event handler so it runs in the browser instead. A package that reads the DOM as it loads, such as `react-markdown`, cannot be used.                                                                       |
| No `Intl` | `Intl.DateTimeFormat`, `Intl.NumberFormat`, and the rest of `Intl` are unavailable, and `toLocaleString` ignores the locale you pass it rather than throwing. Use a library that carries its own locale data, such as `date-fns` or `dayjs`, and format currency and numbers yourself. `luxon` does not work. |

The runtime supplies React 19, so pick package versions that support it. Libraries that reach into React 18 internals fail to load: use `@react-spring/web` 10 or later rather than 9, and `@react-three/fiber` 9 or later rather than 8.

Packages install in the same isolated, single-use sandbox that builds the site, and package install scripts never run. An invalid specifier, a package that fails to install, and a package that cannot be bundled for the browser each surface as a framework diagnostic from `head/check` and `head/publish`, so you catch dependency problems before they can reach the live site. See [Publishing](/docs/concepts/publishing).

## Finding code

`POST /v1/websiteSourceCode/files/search` searches file contents across the whole website and returns the file name, line, and column of every match. It is the fastest way to orient in a site you did not write: search first, then read only the files it names.

```bash theme={null}
curl -X POST "https://api.cactal.ai/v1/websiteSourceCode/files/search" \
  -H "Authorization: Bearer $CACTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "websiteId": "V1StGXR8_Z5jdHi6B-myT",
    "pattern": "Acme Supply",
    "contextLines": 1
  }'
```

`pattern` is literal text unless you set `isRegex`. Patterns are matched one line at a time, so `^` and `$` anchor to line boundaries and a pattern cannot span a newline. You get one entry per matching line, and `column` is the offset of the first match on that line.

| Parameter           | Effect                                                                                         |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `isRegex`           | Read `pattern` as a regular expression instead of literal text                                 |
| `caseSensitive`     | Match case exactly. Defaults to `true`                                                         |
| `nameGlob`          | Only search matching file names. `*` and `?` stop at a `/` segment boundary; `**` crosses them |
| `type`              | Only search files of one type, for example `component`                                         |
| `contextLines`      | Return up to 3 surrounding lines on each side of a match                                       |
| `maxMatchesPerFile` | Cap matches from any one file (default 10, maximum 50)                                         |
| `filesOnly`         | Return one entry per matching file with no line detail                                         |
| `target`, `version` | Search the published version or an explicit historical version                                 |

Results are cursor-paginated over matches, ordered by file type, then file name, then line — the same file order as `GET /v1/websiteSourceCode/files`. Keep paging until `nextCursor` is `null`. A response with `"truncated": true` means a file on that page had more matching lines than `maxMatchesPerFile`; raise it, or narrow the pattern, to see the rest.

`nameGlob` also works on `GET /v1/websiteSourceCode/files` when you want to list a subtree rather than search it.

### Regular expression syntax

Patterns run on Postgres, whose dialect covers essentially everything you would reach for: `\d`, `\w`, `\s`, character classes, lazy quantifiers, backreferences, lookahead, and lookbehind. `\b` and `\B` word boundaries work as you would expect from JavaScript.

Two differences are worth knowing:

* **Named capture groups** (`(?<name>...)`) are not supported and return a `400`. Use a plain group.
* **`$`** matches the end of the line, since matching is line by line.

An invalid pattern returns a `400` naming the problem. Search also runs under a fixed time budget on an isolated connection pool, so a pattern that is too broad to finish is cancelled and returned as a `400` rather than being allowed to run long.

## Edit leases

Every source mutation requires an edit lease. Acquire one per editing session:

```bash theme={null}
curl -X POST "https://api.cactal.ai/v1/websiteSourceCode/lease/acquire" \
  -H "Authorization: Bearer $CACTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"websiteId": "V1StGXR8_Z5jdHi6B-myT"}'
```

```json theme={null}
{
  "leaseToken": "Zk3v...base64url...",
  "headVersion": 42
}
```

A website has exactly one active lease. Acquiring a lease always replaces the previous one, so the latest editing session owns the website. Pass the `leaseToken` in the body of every mutation; a request with a replaced token fails with `409` `Invalid edit lease`.

<Warning>
  If another session, such as a teammate, the dashboard, or another agent, acquires a lease, your token stops working mid-flow. Recover by acquiring a new lease, re-reading the affected files with `POST /v1/websiteSourceCode/files/read`, and reapplying your intended change. Never retry blindly with the old token.
</Warning>

The platform also guards against stale writes at the storage layer. If the head version changes between reading and writing inside a request, the mutation fails with `409` `Website source changed; reload and try again` — acquire a fresh lease and retry.

## Writing files

`PUT /v1/websiteSourceCode/files` upserts one file. Identify it with a nested `file` object: `{ "type": "page" | "component" | "module", "name": "..." }` for named files, or `{ "type": "global_css" }` (and the other singleton types) with no `name`.

```bash theme={null}
curl -X PUT "https://api.cactal.ai/v1/websiteSourceCode/files" \
  -H "Authorization: Bearer $CACTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "websiteId": "V1StGXR8_Z5jdHi6B-myT",
    "leaseToken": "Zk3v...",
    "file": { "type": "page", "name": "home" },
    "content": "import { definePage } from '\''@website'\''\n\nexport default definePage({ route: '\''/'\'', render: () => <main><h1>Welcome</h1></main> })"
  }'
```

```json theme={null}
{ "version": 43, "changed": true }
```

`POST /v1/websiteSourceCode/files/str-replace` makes targeted in-place edits across up to 50 files and 200 edits in total. Each `oldStr` must match exactly once (or set `replaceAll`), and every edit across every file commits as one version or none at all. The response reports per-file replacement counts, while `dryRun` returns those counts without committing. `DELETE /v1/websiteSourceCode/files` removes a file using the same `file` identity plus the `leaseToken`. All file names are unique per website across named types.

## Append-only versioning

Every semantic edit — file upsert, file delete, route edit, metadata edit — advances `headVersion` by exactly one. Writing identical content is a no-op: the response returns `"changed": false` and the version does not move. History is append-only; no edit ever rewrites a previous version, and any version stays readable.

| Endpoint                                  | Returns                                                                                                                                                                                   |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/websiteSourceCode/summary`       | `headVersion`, `publishedVersion`, `hasUnpublishedChanges`                                                                                                                                |
| `GET /v1/websiteSourceCode/files`         | A cursor-paginated page of file `type`, `name`, `bytes`, and `lines` metadata at `target=head` (default), `target=published`, or an explicit `version`, optionally filtered by `nameGlob` |
| `POST /v1/websiteSourceCode/files/read`   | Contents of up to 50 files by name, with optional line and character ranges for a single file                                                                                             |
| `POST /v1/websiteSourceCode/files/search` | A cursor-paginated page of matching lines with file `name`, `line`, and `column` for a literal or regular expression pattern                                                              |
| `GET /v1/websiteSourceCode/changes`       | A cursor-paginated list of added, removed, and modified files between two versions (`from`/`to` default to `published`/`head`)                                                            |
| `GET /v1/websiteSourceCode/files/diff`    | A bounded unified diff for one file name between two versions                                                                                                                             |

<Tip>
  Treat `headVersion` like a commit id. Read the files you plan to change, make your edits, and confirm the returned `version` values advance as expected. An unexpected jump means another session edited in parallel.
</Tip>

## Constraints

| Constraint                | Value                                              |
| ------------------------- | -------------------------------------------------- |
| Maximum file size         | 1,048,576 bytes (1 MiB), UTF-8 encoded             |
| Maximum files per website | 2,000                                              |
| File name length          | 1–255 characters                                   |
| Singletons                | At most one file per singleton type                |
| Mutations                 | Require `leaseToken`; one active lease per website |

Draft edits never touch the live site. Each committed change also triggers a background preview build so the `draft--` host stays close to your head version. See [Preview drafts and versions](/docs/guides/preview-drafts-and-versions).

## Next steps

<Columns cols={2}>
  <Card title="Edit source code" icon="pencil" href="/docs/guides/edit-source-code">
    The full lease-edit-check loop with worked examples.
  </Card>

  <Card title="Configure SEO metadata" icon="search" href="/docs/guides/configure-seo-metadata">
    Define site defaults and create page metadata from CMS content.
  </Card>

  <Card title="Configure routes and redirects" icon="route" href="/docs/guides/configure-routes-and-redirects">
    Define URL patterns and validated migrations.
  </Card>

  <Card title="Style a website" icon="palette" href="/docs/guides/style-a-website">
    Use global CSS and Tailwind CSS.
  </Card>

  <Card title="Publishing" icon="rocket" href="/docs/concepts/publishing">
    Run the quality gate and promote head to the live site.
  </Card>
</Columns>
