The deploy commands are simple:
npm run build
npx wrangler deploy
The work is deciding what those two commands mean. For Yangworks, the answer is deliberately plain: the first command must produce the entire site in public/; the second must publish that directory without adding a runtime application. The landing page, legal pages, handbook language trees, publication pages, sitemap, and 404 page all cross the same build boundary.
Cloudflare now recommends Workers for new projects while continuing to support Pages. That product direction is useful context, but it is not the reason this architecture works. The useful part is having one reproducible artifact and one checked-in deployment contract. The repository history behind that decision is covered in Moving Yangworks to Cloudflare Workers Static Assets.
The shape of the deployment
GitHub repository
|
| npm run build
v
root build.js
|-- copy src/site/ --------> public/
|-- copy src/pages/ --------> public/
|-- copy src/static/ --------> public/
|-- run handbook builder
| |-- Chinese ----------> public/handbook/
| |-- English ----------> public/en/handbook/
| `-- Japanese ---------> public/ja/handbook/
|-- run publication builder
| `-- articles/indexes -> public/{guides,field-notes,reviews,compare}/
|-- write /en/ and /ja/ entry pages
|-- write 404.html
`-- write sitemap.xml
npx wrangler deploy
`-- upload public/ as Workers Static Assets
Cloudflare sees generated files. It does not need to understand the source folders, translation registry, publication registry, or sitemap logic. Those remain repository concerns, which means they can be built and inspected before a deployment exists.
The normal build emits only publication entries marked published. A draft build adds final-route previews with noindex,nofollow, but draft URLs never enter the sitemap. Keeping that rule in the root build prevents local editorial previews from becoming production content.
Make public/ the contract
The package scripts expose one build entry point and one local-only variant of it:
{
"scripts": {
"build": "node build.js",
"build:drafts": "node build.js --drafts",
"test": "node --test"
}
}
Only build is wired to the deployment. build:drafts exists so unpublished pages can be reviewed at their final routes without any path to production.
The root builder deletes public/, recreates it, copies the ordinary site files, invokes the handbook builder, and then writes the generated support files. Cleaning the directory is important. If a source page is removed, its old output must disappear too; a deployment should not preserve stale HTML because a previous build happened to leave it behind.
It also means public/ is disposable. Editing a generated file by hand produces a change that vanishes at the next build. Every public file needs an owner under src/ or in a generator.
Let the nested builder own language paths
The root builder passes the site output root to the handbook builder:
const OUT = path.join(ROOT, 'public');
const hb = require(path.join(SRC, 'handbook', 'build.js'));
hb.build(OUT);
Passing public/handbook/ would look reasonable and be wrong. The nested builder already decides that Chinese belongs in public/handbook/, English in public/en/handbook/, and Japanese in public/ja/handbook/. It needs the site root in order to make that decision once.
The same builder treats a translation as published only when its source fragment exists. A missing language file therefore produces no HTML page, navigation item, language-switch link, or sitemap alternate. The build does not create a thin placeholder merely to keep the directory tree symmetrical.
Keep Wrangler explicit
The current configuration contains no Worker script:
{
"name": "yangworks",
"compatibility_date": "2025-01-01",
"assets": {
"directory": "./public",
"not_found_handling": "404-page"
}
}
assets.directory connects the deployment to the build artifact. not_found_handling connects missing routes to the generated 404.html. Wrangler uploads the files as Workers Static Assets; this is not a Cloudflare Pages deployment with a different label.
The explicit 404 setting is worth calling out because Pages and Workers do not make the same guesses. Cloudflare’s Pages-to-Workers migration guide says Pages inferred SPA or custom-404 behavior from the output, while Workers requires not_found_handling to be chosen. Yangworks uses "404-page" because unknown content routes should remain misses. Use "single-page-application" only when a client router must receive otherwise-unmatched paths.
If a Worker script is added later, routing order becomes another explicit choice. When Worker code must inspect requests that also match static assets, configure run_worker_first; otherwise matching assets bypass Worker code by default. Yangworks does not need that setting today because it has no runtime routes.
Keep the Wrangler file in version control, but keep credentials out of it. Authentication belongs to the deployment environment or Cloudflare integration.
Treat a push to main as a release
The Workers project is connected to GitHub with main as the production branch:
Build command: npm run build
Deploy command: npx wrangler deploy
A push to main rebuilds the repository and publishes the resulting assets. The stable facts are the branch, project name, commands, and checked-in Wrangler file—not the current arrangement of controls in Cloudflare’s dashboard.
If this is a migration from an active Pages project, connecting Workers Builds is only half the cutover. Cloudflare’s migration guide explicitly tells you to disable automatic deployments on the Pages project after the Workers build is connected. Otherwise one repository can continue feeding two deployment systems while you think the move is complete. Validate the Worker first; remove the old project only after traffic and routes are correct.
For Yangworks, local verification matters because production deploys are automatic. A build that exits successfully can still publish a missing page, a wrong canonical, or a broken language link.
Clean URLs are a hosting behavior, not a filename convention
The generator writes files such as public/handbook/niche.html. On the deployed site, the .html URL has been observed to return a 307 redirect to /handbook/niche. Canonicals, sitemap entries, and language-switch links therefore use the extensionless form.
Cloudflare exposes this behavior through assets.html_handling; the default is auto-trailing-slash. Do not assume a local file server reproduces it. Test the deployed response chain before deciding which URL belongs in metadata.
Some handbook links still point to relative paths such as ./niche.html. They resolve, but they take the redirect hop. Removing it is a separate cleanup: generated navigation, body links, output filenames, canonicals, and the sitemap need to agree. Changing one layer alone makes generated links, metadata, and deployed routes disagree.
The 404 page and language roots need status checks
The root build writes a shared 404.html with noindex and links to the available language sections. With not_found_handling: "404-page", Workers serves the nearest applicable 404 document with a not-found status. Verify both pieces. A friendly error page returned as 200 OK is still a routing bug.
The build also writes public/en/index.html and public/ja/index.html. Workers serves them at /en/ and /ja/; the no-slash forms may first receive the provider’s trailing-slash redirect. The generated pages then use a zero-second meta refresh to reach the handbook roots.
Those pages are not HTTP 301 redirects. They have a canonical target, a visible link, and noindex, but a header check will still show an HTML response. If a permanent server-side redirect is required, implement one at the edge rather than describing the current stubs as something they are not.
Generate discovery metadata from the page model
The sitemap is written after the handbook build, when the root script knows which language files were actually generated. Root pages receive ordinary URL entries. Each handbook page receives one entry per available language, and every language cluster is derived from the same alternate list.
That list contains the page itself, the other existing translations, and x-default pointing to the Chinese route. A missing translation never reaches the sitemap because the builder and sitemap share the same availability check. The details are covered in Multilingual SEO for a static site without a framework.
Verify the artifact, then the edge behavior
Run the production build from a clean checkout and inspect the artifact rather than trusting the exit code. Confirm the root page, handbook roots, localized directories, 404.html, and sitemap.xml exist. If the publication registry contains published entries, spot-check a generated article and section index. Draft article files must be absent from a production build, and no draft URL should appear in the sitemap. Then check that every language page canonicals to itself and that the sitemap contains no URL for a missing file.
After deployment, a few requests expose most routing mistakes:
curl -I https://yangworks.dev/
curl -I https://yangworks.dev/handbook/
curl -I https://yangworks.dev/en/handbook/
curl -I https://yangworks.dev/handbook/niche.html
curl -I https://yangworks.dev/a-route-that-does-not-exist
curl https://yangworks.dev/sitemap.xml
Read the status and Location headers instead of relying on what a browser eventually displays. Then open representative pages to catch CSS, JavaScript, relative-link, and mobile-navigation problems. Finally, confirm that the Cloudflare deployment points to the commit you intended to release.
What this setup does—and does not—buy
The result is easy to reason about: one dependency-light Node build, one static output directory, one deployment configuration, and no application server. The static artifact can move to another host, but its redirects, HTML handling, 404 behavior, and Git deployment need to be recreated there.
The custom generator is still infrastructure. Its URL rules, page registry, translation availability, sitemap generation, and tests need maintenance. A full rebuild is sensible at this size and may be wasteful for a much larger publication. Automatic Git deployment also turns branch protection and review quality into production controls.
This case does not establish lower cost, better latency, faster builds, higher rankings, or zero-downtime migration. Cloudflare’s own position is narrower: start new projects on Workers; Pages remains supported; future product investment is centered on Workers. For this site, the decision was about a clear artifact and a deployment model that matched it—not a benchmark victory.
This pattern assumes public routes can be materialized at build time and editors can work through Git. Authentication, request-time personalization, database reads, and application APIs require Worker code or another runtime.