Skip to main content
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. 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
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.

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.
home
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 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 TypeScript import 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 and components import the registered name:
pages/home
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:
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.

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.
components/PriceChart
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: 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 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:
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.
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.
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 — 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.
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.

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.