How to Host a Static Website on Cloudflare (Workers, Custom Domain, First Deploy)

By Simeon Matheka, Founder & Creative Director · Published 2026-09-07 · Updated 2026-09-07 · 16 min read

Get a folder of HTML, CSS, and JavaScript live on Cloudflare: Workers static assets, wrangler.jsonc, the first deploy, then the real website URL. What workers.dev is for, and what to skip if there is no PHP origin.

Terracotta shipping crate labeled LIVE on a slate-indigo workbench, cream tags reading URL, Host, Deploy, and Domain

You have a folder of HTML, CSS, and JavaScript. Maybe it came from Vite, Astro, Eleventy, or a hand-built public/ directory. You want it on the real domain with HTTPS, without renting a PHP box. On Cloudflare that is four jobs: pick where the files live, write a tiny config, deploy once, then attach the website URL.

This is the hands-on host. The order of Domain Name System (DNS), Universal SSL, files, and redirects lives in the Cloudflare static website map. Read that when the padlock is on workers.dev and the custom domain is blank. Start here when the dist folder is sitting on your laptop.

What static website hosting on Cloudflare actually is

Static means the server does not assemble the page per request. You upload files. The edge caches them. A form POST, a contact API, or a database is a different job. You can still host those next to the files. You do not have to, and you should not invent a backend to “make Cloudflare work.”

In 2026 the product that does this for a new site is Workers static assets. Cloudflare Pages still works. The Pages overview now asks if you are sure, and tells you to start on Workers. Treat Pages as the installed base, not the default for a greenfield marketing site.

You haveHost it asSkip
HTML, CSS, JS, maybe a Vite distWorkers assets.directory (or existing Pages)A Node origin, PHP, or a “full-stack” template
A React Single Page Application (SPA) that must rankPrerendered HTML in dist, then the same assets hostAn empty #root and hope crawlers run JavaScript
Images and Lottie that should not live in gitCloudflare R2 plus a public CDN URLPutting those binaries in the Worker assets bundle forever
A contact formStatic HTML plus a POST to an edge function or backendA PHP mailer bolted onto the static deploy

If you are still choosing between a WordPress origin and an edge folder, the monolithic CMS vs serverless edge comparison is the architecture fork. This article assumes you already want the folder on Cloudflare.

Thirty minutes to the first live URL

Do this on a throwaway hostname first. Do not start by moving nameservers for a domain that still has email on cPanel.

  1. Account and zone: Create a Cloudflare account. Add the site so Cloudflare can nameserver it, or use a subdomain on a zone you already own. You cannot attach a Workers custom domain to a hostname Cloudflare does not DNS.
  2. Build output: Produce a directory of files. On this site that is npm run build, which writes dist/. Confirm dist/index.html exists and is not a 2 KB stub.
  3. Config: Add wrangler.jsonc at the repo root with a name, compatibility_date, and assets.directory pointing at that folder. You do not need a main Worker script yet.
  4. First deploy: From the repo: npx wrangler login, then npx wrangler deploy. Wrangler uploads the assets directory. Opt in to workers.dev if you want a preview hostname before the custom domain.
  5. Website URL: In Workers, Custom Domains, attach example.com (or www). Cloudflare writes the proxied DNS record and the certificate. That hostname is the public URL. workers.dev is not.

The wrangler.jsonc this site actually ships

simeoncreatives.com is a Vite React app. Production is a Worker with an assets directory and no fetch handler. After the AdSense review saw a hidden stub, we stopped using a Single Page Application (SPA) fallback for unknown paths. Unknown URLs must 404.

{
  "name": "simeon-creatives",
  "compatibility_date": "2026-07-13",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page",
    "html_handling": "drop-trailing-slash"
  }
}
KnobWhat we setWhat breaks if you guess
assets.directory./dist after the Vite buildWrangler uploads the repo root and you ship source, or it uploads nothing useful
not_found_handling404-pagesingle-page-application returns index.html for junk URLs. Google logs those as soft 404s. AdSense treats them as thin pages
html_handlingdrop-trailing-slashauto-trailing-slash 307s /blog/slug to /blog/slug/. The canonical points back. Google sees “page with redirect” and skips indexing
main / fetch handlerOmittedYou pay for and debug a script that only needs to serve files

A marketing SPA that must rank still needs the article in the first HTML. We prerender every public route into dist/{path}/index.html. The failure mode (curl returned a title and a chat widget) is in why crawlers saw an empty React site. Hosting on Cloudflare does not fix an empty #root. It only serves whatever you put in dist.

Your Cloudflare website URL

Search “Cloudflare website URL” and you get three different hostnames mixed together. They are not interchangeable.

HostnameWho sees itUse it for
project.pages.devYou, or a client on a preview linkPages projects only. Fine for QA. Not the brand URL
name.workers.devYou, if you enabled workers.devFirst deploy proof. Do not print it on a business card
example.com or www.example.comEveryoneCustom Domain on the Worker (or Pages). This is the website URL

A Custom Domain matches one hostname exactly. example.com and www.example.com are two attachments, or one attachment plus a redirect rule. Keep a proxied DNS record on the hostname you redirect from. Originless placeholders are A 192.0.2.0 or AAAA 100::. Traffic never reaches those addresses because the record is orange-clouded.

  • Canonical: Pick apex or www and make every other hostname 301 there. This site is slashless apex.
  • Certificate: Universal SSL covers proxied hostnames on the zone. You do not upload a Let’s Encrypt file for a static Worker custom domain.
  • Preview vs production: Ship to workers.dev, click around, then attach the real domain. Do not debug DNS and the first upload in the same hour.
flowchart LR
    Dist["dist folder<br/>HTML CSS JS"] --> Wrangler["wrangler deploy"]
    Wrangler --> Preview["workers.dev<br/>optional QA"]
    Wrangler --> Custom["Custom Domain<br/>example.com"]
    Custom --> Public["Public website URL"]
    Preview -.->|"do not launch here"| Public

Pages vs Workers for the first static host

If a Pages project already serves the client, do not migrate for sport. If you are starting today, use Workers static assets. The map article has the compatibility table. The hosting decision is shorter:

  • New static or SPA marketing site: Workers assets. That is the default.
  • Domain Cloudflare does not nameserver: Pages can still attach a custom domain. Workers custom domains cannot. That is the real reason to pick Pages, not nostalgia.
  • File-based Pages Functions folder: Native on Pages. On Workers you compile or replace. Not a reason to pick Pages if the site is only files.

After it is live: redirects, headers, cache, media

Put _redirects and _headers in the asset output (public/ so the bundler copies them into dist). Redirects run before headers. First matching line wins. Default status is 302. Use 301 when the move is permanent. If the file never reaches dist, Cloudflare never sees it.

Hashed files in /assets/* can be cached for a year. index.html should not. If a cover image reuses the same URL, scrapers keep the old bytes until purge or Time to Live (TTL). Change the filename when the art changes.

When images and Lottie stop belonging in git, move them to R2 and keep HTML on the Worker. The Cloudflare R2 for websites guide is the upload, credentials, and free-tier math. Do not start there on day one.

Contact forms stay HTML. The POST is not static. Validate at the edge or a backend, store the row, notify Slack later. The dynamic forms with Supabase and edge functions pattern is what we use when a static host still has to collect leads.

Failure modes we see on first deploys

SymptomUsual causeFix
Green padlock on workers.dev, blank custom domainCustom Domain not attached, or DNS still at the old hostAttach the hostname. Confirm nameservers. Wait for Universal SSL
Apex works, www does not (or the reverse)One Custom Domain onlySecond attachment, or a redirect plus a proxied placeholder record
Site live, Outlook mail diesmail.example.com proxiedGrey-cloud the mail host. Leave website orange
Users see the app, Google sees a stubSPA with empty first HTMLPrerender routes into dist. Confirm with curl before celebrating the deploy
Every junk URL returns 200not_found_handling is single-page-application404-page plus a real dist/404.html
Cover images 404 in production, fine on localhostFiles only in public/, CDN base set, nothing in R2Upload the object. Confirm the CDN URL returns 200

What to skip on a static host

  • A Worker fetch handler “just in case”: Add it when you have an /api path or you must rewrite HTML. Not before.
  • Moving nameservers and deploying in the same sitting: Prove the upload on workers.dev. Cut DNS when the files are already correct.
  • Orange-clouding FTP, SSH, or IMAP: The HTTP proxy does not carry those protocols. Website hostnames only.
  • R2, KV, and D1 on day one: Host the HTML first. Add object storage when git complains about binaries.

If you want a site that also converts, hosting is not the strategy. Plan the pages and the one offer first. We do that on website projects when the stack is this one: static files on Workers, media on R2, forms off the origin.

Ship it, then keep the map nearby

Host means: files in dist, wrangler deploy, custom domain attached, 404s that are actually 404s. After that, the map is the checklist for DNS proxy status, Universal SSL, www vs apex, and _redirects. You do not need both articles open on minute one. You will want the map the first time the apex and www disagree.

Print the first Cloudflare deploy checklist and tick it beside wrangler. Next: the Cloudflare static website map for the six jobs in order, then R2 when media leaves the repo.

Frequently asked questions

Can Cloudflare host a static website?

Yes. Put the built HTML, CSS, and JavaScript in a Worker assets directory (or an existing Pages project) and attach a custom domain. You do not need a PHP or Node origin. Static asset requests are not billed like Worker CPU.

Is Cloudflare Pages or Workers better for a new static site?

For a new project in 2026, start on Workers with static assets. Cloudflare’s Pages overview now points new work at Workers. Leave an existing Pages project alone if it already serves production traffic and you have no missing feature.

What is my Cloudflare website URL after the first deploy?

Workers can give you a workers.dev hostname if you opt in. Pages gives pages.dev. Those are preview URLs. The public website URL is the custom domain you attach on a Cloudflare zone you own (example.com, or www.example.com). Attach both hostnames or redirect one to the other.

Do I need a Worker script to host HTML files?

No. assets.directory plus a custom domain is enough. Add a main script later for an API path, authentication in front of files, or HTML rewriting. Until then, skip bindings, Durable Objects, and queues.

How do I host a React or Vite app on Cloudflare without empty crawler HTML?

Build the static files, then prerender a real index.html for every public route into dist before deploy. Set not_found_handling to 404-page so unknown URLs are not a 200 Single Page Application (SPA) shell. This site does that. A client-only SPA with an empty #root is live for users and empty for many crawlers.

Is Cloudflare static web hosting free?

A small marketing site on Workers static assets plus a custom domain typically stays inside Cloudflare’s included plan. You pay when you add paid products (some R2 tiers, extra Workers CPU, extra domains) or you blow request and storage limits. Static files alone are not a compute bill.

Tags: Cloudflare, static website hosting, Cloudflare Workers, Cloudflare Pages, custom domain, wrangler, static site, edge hosting