Cloudflare provides powerful URL manipulation capabilities through Workers, Pages, and Transform Rules. Understanding these features helps you build performant, globally distributed applications.
Key Takeaways
- 1Workers run at edge locations, closest to users
- 2Pages deployments get unique preview URLs
- 3Transform Rules modify URLs without Workers code
- 4URL rewriting happens at the edge before origin
- 5Cache rules can vary by URL parameters
“A serverless platform for building, deploying, and scaling apps across Cloudflare's global network with a single command — no infrastructure to manage, no complex configuration”
Workers URL Routing
Workers run JavaScript at Cloudflare's edge locations, processing requests before they reach your origin server. The URL object in JavaScript provides everything you need to implement routing logic based on paths, query parameters, or any other URL component.
// workers/index.js - URL-based routing
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Route based on path
switch (url.pathname) {
case '/':
return new Response('Home page');
case '/api/users':
return handleUsers(request);
case '/api/health':
return new Response('OK', { status: 200 });
default:
// Check for dynamic routes
if (url.pathname.startsWith('/api/users/')) {
const userId = url.pathname.split('/')[3];
return handleUser(userId);
}
return new Response('Not Found', { status: 404 });
}
}
};
// Query parameter handling
function handleUsers(request) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page')) || 1;
const limit = parseInt(url.searchParams.get('limit')) || 20;
return new Response(JSON.stringify({
page,
limit,
users: []
}), {
headers: { 'Content-Type': 'application/json' }
});
}This Worker demonstrates path-based routing with both static and dynamic routes. The switch handles exact paths while the if statement catches dynamic segments like user IDs. Query parameters are easily extracted using the URL's searchParams interface for pagination and filtering.
Cloudflare Pages gives you automatic preview deployments for every commit and branch. Each deployment gets a unique URL, making it easy to test changes before merging to production.
Cloudflare Pages URLs
| URL Pattern | Description | Example |
|---|---|---|
| projectname.pages.dev | Production | mysite.pages.dev |
| hash.projectname.pages.dev | Unique deployment preview (random hash) | 373f31e2.mysite.pages.dev |
| branch.projectname.pages.dev | Branch alias (latest commit on branch) | feature.mysite.pages.dev |
| custom.domain.com | Custom domain | www.example.com |
The table shows how Pages generates URLs for different deployment contexts. Branch previews are particularly useful for PR reviews since each branch gets its own stable URL. Custom domains connect your Pages site to your own domain with automatic SSL.
// _redirects file for Pages
/old-path /new-path 301
/blog/* /posts/:splat 301
# Proxying: relative URLs only, no external domains
/api/* /backend/:splat 200
// _headers file
/api/*
Access-Control-Allow-Origin: *
Cache-Control: no-cache
/*.js
Cache-Control: public, max-age=31536000
// Functions in /functions directory
// /functions/api/hello.js -> /api/hello
export async function onRequest(context) {
const url = new URL(context.request.url);
return new Response(JSON.stringify({
path: url.pathname,
query: Object.fromEntries(url.searchParams)
}), {
headers: { 'Content-Type': 'application/json' }
});
}Pages supports both declarative files (_redirects, _headers) and programmatic Functions. The _redirects file handles common patterns like path mappings and proxying to other paths on the same site (Pages cannot proxy to external domains via _redirects). Functions in the /functions directory automatically create serverless endpoints matching their file paths.
Transform Rules let you modify URLs through Cloudflare's dashboard without writing Worker code. This is ideal for simple redirects, path rewrites, and query string manipulations that do not require complex logic.
URL Transform Rules
# Rules - modify URLs without code
# URL Rewrite Rule (Transform Rules, invisible to client)
# Incoming: /products/shoes
# Origin sees: /api/v2/products/shoes
When incoming requests match: Wildcard pattern
Request URL: https://example.com/products/*
Then rewrite the path (Dynamic):
Target path: /products/*
Rewrite to: /api/v2/products/${1}
# Single Redirect (Redirect Rules, visible 301/302)
# Incoming: /old-blog/article
# Redirects to: /blog/article
When incoming requests match: Wildcard pattern
Request URL: https://example.com/old-blog/*
Then:
Target URL: https://example.com/blog/${1}
Status code: 301
Preserve query string: on
# Query string rewrite (expression editor)
# Append a tracking parameter to an existing query string
When incoming requests match:
http.host eq "shop.example.com" and http.request.uri.query ne ""
Then rewrite the query (Dynamic):
concat(http.request.uri.query, "&source=cloudflare")These rules match requests with wildcard patterns or Cloudflare's Rules language expressions; ${1} inserts the text captured by the first *, and dynamic rewrites can use functions such as concat(). Rewrites are invisible to the client (the URL in their browser does not change), while redirects send a response that changes the browser's URL.
When Transform Rules are not flexible enough, Workers give you full programmatic control over URL manipulation. You can implement complex routing logic, A/B testing, or conditional redirects based on any request property.
URL Rewriting in Workers
export default {
async fetch(request) {
const url = new URL(request.url);
// Remove trailing slashes
if (url.pathname !== '/' && url.pathname.endsWith('/')) {
url.pathname = url.pathname.slice(0, -1);
return Response.redirect(url.toString(), 301);
}
// Lowercase paths
if (url.pathname !== url.pathname.toLowerCase()) {
url.pathname = url.pathname.toLowerCase();
return Response.redirect(url.toString(), 301);
}
// API versioning - /api/users -> /api/v2/users
if (url.pathname.startsWith('/api/')) {
if (!url.pathname.startsWith('/api/v')) {
url.pathname = '/api/v2' + url.pathname.slice(4);
}
} else if (!url.pathname.includes('.') && url.pathname !== '/') {
// Clean URLs - /about -> /about.html
url.pathname = url.pathname + '.html';
}
// Proxy to origin with modified URL
return fetch(url.toString(), request);
}
};This Worker demonstrates common URL normalization patterns. The trailing slash and case redirects run first so every later rewrite sees a normalized path. API versioning injects version prefixes for backward compatibility, and clean URLs add .html extensions for static file serving on non-API paths.
Cloudflare's cache can be configured to vary by URL patterns, ensuring different paths or query parameters get cached separately. Workers give you fine-grained control over cache keys and TTLs based on URL structure.
Cache by URL
// Workers cache API with URL-based keys
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Cache key variations
const cacheKey = new Request(url.toString(), request);
// Check cache
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
// Fetch from origin
response = await fetch(request);
// Clone for caching
response = new Response(response.body, response);
// Set cache headers based on URL
if (url.pathname.startsWith('/static/')) {
response.headers.set('Cache-Control', 'public, max-age=31536000');
} else if (url.pathname.startsWith('/api/')) {
response.headers.set('Cache-Control', 'private, max-age=60');
}
// Store in cache
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
};
// Vary cache by query parameters
function getCacheKey(request) {
const url = new URL(request.url);
// Only include specific params in cache key
const cacheUrl = new URL(url.origin + url.pathname);
['page', 'limit', 'sort'].forEach(param => {
if (url.searchParams.has(param)) {
cacheUrl.searchParams.set(param, url.searchParams.get(param));
}
});
return new Request(cacheUrl.toString());
}The caching code sets different Cache-Control headers based on URL paths. Static assets get long cache times since they rarely change, while API responses get short TTLs for fresher data. The getCacheKey function creates cache keys that only include relevant query parameters, preventing cache fragmentation from irrelevant parameters like tracking codes.
R2 is Cloudflare's object storage service that integrates seamlessly with Workers. You can serve files from R2 through custom URLs, adding authentication, transformations, or access controls at the edge.
R2 Storage URLs
// Serve R2 objects with custom URLs
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Map URL path to R2 key
// /images/photo.jpg -> images/photo.jpg
const key = url.pathname.slice(1);
// Fetch from R2
const object = await env.MY_BUCKET.get(key);
if (!object) {
return new Response('Not Found', { status: 404 });
}
// Return with proper headers
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=86400',
'ETag': object.httpEtag
}
});
}
};
// R2 public bucket URL
// https://pub-xxx.r2.dev/images/photo.jpg
// R2 with custom domain
// https://assets.example.com/images/photo.jpgThe Worker maps URL paths directly to R2 object keys, serving files with appropriate content types and cache headers. R2 public buckets provide direct URLs, but Workers give you more control over access patterns, content negotiation, and URL structures.