> ## 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.

# Edit source code

> Learn how to edit website source code over the Cactal API: acquire a lease, write pages, components, and modules, then validate before publishing.

This guide walks the full editing loop over the API: lease, write files, edit routes and metadata, and validate the result.

## Prerequisites

* An API key with `full_editor` access — see [Create an API key](/docs/create-an-api-key)
* A website to edit — see the [Quickstart](/docs/quickstart)

## Source file model

Every website owns a set of versioned source files. Each write that changes anything creates a new head version; history is append-only.

| Type                                   | Named | Purpose                                                                 |
| -------------------------------------- | ----- | ----------------------------------------------------------------------- |
| `page`                                 | Yes   | A routable page. Exports `definePage({ ... })`.                         |
| `component`                            | Yes   | A reusable component imported by pages.                                 |
| `module`                               | Yes   | Reusable constants, helpers, hooks, SDK setup, and shared project code. |
| `global_css`                           | No    | Site-wide CSS (one per website).                                        |
| `not_found`                            | No    | The 404 page.                                                           |
| `website_config`                       | No    | Site-wide metadata and redirect rules.                                  |
| `custom_head_start`, `custom_head_end` | No    | Raw HTML injected at the start or end of `<head>`.                      |
| `custom_body_start`, `custom_body_end` | No    | Raw HTML injected at the start or end of `<body>`.                      |

Named files take a `name` of 1–255 characters. Singleton types take no `name`. A file's content can be at most 1,048,576 bytes, and a website can hold at most 2,000 source files.

<Steps>
  <Step title="Acquire an edit lease">
    All source mutations require an edit lease. Acquire one with `POST /v1/websiteSourceCode/lease/acquire`.

    ```bash Acquire a lease 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 Response theme={null}
    {
      "leaseToken": "exampleLeaseToken00000000000000000000000000",
      "headVersion": 4
    }
    ```

    A website has exactly one active lease. Acquiring a lease replaces the previous token, so any other editor's token stops working immediately. Include `leaseToken` in every mutation below.
  </Step>

  <Step title="List and read files">
    Orient with `GET /v1/websiteSourceCode/files`. It lists file metadata with size and line count, without contents, in pages of 20 by default (maximum 100).

    ```bash List the head files theme={null}
    curl 'https://api.cactal.ai/v1/websiteSourceCode/files?websiteId=V1StGXR8_Z5jdHi6B-myT&limit=100' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    ```json Response (truncated) theme={null}
    {
      "websiteId": "V1StGXR8_Z5jdHi6B-myT",
      "version": 4,
      "items": [
        { "type": "global_css", "name": "global_css", "bytes": 27, "lines": 1 },
        { "type": "page", "name": "home", "bytes": 512, "lines": 24 }
      ],
      "nextCursor": null
    }
    ```

    When `nextCursor` is not `null`, pass it as `cursor` and pass the response `version` as `version` on the next request. Pinning the resolved version keeps the file inventory stable if the head changes while you page.

    Then fetch only the files you need with `POST /v1/websiteSourceCode/files/read` (up to 50 names per call; singletons are addressed by their type name):

    ```bash Read specific files theme={null}
    curl -X POST 'https://api.cactal.ai/v1/websiteSourceCode/files/read' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "websiteId": "V1StGXR8_Z5jdHi6B-myT", "names": ["home", "global_css"] }'
    ```

    The response returns each file's `content` and `totalLines`. When reading a single file, `startLine` and `endLine` select a line range. If `truncated` is `true`, continue by passing `nextStartCharacter` as `startCharacter` with the same line range and the response `version` as `version`. Pinning the version prevents concurrent edits from changing the file between chunks. `endCharacter` can bound the character range. Pass `target: "published"` for the live version.

    When you know what you are looking for but not where it lives, skip the listing and search instead with `POST /v1/websiteSourceCode/files/search`:

    ```bash Find every file mentioning the old brand name 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 }'
    ```

    ```json Response theme={null}
    {
      "websiteId": "V1StGXR8_Z5jdHi6B-myT",
      "version": 4,
      "items": [
        {
          "type": "page",
          "name": "home",
          "line": 12,
          "column": 8,
          "match": "\t\t\t<h1>Acme Supply</h1>",
          "before": ["\t\t<main>"],
          "after": ["\t\t\t<p>Parts, fast.</p>"]
        }
      ],
      "nextCursor": null
    }
    ```

    See [Finding code](/docs/concepts/source-code#finding-code) for regular expressions, glob and type filters, and the `filesOnly` mode.
  </Step>

  <Step title="Write a page">
    A page module imports `definePage` from `@website` and default-exports the call. The `route` must be an inline string literal, for example `'/'` or `'/blog/:slug'`.

    ```tsx pages/about theme={null}
    import { definePage } from '@website'

    export default definePage({
    	route: '/about',
    	metadata: { title: 'About us' },
    	render: () => (
    		<main>
    			<h1>About us</h1>
    		</main>
    	)
    })
    ```

    Upsert it with `PUT /v1/websiteSourceCode/files`. The file identity is a nested `file` object: `{ "type": "page", "name": "about" }` for named types, or `{ "type": "global_css" }` for singletons.

    <CodeGroup>
      ```bash Upsert a page 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",
          "file": { "type": "page", "name": "about" },
          "content": "import { definePage } from '\''@website'\''\n\nexport default definePage({\n\troute: '\''/about'\'',\n\tmetadata: { title: '\''About us'\'' },\n\trender: () => (\n\t\t<main>\n\t\t\t<h1>About us</h1>\n\t\t</main>\n\t)\n})",
          "leaseToken": "exampleLeaseToken00000000000000000000000000"
        }'
      ```

      ```ts TypeScript theme={null}
      const response = await fetch('https://api.cactal.ai/v1/websiteSourceCode/files', {
      	method: 'PUT',
      	headers: {
      		Authorization: `Bearer ${process.env.CACTAL_API_KEY}`,
      		'Content-Type': 'application/json'
      	},
      	body: JSON.stringify({
      		websiteId: 'V1StGXR8_Z5jdHi6B-myT',
      		file: { type: 'page', name: 'about' },
      		content: pageSource,
      		leaseToken
      	})
      })
      if (!response.ok) throw new Error(`Upsert failed: ${response.status}`)
      const { version, changed } = await response.json()
      ```
    </CodeGroup>

    The response is `{ "version": 5, "changed": true }`. A write that changes nothing returns `changed: false` and does not create a version. `DELETE /v1/websiteSourceCode/files` removes a file with the same `{ websiteId, file, leaseToken }` body.

    Reusable modules use the same endpoint with `type: "module"`. Their path-like name becomes the stable `@modules/<name>` import. A module exports plain TypeScript values and can import other registered modules; it does not need a default export or a React component:

    ```tsx modules/lib/config theme={null}
    export const siteName = 'Acme Journal'
    export const publicSdkKey = 'pk_example'
    ```

    ```bash Upsert a reusable module 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",
        "file": { "type": "module", "name": "lib/config" },
        "content": "export const siteName = '\''Acme Journal'\''\nexport const publicSdkKey = '\''pk_example'\''",
        "leaseToken": "exampleLeaseToken00000000000000000000000000"
      }'
    ```

    Import it from a page, component, or another module with `import { siteName } from '@modules/lib/config'`. As with components, the extensionless name, explicit `.tsx` form, nested names, and a root registry import are supported. `import modules from '@modules'` exposes each file as a namespace such as `modules['lib/config']`. Relative imports remain unsupported. Module values ship to the browser with the rest of the site source, so never store secrets in them.

    Singletons work the same way without a name. For example, third-party tags go in a custom-code slot:

    ```bash Set a custom head snippet 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",
        "file": { "type": "custom_head_end" },
        "content": "<script defer src=\"https://example.com/widget.js\"></script>",
        "leaseToken": "exampleLeaseToken00000000000000000000000000"
      }'
    ```

    Use the focused guides for each singleton's source contract:

    * [Configure routes and redirects](/docs/guides/configure-routes-and-redirects) for page routes and `website_config.redirects`
    * [Style a website](/docs/guides/style-a-website) for raw CSS, Tailwind CSS, and class discovery
    * [Customize site-wide files](/docs/guides/customize-site-wide-files) for `not_found` and the four raw HTML slots
  </Step>

  <Step title="Make targeted edits with string replacement">
    For targeted changes, `POST /v1/websiteSourceCode/files/str-replace` edits files in place without resending their whole content. Each edit's `oldStr` must match the current content exactly once; set `replaceAll: true` to replace every occurrence. Edits apply in order against the evolving content of their file, and every edit across every file commits as a single version. Send up to 50 files and 200 edits per call.

    ```bash Rename a component across two pages theme={null}
    curl -X POST 'https://api.cactal.ai/v1/websiteSourceCode/files/str-replace' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "files": [
          {
            "name": "about",
            "edits": [
              { "oldStr": "<h1>About us</h1>", "newStr": "<h1>Our story</h1>" }
            ]
          },
          {
            "name": "home",
            "edits": [
              { "oldStr": "About us", "newStr": "Our story", "replaceAll": true }
            ]
          }
        ],
        "leaseToken": "exampleLeaseToken00000000000000000000000000"
      }'
    ```

    The response reports the blast radius per file:

    ```json theme={null}
    {
      "version": 6,
      "changed": true,
      "files": [
        { "name": "about", "replacements": 1 },
        { "name": "home", "replacements": 3 }
      ]
    }
    ```

    If any `oldStr` matches zero or multiple locations, the call fails with a `400` naming the offending file and edit, and nothing is written. Read the file again, or add more surrounding context to make the match unique.

    Before a wide replace, send the same body with `"dryRun": true`. It validates every edit and returns the same per-file counts with `"changed": false`, leaving `version` where it was.
  </Step>

  <Step title="Edit routes and page metadata">
    Route and metadata edits rewrite the page source for you, so you avoid string manipulation. `PATCH /v1/websiteSourceCode/pages/route` takes the page's source `name` and the new `route`:

    ```bash Change a route theme={null}
    curl -X PATCH 'https://api.cactal.ai/v1/websiteSourceCode/pages/route' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "name": "about",
        "route": "/company",
        "leaseToken": "exampleLeaseToken00000000000000000000000000"
      }'
    ```

    The response adds `"route": "/company"` and `"routeChanged": true` to the usual `{ version, changed }`.

    `PATCH /v1/websiteSourceCode/metadata` sets a `metadata` object (`title`, `description`, `openGraph`, `robots`, and more). With `page` set it edits that page's metadata; without `page` it edits the site-wide defaults in `website_config`, creating the file when missing.

    Page metadata also supports `{{ ... }}` templates that resolve CMS query results, route parameters, and query parameters at request time. See [Configure SEO metadata](/docs/guides/configure-seo-metadata) for the complete schema, template syntax, fallbacks, and rendering behavior.

    ```bash Set website default metadata theme={null}
    curl -X PATCH 'https://api.cactal.ai/v1/websiteSourceCode/metadata' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "metadata": { "title": "Acme", "titleTemplate": "%s | Acme" },
        "leaseToken": "exampleLeaseToken00000000000000000000000000"
      }'
    ```

    The response returns `metadata` and `metadataChanged` alongside `{ version, changed }`.
  </Step>

  <Step title="Validate the head">
    Review progressively before running the framework gate. First list file identities that changed from published to head:

    ```bash List changed files theme={null}
    curl 'https://api.cactal.ai/v1/websiteSourceCode/changes?websiteId=V1StGXR8_Z5jdHi6B-myT' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    Then request a unified diff only for files that need source-level inspection:

    ```bash Diff one file theme={null}
    curl 'https://api.cactal.ai/v1/websiteSourceCode/files/diff?websiteId=V1StGXR8_Z5jdHi6B-myT&name=home' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    Both operations default to `from=published` and `to=head`. The changed-file list is cursor-paginated; continue with `nextCursor` and the returned `fromVersion` and `toVersion` so later edits do not move the comparison.

    Run the framework quality gate with `POST /v1/websiteSourceCode/head/check` before you publish. It compiles and validates the head version without changing anything.

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

    <Check>
      You should see `{ "version": 6, "ok": true }`. A failing check returns `{ "ok": false, "message": "...", "details": { ... } }` pointing at the broken file. Fix it and check again.
    </Check>

    When the check passes, publish with `POST /v1/websiteSourceCode/head/publish` (body `{ websiteId, leaseToken }`). See [Publishing](/docs/concepts/publishing) for deployment states.
  </Step>
</Steps>

## Version conflicts and recovery

Source writes are guarded two ways, and both failures return `409` with kind `conflict`:

* `Invalid edit lease` — your `leaseToken` was replaced because someone else acquired the lease, or it never matched.
* `Website source changed; reload and try again` — the head version moved between your read and your write.

Recovery is the same loop in both cases:

1. Re-acquire the lease with `POST /v1/websiteSourceCode/lease/acquire` and note the returned `headVersion`.
2. Re-read the affected files with `POST /v1/websiteSourceCode/files/read`.
3. Re-apply your edit against the current content.

<Warning>
  Re-acquiring a lease invalidates the other editor's token. Coordinate before you take a lease from a human editing in the dashboard.
</Warning>

## Troubleshooting

| Error                                               | Cause                                                             | Fix                                                          |
| --------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------ |
| `409` `Source already exists`                       | A file with that name exists under a different type.              | Pick another name or delete the existing file.               |
| `400` `Page name is reserved`                       | Named files cannot use singleton type names.                      | Rename the page, component, or module.                       |
| `400` `Source files must be 1048576 bytes or less`  | The content exceeds 1 MiB.                                        | Split the file into components or modules.                   |
| `400` `Websites can have at most 2000 source files` | The website hit the file cap.                                     | Delete unused files first.                                   |
| `404` `Page not found`                              | Route or metadata edits target a `page` name that does not exist. | List names with `GET /v1/websiteSourceCode/files` and retry. |

## Next steps

* [Preview drafts and versions](/docs/guides/preview-drafts-and-versions) to review the head before publishing
* [Build pages with CMS data](/docs/guides/build-pages-with-cms-data) for `definePage`, typed CMS queries, and render props
* [Configure routes and redirects](/docs/guides/configure-routes-and-redirects) for URL matching and migrations
* [Style a website](/docs/guides/style-a-website) for global CSS and Tailwind CSS
* [Configure SEO metadata](/docs/guides/configure-seo-metadata) to build titles, social previews, and structured data from CMS content
* [Customize site-wide files](/docs/guides/customize-site-wide-files) for the custom 404 page and HTML slots
* [Roll back and recover](/docs/guides/roll-back-and-recover) when a change needs undoing
* [Source code concepts](/docs/concepts/source-code) for the full file and version model
