File types
Every source file has atype. Pages, components, and modules are named; the rest are singletons with at most one file of each type per website.
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.
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 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:defineWebsiteConfig supports three fields:
website_config
- Configure SEO metadata documents the complete metadata schema and template language.
- Configure routes and redirects documents route patterns, redirect precedence, status codes, parameters, and validation.
- Configure response headers documents the header allowlist, platform defaults, and validation.
- Style a website documents raw CSS, Tailwind CSS, theme tokens, and class discovery.
- 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 isdefinePage from the generated @website SDK. The route literal is the page’s URL path; render returns its JSX.
home
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 for the full metadata schema and template language.
See 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 for route normalization, matching priority, ambiguity, and reserved paths.
Imports and dependencies
Page, component, and module files use ordinary TypeScriptimport statements. Seven kinds of specifier resolve:
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.modules/lib/config
modules/lib/format
pages/home
.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:
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 nopackage.json or manifest to maintain.
components/PriceChart
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:
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:
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.
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.
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.
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 a400. Use a plain group. $matches the end of the line, since matching is line by line.
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:leaseToken in the body of every mutation; a request with a replaced token fails with 409 Invalid edit lease.
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.
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 — advancesheadVersion 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.
Constraints
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.
Next steps
Edit source code
The full lease-edit-check loop with worked examples.
Configure SEO metadata
Define site defaults and create page metadata from CMS content.
Configure routes and redirects
Define URL patterns and validated migrations.
Style a website
Use global CSS and Tailwind CSS.
Publishing
Run the quality gate and promote head to the live site.