Last Updated: 3/9/2026
Cloudflare Pages
Cloudflare Pages is a full-stack edge platform that combines static site hosting with dynamic server-side rendering powered by Cloudflare Workers. It offers lightning-fast deployments, automatic HTTPS, and global CDN distribution.
Hono provides excellent support for Cloudflare Pages with Vite integration for fast local development and seamless deployment.
Quick Start
Create a new Hono app for Cloudflare Pages:
npm create hono@latest my-appSelect cloudflare-pages when prompted.
Setup
1. Create Project
::: code-group
npm create hono@latest my-app
cd my-app
npm installyarn create hono my-app
cd my-app
yarnpnpm create hono@latest my-app
cd my-app
pnpm installbun create hono@latest my-app
cd my-app
bun install:::
2. Project Structure
.
├── package.json
├── public/
│ └── static/ # Static assets
│ └── style.css # Served as /static/style.css
├── src/
│ ├── index.tsx # Server-side entry point
│ └── renderer.tsx # JSX renderer setup
├── tsconfig.json
└── vite.config.ts # Vite configuration3. Write Your Application
Edit src/index.tsx:
import { Hono } from 'hono'
import { renderer } from './renderer'
const app = new Hono()
// Apply renderer middleware
app.get('*', renderer)
app.get('/', (c) => {
return c.render(
<div>
<h1>Hello, Cloudflare Pages!</h1>
<p>Built with Hono and Vite</p>
</div>
)
})
app.get('/api/hello', (c) => {
return c.json({
message: 'Hello from Cloudflare Pages API',
timestamp: new Date().toISOString(),
})
})
export default appThe renderer.tsx provides a layout:
import { jsxRenderer } from 'hono/jsx-renderer'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<title>My Hono App</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
{children}
</body>
</html>
)
})4. Run Locally
Start the Vite development server:
::: code-group
npm run devyarn devpnpm devbun run dev:::
Access http://localhost:5173 in your browser.
5. Deploy
Deploy with Wrangler:
::: code-group
npm run deployyarn deploypnpm run deploybun run deploy:::
Deploy from GitHub
Cloudflare Pages integrates directly with GitHub:
- Push your code to GitHub
- Log in to Cloudflare dashboard
- Go to Workers & Pages → Create application → Pages → Connect to Git
- Select your repository
- Configure build settings:
| Setting | Value |
|---|---|
| Production branch | main |
| Build command | npm run build |
| Build directory | dist |
- Click Save and Deploy
Every push to your repository will trigger automatic deployment.
Bindings
Access Cloudflare services like KV, D1, R2, and environment variables.
Configure Local Bindings
Create wrangler.toml:
# Environment variables
[vars]
MY_NAME = "Hono"
API_URL = "https://api.example.com"
# KV Namespace
[[kv_namespaces]]
binding = "MY_KV"
id = "your-kv-id"
preview_id = "your-preview-kv-id"
# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
# R2 Bucket
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"Create KV Namespace
Create a KV namespace for preview:
wrangler kv namespace create MY_KV --previewNote the preview_id from the output and add it to wrangler.toml.
Configure Vite
Edit vite.config.ts to enable Cloudflare adapter:
import devServer from '@hono/vite-dev-server'
import adapter from '@hono/vite-dev-server/cloudflare'
import build from '@hono/vite-cloudflare-pages'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
devServer({
entry: 'src/index.tsx',
adapter, // Cloudflare adapter for local dev
}),
build(),
],
})Use Bindings in Your App
Define types and access bindings:
type Bindings = {
MY_NAME: string
MY_KV: KVNamespace
DB: D1Database
MY_BUCKET: R2Bucket
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', async (c) => {
// Use environment variable
const name = c.env.MY_NAME
// Use KV
await c.env.MY_KV.put('visitor', name)
const visitor = await c.env.MY_KV.get('visitor')
return c.render(
<h1>Hello, {visitor}!</h1>
)
})
app.get('/api/data', async (c) => {
// Query D1 database
const result = await c.env.DB
.prepare('SELECT * FROM users LIMIT 10')
.all()
return c.json(result)
})
app.put('/upload/:key', async (c) => {
// Upload to R2
const key = c.req.param('key')
await c.env.MY_BUCKET.put(key, c.req.body)
return c.text(`Uploaded ${key} successfully`)
})Production Bindings
For production, set bindings in the Cloudflare dashboard:
- Go to your Pages project
- Navigate to Settings → Functions
- Add KV namespaces, D1 databases, R2 buckets
- Add environment variables under Settings → Environment variables
Client-Side Scripts
Vite enables client-side JavaScript with hot module replacement.
Create Client Script
Create src/client.ts:
console.log('Hello from client!')
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('button')
button?.addEventListener('click', () => {
alert('Button clicked!')
})
})Include in HTML
Use import.meta.env.PROD to detect environment:
app.get('/', (c) => {
return c.html(
<html>
<head>
{import.meta.env.PROD ? (
// Production: use built file
<script type="module" src="/static/client.js"></script>
) : (
// Development: use source file with HMR
<script type="module" src="/src/client.ts"></script>
)}
</head>
<body>
<h1>Hello</h1>
<button>Click me</button>
</body>
</html>
)
})Configure Client Build
Update vite.config.ts to build client scripts:
import pages from '@hono/vite-cloudflare-pages'
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
if (mode === 'client') {
// Client build configuration
return {
build: {
rollupOptions: {
input: './src/client.ts',
output: {
entryFileNames: 'static/client.js',
},
},
},
}
} else {
// Server build configuration
return {
plugins: [
pages(),
devServer({
entry: 'src/index.tsx',
}),
],
}
}
})Build Both Server and Client
vite build --mode client && vite buildAdd to package.json:
{
"scripts": {
"build": "vite build --mode client && vite build"
}
}Cloudflare Pages Middleware
Cloudflare Pages has its own middleware system separate from Hono.
Using Hono Middleware as Pages Middleware
Create functions/_middleware.ts:
import { handleMiddleware } from 'hono/cloudflare-pages'
export const onRequest = handleMiddleware(async (c, next) => {
console.log(`Accessing: ${c.req.url}`)
await next()
})Apply Built-in Middleware
Use Hono’s built-in middleware:
import { handleMiddleware } from 'hono/cloudflare-pages'
import { basicAuth } from 'hono/basic-auth'
export const onRequest = handleMiddleware(
basicAuth({
username: 'admin',
password: 'secret',
})
)Multiple Middleware
Apply multiple middleware:
import { handleMiddleware } from 'hono/cloudflare-pages'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
export const onRequest = [
handleMiddleware(logger()),
handleMiddleware(cors()),
handleMiddleware(async (c, next) => {
// Custom middleware
await next()
}),
]Access EventContext
Access Cloudflare Pages EventContext:
// functions/_middleware.ts
import { handleMiddleware } from 'hono/cloudflare-pages'
export const onRequest = handleMiddleware(async (c, next) => {
// Store data in EventContext
c.env.eventContext.data.user = 'Alice'
await next()
})Access in your handler:
// functions/api/[[route]].ts
import { Hono } from 'hono'
import { handle } from 'hono/cloudflare-pages'
import type { EventContext } from 'hono/cloudflare-pages'
type Env = {
Bindings: {
eventContext: EventContext
}
}
const app = new Hono<Env>().basePath('/api')
app.get('/hello', (c) => {
const user = c.env.eventContext.data.user
return c.json({ message: `Hello, ${user}!` })
})
export const onRequest = handle(app)Static Assets
Files in the public/ directory are served automatically:
public/
├── favicon.ico # /favicon.ico
├── robots.txt # /robots.txt
└── static/
├── style.css # /static/style.css
└── logo.png # /static/logo.pngBest Practices
Use Environment Variables for Secrets
Never commit secrets to your repository:
// ❌ Don't
const apiKey = 'secret-key-123'
// ✅ Do
const apiKey = c.env.API_KEYOptimize Assets
Vite automatically optimizes assets during build:
- Minifies JavaScript and CSS
- Optimizes images
- Generates cache-friendly filenames
Use Streaming for Large Responses
import { streamText } from 'hono/streaming'
app.get('/stream', (c) => {
return streamText(c, async (stream) => {
for (let i = 0; i < 10; i++) {
await stream.writeln(`Line ${i}`)
await stream.sleep(100)
}
})
})What’s Next
- Cloudflare Workers - API-only deployment
- Vite Plugins - Hono + Vite integration
- D1 Documentation - Cloudflare’s SQL database