Skip to content

Commit 9397bde

Browse files
authored
Merge pull request #41 from classmethod/feature/cache-control
Add Cache-Control header optimization
2 parents 34fe76f + 89bfb4d commit 9397bde

2 files changed

Lines changed: 141 additions & 7 deletions

File tree

src/App.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import code, {
4444
BrandingOptions,
4545
SeoOptions,
4646
AnalyticsOptions,
47+
CachingOptions,
4748
CustomHtmlOptions,
4849
Custom404Options,
4950
SubdomainRedirect,
@@ -164,6 +165,12 @@ export default function App() {
164165
googleTagId: "",
165166
facebookPixelId: "",
166167
});
168+
const [caching, setCaching] = useState<CachingOptions>({
169+
enabled: false,
170+
htmlTtl: 60,
171+
staticAssetsTtl: 86400,
172+
imageTtl: 604800,
173+
});
167174
const [customHtml, setCustomHtml] = useState<CustomHtmlOptions>({
168175
headerHtml: "",
169176
});
@@ -291,6 +298,17 @@ export default function App() {
291298
setCopied(false);
292299
}
293300

301+
function handleCachingChange(
302+
field: keyof CachingOptions,
303+
value: boolean | number,
304+
): void {
305+
setCaching({
306+
...caching,
307+
[field]: value,
308+
});
309+
setCopied(false);
310+
}
311+
294312
function handleCustomHtmlChange(
295313
field: keyof CustomHtmlOptions,
296314
value: string,
@@ -426,6 +444,7 @@ export default function App() {
426444
branding,
427445
seo,
428446
analytics,
447+
caching,
429448
customHtml,
430449
custom404,
431450
subdomainRedirects,
@@ -970,6 +989,78 @@ export default function App() {
970989
/>
971990
</Box>
972991

992+
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: "grey.300" }}>
993+
<Stack
994+
direction="row"
995+
alignItems="center"
996+
justifyContent="space-between"
997+
>
998+
<Box>
999+
<Typography variant="subtitle2" color="text.secondary">
1000+
Cache-Control Headers
1001+
</Typography>
1002+
<Typography variant="caption" color="text.secondary">
1003+
Improve performance with browser caching
1004+
</Typography>
1005+
</Box>
1006+
<Switch
1007+
checked={caching.enabled}
1008+
onChange={(e) =>
1009+
handleCachingChange("enabled", e.target.checked)
1010+
}
1011+
/>
1012+
</Stack>
1013+
<Collapse in={caching.enabled} timeout="auto" unmountOnExit>
1014+
<Box sx={{ mt: 2 }}>
1015+
<TextField
1016+
fullWidth
1017+
type="number"
1018+
label="HTML Page TTL (seconds)"
1019+
margin="dense"
1020+
placeholder="60"
1021+
helperText="Cache duration for HTML pages (default: 60s)"
1022+
onChange={(e) =>
1023+
handleCachingChange("htmlTtl", Number(e.target.value))
1024+
}
1025+
value={caching.htmlTtl}
1026+
variant="outlined"
1027+
size="small"
1028+
/>
1029+
<TextField
1030+
fullWidth
1031+
type="number"
1032+
label="Static Assets TTL (seconds)"
1033+
margin="dense"
1034+
placeholder="86400"
1035+
helperText="Cache duration for JS, CSS, fonts (default: 1 day)"
1036+
onChange={(e) =>
1037+
handleCachingChange(
1038+
"staticAssetsTtl",
1039+
Number(e.target.value),
1040+
)
1041+
}
1042+
value={caching.staticAssetsTtl}
1043+
variant="outlined"
1044+
size="small"
1045+
/>
1046+
<TextField
1047+
fullWidth
1048+
type="number"
1049+
label="Image TTL (seconds)"
1050+
margin="dense"
1051+
placeholder="604800"
1052+
helperText="Cache duration for images (default: 1 week)"
1053+
onChange={(e) =>
1054+
handleCachingChange("imageTtl", Number(e.target.value))
1055+
}
1056+
value={caching.imageTtl}
1057+
variant="outlined"
1058+
size="small"
1059+
/>
1060+
</Box>
1061+
</Collapse>
1062+
</Box>
1063+
9731064
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: "grey.300" }}>
9741065
<Typography
9751066
variant="subtitle2"

src/code.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export interface AnalyticsOptions {
3939
facebookPixelId?: string;
4040
}
4141

42+
export interface CachingOptions {
43+
enabled: boolean;
44+
htmlTtl?: number;
45+
staticAssetsTtl?: number;
46+
imageTtl?: number;
47+
}
48+
4249
export interface CustomHtmlOptions {
4350
headerHtml?: string;
4451
}
@@ -73,6 +80,7 @@ export interface CodeData {
7380
branding: BrandingOptions;
7481
seo: SeoOptions;
7582
analytics: AnalyticsOptions;
83+
caching: CachingOptions;
7684
customHtml: CustomHtmlOptions;
7785
custom404: Custom404Options;
7886
subdomainRedirects: SubdomainRedirect[];
@@ -105,6 +113,7 @@ export default function code(data: CodeData): string {
105113
branding,
106114
seo,
107115
analytics,
116+
caching,
108117
customHtml,
109118
custom404,
110119
subdomainRedirects,
@@ -174,6 +183,15 @@ ${slugs
174183
const GOOGLE_TAG_ID = '${analytics?.googleTagId || ""}';
175184
const FACEBOOK_PIXEL_ID = '${analytics?.facebookPixelId || ""}';
176185
186+
/*
187+
* Step 3.5.1: caching configuration (optional)
188+
* Add Cache-Control headers for better performance
189+
*/
190+
const CACHING_ENABLED = ${caching?.enabled || false};
191+
const HTML_TTL = ${caching?.htmlTtl || 60};
192+
const STATIC_ASSETS_TTL = ${caching?.staticAssetsTtl || 86400};
193+
const IMAGE_TTL = ${caching?.imageTtl || 604800};
194+
177195
/*
178196
* Step 3.6: custom HTML header injection (optional)
179197
* Add custom HTML to the top of the page body (e.g., navigation, announcements)
@@ -273,6 +291,29 @@ ${
273291
return options;
274292
}
275293
294+
// Apply Cache-Control headers based on content type (Issue #33)
295+
function applyCacheHeaders(response, url, contentType) {
296+
if (!CACHING_ENABLED) return response;
297+
298+
const newResponse = new Response(response.body, response);
299+
const pathname = url.pathname;
300+
301+
// Static assets (JS, CSS, fonts)
302+
if (pathname.match(/\\.(js|css|woff2?|ttf|eot)$/)) {
303+
newResponse.headers.set('Cache-Control', \`public, max-age=\${STATIC_ASSETS_TTL}, immutable\`);
304+
}
305+
// Images
306+
else if (pathname.startsWith('/image') || pathname.match(/\\.(jpg|jpeg|png|gif|webp|avif|svg|ico)$/)) {
307+
newResponse.headers.set('Cache-Control', \`public, max-age=\${IMAGE_TTL}\`);
308+
}
309+
// HTML pages
310+
else if (!contentType || contentType.includes('text/html')) {
311+
newResponse.headers.set('Cache-Control', \`public, max-age=\${HTML_TTL}, stale-while-revalidate=60\`);
312+
}
313+
314+
return newResponse;
315+
}
316+
276317
function generateSitemap() {
277318
let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
278319
slugs.forEach(
@@ -375,15 +416,15 @@ ${
375416
body = rewriteDomainInBody(body);
376417
response = new Response(body, response);
377418
response.headers.set('Content-Type', 'application/x-javascript');
378-
return response;
419+
return applyCacheHeaders(response, url, 'application/javascript');
379420
} else if (url.pathname.startsWith('/_assets/') && url.pathname.endsWith('.js')) {
380421
// Handle Notion's new asset paths
381422
response = await fetch(url.toString());
382423
let body = await response.text();
383424
body = rewriteDomainInBody(body);
384425
response = new Response(body, response);
385426
response.headers.set('Content-Type', 'application/javascript');
386-
return response;
427+
return applyCacheHeaders(response, url, 'application/javascript');
387428
} else if (url.pathname.startsWith('/api/v3/getPublicPageData')) {
388429
// Proxy getPublicPageData and rewrite domain info
389430
response = await fetch(url.toString(), {
@@ -450,7 +491,7 @@ ${
450491
return response;
451492
} else if (url.pathname.startsWith('/image') && IMAGE_OPTIMIZATION !== 'none') {
452493
const response = await fetch(url, rewriteImageOptions());
453-
return response;
494+
return applyCacheHeaders(response, url, 'image');
454495
} else if (slugs.indexOf(url.pathname.slice(1)) > -1) {
455496
const pageId = SLUG_TO_PAGE[url.pathname.slice(1)];
456497
return Response.redirect('https://' + MY_DOMAIN + '/' + pageId, 302);
@@ -481,14 +522,14 @@ ${
481522
});
482523
response.headers.delete('Content-Security-Policy');
483524
response.headers.delete('X-Content-Security-Policy');
484-
return appendJavascript(response, SLUG_TO_PAGE, '404');
525+
return appendJavascript(response, SLUG_TO_PAGE, '404', url);
485526
}
486527
487528
// Get current slug from page ID for canonical URL
488529
const pageId = url.pathname.slice(-32);
489530
const currentSlug = PAGE_TO_SLUG[pageId] || '';
490531
491-
return appendJavascript(response, SLUG_TO_PAGE, currentSlug);
532+
return appendJavascript(response, SLUG_TO_PAGE, currentSlug, url);
492533
}
493534
494535
class MetaRewriter {
@@ -798,16 +839,18 @@ ${
798839
}
799840
}
800841
801-
async function appendJavascript(res, SLUG_TO_PAGE, slug) {
842+
async function appendJavascript(res, SLUG_TO_PAGE, slug, url) {
802843
const metaRewriter = new MetaRewriter(slug);
803844
const headRewriter = new HeadRewriter(slug);
804845
const linkRewriter = new LinkRewriter();
805-
return new HTMLRewriter()
846+
const contentType = res.headers.get('content-type');
847+
let transformed = new HTMLRewriter()
806848
.on('title', metaRewriter)
807849
.on('meta', metaRewriter)
808850
.on('link', linkRewriter)
809851
.on('head', headRewriter)
810852
.on('body', new BodyRewriter(SLUG_TO_PAGE))
811853
.transform(res);
854+
return applyCacheHeaders(transformed, url, contentType);
812855
}`;
813856
}

0 commit comments

Comments
 (0)