@@ -3,43 +3,43 @@ import type { Response } from 'express'
33import { createLogger } from '@/observability/logger'
44const logger = createLogger ( import . meta. url )
55
6+ type CacheControlKey = 'cache-control' | 'surrogate-control'
7+
68interface CacheControlOptions {
7- key ?: string
8- public_ ?: boolean
9+ key ?: CacheControlKey
910 immutable ?: boolean
10- maxAgeZero ?: boolean
11+ staleWhileRevalidate ?: number
12+ staleIfError ?: number
1113}
1214
13- // Return a function you can pass a Response object to and it will
14- // set the `Cache-Control` header.
15- //
16- // For example:
17- //
18- // const cacheControlYear = getCacheControl(60 * 60 * 24 * 365)
19- // ...
20- // cacheControlYear(res)
21- // res.send(body)
22- //
23- // Max age is in seconds
24- // Max age should not be greater than 31536000 https://www.ietf.org/rfc/rfc2616.txt
15+ const ONE_MINUTE = 60
16+ const TEN_MINUTES = 10 * ONE_MINUTE
17+ const ONE_HOUR = 60 * ONE_MINUTE
18+ const ONE_DAY = 24 * ONE_HOUR
19+ const ONE_WEEK = 7 * ONE_DAY
20+ const ONE_YEAR = 365 * ONE_DAY
21+
22+ // Return a function you can pass a Response object to and it will set the `Cache-Control` header.
23+ // Max age is in seconds.
24+ // Max age should not be greater than 31536000, per <https://www.ietf.org/rfc/rfc2616.txt>.
2525function cacheControlFactory (
26- maxAge : number = 60 * 60 ,
26+ maxAge : number = 0 ,
2727 {
2828 key = 'cache-control' ,
29- public_ = true ,
3029 immutable = false ,
31- maxAgeZero = false ,
30+ staleWhileRevalidate = 0 ,
31+ staleIfError = 0 ,
3232 } : CacheControlOptions = { } ,
3333) : ( res : Response ) => void {
3434 const directives = [
35- maxAge && public_ && 'public' ,
36- maxAge && `max-age=${ maxAge } ` ,
37- maxAge && immutable && 'immutable ' ,
38- ! maxAge && 'private ',
39- ! maxAge && 'no-store ' ,
40- maxAge >= 60 * 60 && `stale-while-revalidate= ${ 60 * 60 } ` ,
41- maxAge >= 60 * 60 && `stale-if-error =${ 24 * 60 * 60 } ` ,
42- ( maxAgeZero || maxAge === 0 ) && 'max-age=0' ,
35+ maxAge > 0 && 'public' ,
36+ maxAge > 0 && `max-age=${ maxAge } ` ,
37+ maxAge <= 0 && 'max-age=0 ' ,
38+ maxAge > 0 && immutable && 'immutable ',
39+ maxAge <= 0 && 'private ' ,
40+ maxAge <= 0 && 'no-store' ,
41+ maxAge > 0 && staleWhileRevalidate > 0 && `stale-while-revalidate =${ staleWhileRevalidate } ` ,
42+ maxAge > 0 && staleIfError > 0 && `stale-if-error= ${ staleIfError } ` ,
4343 ]
4444 . filter ( Boolean )
4545 . join ( ', ' )
@@ -53,64 +53,73 @@ function cacheControlFactory(
5353 }
5454}
5555
56- // These are roughly in order from shortest to longest
56+ // ### These are roughly in order from shortest to longest. ###
5757
58- // If you do not want caching
58+ // If you do not want caching.
5959export const noCacheControl = cacheControlFactory ( 0 )
6060
61- // Short cache for 4xx errors
62- export const errorCacheControl = cacheControlFactory ( 60 ) // 1 minute
63-
64- // This means we tell the browser to cache the XHR request for 1h
65- const searchBrowserCacheControl = cacheControlFactory ( 60 * 60 )
66- // This tells the CDN to cache the response for 24 hours
67- const searchCdnCacheControl = cacheControlFactory ( 60 * 60 * 24 , {
68- key : 'surrogate-control' ,
69- } )
70- export function searchCacheControl ( res : Response ) : void {
71- searchBrowserCacheControl ( res )
72- searchCdnCacheControl ( res )
73- }
61+ // Short cache for 4xx errors.
62+ export const errorCacheControl = cacheControlFactory ( ONE_MINUTE )
7463
75- // 24 hours for CDN, we soft-purge this with each deploy
76- const defaultCDNCacheControl = cacheControlFactory ( 60 * 60 * 24 , {
64+ // For default cache control, up to one week in cache but much shorter in browser.
65+ // Most responses are under the default cache control policy.
66+ const browserCacheControl = cacheControlFactory ( ONE_MINUTE )
67+ const defaultCDNCacheControl = cacheControlFactory ( TEN_MINUTES , {
7768 key : 'surrogate-control' ,
69+ staleWhileRevalidate : ONE_WEEK ,
70+ staleIfError : ONE_WEEK ,
7871} )
79- // Shorter because between deployments and their (sort) purges,
80- // we don't want the browser to overly cache because with them we
81- // can't control purging.
82- const defaultBrowserCacheControl = cacheControlFactory ( 60 )
83- // A general default configuration that is useful to almost all responses
84- // that can be cached.
8572export function defaultCacheControl ( res : Response ) : void {
73+ browserCacheControl ( res )
8674 defaultCDNCacheControl ( res )
87- defaultBrowserCacheControl ( res )
8875}
76+ export const searchCacheControl = defaultCacheControl
8977
90- // Vary on content type for pages that support content negotiation (HTML vs markdown)
78+ // For requests where the response can vary between a HTML and Markdown response
79+ // using the accept header.
9180export function contentTypeCacheControl ( res : Response ) : void {
9281 defaultCacheControl ( res )
9382 res . append ( 'vary' , 'accept' )
9483}
9584
96- // Vary on language when needed
97- // x-user-language is a custom request header derived from req.cookie:user_language
98- // accept-language is truncated to one of our available languages
85+ // Vary on language when needed.
86+ // ` x-user-language` is a custom request header derived from ` req.cookie:user_language`.
87+ // ` accept-language` is truncated to one of our available languages.
9988// https://bit.ly/3u5UeRN
10089export function languageCacheControl ( res : Response ) : void {
10190 defaultCacheControl ( res )
10291 res . append ( 'vary' , 'accept-language, x-user-language' )
10392}
10493
105- // Vary on both language and version for homepage redirects
106- // x-user-version is a custom request header derived from req.cookie:user_version
94+ // Vary on both language and version for homepage redirects.
95+ // ` x-user-version` is a custom request header derived from ` req.cookie:user_version`.
10796export function languageAndVersionCacheControl ( res : Response ) : void {
10897 defaultCacheControl ( res )
10998 res . append ( 'vary' , 'accept-language, x-user-language, x-user-version' )
11099}
111100
112- // Long cache control for versioned assets: images, CSS, JS...
113- export const assetCacheControl = cacheControlFactory ( 60 * 60 * 24 * 7 , { immutable : true } )
101+ // Long cache control for versioned assets: such as images, CSS, prebuilt JS.
102+ const assetBrowserCacheControl = cacheControlFactory ( TEN_MINUTES )
103+ const assetCDNCacheControl = cacheControlFactory ( ONE_WEEK , {
104+ key : 'surrogate-control' ,
105+ immutable : true ,
106+ staleWhileRevalidate : ONE_WEEK ,
107+ staleIfError : ONE_WEEK ,
108+ } )
109+ export function assetCacheControl ( res : Response ) : void {
110+ assetBrowserCacheControl ( res )
111+ assetCDNCacheControl ( res )
112+ }
114113
115- // Long caching for archived pages and assets
116- export const archivedCacheControl = cacheControlFactory ( 60 * 60 * 24 * 365 )
114+ // Long caching for archived pages and assets.
115+ const archivedBrowserCacheControl = cacheControlFactory ( TEN_MINUTES )
116+ const archivedCDNCacheControl = cacheControlFactory ( ONE_YEAR , {
117+ key : 'surrogate-control' ,
118+ immutable : true ,
119+ staleWhileRevalidate : ONE_WEEK ,
120+ staleIfError : ONE_WEEK ,
121+ } )
122+ export function archivedCacheControl ( res : Response ) : void {
123+ archivedBrowserCacheControl ( res )
124+ archivedCDNCacheControl ( res )
125+ }
0 commit comments