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.
Capabilities after installation
1. Preparation
Prepare the following before starting the installation.
node -v.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.Information to have on hand
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
mkdir company-siteops cd company-siteops npm init -y npm install ../vendor/siteops.tgz npx siteops setup .
Company nameName shown in the admin console
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
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
| File | Role | Git handling |
|---|---|---|
siteops.config.json | Company name, SiteOps ID, and adapter settings. The public site URL is added at connection time | Commit |
package.json and lockfile | Adds the siteops dependency and siteops:* operation commands (npm scripts) such as siteops:deploy | Commit |
.gitignore | Excludes .siteops/ from Git | Commit |
.siteops/ | Administrator token, Worker secrets, deployment state, and local connection targets | Not tracked by Git |
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/).
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.
- Fetch the connection details
npx siteops website manifest --jsonreturns 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. - Register the public URL and publishing modeRegister with
npx siteops connect live --origin, then runnpm run siteops:deployagain to apply it. There are three modes:livefor SSR and dynamic sites that render at request time,webhookfor static sites that generate HTML at build time, andmanualfor workflows with an approval step (publishing mode details). For staging environments without a build hook, or during migration, start withmanualand switch later by rerunningconnect webhookand redeploying. Staging URLs such ashttp://127.0.0.1:8788can also be registered. On success, the publishing mode and allowed origins are printed. - 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.
GET /api/public/sites/{siteKey}/content/{contentType}/entries — returns published articles as {entries}.GET /api/public/sites/{siteKey}/content/{contentType}/entries/{slug} — returns one entry as {entry}. Drafts stay off the public API.GET /api/public/sites/{siteKey}/assets/{key} — returns a published image. Usable directly as an img src.GET /api/public/sites/{siteKey}/forms/security — returns the Turnstile public key and action.POST /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.
| Type | Key | Contents |
|---|---|---|
| Content type | news | News. Fields: title (required) / displayDate / category / excerpt / body / externalUrl / important / seoTitle / seoDescription |
| Content type | columns / interviews / pages | Columns / interviews / static pages |
| Form | contact | Contact. Accepted fields: name (required) / email (required) / company / message (required) |
| Form | partner | Inquiries 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");
}
- Read the form security settingsFetch the Turnstile public key and action from
GET .../forms/securityand pass them to the widget. - Post directly to the public APISiteOps validates the allowed origin, size, and Turnstile, then saves the submission to the inquiry list.
- Show completion from the stored resultTreat
accepted: trueas a completed intake. Email notification results come back separately in thedeliveryfield 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.
5. Verification
Before going live, check the SiteOps runtime, the existing site, the forms, and mail (if configured).
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).
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.
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.
--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.
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.
npm run siteops:status shows the four-stage progress and the single next command to run. Resuming after an interruption also starts here.site_not_found still appear, npx siteops doctor --remote runs initialization and diagnostics. Retry afterwards.npm 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.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.npx siteops doctor --remote inspects the configuration, credentials, and the Cloudflare-side state as PASS/WARN/FAIL and locates the cause.npx siteops connect live --origin and apply it with npm run siteops:deploy.npm run siteops:deploy, and the existing site is publicly reachable.npx siteops auth reset issues a new token and invalidates existing browser sessions.npx siteops backup verify <backup directory> to pass hash verification and a restore rehearsal before restoring.Appendix
Configuration details
Configuration concepts, the public API reference, publishing modes, and the technical steps for form security.
Appendix
Configuration details
Configuration concepts, the public API reference, publishing modes, and the technical steps for form security.
Configuration concepts
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.
Connection method for the existing site
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
SEO and publishing modes
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
Contact forms
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.
- 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.
- 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. - 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 asturnstileToken. - 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 key | Meaning | Action |
|---|---|---|
public_form_turnstile_token_required | Token missing | Send the widget response value as turnstileToken |
public_form_turnstile_verification_failed | Token verification failed | Confirm the site key and secret key belong to the same widget |
public_form_turnstile_action_invalid | action mismatch | Set the action at widget render time to siteops-form |
public_form_turnstile_hostname_invalid | Submitting hostname mismatch | Register the public hostname on the widget (see also the test key note below) |
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).