Skip to content

Commit e283160

Browse files
committed
Adds a page for serving tailored content
1 parent 07a1a74 commit e283160

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
title: Serving tailored content with Cloudflare
3+
pcx_content_type: concept
4+
sidebar:
5+
order: 3
6+
label: Serving tailored content
7+
---
8+
9+
import { DashButton } from "~/components";
10+
11+
Content negotiation is the practice of serving different versions of a resource from a single URL, tailoring the experience to the end user. Common examples include delivering content in a specific language (`Accept-Language`), optimizing for a device (`User-Agent`), or serving modern image formats (`Accept`).
12+
13+
Cloudflare's global network is designed to handle this at scale. For common scenarios such as serving next-generation images, this negotiation is streamlined with a dedicated feature. For more customized logic, Cloudflare provides a toolkit including Transform Rules, Snippets, Custom Cache Keys, and Workers, giving you granular control to ensure the right content is served to every user, every time.
14+
15+
---
16+
17+
## Use query strings
18+
19+
The [Transform Rule](/rules/transform/) method is ideal when creating a distinct URL is acceptable, such as serving content based on a visitor's location.
20+
21+
### Geolocation example
22+
23+
In this example, you run an e-commerce site and want to display prices in the local currency based on the visitor's country.
24+
25+
1. In the Cloudflare dashboard, go to the Rules **Overview** page.
26+
27+
<DashButton url="/?to=/:account/:zone/rules/overview" />
28+
29+
2. Select **Create rule** and select the option **URL Rewrite Rule**.
30+
3. Enter a descriptive name, such as `Vary by Country - Canada`.
31+
4. In **If incoming requests match...**, select **Custom filter expression**.
32+
5. Under **When incoming requests match...**, create the following expression:
33+
- **Field:** `Country`
34+
- **Operator:** `equals`
35+
- **Value:** `Canada`
36+
6. Under **Then...**
37+
- for **Path**, select **Preserve**.
38+
- for **Query**, select **Rewrite to**: **Dynamic** `loc=ca`
39+
7. Select **Save**.
40+
41+
Now, requests from Canada to `/products/item` will be transformed to `/products/item?loc=ca` before reaching your origin or the cache, creating a distinct cache entry.
42+
43+
:::note[Availability]
44+
Free, Pro, Business, and Enterprise plans
45+
:::
46+
47+
---
48+
49+
## Vary for Images
50+
51+
[Vary for Images](/cache/advanced-configuration/vary-for-images/) tells Cloudflare which variants your origin supports. Cloudflare then caches each version separately and serves the correct one to browsers without contacting your origin each time. This feature is managed via the Cloudflare API.
52+
53+
### Enable Vary for Images
54+
55+
To enable this feature, create a _variants rule_ using the API. This rule maps file extensions to the image formats your origin can serve.
56+
57+
For example, the following API call tells Cloudflare that for `.jpeg` and `.jpg` files, your origin can serve `image/webp` and `image/avif` variants:
58+
59+
```bash
60+
# Replace $ZONE_ID and $CLOUDFLARE_API_TOKEN with your actual values
61+
curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/variants" \
62+
--request PATCH \
63+
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
64+
--header "Content-Type: application/json" \
65+
--json '{
66+
"value": {
67+
"jpeg": [
68+
"image/webp",
69+
"image/avif"
70+
],
71+
"jpg": [
72+
"image/webp",
73+
"image/avif"
74+
]
75+
}
76+
}'
77+
```
78+
79+
After creating the rule, Cloudflare will create distinct cache entries for each image variant, improving performance for users with modern browsers.
80+
81+
:::note[Availability]
82+
Pro, Business, and Enterprise plans
83+
:::
84+
85+
## Use Snippets for programmatic caching
86+
87+
[Snippets](/rules/snippets/) are self-contained JavaScript fetch handlers that run at the edge on your requests through Cloudflare. They allow you to programmatically interact with the cache, providing full control over the cache key and response behavior without changing the user-facing URL.
88+
89+
### Example: A/B testing
90+
91+
In this example, you run an A/B test controlled by a cookie named `ab-test` (with values `group-a` or `group-b`). You want to cache a different version of the page for each group.
92+
93+
1. In the Cloudflare dashboard, go to the **Snippets** page.
94+
95+
<DashButton url="/?to=/:account/:zone/rules/snippets" />
96+
97+
2. Select **Create new Snippet** and name it `ab-test-caching`.
98+
3. Paste the following code. It modifies the cache key based on the `ab-test` cookie and caches the response for 30 days.
99+
100+
```JavaScript
101+
const CACHE_DURATION = 30 * 24 * 60 * 60; // 30 days
102+
103+
export default {
104+
async fetch(request) {
105+
// Construct a new URL for the cache key based on the A/B cookie
106+
const abCookie = request.headers.get('Cookie')?.match(/ab-test=([^;]+)/)?.[1] || 'control';
107+
const url = new URL(request.url);
108+
url.pathname = `/ab-test/${abCookie}${url.pathname}`;
109+
110+
const cacheKey = new Request(url, request);
111+
const cache = caches.default;
112+
113+
let response = await cache.match(cacheKey);
114+
if (!response) {
115+
// If not in cache, fetch from origin
116+
response = await fetch(request);
117+
response = new Response(response.body, response);
118+
response.headers.set("Cache-Control", `s-maxage=${CACHE_DURATION}`);
119+
// Put the response into cache with the custom key
120+
await cache.put(cacheKey, response.clone());
121+
}
122+
return response;
123+
},
124+
};
125+
```
126+
127+
4. Save and deploy the Snippet.
128+
5. From the Snippets dashboard, select **Attach to routes** to assign the Snippet.
129+
130+
:::note[Availability]
131+
Pro, Business, and Enterprise plans
132+
:::
133+
134+
## Custom Cache Keys (Enterprise)
135+
136+
If your account is on an Enterprise plan, the [Custom Cache Keys](/cache/how-to/cache-keys) feature provides a no-code interface to define which request properties are included in the cache key.
137+
138+
Custom Cache Key options:
139+
140+
- Cache by device type
141+
- Query string option `No query string parameters except`
142+
- Include headers and values
143+
- Include cookie names and values
144+
- User: Device type, Country, Language
145+
146+
### Example: Same URL, different content
147+
148+
If your origin serves different content types (for example, `application/json` vs. `text/html`) at the same URL based on the `Accept` header, use a custom cache key to cache them separately.
149+
150+
1. In the Cloudflare dashboard, go to the **Cache Rules** page.
151+
152+
<DashButton url="/?to=/:account/:zone/caching/cache-rules" />
153+
154+
2. Select **Create rule**.
155+
3. Enter rule name, such as `Vary by Accept Header`.
156+
4. Set the condition for the rule to apply (for example, a specific hostname or path).
157+
5. Under **Cache key**, select **Use custom key**.
158+
6. Select **Add new**.
159+
- **Type**: `Header`
160+
- **Name**: `Accept`
161+
- **Value**: Add each `value`, or leave empty for all.
162+
7. Select **Deploy**.
163+
164+
This configuration creates separate cache entries based on the `Accept` header value, respecting your API's content negotiation.
165+
166+
:::note[Availability]
167+
Enterprise plans only
168+
:::
169+
170+
## Use Cloudflare Workers for advanced logic
171+
172+
For complex caching scenarios, [Cloudflare Workers](/cache/interaction-cloudflare-products/workers/) provide a full serverless environment ideal for custom logic at scale.
173+
174+
### Example: Device type – Free/Pro/Biz (without Tiered Cache)
175+
176+
This Worker detects whether a visitor is on a mobile or desktop device and creates separate cache entries for each, ensuring the correct version of the site is served and cached.
177+
178+
```JavaScript
179+
export default {
180+
async fetch(request, env, ctx) {
181+
const userAgent = request.headers.get('User-Agent') || '';
182+
const deviceType = userAgent.includes('Mobile') ? 'mobile' : 'desktop';
183+
184+
// Create a new URL for the cache key that includes the device type
185+
const url = new URL(request.url);
186+
url.pathname = `/${deviceType}${url.pathname}`;
187+
188+
const cacheKey = new Request(url, request);
189+
const cache = caches.default;
190+
191+
let response = await cache.match(cacheKey);
192+
193+
if (!response) {
194+
console.log(`Cache miss for ${deviceType} device. Fetching from origin.`);
195+
response = await fetch(request);
196+
let responseToCache = response.clone();
197+
ctx.waitUntil(cache.put(cacheKey, responseToCache));
198+
}
199+
200+
return response;
201+
},
202+
};
203+
```
204+
205+
:::note[Availability]
206+
Free and Paid plans
207+
:::
208+
209+
### Example: Device type – Enterprise (with Tiered Cache)
210+
211+
This Worker detects if a visitor is on a mobile device or a desktop and creates a separate cache entry for each, ensuring the correct version of the site is served and cached. Uses the Enterprise `cf.customCacheKey` feature.
212+
213+
```JavaScript
214+
export default {
215+
async fetch(request) {
216+
// 1. Determine the device type from the User-Agent header
217+
const userAgent = request.headers.get('User-Agent') || '';
218+
const deviceType = userAgent.includes('Mobile') ? 'mobile' : 'desktop';
219+
220+
// 2. Create a custom cache key by appending the device type to the URL
221+
const customCacheKey = `${request.url}-${deviceType}`;
222+
223+
// 3. Fetch the response. Cloudflare's cache automatically uses the
224+
// customCacheKey for cache operations (match, put).
225+
const response = await fetch(request, {
226+
cf: {
227+
cacheKey: customCacheKey,
228+
},
229+
});
230+
231+
// Optionally, you can modify the response before returning it
232+
// For example, add a header to indicate which cache key was used
233+
const newResponse = new Response(response.body, response);
234+
newResponse.headers.set("X-Cache-Key", customCacheKey);
235+
return newResponse;
236+
},
237+
};
238+
```
239+
240+
:::note[Availability]
241+
Enterprise only
242+
:::
243+
244+
## Example: Caching Next.js RSC payloads
245+
246+
A common challenge is caching content from frameworks like Next.js, which uses an `RSC` (React Server Components) request header to differentiate between HTML page loads and RSC data payloads for the same URL. Here are the best ways to handle this.
247+
248+
### Method 1: Transform Rules
249+
250+
The simplest solution is to create a [Transform Rule](/rules/transform/) that checks for the `RSC` header and adds a unique query parameter on the request, creating two distinct cacheable URLs: `/page` (for HTML) and `/page?_rsc=1` (for the RSC payload).
251+
252+
1. In the Cloudflare dashboard, go to the Rules **Overview** page.
253+
254+
<DashButton url="/?to=/:account/:zone/rules/overview" />
255+
256+
2. Select **Create rule** and select the option **URL Rewrite Rule**.
257+
3. Enter a name, such as `Vary by RSC Header`.
258+
4. In **If incoming requests match...**, select **Custom filter expression**.
259+
5. Under **When incoming requests match...**, set this expression to check for the presence of `RSC` header:
260+
- `(http.request.headers["rsc"] is not null)`
261+
262+
6. Under `Then...`
263+
- for **Path**, select **Preserve**
264+
- for **Query**, select **Rewrite to**, select **Static**: `_rsc=1`
265+
266+
7. Select **Save**.
267+
268+
### Method 2: Snippets or Custom Cache Keys
269+
270+
Alternatively, use [Snippets](/rules/snippets/) or [Custom Cache Keys](/cache/how-to/cache-keys) to add the `RSC` header directly to the cache key without modifying the visible URL. This provides a cleaner URL but requires more advanced configuration.
271+
272+
:::note[Availability]
273+
- Snippets: Pro, Business, Enterprise
274+
- Custom Cache Keys: Enterprise only
275+
:::
276+
277+

0 commit comments

Comments
 (0)