Skip to Content
deploymentCloudflare Pages

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-app

Select cloudflare-pages when prompted.

Setup

1. Create Project

::: code-group

npm create hono@latest my-app cd my-app npm install
yarn create hono my-app cd my-app yarn
pnpm create hono@latest my-app cd my-app pnpm install
bun 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 configuration

3. 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 app

The 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 dev
yarn dev
pnpm dev
bun run dev

:::

Access http://localhost:5173 in your browser.

5. Deploy

Deploy with Wrangler:

::: code-group

npm run deploy
yarn deploy
pnpm run deploy
bun run deploy

:::

Deploy from GitHub

Cloudflare Pages integrates directly with GitHub:

  1. Push your code to GitHub
  2. Log in to Cloudflare dashboard 
  3. Go to Workers & Pages → Create application → Pages → Connect to Git
  4. Select your repository
  5. Configure build settings:
SettingValue
Production branchmain
Build commandnpm run build
Build directorydist
  1. 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 --preview

Note 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:

  1. Go to your Pages project
  2. Navigate to Settings → Functions
  3. Add KV namespaces, D1 databases, R2 buckets
  4. 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 build

Add 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.png

Best 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_KEY

Optimize 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