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

- Source: https://simeoncreatives.com/blog/host-a-static-website-on-cloudflare
- Hub: Website Design & Development
- Author: Simeon Matheka, Founder & Creative Director
- Published: 2026-09-07
- Updated: 2026-09-07
- Reading time: 16 min

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.

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](https://simeoncreatives.com/blog/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 have | Host it as | Skip |
| --- | --- | --- |
| HTML, CSS, JS, maybe a Vite dist | Workers assets.directory (or existing Pages) | A Node origin, PHP, or a “full-stack” template |
| A React Single Page Application (SPA) that must rank | Prerendered HTML in dist, then the same assets host | An empty #root and hope crawlers run JavaScript |
| Images and Lottie that should not live in git | Cloudflare R2 plus a public CDN URL | Putting those binaries in the Worker assets bundle forever |
| A contact form | Static HTML plus a POST to an edge function or backend | A 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](https://simeoncreatives.com/blog/monolithic-cms-vs-serverless-edge-stack) 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.

> If mail already lives on cPanel, proxy only the website hostnames. Set mail.example.com to DNS only. Mail Exchanger (MX) records cannot be proxied. The Outlook plus cPanel article covers the failure when someone oranges the mail host.

## 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.

```jsonc
{
  "name": "simeon-creatives",
  "compatibility_date": "2026-07-13",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page",
    "html_handling": "drop-trailing-slash"
  }
}
```

| Knob | What we set | What breaks if you guess |
| --- | --- | --- |
| assets.directory | ./dist after the Vite build | Wrangler uploads the repo root and you ship source, or it uploads nothing useful |
| not_found_handling | 404-page | single-page-application returns index.html for junk URLs. Google logs those as soft 404s. AdSense treats them as thin pages |
| html_handling | drop-trailing-slash | auto-trailing-slash 307s /blog/slug to /blog/slug/. The canonical points back. Google sees “page with redirect” and skips indexing |
| main / fetch handler | Omitted | You 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](https://simeoncreatives.com/blog/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.

| Hostname | Who sees it | Use it for |
| --- | --- | --- |
| project.pages.dev | You, or a client on a preview link | Pages projects only. Fine for QA. Not the brand URL |
| name.workers.dev | You, if you enabled workers.dev | First deploy proof. Do not print it on a business card |
| example.com or www.example.com | Everyone | Custom 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.

```mermaid
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.

> A static site does not need Durable Objects, Queues, or a D1 database to “be hosted.” Those products are for apps. Adding them to a brochure site is how a $0 host becomes a debugging hobby.

## 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](https://simeoncreatives.com/blog/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](https://simeoncreatives.com/blog/dynamic-web-forms-supabase-edge-functions) pattern is what we use when a static host still has to collect leads.

## Failure modes we see on first deploys

| Symptom | Usual cause | Fix |
| --- | --- | --- |
| Green padlock on workers.dev, blank custom domain | Custom Domain not attached, or DNS still at the old host | Attach the hostname. Confirm nameservers. Wait for Universal SSL |
| Apex works, www does not (or the reverse) | One Custom Domain only | Second attachment, or a redirect plus a proxied placeholder record |
| Site live, Outlook mail dies | mail.example.com proxied | Grey-cloud the mail host. Leave website orange |
| Users see the app, Google sees a stub | SPA with empty first HTML | Prerender routes into dist. Confirm with curl before celebrating the deploy |
| Every junk URL returns 200 | not_found_handling is single-page-application | 404-page plus a real dist/404.html |
| Cover images 404 in production, fine on localhost | Files only in public/, CDN base set, nothing in R2 | Upload 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](https://simeoncreatives.com/websites) 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](https://simeoncreatives.com/resources/first-cloudflare-deploy-checklist) and tick it beside wrangler. Next: the [Cloudflare static website map](https://simeoncreatives.com/blog/cloudflare-static-website-map) for the six jobs in order, then R2 when media leaves the repo.

## FAQs

### 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.
