Static site + Cloudflare Pages + GitHub Actions CI/CD.
Build your site. Push to deploy. Free and fast.
English | 한국어
Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages
Via create-starter (recommended):
npx @starter-series/create my-site --template cloudflare-pages
cd my-site && npm install && npm run devOr clone directly:
git clone https://github.com/starter-series/cloudflare-pages-starter my-site
cd my-site && npm install && npm run dev
⚠️ Before deploying: renamepackage.jsonname(from"my-site"to your Cloudflare Pages project name) and updaterepository.url(replaceYOUR_USERNAME/YOUR_SITE). Thedeployscript uses$npm_package_nameas the Cloudflare Pages project name — CD will silently deploy to the wrong project (or fail) if you skip this. (create-starter handles thenameautomatically; you still need to setrepository.url.)
├── src/
│ ├── index.html # Site entry point (replace with your site)
│ ├── style.css # Styles
│ └── main.js # JavaScript
├── functions/
│ └── api/
│ ├── hello.js # Example Pages Function → GET /api/hello
│ └── visits.js # KV-backed visit counter → GET /api/visits
├── tests/
│ ├── functions.test.js # node:test unit tests for /api/hello
│ └── visits.test.js # Unit tests for /api/visits with mock KV
├── wrangler.toml # Pages config + commented KV binding example
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Lint, security scan
│ │ ├── cd.yml # Deploy to Cloudflare Pages
│ │ └── setup.yml # Auto setup checklist on first use
│ └── PULL_REQUEST_TEMPLATE.md
├── docs/
│ └── CLOUDFLARE_PAGES_SETUP.md # Deployment setup guide
├── scripts/
│ └── bump-version.cjs # Semver version bumper
├── eslint.config.js # ESLint v9 flat config
├── .gitignore
└── package.json
- Cloudflare Pages — Global CDN, unlimited bandwidth, free
- Wrangler CLI — Deploy via CI or locally with
npm run deploy - CI Pipeline — Secret scanning, large file check, lint on every push and PR
- CD Pipeline — One-click deploy to Cloudflare Pages + auto GitHub Release
- Version management —
npm run version:patch/minor/major - Local dev —
npm run devwith Cloudflare Pages emulation - Template setup — Auto-creates setup checklist issue on first use
- Minimal — 4 devDependencies, no build step required
| Step | What it does |
|---|---|
| Secret scan | gitleaks scans for leaked credentials |
| Large file check | Prevents files over 5 MB (Cloudflare limit: 25 MB) |
| Install | npm ci with lockfile verification |
| Lint | ESLint v9 flat config |
| Test | node --test runs Pages Functions unit tests |
| Workflow | What it does |
|---|---|
CodeQL (codeql.yml) |
Static analysis for security vulnerabilities (push/PR + weekly) |
Maintenance (maintenance.yml) |
Weekly CI health check — auto-creates issue on failure |
Stale (stale.yml) |
Labels inactive issues/PRs after 30 days, auto-closes after 7 more |
| Step | What it does |
|---|---|
| CI | Runs full CI pipeline first |
| Version guard | Fails if git tag already exists for this version |
| Deploy | wrangler pages deploy src to Cloudflare Pages |
| GitHub Release | Creates a tagged release with auto-generated notes |
How to deploy:
- Set up Cloudflare (see below)
- Bump version:
npm run version:patch(orversion:minor/version:major) - Commit and push to
main - Go to Actions tab → Deploy to Cloudflare Pages → Run workflow
| Secret | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN |
Wrangler authentication |
CLOUDFLARE_ACCOUNT_ID |
Target Cloudflare account |
See docs/CLOUDFLARE_PAGES_SETUP.md for the one-time setup.
- Create a Cloudflare account (free)
- Create a Pages project (Workers & Pages → Create → Pages)
- Create an API token with Cloudflare Pages: Edit permission
- Add
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_IDas GitHub Secrets - Create a GitHub Environment named
cloudflare - Set
PROJECT_NAMEas a GitHub variable
That's it. See docs/CLOUDFLARE_PAGES_SETUP.md for detailed steps.
npm run version:patch # 0.1.0 → 0.1.1
# commit, push
# Actions → Deploy to Cloudflare Pages → Run workflowYour site will be live at https://PROJECT_NAME.pages.dev.
# Local dev server (Cloudflare Pages emulation)
npm run dev
# Bump version
npm run version:patch # 0.1.0 → 0.1.1
npm run version:minor # 0.1.0 → 0.2.0
npm run version:major # 0.1.0 → 1.0.0
# Lint
npm run lint
# Run tests
npm test
# Deploy manually
npm run deployNeed an API route? Add a file to functions/ — it's picked up automatically, no config.
functions/api/hello.js → GET /api/hello
functions/users/[id].js → GET /users/:id
This starter ships with functions/api/hello.js wired into src/index.html as a demo. Reading the request and returning a Response is the whole API:
export async function onRequest(context) {
const { request } = context;
const url = new URL(request.url);
const name = url.searchParams.get('name') ?? 'World';
return new Response(JSON.stringify({ greeting: `Hello, ${name}!` }), {
headers: { 'content-type': 'application/json' },
});
}Local dev — wrangler pages dev auto-discovers functions/ next to your assets directory:
npm run dev
# which runs: wrangler pages dev src --port 3000
# Open http://localhost:3000 — the page calls /api/hello and renders the greeting.If you want to pin a Workers runtime version, pass --compatibility-date:
npx wrangler pages dev src --compatibility-date=2026-04-24Testing — Pages Functions are plain ES modules that accept a Request and return a Response, so node:test runs them with zero mocks:
npm testSee Cloudflare Pages Functions docs for middleware, [param] routing, env bindings (KV, D1, R2), and more.
functions/api/visits.js is a tiny visit counter backed by Cloudflare Workers KV. It reads count from a KV namespace bound as VISITS, increments it, and returns JSON:
export async function onRequest(context) {
const { env } = context;
const current = parseInt(await env.VISITS.get('count'), 10) || 0;
const next = current + 1;
await env.VISITS.put('count', String(next));
return new Response(JSON.stringify({ visits: next }), {
headers: { 'content-type': 'application/json' },
});
}One-time setup — create a KV namespace (plus a preview one for local dev):
npx wrangler kv namespace create VISITS
npx wrangler kv namespace create VISITS --previewEach command prints an ID. Open wrangler.toml, uncomment the [[kv_namespaces]] block, and paste the IDs:
[[kv_namespaces]]
binding = "VISITS"
id = "<paste-production-namespace-id-here>"
preview_id = "<paste-preview-namespace-id-here>"Local dev — wrangler pages dev uses a local KV simulator by default, so you don't need to touch production data:
npm run dev
# Open http://localhost:3000 — /api/visits increments on each page load.Until you create the namespace, /api/visits returns 503 and the counter element is hidden on the page — wrangler pages dev still boots cleanly.
Deploy — once the IDs are in wrangler.toml, the existing CD workflow deploys the binding automatically (no extra secrets).
See the KV bindings and Wrangler KV commands docs for the full API.
| Cloudflare Pages | GitHub Pages | Vercel / Netlify | |
|---|---|---|---|
| Bandwidth | Unlimited (free) | 100 GB/month | 100 GB/month |
| Global CDN | 300+ edge locations | Limited | Yes |
| Custom domains | Free SSL, auto-config | Free SSL | Free SSL |
| Build minutes | 500/month (free) | 10 min/build | 6000 min/month |
| Pricing | Free | Free | Free tier + paid |
This template starts with plain HTML/CSS/JS. To add a framework:
Vite:
npm install -D vite
# Add "build": "vite build --outDir dist" to package.json scripts
# Change deploy directory from src/ to dist/ in cd.yml and package.jsonAstro:
npm create astro@latest
# Follow the prompts, then update cd.yml deploy directoryThe template is intentionally framework-free so you can add what you need.
PRs welcome. Please use the PR template.