Skip to content

Commit 464e804

Browse files
committed
feat: add full page caching
Cache most pages for 1 hour on the Netlify CDN with stale-while-revalidate (allowing 1 min for background revalidation to complete). Cache these also on browsers (and anything else upstream) but require a conditional request to revalidate. On the search page, vary on the search input. Disable this caching on data loaders that have personalization: account/*, cart.
1 parent 8a5fb47 commit 464e804

File tree

8 files changed

+77
-11
lines changed

8 files changed

+77
-11
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Remix resource routes result in the same URL serving both an HTML page response and a JSON data
2+
// response, so we need to check for `?_data=` to avoid conflating them in the cache.
3+
// See https://remix.run/docs/en/main/guides/resource-routes.
4+
const CACHE_REMIX_RESOURCE_ROUTES = {
5+
'Netlify-Vary': 'query=_data',
6+
};
7+
8+
export const NO_CACHE = {
9+
...CACHE_REMIX_RESOURCE_ROUTES,
10+
'Cache-Control': 'private, no-store, no-cache, must-revalidate',
11+
};
12+
13+
export const CACHE_1_DAY = {
14+
...CACHE_REMIX_RESOURCE_ROUTES,
15+
'Cache-Control': 'public, max-age=0',
16+
};
17+
18+
export const CACHE_1_HOUR_SWR = {
19+
...CACHE_REMIX_RESOURCE_ROUTES,
20+
'Cache-Control': 'public, max-age=0, must-revalidate',
21+
'Netlify-CDN-Cache-Control':
22+
'public, max-age=3600, stale-while-revalidate=60',
23+
};

examples/frameworks/hydrogen-caching/app/root.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import {useNonce, getShopAnalytics, Analytics} from '@shopify/hydrogen';
2-
import {defer, type LoaderFunctionArgs} from '@netlify/remix-runtime';
2+
import {
3+
defer,
4+
type HeadersFunction,
5+
type LoaderFunctionArgs,
6+
} from '@netlify/remix-runtime';
37
import {
48
Links,
59
Meta,
@@ -17,6 +21,11 @@ import appStyles from '~/styles/app.css?url';
1721
import {PageLayout} from '~/components/PageLayout';
1822
import {CartProvider, useCart} from '~/components/CartProvider';
1923
import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
24+
import {CACHE_1_HOUR_SWR} from '~/lib/page-cache';
25+
26+
export const headers: HeadersFunction = () => ({
27+
...CACHE_1_HOUR_SWR,
28+
});
2029

2130
export type RootLoader = typeof loader;
2231

examples/frameworks/hydrogen-caching/app/routes/[robots.txt].tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {type LoaderFunctionArgs} from '@netlify/remix-runtime';
22
import {useRouteError, isRouteErrorResponse} from '@remix-run/react';
33
import {parseGid} from '@shopify/hydrogen';
4+
import {CACHE_1_DAY} from '~/lib/page-cache';
45

56
export async function loader({request, context}: LoaderFunctionArgs) {
67
const url = new URL(request.url);
@@ -13,9 +14,8 @@ export async function loader({request, context}: LoaderFunctionArgs) {
1314
return new Response(body, {
1415
status: 200,
1516
headers: {
17+
...CACHE_1_DAY,
1618
'Content-Type': 'text/plain',
17-
18-
'Cache-Control': `max-age=${60 * 60 * 24}`,
1919
},
2020
});
2121
}

examples/frameworks/hydrogen-caching/app/routes/[sitemap.xml].tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {flattenConnection} from '@shopify/hydrogen';
22
import type {LoaderFunctionArgs} from '@netlify/remix-runtime';
33
import type {SitemapQuery} from 'storefrontapi.generated';
4+
import {CACHE_1_DAY} from '~/lib/page-cache';
45

56
/**
67
* the google limit is 50K, however, the storefront API
@@ -38,9 +39,8 @@ export async function loader({
3839

3940
return new Response(sitemap, {
4041
headers: {
42+
...CACHE_1_DAY,
4143
'Content-Type': 'application/xml',
42-
43-
'Cache-Control': `max-age=${60 * 60 * 24}`,
4444
},
4545
});
4646
}

examples/frameworks/hydrogen-caching/app/routes/account.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import {json, type LoaderFunctionArgs} from '@netlify/remix-runtime';
1+
import {
2+
json,
3+
type HeadersFunction,
4+
type LoaderFunctionArgs,
5+
} from '@netlify/remix-runtime';
26
import {Form, NavLink, Outlet, useLoaderData} from '@remix-run/react';
37
import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery';
8+
import {NO_CACHE} from '~/lib/page-cache';
49

510
export function shouldRevalidate() {
611
return true;
712
}
813

14+
export const headers: HeadersFunction = () => ({
15+
...NO_CACHE,
16+
});
17+
918
export async function loader({context}: LoaderFunctionArgs) {
1019
const {data, errors} = await context.customerAccount.query(
1120
CUSTOMER_DETAILS_QUERY,
@@ -19,7 +28,7 @@ export async function loader({context}: LoaderFunctionArgs) {
1928
{customer: data.customer},
2029
{
2130
headers: {
22-
'Cache-Control': 'no-cache, no-store, must-revalidate',
31+
...NO_CACHE,
2332
},
2433
},
2534
);

examples/frameworks/hydrogen-caching/app/routes/cart.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type LoaderFunctionArgs,
88
} from '@netlify/remix-runtime';
99
import {CartMain} from '~/components/CartMain';
10+
import {NO_CACHE} from '~/lib/page-cache';
1011

1112
export const meta: MetaFunction = () => {
1213
return [{title: `Hydrogen | Cart`}];
@@ -16,13 +17,15 @@ export async function loader({context}: LoaderFunctionArgs) {
1617
const {cart} = context;
1718
const existingCart = await cart.get();
1819
if (existingCart) {
19-
return json(existingCart);
20+
return json(existingCart, {headers: {...NO_CACHE}});
2021
}
2122

2223
const {cart: createdCart} = await cart.create({});
2324
// The Cart ID might change after each mutation, so update it each time.
2425
const headers = cart.setCartId(createdCart.id);
25-
return json(createdCart, {headers});
26+
return json(createdCart, {
27+
headers: {...Object.fromEntries(headers), ...NO_CACHE},
28+
});
2629
}
2730

2831
export async function action({request, context}: ActionFunctionArgs) {

examples/frameworks/hydrogen-caching/app/routes/products.$handle.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {Suspense} from 'react';
2-
import {defer, redirect, type LoaderFunctionArgs} from '@netlify/remix-runtime';
2+
import {
3+
defer,
4+
redirect,
5+
type HeadersFunction,
6+
type LoaderFunctionArgs,
7+
} from '@netlify/remix-runtime';
38
import {Await, useLoaderData, type MetaFunction} from '@remix-run/react';
49
import type {ProductFragment} from 'storefrontapi.generated';
510
import {
@@ -17,6 +22,11 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
1722
return [{title: `Hydrogen | ${data?.product.title ?? ''}`}];
1823
};
1924

25+
export const headers: HeadersFunction = ({parentHeaders, loaderHeaders}) => ({
26+
...Object.fromEntries(parentHeaders),
27+
...Object.fromEntries(loaderHeaders),
28+
});
29+
2030
export async function loader(args: LoaderFunctionArgs) {
2131
// Start fetching non-critical data without blocking time to first byte
2232
const deferredData = loadDeferredData(args);

examples/frameworks/hydrogen-caching/app/routes/search.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
json,
33
type LoaderFunctionArgs,
44
type ActionFunctionArgs,
5+
type HeadersFunction,
56
} from '@netlify/remix-runtime';
67
import {useLoaderData, type MetaFunction} from '@remix-run/react';
78
import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
89
import {SearchForm} from '~/components/SearchForm';
910
import {SearchResults} from '~/components/SearchResults';
11+
import {CACHE_1_HOUR_SWR} from '~/lib/page-cache';
1012
import {
1113
type RegularSearchReturn,
1214
type PredictiveSearchReturn,
@@ -17,6 +19,11 @@ export const meta: MetaFunction = () => {
1719
return [{title: `Hydrogen | Search`}];
1820
};
1921

22+
export const headers: HeadersFunction = ({parentHeaders, loaderHeaders}) => ({
23+
...Object.fromEntries(parentHeaders),
24+
...Object.fromEntries(loaderHeaders),
25+
});
26+
2027
export async function loader({request, context}: LoaderFunctionArgs) {
2128
const url = new URL(request.url);
2229
const isPredictive = url.searchParams.has('predictive');
@@ -29,7 +36,12 @@ export async function loader({request, context}: LoaderFunctionArgs) {
2936
return {term: '', result: null, error: error.message};
3037
});
3138

32-
return json(await searchPromise);
39+
return json(await searchPromise, {
40+
headers: {
41+
...CACHE_1_HOUR_SWR,
42+
'Netlify-Vary': 'query=q|limit|predictive',
43+
},
44+
});
3345
}
3446

3547
/**

0 commit comments

Comments
 (0)