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
- Node.js 20+
- A static host that can set response headers — Cloudflare Pages, Netlify and self-hosted nginx all work. GitHub Pages does not (it cannot set custom response headers, so cross-origin isolation is impossible).
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:
| Item | Value |
|---|---|
| Build command | npm run build |
| Output directory | dist |
| Environment variables | None 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:
-
public/_headersis a Cloudflare-specific format. Other platforms need their own equivalent — Netlify uses a same-named_headers, nginx usesadd_header, EdgeOne uses response-header rules. The isolation headers must actually take effect, otherwise you drop to single-threaded WASM. - The ORT runtime lives in
dist/ort/— don't forget to upload it. -
The upscaler weights live in
dist/models/(already committed to the repo). The cutout weights are not indist/— 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:
index.html,en/index.html,zh-CN/index.htmlupscale/index.html,en/upscale/index.html,zh-CN/upscale/index.htmlcutout/index.html,en/cutout/index.html,zh-CN/cutout/index.htmlpublic/blog/**/*.html,public/en/blog/**/*.html,public/zh-CN/blog/**/*.htmlpublic/docs/**/*.html,public/en/docs/**/*.htmlpublic/sitemap.xml,public/robots.txt- the
SITE_ORIGINconstant insrc/i18n/index.ts
/,
/upscale/ and /cutout/; /zh-CN/** are earlier paths whose canonicals
all point back to the non-prefixed versions — they are aliases.