Skip to content

Deployment ​

The build is a static SPA. Any static host works.

bash
pnpm build     # -> dist/

Use build, not build:demo

build:demo bundles the mock server, which will intercept requests meant for your real API. It also copies demo-public/ — including a robots.txt that closes the entire site to crawlers. Correct for a demo behind an SPA fallback; it would silently deindex your app.

SPA fallback, without the crawl trap ​

Client-side routing needs real paths served index.html, or a deep link returns 404 from the host before React ever runs.

The usual advice is a catch-all:

/*  /index.html  200

Do not do that. It answers 200 for every URL. A crawler that invents /a/b/c gets a page, finds more invented links, and never terminates — an unbounded crawl space. That exact shape ran up a four-figure Cloudflare bill on another Colorlib demo in 2026.

pnpm build instead writes the SPA shell to a real file per route — dist/table.html, dist/dashboard/analysis.html, and so on — from the list in src/config/routes.ts. Every real route is genuine static content, so it is a plain 200; anything else has no file and falls through to 404.html with a real 404, which is where crawling stops.

This needs no host-specific configuration, so it behaves the same on Cloudflare Pages, Netlify, S3 or nginx. Nothing to keep in sync with a redirects file.

<route>.html rather than <route>/index.html is deliberate: static hosts serve the former directly, while the latter earns a 308 to the trailing-slash form on every deep link. Both work; only one costs a round trip.

An earlier attempt used _redirects listing each route, and it did not work: Cloudflare Pages canonicalises /index.html to /, so a rewrite pointing there inherits the redirect and a deep link 308s to the root, losing the page the visitor asked for.

scripts/routes.test.ts fails if the route list and the router disagree; scripts/crawl-trap.test.ts fails if a shell goes missing or a catch-all reappears.

Compression ​

Serve gzip or brotli. The numbers this project quotes are gzip; uncompressed the heaviest route is roughly 3× larger, and an uncompressed host quietly undoes the bundle work.

Caching ​

Assets are content-hashed, so cache assets/* hard and index.html not at all:

nginx
location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location = /index.html { add_header Cache-Control "no-cache"; }

Base path ​

Deploying under a subpath needs base in vite.config.ts and basename on the router. Setting only one produces a blank page with 200s in the network tab — assets load, routes do not match.

China ​

If your users are in mainland China, verify the host is reachable from there before launch rather than after. Consider mirroring the repository to Gitee — GitHub is slow and intermittently unreachable, and Gitee is where many Chinese developers browse.

Released under the MIT License.