Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import code, {
BrandingOptions,
SeoOptions,
AnalyticsOptions,
CachingOptions,
CustomHtmlOptions,
Custom404Options,
SubdomainRedirect,
Expand Down Expand Up @@ -162,6 +163,12 @@ export default function App() {
const [analytics, setAnalytics] = useState<AnalyticsOptions>({
googleTagId: "",
});
const [caching, setCaching] = useState<CachingOptions>({
enabled: false,
htmlTtl: 60,
staticAssetsTtl: 86400,
imageTtl: 604800,
});
const [customHtml, setCustomHtml] = useState<CustomHtmlOptions>({
headerHtml: "",
});
Expand Down Expand Up @@ -288,6 +295,17 @@ export default function App() {
setCopied(false);
}

function handleCachingChange(
field: keyof CachingOptions,
value: boolean | number,
): void {
setCaching({
...caching,
[field]: value,
});
setCopied(false);
}

function handleCustomHtmlChange(
field: keyof CustomHtmlOptions,
value: string,
Expand Down Expand Up @@ -400,6 +418,7 @@ export default function App() {
branding,
seo,
analytics,
caching,
customHtml,
custom404,
subdomainRedirects,
Expand Down Expand Up @@ -930,6 +949,78 @@ export default function App() {
/>
</Box>

<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: "grey.300" }}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Box>
<Typography variant="subtitle2" color="text.secondary">
Cache-Control Headers
</Typography>
<Typography variant="caption" color="text.secondary">
Improve performance with browser caching
</Typography>
</Box>
<Switch
checked={caching.enabled}
onChange={(e) =>
handleCachingChange("enabled", e.target.checked)
}
/>
</Stack>
<Collapse in={caching.enabled} timeout="auto" unmountOnExit>
<Box sx={{ mt: 2 }}>
<TextField
fullWidth
type="number"
label="HTML Page TTL (seconds)"
margin="dense"
placeholder="60"
helperText="Cache duration for HTML pages (default: 60s)"
onChange={(e) =>
handleCachingChange("htmlTtl", Number(e.target.value))
}
value={caching.htmlTtl}
variant="outlined"
size="small"
/>
<TextField
fullWidth
type="number"
label="Static Assets TTL (seconds)"
margin="dense"
placeholder="86400"
helperText="Cache duration for JS, CSS, fonts (default: 1 day)"
onChange={(e) =>
handleCachingChange(
"staticAssetsTtl",
Number(e.target.value),
)
}
value={caching.staticAssetsTtl}
variant="outlined"
size="small"
/>
<TextField
fullWidth
type="number"
label="Image TTL (seconds)"
margin="dense"
placeholder="604800"
helperText="Cache duration for images (default: 1 week)"
onChange={(e) =>
handleCachingChange("imageTtl", Number(e.target.value))
}
value={caching.imageTtl}
variant="outlined"
size="small"
/>
</Box>
</Collapse>
</Box>

<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: "grey.300" }}>
<Typography
variant="subtitle2"
Expand Down
57 changes: 50 additions & 7 deletions src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export interface AnalyticsOptions {
googleTagId?: string;
}

export interface CachingOptions {
enabled: boolean;
htmlTtl?: number;
staticAssetsTtl?: number;
imageTtl?: number;
}

export interface CustomHtmlOptions {
headerHtml?: string;
}
Expand Down Expand Up @@ -66,6 +73,7 @@ export interface CodeData {
branding: BrandingOptions;
seo: SeoOptions;
analytics: AnalyticsOptions;
caching: CachingOptions;
customHtml: CustomHtmlOptions;
custom404: Custom404Options;
subdomainRedirects: SubdomainRedirect[];
Expand Down Expand Up @@ -97,6 +105,7 @@ export default function code(data: CodeData): string {
branding,
seo,
analytics,
caching,
customHtml,
custom404,
subdomainRedirects,
Expand Down Expand Up @@ -164,6 +173,15 @@ ${slugs
*/
const GOOGLE_TAG_ID = '${analytics?.googleTagId || ""}';

/*
* Step 3.5.1: caching configuration (optional)
* Add Cache-Control headers for better performance
*/
const CACHING_ENABLED = ${caching?.enabled || false};
const HTML_TTL = ${caching?.htmlTtl || 60};
const STATIC_ASSETS_TTL = ${caching?.staticAssetsTtl || 86400};
const IMAGE_TTL = ${caching?.imageTtl || 604800};

/*
* Step 3.6: custom HTML header injection (optional)
* Add custom HTML to the top of the page body (e.g., navigation, announcements)
Expand Down Expand Up @@ -248,6 +266,29 @@ ${
return options;
}

// Apply Cache-Control headers based on content type (Issue #33)
function applyCacheHeaders(response, url, contentType) {
if (!CACHING_ENABLED) return response;

const newResponse = new Response(response.body, response);
const pathname = url.pathname;

// Static assets (JS, CSS, fonts)
if (pathname.match(/\\.(js|css|woff2?|ttf|eot)$/)) {
newResponse.headers.set('Cache-Control', \`public, max-age=\${STATIC_ASSETS_TTL}, immutable\`);
}
// Images
else if (pathname.startsWith('/image') || pathname.match(/\\.(jpg|jpeg|png|gif|webp|avif|svg|ico)$/)) {
newResponse.headers.set('Cache-Control', \`public, max-age=\${IMAGE_TTL}\`);
}
// HTML pages
else if (!contentType || contentType.includes('text/html')) {
newResponse.headers.set('Cache-Control', \`public, max-age=\${HTML_TTL}, stale-while-revalidate=60\`);
}

return newResponse;
}

function generateSitemap() {
let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
slugs.forEach(
Expand Down Expand Up @@ -340,15 +381,15 @@ ${
body = rewriteDomainInBody(body);
response = new Response(body, response);
response.headers.set('Content-Type', 'application/x-javascript');
return response;
return applyCacheHeaders(response, url, 'application/javascript');
} else if (url.pathname.startsWith('/_assets/') && url.pathname.endsWith('.js')) {
// Handle Notion's new asset paths
response = await fetch(url.toString());
let body = await response.text();
body = rewriteDomainInBody(body);
response = new Response(body, response);
response.headers.set('Content-Type', 'application/javascript');
return response;
return applyCacheHeaders(response, url, 'application/javascript');
} else if (url.pathname.startsWith('/api/v3/getPublicPageData')) {
// Proxy getPublicPageData and rewrite domain info
response = await fetch(url.toString(), {
Expand Down Expand Up @@ -415,7 +456,7 @@ ${
return response;
} else if (url.pathname.startsWith('/image') && IMAGE_OPTIMIZATION !== 'none') {
const response = await fetch(url, rewriteImageOptions());
return response;
return applyCacheHeaders(response, url, 'image');
} else if (slugs.indexOf(url.pathname.slice(1)) > -1) {
const pageId = SLUG_TO_PAGE[url.pathname.slice(1)];
return Response.redirect('https://' + MY_DOMAIN + '/' + pageId, 302);
Expand Down Expand Up @@ -446,14 +487,14 @@ ${
});
response.headers.delete('Content-Security-Policy');
response.headers.delete('X-Content-Security-Policy');
return appendJavascript(response, SLUG_TO_PAGE, '404');
return appendJavascript(response, SLUG_TO_PAGE, '404', url);
}

// Get current slug from page ID for canonical URL
const pageId = url.pathname.slice(-32);
const currentSlug = PAGE_TO_SLUG[pageId] || '';

return appendJavascript(response, SLUG_TO_PAGE, currentSlug);
return appendJavascript(response, SLUG_TO_PAGE, currentSlug, url);
}

class MetaRewriter {
Expand Down Expand Up @@ -744,16 +785,18 @@ ${
}
}

async function appendJavascript(res, SLUG_TO_PAGE, slug) {
async function appendJavascript(res, SLUG_TO_PAGE, slug, url) {
const metaRewriter = new MetaRewriter(slug);
const headRewriter = new HeadRewriter(slug);
const linkRewriter = new LinkRewriter();
return new HTMLRewriter()
const contentType = res.headers.get('content-type');
let transformed = new HTMLRewriter()
.on('title', metaRewriter)
.on('meta', metaRewriter)
.on('link', linkRewriter)
.on('head', headRewriter)
.on('body', new BodyRewriter(SLUG_TO_PAGE))
.transform(res);
return applyCacheHeaders(transformed, url, contentType);
}`;
}
Loading