Caching Strategy
The template uses a multi-layer caching architecture combining Next.js server-side caching with React Query client-side caching. This dual approach ensures fast page loads, efficient data fetching, and minimal unnecessary network requests.
Architecture Overview
lib/cache-config.ts # Cache TTL values and tag definitions
lib/cache-invalidation.ts # Server-side cache invalidation functions
lib/react-query-config.ts # React Query client configuration
The two caching layers serve different purposes:
| Layer | Technology | Scope | Purpose |
|---|---|---|---|
| Server | Next.js unstable_cache / revalidateTag | SSR, API routes | Cache filesystem reads and database queries |
| Client | React Query (@tanstack/react-query) | Browser | Cache API responses and manage stale data |
Server-Side Cache Configuration
Cache TTL
All TTL values are defined in lib/cache-config.ts as seconds:
export const CACHE_TTL = {
CONTENT: 600, // 10 minutes
ITEM: 600, // 10 minutes
CONFIG: 600, // 10 minutes
PAGES: 600, // 10 minutes
} as const;
The uniform 10-minute TTL reduces filesystem reads while ensuring content updates propagate within a reasonable window.
Cache Tags
Cache tags enable targeted invalidation. The CACHE_TAGS object provides both static tags and dynamic tag factories:
export const CACHE_TAGS = {
CONTENT: 'content',
ITEMS: 'items',
ITEM: (slug: string) => `item:${slug}`,
CATEGORIES: 'categories',
TAGS: 'tags',
COLLECTIONS: 'collections',
CONFIG: 'config',
PAGES: 'pages',
PAGE: (slug: string) => `page:${slug}`,
ITEMS_LOCALE: (locale: string) => `items:${locale}`,
CATEGORIES_LOCALE: (locale: string) => `categories:${locale}`,
TAGS_LOCALE: (locale: string) => `tags:${locale}`,
COLLECTIONS_LOCALE: (locale: string) => `collections:${locale}`,
} as const;
Tag Hierarchy
Tags follow a hierarchical pattern for efficient invalidation:
| Tag | Scope | Invalidates |
|---|---|---|
content | Master | All content-related caches |
items | Collection | All items across all locales |
items:en | Locale-specific | English items only |
item:my-tool | Individual | One specific item |
categories | Collection | All categories |
categories:fr | Locale-specific | French categories only |
config | Global | Site configuration |
pages | Collection | All static pages |
page:about | Individual | One specific page |
Using Cache Tags in Data Fetching
import { unstable_cache } from 'next/cache';
import { CACHE_TTL, CACHE_TAGS } from '@/lib/cache-config';
const getCachedItems = unstable_cache(
async (locale: string) => {
return await fetchItems(locale);
},
['items'],
{
revalidate: CACHE_TTL.CONTENT,
tags: [CACHE_TAGS.ITEMS, CACHE_TAGS.ITEMS_LOCALE(locale)],
}
);
Cache Invalidation
Invalidation Functions
The lib/cache-invalidation.ts module provides three invalidation functions:
// Invalidate ALL content caches (after repository sync)
await invalidateContentCaches();
// Invalidate a specific item
await invalidateItemCache('my-tool-slug');
// Invalidate a specific page
await invalidatePageCache('about');
Full Content Invalidation
The invalidateContentCaches function clears all content-related caches and the in-memory fetch cache:
export async function invalidateContentCaches(): Promise<void> {
safeRevalidateTag(CACHE_TAGS.CONTENT);
safeRevalidateTag(CACHE_TAGS.ITEMS);
safeRevalidateTag(CACHE_TAGS.CATEGORIES);
safeRevalidateTag(CACHE_TAGS.TAGS);
safeRevalidateTag(CACHE_TAGS.COLLECTIONS);
safeRevalidateTag(CACHE_TAGS.PAGES);
await clearFetchItemsCache();
}
This is typically called after a Git repository sync completes, ensuring fresh content is served.
Safe Revalidation
The safeRevalidateTag wrapper handles a critical edge case: calling revalidateTag during a React render phase throws an error in Next.js. The wrapper catches this specific error and logs a warning instead:
function safeRevalidateTag(tag: string): void {
try {
revalidateTag(tag, 'max');
} catch (error) {
if (error instanceof Error && isRenderPhaseError(error)) {
console.warn(
`[CACHE] Skipping cache invalidation during render phase (tag: ${tag})`
);
} else {
throw error;
}
}
}
The render phase detection checks multiple patterns to be resilient against Next.js error message changes:
function isRenderPhaseError(error: Error): boolean {
const message = error.message.toLowerCase();
return (
message.includes('during render') ||
message.includes('render phase') ||
(message.includes('revalidate') && message.includes('render')) ||
(message.includes('unsupported') && message.includes('render'))
);
}