Deploying

This is a purely static site with no server-side code. But there is one hard requirement: you must be able to set custom response headers.

Requirements

Building locally

npm install
npm run build      # prepare ORT assets → type-check → bundle
npm run preview    # serve locally with COOP/COEP headers

The build output looks like this:

dist/
├── index.html            Chinese home (compression)
├── upscale/index.html    Chinese upscaler
├── cutout/index.html     Chinese cutout
├── blog/                 Chinese blog (hand-written static pages)
├── docs/                 This documentation
├── en/  zh-CN/           English / legacy Chinese paths, same structure
├── assets/               Bundled JS + CSS
├── ort/                  ONNX Runtime assets (generated at build time, stored gzipped)
├── models/               Upscaler model weights
├── _headers              Cloudflare response headers (isolation + caching)
└── robots.txt  sitemap.xml  site.webmanifest

Option 1: Cloudflare Pages (recommended)

The repo ships a wrangler.toml, so you can push straight to Cloudflare:

npm run deploy     # = npm run build && wrangler pages deploy dist --project-name=image-tools

Or connect the Git repo in the Cloudflare dashboard and fill in:

ItemValue
Build commandnpm run build
Output directorydist
Environment variablesNone needed

public/_headers is a native Cloudflare Pages feature, so the isolation headers take effect automatically with no extra configuration.

Option 2: Other static hosts

There is no server-side code — just upload dist/. Three things to watch:

  1. public/_headers is a Cloudflare-specific format. Other platforms need their own equivalent — Netlify uses a same-named _headers, nginx uses add_header, EdgeOne uses response-header rules. The isolation headers must actually take effect, otherwise you drop to single-threaded WASM.
  2. The ORT runtime lives in dist/ort/ — don't forget to upload it.
  3. The upscaler weights live in dist/models/ (already committed to the repo). The cutout weights are not in dist/ — they are fetched on demand from HuggingFace at runtime and cached, so there is nothing to prepare at deploy time.

nginx example

location / {
  add_header Cross-Origin-Opener-Policy "same-origin" always;
  add_header Cross-Origin-Embedder-Policy "require-corp" always;
  try_files $uri $uri/ $uri/index.html =404;
}

Things you must know before deploying

1. Cross-origin isolation is a performance switch, not an optional extra

Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp are what unlock SharedArrayBuffer, and therefore multi-threaded WASM inference. Without them the site still works, but machines without WebGPU fall back to single-threaded WASM and run several times slower. See In-browser inference.

2. require-corp blocks no-cors subresources

The page currently only loads its own assets, so this is safe. But if you add a CDN script, a remote font or an analytics snippet, you have to give them proper CORP headers or they will be blocked — or reconsider the whole policy.

fetch() is the one exception, and that is exactly what the cutout model uses to pull its weights.

3. The ORT wasm file is 25.5 MB, over Cloudflare's 25 MiB per-file limit

That is why scripts/prepare-ort.mjs converts it to gzip storage (about 6.3 MB) at build time and the browser decompresses it with DecompressionStream. This also cuts the initial download to a quarter of its original size.

For the same reason, the upscaler weights must be committed to the repo: the build environment has neither Python nor an upstream model source, so downloading them at build time is not an option. (The cutout weights are 94–452 MB, far beyond what any static host will accept, which is why they are fetched at runtime instead.)

Changing the domain

canonical, hreflang, og:url, og:image, the JSON-LD, the sitemap and robots.txt all use absolute URLs, so a domain change means a global find-and-replace in these files:

The canonical Chinese paths carry no language prefix. Chinese lives at /, /upscale/ and /cutout/; /zh-CN/** are earlier paths whose canonicals all point back to the non-prefixed versions — they are aliases.