Skip to content

Commit 3a29a4d

Browse files
committed
Add lists, tables, and highlighter to cdn-edge-caching-cache-keys-vary-headers articles
1 parent 59af863 commit 3a29a4d

9 files changed

Lines changed: 203 additions & 128 deletions

File tree

‎src/components/Code/CodeTabs/client/selectors.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,3 @@ export function queryCodeTabsIconMarkup(params: {
7171
const markup = iconHostCandidate.innerHTML.trim()
7272
return markup ? markup : null
7373
}
74-
75-
export function hasHighlighterElement(scope: ParentNode = document): boolean {
76-
return scope.querySelector('highlighter-element') !== null
77-
}
78-
79-
export function hasMastodonModalElement(scope: ParentNode = document): boolean {
80-
return scope.querySelector('mastodon-modal-element') !== null
81-
}

‎src/components/Code/CodeTabs/index.astro‎

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,6 @@ const iconBankId = 'code-tabs-icon-bank'
1616

1717
<script>
1818
import { registerCodeTabsWebComponent } from './client'
19-
import { hasHighlighterElement, hasMastodonModalElement } from './client/selectors'
20-
registerCodeTabsWebComponent()
21-
22-
const registerOptionalMarkdownWebComponents = async () => {
23-
if (hasHighlighterElement()) {
24-
try {
25-
const { registerHighlighterWebComponent } = await import(
26-
'@components/Social/Highlighter/client'
27-
)
28-
registerHighlighterWebComponent()
29-
} catch {
30-
// Best effort only.
31-
}
32-
}
3319

34-
if (hasMastodonModalElement()) {
35-
try {
36-
const { registerMastodonModalWebComponent } = await import(
37-
'@components/Social/Mastodon/client'
38-
)
39-
registerMastodonModalWebComponent()
40-
} catch {
41-
// Best effort only.
42-
}
43-
}
44-
}
45-
46-
registerOptionalMarkdownWebComponents()
47-
document.addEventListener('astro:page-load', () => {
48-
registerOptionalMarkdownWebComponents()
49-
})
20+
registerCodeTabsWebComponent()
5021
</script>

‎src/components/Inset/layouts/DefaultInset.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type Props = {
99
const { classes, color }: Props = Astro.props
1010
1111
const backgroundClass = color ? `bg-${color}` : 'bg-content-inverse-active'
12-
const layoutClass = `font-mono ${backgroundClass} border-2 border-trim rounded-md px-6 pt-6`
12+
const layoutClass = `font-mono whitespace-pre-wrap ${backgroundClass} border-2 border-trim rounded-md px-6 pt-6`
1313
---
1414

1515
<div class:list={[layoutClass, classes?.layout]}>

‎src/components/List/__tests__/index.spec.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,36 @@ describe('List (Astro)', () => {
100100
expect(leads).toHaveLength(2)
101101
expect(badges[0]?.textContent?.trim()).toBe('4')
102102
expect(badges[1]?.textContent?.trim()).toBe('5')
103-
expect(badges[0]?.className).toContain('bg-info')
104-
expect(leads[0]?.className).toContain('text-info')
103+
expect(badges[0]?.getAttribute('style')).toContain('background-color: var(--color-info);')
104+
expect(leads[0]?.getAttribute('style')).toContain('color: var(--color-info);')
105+
})
106+
})
107+
108+
test('uses the default marker color for numbered-with-background-list when no color is provided', async () => {
109+
const List = (await import('@components/List/index.astro')).default
110+
111+
const renderedHtml = await container.renderToString(List, {
112+
props: {
113+
variant: 'numbered-with-background-list',
114+
items: [
115+
{
116+
lead: 'Default step',
117+
text: 'Uses the default marker color.',
118+
},
119+
],
120+
},
121+
})
122+
123+
await withJsdomEnvironment(async ({ window }) => {
124+
window.document.body.innerHTML = renderedHtml
125+
126+
const badge = window.document.querySelector('li > span:first-child')
127+
const lead = window.document.querySelector('li em')
128+
129+
expect(badge?.className).toContain('bg-primary-offset')
130+
expect(lead?.className).toContain('text-primary-offset')
131+
expect(badge?.getAttribute('style')).toBeNull()
132+
expect(lead?.getAttribute('style')).toBeNull()
105133
})
106134
})
107135
})

‎src/components/List/layouts/NumberedWithBackgroundList.astro‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,31 @@ export type Props = {
1414
}
1515
1616
const { items, classes, color, startNumber = 1 }: Props = Astro.props
17-
const resolvedColor = typeof color === 'string' && color.trim().length > 0 ? color.trim() : 'primary-offset'
18-
const pillColorClass = `bg-${resolvedColor}`
17+
const resolvedColor = typeof color === 'string' && color.trim().length > 0 ? color.trim() : undefined
18+
const safeColorToken = resolvedColor && /^[-a-z0-9]+$/i.test(resolvedColor) ? resolvedColor : undefined
19+
const pillStyle = safeColorToken ? `background-color: var(--color-${safeColorToken});` : undefined
20+
const leadStyle = safeColorToken ? `color: var(--color-${safeColorToken});` : undefined
1921
2022
const olClass = ["space-y-3", classes?.ol]
2123
const liClass = ["flex items-start gap-4 bg-page-offset/50 rounded-lg p-4", classes?.li]
22-
const emClass = [`text-${resolvedColor}`, "font-semibold not-italic", classes?.em]
24+
const emClass = ["font-semibold not-italic", !safeColorToken && 'text-primary-offset', classes?.em]
2325
---
2426

2527
<ol class:list={olClass}>
2628
{
2729
items.map((item, index) => (
2830
<li class:list={liClass}>
29-
<span class:list={["mt-2 shrink-0 w-8 h-8 text-content-inverse rounded-full flex items-center justify-center text-sm font-bold", pillColorClass]}>{startNumber + index}</span>
31+
<span
32+
class:list={[
33+
"mt-2 shrink-0 w-8 h-8 text-content-inverse rounded-full flex items-center justify-center text-sm font-bold",
34+
!safeColorToken && 'bg-primary-offset',
35+
]}
36+
style={pillStyle}
37+
>
38+
{startNumber + index}
39+
</span>
3040
<div class="pt-1">
31-
{item.lead && <em class:list={emClass} set:html={item.lead}></em>}
41+
{item.lead && <em class:list={emClass} style={leadStyle} set:html={item.lead}></em>}
3242
<div set:html={item.text}></div>
3343
</div>
3444
</li>

‎src/components/Social/Highlighter/index.astro‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
* ```
1919
*/
2020
import Icon from '@components/Icon/index.astro'
21-
import './index.css'
2221
2322
export interface Props {
2423
/**
@@ -49,13 +48,3 @@ const { ariaLabel, class: className } = Astro.props
4948
<slot />
5049
</span>
5150
</highlighter-element>
52-
53-
<script>
54-
import { registerHighlighterWebComponent } from '@components/Social/Highlighter/client'
55-
56-
registerHighlighterWebComponent()
57-
58-
document.addEventListener('astro:page-load', () => {
59-
registerHighlighterWebComponent()
60-
})
61-
</script>

‎src/content/articles/cdn-edge-caching-cache-keys-vary-headers/index.mdx‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ The cache hit ratio looked fantastic. 94% of requests served from edge. Response
1818

1919
This is the fundamental tension in edge caching. Aggressive caching dramatically improves performance and reduces origin load, but incorrect configuration serves wrong content to users — sometimes catastrophically. Edge caching is a __data integrity__ problem first, performance problem second.
2020

21-
The good news: two concepts cause the vast majority of CDN cache bugs, and once you understand them, you can cache aggressively without fear. Those concepts are cache keys and Vary headers.
21+
The good news: <Highlighter>two concepts cause the vast majority of CDN cache bugs, and once you understand them, you can cache aggressively without fear.</Highlighter> Those concepts are cache keys and Vary headers.
2222

2323
## Cache Keys Determine Correctness
2424

2525
A cache key is the identifier the CDN uses to store and retrieve cached responses. When a request arrives, the edge generates a cache key from the request attributes and looks for a matching entry. If found, it returns the cached response. If not, it fetches from origin and stores the response under that key.
2626

2727
By default, most CDNs use a simplified version of the URL:
2828

29-
```text
29+
<Inset color="warning-inverse">
3030
Default cache key:
3131
SCHEME + HOST + PATH + QUERY_STRING
3232

@@ -35,7 +35,7 @@ Example request:
3535

3636
Cache key:
3737
"https://example.com/api/products?category=shoes"
38-
```
38+
</Inset>
3939

4040
This seems straightforward, but the details matter enormously. Two requests that __should__ return identical content but generate different cache keys create duplicate cache entries — wasting storage and reducing hit ratios. Two requests that __should__ return different content but generate the same cache key serve wrong content to users — exactly the bug we opened with.
4141

@@ -45,15 +45,15 @@ The rule is simple: every attribute that affects your response must be in the ca
4545

4646
Query parameters are the most common source of cache key problems. Consider these two URLs:
4747

48-
```text
48+
<Inset color="danger-inverse">
4949
Problem: Query parameter order
5050

5151
These are the same request but different cache keys:
52-
/products?color=red&size=large
53-
/products?size=large&color=red
52+
&nbsp;&nbsp;&nbsp;/products?color=red&size=large
53+
&nbsp;&nbsp;&nbsp;/products?size=large&color=red
5454

5555
Result: Two cache entries for identical content
56-
```
56+
</Inset>
5757

5858
Different parameter order, different cache keys, same content — you're now storing two copies and reducing your hit ratio. Marketing teams compound this by adding tracking parameters to URLs. Every `utm_source`, `fbclid`, and `gclid` creates a unique cache key for content that's identical regardless of how the user arrived.
5959

@@ -89,6 +89,8 @@ Query parameter pollution is one of the most common causes of poor cache hit rat
8989

9090
The `Vary` header tells caches "this response differs based on these request headers." It's the origin's way of saying "I returned different content to different clients based on header X, so you need to store separate cached versions."
9191

92+
<Newsletter />
93+
9294
When a CDN sees `Vary: Accept-Language`, it doesn't just store one cached response per URL — it stores one per URL __per unique Accept-Language value__. The cache key effectively becomes URL + the values of all headers listed in Vary.
9395

9496
```http title="vary-example.http"
@@ -105,7 +107,8 @@ With two headers in Vary, you get a combinatorial explosion of cache variants. E
105107
The guideline: only vary on headers with a small, known set of values.
106108

107109
<Table
108-
variant="vertical-column-delineation-table"
110+
variant="grid-and-accent-header-table"
111+
fullWidth={false}
109112
content={{
110113
figure: "Vary header guidance for common headers.",
111114
thead: { th: ["Vary On", "Good Idea?", "Reason"] },
@@ -138,8 +141,6 @@ The guideline: only vary on headers with a small, known set of values.
138141

139142
`Accept-Encoding` is safe because there are only a few compression algorithms. `Accept-Language` is risky because browsers send values like `en-US, en;q=0.9, de;q=0.8`—technically unique per user's language preferences. If you vary on it, normalize the header first to extract just the primary language.
140143

141-
<Newsletter />
142-
143144
### The Cookie Vary Trap
144145

145146
The most common Vary mistake is `Vary: Cookie`. It seems logical — if your response depends on cookies, tell the cache to vary on them. But cookies include session IDs, authentication tokens, and tracking identifiers that are unique per user. Even when the actual content-affecting cookie (`preferences=dark`) is the same, the unique session IDs create separate cache entries.
@@ -188,14 +189,31 @@ Understanding cache keys and Vary headers lets you avoid the two most dangerous
188189

189190
This is the bug from our opening story — serving one user's content to another. It happens when:
190191

191-
- Your origin returns `Cache-Control: public` for personalized responses
192-
- You vary on session cookie but session IDs still contaminate the cache key
193-
- You cache without checking for authentication headers
192+
<List
193+
variant="colored-marker-list"
194+
classes={{
195+
wrapper: "sm:ml-12 sm:mr-12 mb-6",
196+
}}
197+
items={[
198+
{
199+
color: "bg-danger-offset",
200+
text: 'Your origin returns <span class="font-mono">Cache-Control: public</span> for personalized responses',
201+
},
202+
{
203+
color: "bg-danger-offset",
204+
text: "You vary on session cookie but session IDs still contaminate the cache key",
205+
},
206+
{
207+
color: "bg-danger-offset",
208+
text: "You cache without checking for authentication headers",
209+
},
210+
]}
211+
/>
194212

195213
The patterns that keep you safe:
196214

197215
<Table
198-
variant="vertical-column-delineation-table"
216+
variant="criteria-highlight-table"
199217
content={{
200218
figure: "Personalized content caching patterns.",
201219
thead: { th: ["Pattern", "Risk Level", "Why"] },
@@ -261,6 +279,10 @@ function handler(event: any): any {
261279

262280
With these headers normalized or stripped, your origin never sees the attack vectors that could poison your cache.
263281

282+
## Getting It Right
283+
284+
Both of these bugs — and most CDN issues you'll encounter — stem from the same root cause: a mismatch between what's in the cache key and what actually affects the response.
285+
264286
<Download
265287
resource="cdn-edge-caching-cache-keys-vary-headers"
266288
title="Download the Edge Caching Guide"
@@ -281,10 +303,6 @@ With these headers normalized or stripped, your origin never sees the attack vec
281303
]}
282304
/>
283305

284-
## Getting It Right
285-
286-
Both of these bugs — and most CDN issues you'll encounter — stem from the same root cause: a mismatch between what's in the cache key and what actually affects the response.
287-
288306
A properly-configured cache with 70% hit ratio beats a misconfigured cache with 95% hit ratio. The latter is serving wrong content to 95% of users — or worse, leaking private data between users.
289307

290308
Start with serving correct content: audit your cache keys, normalize your Vary headers, and verify that personalized content can never be cached. Once you've established that foundation, you can optimize for performance with confidence.

0 commit comments

Comments
 (0)