SiteOps Installation Guide

SiteOps adds article management, contact forms, and inquiry handling to an existing company website. The runtime is a Worker, D1, and R2 on your own Cloudflare account, and the existing site keeps its appearance, URLs, and hosting unchanged.

Dedicated SiteOps environmentManages the Worker, D1, R2, and credentials
Existing siteAppearance, URLs, and hosting stay unchanged
Connection methodConnects over a public HTTPS API. No code is generated into the existing site

Capabilities after installation

Publish and update articles and images from the admin console
Published articles reach the existing site through the publishing mode you configure
Form submissions are saved to the inquiry list
The existing site keeps its appearance, URLs, and hosting
The admin console after installation. It shows the status of articles, forms, images, and inquiries.

1. Preparation

Prepare the following before starting the installation.

Cloudflare accountHosts the SiteOps runtime (Worker, D1, R2). Deployment opens a browser login for approval.
Node.js 20 or laterCheck the version with node -v.
SiteOps distribution fileDelivered with each release (for example, siteops-0.8.0.tgz). Place it as vendor/siteops.tgz, compare it against the SHA-512 value listed in the release, and confirm it has not been tampered with before use.
Supported existing siteA site served over HTTPS. Any framework can connect as long as a server-side process or build process can call the public API.

Information to have on hand

Company name and administrator email address
Public URL of the existing site
Publishing mode for articles (live / webhook / manual)
Deployment procedure of the existing site

Directory layout

The dedicated SiteOps folder can be created anywhere. It is fully independent of the existing site's folder, and the single placement constraint is to keep it out of the existing site's project. The commands below assume the distribution file is placed as follows.

work/                    ← any working folder (location is up to you)
├─ vendor/
│   └─ siteops.tgz       ← place the received distribution file under this name
└─ company-siteops/      ← the dedicated SiteOps folder you will create next

2. Setup

Create the dedicated SiteOps project. Run the commands in the working folder that holds the distribution file (vendor/), separate from the existing site's folder.

Verifying the distribution file

Compare the output of the following command against the SHA-512 value listed in the release.

shasum -a 512 vendor/siteops.tgz
# Windows (PowerShell) — paste the value from the release notes to get True/False:
(Get-FileHash vendor\siteops.tgz -Algorithm SHA512).Hash -eq "<SHA-512 from the release notes>"

Running setup

Run in: the working folder that holds vendor/
mkdir company-siteops
cd company-siteops
npm init -y
npm install ../vendor/siteops.tgz
npx siteops setup .
1

Company nameName shown in the admin console

2

Administrator emailContact address of the initial administrator

# In non-interactive environments, pass the two values as arguments
npx siteops setup . --yes --name "Example Inc." --admin-email admin@example.com
Saving the administrator token

When setup completes, an administrator token starting with so_admin_ is shown once. Save it to a password manager; it is used to sign in to the admin console. A copy is kept in .siteops/credentials.json (excluded from Git), and npx siteops auth reset reissues it if lost.

Files created in the dedicated SiteOps project

FileRoleGit handling
siteops.config.jsonCompany name, SiteOps ID, and adapter settings. The public site URL is added at connection timeCommit
package.json and lockfileAdds the siteops dependency and siteops:* operation commands (npm scripts) such as siteops:deployCommit
.gitignoreExcludes .siteops/ from GitCommit
.siteops/Administrator token, Worker secrets, deployment state, and local connection targetsNot tracked by Git
Note

npm run siteops:* are the operation commands setup adds to package.json; they run the same processing as npx siteops *. Run all following commands in the dedicated SiteOps folder. npm run siteops:status shows the current progress and the next command.

3. Deployment

Place the SiteOps runtime (Worker, D1, R2, and the admin console) on the company Cloudflare account. Run the command in the dedicated SiteOps folder (company-siteops/).

Cloudflare login

When you are not signed in, npm run siteops:deploy automatically opens the browser login screen (wrangler login). Choose and approve the account that will hold SiteOps. In environments that cannot open a browser, such as CI and automation, set an API token created in the Cloudflare dashboard as the CLOUDFLARE_API_TOKEN environment variable. The --temporary option wrangler suggests on authentication errors is meant for temporary accounts; for placement on a company account, prepare one of the credentials above instead. A confirmation prompt appears before deployment, so add --yes in non-interactive runs, as in npm run siteops:deploy -- --yes.

# Run in: company-siteops/
npm run siteops:deploy

On success, the command prints SiteOps deployed, the Administration URL, and the site data initialization result. Once initialization completes, the public API is immediately available. Open the printed URL (/login on the SiteOps URL) and confirm that the saved administrator token signs you in.

If the process stops partway, npm run siteops:status shows the current progress and the next command to run.

4. Connecting the existing site

SiteOps and the existing site share no code; they connect over the public API. Proceed in this order: fetch the connection details, register the public URL, then implement the existing-site side.

  1. Fetch the connection detailsnpx siteops website manifest --json returns the SiteOps URL, the SiteOps ID ({siteKey} in API paths), and the URLs of the article, image, and form APIs. Only public information reaches the browser.
  2. Register the public URL and publishing modeRegister with npx siteops connect live --origin, then run npm run siteops:deploy again to apply it. There are three modes: live for SSR and dynamic sites that render at request time, webhook for static sites that generate HTML at build time, and manual for workflows with an approval step (publishing mode details). For staging environments without a build hook, or during migration, start with manual and switch later by rerunning connect webhook and redeploying. Staging URLs such as http://127.0.0.1:8788 can also be registered. On success, the publishing mode and allowed origins are printed.
  3. Implement in the existing siteWire the public API below into the existing site's data fetching and forms. After the implementation, deploy the existing site with your usual procedure and continue to "5. Verification".
# Run in: company-siteops/
npx siteops website manifest --json                             # Step 1
npx siteops connect live --origin "https://www.example.com"     # Step 2
npx siteops doctor                                              # Configuration check (optional)
npm run siteops:deploy                                          # Applies step 2
# Step 3: implement the code below → deploy the existing site with your usual procedure
npm run siteops:verify

Replace https://www.example.com with the actual public URL. See article publishing modes for static sites, and mail connection when using email.

Public API endpoints

website manifest --json returns this list expanded to the actual URLs. It reaches published data and form intake without an admin token.

Article listGET /api/public/sites/{siteKey}/content/{contentType}/entries — returns published articles as {entries}.
Article detailGET /api/public/sites/{siteKey}/content/{contentType}/entries/{slug} — returns one entry as {entry}. Drafts stay off the public API.
ImagesGET /api/public/sites/{siteKey}/assets/{key} — returns a published image. Usable directly as an img src.
Form security settingsGET /api/public/sites/{siteKey}/forms/security — returns the Turnstile public key and action.
Form submissionPOST /api/public/sites/{siteKey}/forms/{formKey}/submissions — validates and stores the submission and returns {accepted: true}.

Default resources available right after setup

The following content types and forms are provided from the start. Fields can be added and changed in the admin console.

TypeKeyContents
Content typenewsNews. Fields: title (required) / displayDate / category / excerpt / body / externalUrl / important / seoTitle / seoDescription
Content typecolumns / interviews / pagesColumns / interviews / static pages
FormcontactContact. Accepted fields: name (required) / email (required) / company / message (required)
FormpartnerInquiries from agencies and partner companies. Accepted fields: company (required) / name (required) / email (required) / phone / businessType / message (required)

Article entry structure

The list returns {entries: [...]} and the detail returns {entry: {...}}. In the default editor, the body is stored as plain text (paragraphs separated by blank lines), so the site side converts it to HTML.

{
  "entries": [
    {
      "slug": "sakura-no-ie-open-house",
      "publishedAt": "2026-07-17T03:25:00.000Z",
      "updatedAt": "2026-07-17T03:25:00.000Z",
      "data": {
        "title": "Open house at the new model home",
        "displayDate": "2026-07-20",
        "category": "Events",
        "excerpt": "Summary shown in lists and search results",
        "body": "The body is returned as plain text.\n\nParagraphs are separated by blank lines.",
        "seoTitle": "SEO title (optional)",
        "seoDescription": "SEO description (optional)"
      }
    }
  ]
}

Implementing the article list and detail

Fetch the public API on the server side (SSR or the build process) and emit title, meta description, canonical, and structured data into the existing site's HTML. "news" is a content type (an ID for the kind of article); match it to the article settings in the admin console.

const SITEOPS_URL = "https://siteops-xxxxx.workers.dev"; // SITEOPS_URL from the manifest
const SITE_KEY = "main";                                  // SITEOPS_SITE_KEY from the manifest
const api = SITEOPS_URL + "/api/public/sites/" + SITE_KEY;

const list = await fetch(api + "/content/news/entries");
const { entries } = await list.json();

const detail = await fetch(api + "/content/news/entries/" + slug);
if (detail.status === 404) {
  // connect to the existing site's 404 handling
}
const { entry } = await detail.json();

Implementing the contact form

The form posts directly from the browser to the public API. Allowed-origin validation, rate limiting, size limits, and Turnstile verification run on the SiteOps side. contact is a form ID created in the admin console, and each JSON key matches an accepted field key. For creating the Turnstile widget and registering the secret key, see form security settings.

const response = await fetch(api + "/forms/contact/submissions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name,
    email,
    message,
    turnstileToken // when form protection is Turnstile; omit when protection is none
  })
});

const result = await response.json();
if (!response.ok || result.accepted !== true) {
  throw new Error(result.error || "inquiry_submission_failed");
}
  1. Read the form security settingsFetch the Turnstile public key and action from GET .../forms/security and pass them to the widget.
  2. Post directly to the public APISiteOps validates the allowed origin, size, and Turnstile, then saves the submission to the inquiry list.
  3. Show completion from the stored resultTreat accepted: true as a completed intake. Email notification results come back separately in the delivery field of the response.

Allowed-origin validation applies to browser submissions (requests with an Origin header). Server-to-server requests without an Origin header fall outside this validation; the actual bot countermeasure is Turnstile.

After implementation

Deploy the existing site with your usual procedure and continue to "5. Verification".

Where secrets live

Cloudflare credentials, D1, R2, mail credentials, and the administrator token are managed by the dedicated SiteOps environment. The information passed to the existing site is two values: the SiteOps URL and the SiteOps ID.

5. Verification

Before going live, check the SiteOps runtime, the existing site, the forms, and mail (if configured).

Published articles appear on the existing site
Drafts stay off the public API
title, canonical, and structured data are present in the HTML
Browser form submissions are restricted to allowed origins
Invalid Turnstile tokens are rejected
Inquiries are stored regardless of email delivery results
Notification recipients and acknowledgment email contents are correct
No horizontal overflow or input zoom on phones
npx siteops doctor --remote
npm run siteops:verify
npx siteops backup create
npx siteops backup verify backups/<created directory>

backup create prints the destination (backups/<timestamp> by default); pass that value to backup verify.

Example commands for the checklist

# Drafts stay off the public API (an unpublished slug returns 404)
curl -i {SITEOPS_URL}/api/public/sites/{siteKey}/content/news/entries/draft-slug

# Browser submissions from a non-allowed origin are rejected (403)
curl -i -H "Origin: https://evil.example.com" {SITEOPS_URL}/api/public/sites/{siteKey}/forms/security

Output of a successful siteops verify

PASS  runtime          SiteOps本体へ接続できました (312 ms)。
PASS  website          公開APIが https://www.example.com からの接続を許可しています(フォーム保護: turnstile)。ページ表示とフォームは既存サイト側で確認します。

The CLI currently reports in Japanese. A single FAIL stops the run with exit code 1 and prints the location of the cause and the next action.

Updating SiteOps

With distribution-file operation, replace vendor/siteops.tgz with the tgz of the new release and switch to it with npm install ../vendor/siteops.tgz. Then pass npx siteops doctor --remote and npx siteops deploy --dry-run before deploying. npm run siteops:update is the update path when using npm registry distribution.

Recovery

If the administrator token is lost, reissue it with npx siteops auth reset. Keep D1 and R2 backups in a state that has passed hash verification and a restore rehearsal (backup verify).

The admin console works on both desktop and phone. Check on a real device before going live.

Mail connection

The basic setup stores form inquiries in the admin console. Connect a sending service at the stage where you use internal notifications, acknowledgment emails, or replies from the admin console.

Inquiry storageBasic setupRecords form submissions in the inquiry list
Internal notifications and acknowledgment emailsAdd a sending connectionChoose from Cloudflare Email Sending, SendGrid, Amazon SES, or Agentic Inbox
Company mail intakeAdd a receiving connectionConsolidates mail to contact@ and support@ into the inquiry list
In the admin console, choosing the features you want shows the required connection steps in order.
Review the mail connection optionsPer-configuration steps for sending services, DNS, and company mail intake

The "\" in multi-line commands is the bash line-continuation character. In PowerShell, join the command onto one line before running it.

MX settings when taking in company mail

--forms-only, SendGrid, and SES keep the current destination for incoming company mail. In configurations where Cloudflare also takes in company mail, the receiving MX switches to Cloudflare. The CLI states the changes before running.

Cloudflare DNS skips API key registrationIf the company domain is on Cloudflare DNS, Cloudflare Email Sending is available. It uses the SiteOps Worker's sending binding, which removes the work of adding SendGrid or AWS API keys.
Existing company mailThe sending setup adds authentication records for the sending domain. The current receiving MX — Sakura, Xserver, Google Workspace, and so on — can stay until you choose company mail intake.
Existing SMTPThe current SiteOps sending adapters are the four of Cloudflare Email Sending, SendGrid, Amazon SES, and Agentic Inbox. A path that registers shared-hosting SMTP credentials directly is planned for a future update.
Start with inquiry storageKeeps the current company mail

Storage to the inquiry list is enabled by the basic setup. This recipe records the sending-disabled state explicitly in the configuration. Adding a sending connection later also enables internal notifications and acknowledgment emails.

npx siteops mail setup disabled
npx siteops deploy
Send notifications with Cloudflare DNSThe company mail MX stays as it is

Authenticates with Cloudflare in the browser, then runs Email Sending setup, the Worker deployment, and verification automatically. Credentials are managed by the Cloudflare CLI.

npx siteops cloudflare connect \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --notification-emails team@example.com \
  --forms-only
Send with Cloudflare DNS plus company mail intakeEnabling Email Routing changes the MX

Connects the specified company addresses to the SiteOps Worker. The CLI states the MX change before running and stops when it conflicts with existing rules.

npx siteops cloudflare connect \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --notification-emails team@example.com \
  --addresses contact@example.com,support@example.com
Non-Cloudflare DNS plus SendGridAdds CNAME records to the current DNS

Registers a send-only API key with SiteOps. After deployment, issue the authentication records in the admin console, add them to the current DNS — Xserver, Sakura, Route 53, and so on — and press "Check DNS propagation".

npx siteops mail setup sendgrid \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com
npx siteops secret set SITEOPS_SENDGRID_API_KEY
npx siteops deploy
Non-Cloudflare DNS plus Amazon SESAdds DKIM records to the current DNS

Register IAM credentials dedicated to SiteOps and limit their permissions to the SES sending API.

npx siteops mail setup ses \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --region ap-northeast-1
npx siteops secret set SITEOPS_AWS_ACCESS_KEY_ID
npx siteops secret set SITEOPS_AWS_SECRET_ACCESS_KEY
npx siteops deploy
Agentic InboxUses an external mailbox for sending and receiving

Specify a company-owned mailbox as the sending and receiving address, and protect it with Cloudflare Access Service Auth.

npx siteops mail setup agentic-inbox \
  --from-email support@example.com \
  --from-name "Example Company" \
  --base-url https://inbox.example.com \
  --mailbox support@example.com
npx siteops secret set SITEOPS_AGENTIC_INBOX_ACCESS_CLIENT_ID
npx siteops secret set SITEOPS_AGENTIC_INBOX_ACCESS_CLIENT_SECRET
npx siteops deploy

Troubleshooting

Installation can resume from any stage.

Unsure of the current stagenpm run siteops:status shows the four-stage progress and the single next command to run. Resuming after an interruption also starts here.
404 right after deploymentDeployment runs automatically through site data initialization. If errors such as site_not_found still appear, npx siteops doctor --remote runs initialization and diagnostics. Retry afterwards.
Deleted a resource on Cloudflarenpm run siteops:deploy detects the loss of D1 and R2 and recreates them (data returns to an empty state). Restore data from a backup that has passed backup verify.
Running in CI or automationAuthentication uses CLOUDFLARE_API_TOKEN, confirmation prompts take --yes, and secret values pass through --secret-env <ENV_NAME> or npx siteops secret set <NAME> --from-env <ENV_NAME>. Every interactive prompt in the basic flow (setup through verification) has a non-interactive alternative. The exception is the Cloudflare mail connection (siteops cloudflare connect): it requires browser OAuth approval, so run it once on a terminal that allows interaction.
An error appearednpx siteops doctor --remote inspects the configuration, credentials, and the Cloudflare-side state as PASS/WARN/FAIL and locates the cause.
Changing the public URLRegister the new public URL with npx siteops connect live --origin and apply it with npm run siteops:deploy.
website FAILs in verifyCheck, in order: the public URL (allowedOrigins) is registered, it was applied with npm run siteops:deploy, and the existing site is publicly reachable.
Lost the administrator tokennpx siteops auth reset issues a new token and invalidates existing browser sessions.
Restoring dataRun npx siteops backup verify <backup directory> to pass hash verification and a restore rehearsal before restoring.

Configuration details

Configuration concepts, the public API reference, publishing modes, and the technical steps for form security.

Connecting the existing site, SiteOps, and mail separately

Configurations such as the public site on Xserver with DNS on Cloudflare are also supported. Hosting, DNS, and mail can each follow the current contracts.

Existing site hostingEvery hosting connects over the public HTTPS API.
Company domain DNSCloudflare mail connects with Cloudflare DNS. With SendGrid and SES, the current DNS continues to be used.
Incoming company mailSite form operation keeps the current MX. Choosing company address intake switches the receiving MX to Cloudflare.

Connecting without moving the site

The existing site stays on its current hosting — Vercel, Xserver, Sakura, AWS, Cloudflare, and so on — and connects to SiteOps over the public HTTPS API. The connection method is the same for every framework, with no code generation or package additions in the existing site.

Public API reference

website manifest --json returns the following URLs expanded with the actual SiteOps URL and SiteOps ID. They reach published data and form intake without an admin token.

GET  /api/public/sites/{siteKey}/content/{contentType}/entries
GET  /api/public/sites/{siteKey}/content/{contentType}/entries/{slug}
GET  /api/public/sites/{siteKey}/assets/{key}
GET  /api/public/sites/{siteKey}/forms/security
POST /api/public/sites/{siteKey}/forms/{formKey}/submissions
In the admin console, choose how articles reach the existing site. No hosting migration occurs, and the current publishing procedure stays in place.

Article publishing modes

The public API returns published articles. Static sites can generate complete HTML at build time, allowing the same SEO structure as a regular static CMS. The "\" in multi-line commands is the bash line-continuation character. In PowerShell, join the command onto one line before running it.

Rendering from the API

SSR, servers, and edge runtimes fetch published articles at request time and generate title, canonical, and JSON-LD on the server side.

npx siteops connect live \
  --origin https://www.example.com

Rebuilding a static site

After an article is published, the private build URL is called. When a build fails, the previously successful site stays up, and the article list is served from the successful build.

npx siteops connect webhook \
  --origin https://www.example.com \
  --webhook-url-env SITE_BUILD_HOOK

Manual publishing

Used during migration or when passing a separate approval step. Published articles are stored in SiteOps and applied to the existing site after approval.

npx siteops connect manual --origin https://www.example.com
Manage publication state, body text, and SEO fields, and deliver articles to the existing site through the chosen publishing mode.

Form security settings

SiteOps manages form fields, notification conditions, and automatic replies. The public site owns the appearance and the input UI, and passes the token confirmed by Turnstile to SiteOps.

  1. Create a Turnstile widgetCreate a widget under "Turnstile" in the Cloudflare dashboard and register the exact hostname you publish (for example, www.example.com; for staging, localhost or 127.0.0.1). Creation issues two keys: a site key (public) and a secret key (secret). This works even when the site itself is hosted outside Cloudflare.
  2. Connect the two keys to SiteOpsPass the site key as a command argument, and enter the secret key at the hidden prompt or, in non-interactive environments, pass it from an environment variable with --secret-env. They are stored in the local credentials and the Worker secret.
  3. Embed the widget in the formAs in the embedding example below, render the widget with the site key and action fetched from /forms/security, and include the response token in the submission JSON as turnstileToken.
  4. Check a test inquiryCheck storage success and email delivery results separately. Even when email fails, the inquiry data stays stored.
# Interactive environment (enter the secret key at the hidden prompt)
npx siteops forms protect turnstile \
  --turnstile-site-key "0x4AAAA..."

# Non-interactive environment (CI / automation)
TURNSTILE_SECRET="secret key" npx siteops forms protect turnstile \
  --turnstile-site-key "0x4AAAA..." --secret-env TURNSTILE_SECRET

npx siteops deploy

The "\" in multi-line commands is the bash line-continuation character. In PowerShell, join the command onto one line before running it. Changes to the mode and site key are applied by siteops deploy; replacing the secret key (npx siteops secret set SITEOPS_PUBLIC_FORM_TURNSTILE_SECRET_KEY --from-env <ENV_NAME>) takes effect immediately in the deployed environment.

Widget embedding example

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div id="turnstile-widget"></div>

// fetch the form security settings and render the widget
const { protection } = await (await fetch(api + "/forms/security")).json();
if (protection.mode === "turnstile") {
  turnstile.render("#turnstile-widget", {
    sitekey: protection.turnstileSiteKey,
    action: protection.action
  });
}
// on submit: include the turnstile.getResponse() value in the JSON as turnstileToken

Server-side validation and errors

For each submission, SiteOps validates four stages: (1) the token is present, (2) the token verifies with Cloudflare, (3) the action matches siteops-form, and (4) the hostname of the submitting Origin matches the hostname registered on the widget.

Error keyMeaningAction
public_form_turnstile_token_requiredToken missingSend the widget response value as turnstileToken
public_form_turnstile_verification_failedToken verification failedConfirm the site key and secret key belong to the same widget
public_form_turnstile_action_invalidaction mismatchSet the action at widget render time to siteops-form
public_form_turnstile_hostname_invalidSubmitting hostname mismatchRegister the public hostname on the widget (see also the test key note below)
Limits of the official test keys

Cloudflare's official test keys (always-pass site key 1x00000000000000000000AA + secret 1x0000000000000000000000000000000AA, and the rejection-check secret 2x0000000000000000000000000000000AA) let you check the flow. However, the verification response for the test keys fixes the hostname to example.com, so browser submissions from a local environment always end in public_form_turnstile_hostname_invalid. To confirm acceptance end to end including the browser, register the public hostname on a real widget (server-to-server submissions without an Origin header fall outside hostname validation).

Form protection status is visible in the admin console regardless of where the site is hosted.