diff --git a/examples/redis-minimal/README.md b/examples/redis-minimal/README.md index 82c0dc9..5665113 100644 --- a/examples/redis-minimal/README.md +++ b/examples/redis-minimal/README.md @@ -1,33 +1,130 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Next.js Cache Handler Examples + +This example application demonstrates various Next.js caching functionalities using the Redis cache handler. It provides a comprehensive UI to explore and test different caching strategies. ## Getting Started -First, run the development server: +First, install dependencies: ```bash npm i -npm run build # important to run once to create cacheHandler dist file -npm run dev ``` -Or run build and run production server: +### Important: Development vs Production Mode + +**Next.js does not use the cache handler in development mode.** This is a Next.js limitation - caching is intentionally disabled in dev mode for faster hot reloading and to ensure developers always see fresh data. + +To test caching functionality, you must use **production mode**: ```bash -npm i npm run build npm run start ``` +For development (without caching): + +```bash +npm run dev +``` + Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -Modify `.env` file if you need to. +## Configuration + +Modify the `.env` file if you need to configure Redis connection settings. The default Redis URL is used if not specified. ## Examples -- http://localhost:3000 -- http://localhost:3000/fetch-example -- http://localhost:3000/static-params-test/cache +The application includes several examples demonstrating different Next.js caching features: + +### 1. Home Page (`/`) + +Overview page listing all available examples with descriptions and features. + +### 2. Fetch with Tags (`/examples/fetch-tags`) + +Demonstrates fetch caching with tags and time-based revalidation. + +**Features:** + +- Time-based revalidation (24 hours) +- Cache tags for selective invalidation +- Clear cache button to test tag revalidation +- Shows character data from Futurama API +- Displays cache information and rendered timestamp + +**Try it:** + +- Visit `/examples/fetch-tags` to see cached data +- Click "Clear Cache" to invalidate the cache +- Reload the page to see fresh data + +### 3. ISR with Static Params (`/examples/isr/blog/[id]`) + +Incremental Static Regeneration with `generateStaticParams`. + +**Features:** + +- Static generation at build time +- On-demand regeneration +- Time-based revalidation (1 hour) +- Multiple blog post routes + +**Try it:** + +- Visit `/examples/isr/blog/1` for the first post +- Try different IDs like `/examples/isr/blog/2`, `/examples/isr/blog/3` +- Check the rendered timestamp to see caching in action + +### 5. Static Params Test (`/examples/static-params/[testName]`) + +Tests static params generation with dynamic routes. + +**Features:** + +- Static params generation +- Dynamic params support +- Short revalidation period (5 seconds) for testing +- Shows generation type (static vs dynamic) + +**Try it:** + +- Visit `/examples/static-params/cache` (pre-generated) +- Try `/examples/static-params/test1` or `/examples/static-params/test2` (on-demand) + +## API Routes + +### Clear Cache (`/api/cache`) + +Clears cache for a specific tag. + +**Usage:** + +- `GET /api/cache?tag=futurama` - Clears cache for the "futurama" tag +- Default tag is "futurama" if not specified + +**Example:** + +```bash +curl http://localhost:3000/api/cache?tag=futurama +``` + +## Cache Handler + +This example uses a custom Redis cache handler configured in `cache-handler.mjs`. The handler supports: + +- Redis string-based caching +- Local LRU fallback +- Composite caching strategy +- Tag-based cache invalidation + +**Note:** The cache handler only works in production mode. In development mode, Next.js bypasses the cache handler entirely. You'll see a warning message in the console: `"Next.js does not use the cache in development mode. Use production mode to enable caching."` -## Clear fetch example cache +## Technologies -- http://localhost:3000/api/cache +- Next.js 16 +- React 19 +- TypeScript +- Tailwind CSS +- Redis +- @fortedigital/nextjs-cache-handler diff --git a/examples/redis-minimal/src/app/api/cache/route.ts b/examples/redis-minimal/src/app/api/cache/route.ts index 7283a53..68f3bdb 100644 --- a/examples/redis-minimal/src/app/api/cache/route.ts +++ b/examples/redis-minimal/src/app/api/cache/route.ts @@ -1,6 +1,18 @@ import { revalidateTag } from "next/cache"; +import { NextRequest } from "next/server"; -export async function GET() { - revalidateTag("futurama", "max"); - return new Response("Cache cleared for futurama tag"); +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const tag = searchParams.get("tag") || "futurama"; + + try { + revalidateTag(tag, "max"); + return new Response(`Cache cleared for tag: ${tag}`, { + status: 200, + }); + } catch (error) { + return new Response(`Error clearing cache for tag: ${tag}`, { + status: 500, + }); + } } diff --git a/examples/redis-minimal/src/app/examples/fetch-tags/page.tsx b/examples/redis-minimal/src/app/examples/fetch-tags/page.tsx new file mode 100644 index 0000000..4d351a7 --- /dev/null +++ b/examples/redis-minimal/src/app/examples/fetch-tags/page.tsx @@ -0,0 +1,168 @@ +import { ExampleLayout } from "@/components/ExampleLayout"; +import { ClearCacheButton } from "@/components/ClearCacheButton"; +import { InfoCard } from "@/components/InfoCard"; +import { CodeBlock } from "@/components/CodeBlock"; +import { FuturamaCharacter } from "@/types/futurama"; + +export default async function FetchTagsExample() { + let name: string; + let character: FuturamaCharacter; + const timestamp = new Date().toISOString(); + + try { + const characterResponse = await fetch( + "https://api.sampleapis.com/futurama/characters/1", + { + next: { + revalidate: 86400, + tags: ["futurama"], + }, + } + ); + character = await characterResponse.json(); + name = character.name.first; + } catch (error) { + console.error("Error fetching character data:", error); + return ( + +
+ An error occurred during fetch. Please check your network connection. +
+
+ ); + } + + return ( + } + > +
+ +
    +
  • + Data is fetched with a 24-hour revalidation period ( + + revalidate: 86400 + + ) +
  • +
  • + Cache is tagged with{" "} + + "futurama" + {" "} + for selective invalidation +
  • +
  • + Click "Clear Cache" to invalidate the cache and see + fresh data on reload +
  • +
  • + The timestamp shows when this page was rendered (cached pages will + show the same timestamp) +
  • +
+
+ +
+
+

+ Character Data +

+
+
+ + Name: + {" "} + {name} +
+ {character.name.middle && ( +
+ + Middle Name: + {" "} + + {character.name.middle} + +
+ )} +
+ + Last Name: + {" "} + + {character.name.last} + +
+ {character.occupation && ( +
+ + Occupation: + {" "} + + {character.occupation} + +
+ )} +
+
+ +
+

+ Cache Information +

+
+
+ + Rendered at: + {" "} + + {timestamp} + +
+
+ + Cache Tag: + {" "} + + futurama + +
+
+ + Revalidation: + {" "} + + 24 hours + +
+
+
+
+ +
+

+ Code Example +

+ + {`const response = await fetch( + "https://api.sampleapis.com/futurama/characters/1", + { + next: { + revalidate: 86400, + tags: ["futurama"], + }, + } +);`} + +
+
+
+ ); +} + diff --git a/examples/redis-minimal/src/app/examples/isr/blog/[id]/page.tsx b/examples/redis-minimal/src/app/examples/isr/blog/[id]/page.tsx new file mode 100644 index 0000000..86c6c7b --- /dev/null +++ b/examples/redis-minimal/src/app/examples/isr/blog/[id]/page.tsx @@ -0,0 +1,165 @@ +import { ExampleLayout } from "@/components/ExampleLayout"; +import { InfoCard } from "@/components/InfoCard"; +import { CodeBlock } from "@/components/CodeBlock"; + +interface Post { + id: string; + title: string; + content: string; +} + +export const revalidate = 3600; + +export async function generateStaticParams() { + try { + const posts: Post[] = await fetch("https://api.vercel.app/blog").then( + (res) => res.json() + ); + return posts.map((post) => ({ + id: String(post.id), + })); + } catch (error) { + console.error("Error generating static params:", error); + return []; + } +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const timestamp = new Date().toISOString(); + + let post: Post; + try { + post = await fetch(`https://api.vercel.app/blog/${id}`).then((res) => + res.json() + ); + } catch (error) { + return ( + +
+ An error occurred fetching the post. Please check your network + connection. +
+
+ ); + } + + return ( + +
+ +
    +
  • + + generateStaticParams + {" "} + generates static paths at build time +
  • +
  • + Pages are statically generated and cached for 1 hour ( + + revalidate: 3600 + + ) +
  • +
  • + After the revalidation period, pages are regenerated on the next + request +
  • +
  • + Try visiting different blog IDs (1, 2, 3, etc.) to see different + posts +
  • +
+
+ +
+
+

+ Blog Post +

+
+
+ + Post ID: + {" "} + {id} +
+

+ {post.title} +

+

+ {post.content} +

+
+
+ +
+

+ Cache Information +

+
+
+ + Rendered at: + {" "} + + {timestamp} + +
+
+ + Revalidation: + {" "} + 1 hour +
+
+ + Generation: + {" "} + + Static (ISR) + +
+
+
+
+ +
+

+ Code Example +

+ +{`export async function generateStaticParams() { + const posts = await fetch("https://api.vercel.app/blog") + .then((res) => res.json()); + return posts.map((post) => ({ + id: String(post.id), + })); +} + +export const revalidate = 3600; + +export default async function Page({ params }) { + const { id } = await params; + const post = await fetch(\`https://api.vercel.app/blog/\${id}\`) + .then((res) => res.json()); + return
{post.title}
; +}`} +
+
+
+
+ ); +} + diff --git a/examples/redis-minimal/src/app/examples/static-params/[testName]/page.tsx b/examples/redis-minimal/src/app/examples/static-params/[testName]/page.tsx new file mode 100644 index 0000000..ddd4fca --- /dev/null +++ b/examples/redis-minimal/src/app/examples/static-params/[testName]/page.tsx @@ -0,0 +1,135 @@ +import { ExampleLayout } from "@/components/ExampleLayout"; +import { InfoCard } from "@/components/InfoCard"; +import { CodeBlock } from "@/components/CodeBlock"; + +export const dynamicParams = true; + +export const revalidate = 5; + +export default async function TestPage({ + params, +}: { + params: Promise<{ testName: string }>; +}) { + const { testName } = await params; + const timestamp = new Date().toISOString(); + + return ( + +
+ +
    +
  • + + generateStaticParams + {" "} + generates the "cache" route at build time +
  • +
  • + + dynamicParams: true + {" "} + allows other dynamic routes to be generated on demand +
  • +
  • + Very short revalidation period (5 seconds) for testing purposes +
  • +
  • + Try visiting different routes like{" "} + + /examples/static-params/test1 + {" "} + or{" "} + + /examples/static-params/test2 + +
  • +
+
+ +
+
+

+ Route Information +

+
+
+ + Route Parameter: + {" "} + + {testName} + +
+
+ + Rendered at: + {" "} + + {timestamp} + +
+
+
+ +
+

+ Cache Information +

+
+
+ + Revalidation: + {" "} + 5 seconds +
+
+ + Dynamic Params: + {" "} + Enabled +
+
+ + Generation: + {" "} + + {testName === "cache" + ? "Static (pre-generated)" + : "On-demand (dynamic)"} + +
+
+
+
+ +
+

+ Code Example +

+ +{`export const dynamicParams = true; +export const revalidate = 5; + +export async function generateStaticParams() { + return [{ testName: "cache" }]; +} + +export default async function TestPage({ params }) { + const { testName } = await params; + return
{testName}
; +}`} +
+
+
+
+ ); +} + +export async function generateStaticParams() { + return [{ testName: "cache" }]; +} + diff --git a/examples/redis-minimal/src/app/fetch-example/page.tsx b/examples/redis-minimal/src/app/fetch-example/page.tsx deleted file mode 100644 index 67c19fe..0000000 --- a/examples/redis-minimal/src/app/fetch-example/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -export default async function Home() { - let name: string; - try { - const characterResponse = await fetch( - "https://api.sampleapis.com/futurama/characters/1", - { - next: { - revalidate: 86400, // 24 hours in seconds - tags: ["futurama"], - }, - } - ); - const character = await characterResponse.json(); - name = character.name.first; - } catch (error) { - console.error("Error fetching character data:", error); - return ( -
- An error occurred during fetch -
- ); - } - - return ( -
-

Name: {name}

- {new Date().toISOString()} -
- ); -} diff --git a/examples/redis-minimal/src/app/isr-example/blog/[id]/page.tsx b/examples/redis-minimal/src/app/isr-example/blog/[id]/page.tsx deleted file mode 100644 index 983a54c..0000000 --- a/examples/redis-minimal/src/app/isr-example/blog/[id]/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -interface Post { - id: string; - title: string; - content: string; -} - -export const revalidate = 3600; - -export async function generateStaticParams() { - const posts: Post[] = await fetch("https://api.vercel.app/blog").then((res) => - res.json() - ); - return posts.map((post) => ({ - id: String(post.id), - })); -} - -export default async function Page({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const { id } = await params; - const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then( - (res) => res.json() - ); - return ( -
-

{post.title}

-

{post.content}

-
- ); -} diff --git a/examples/redis-minimal/src/app/layout.tsx b/examples/redis-minimal/src/app/layout.tsx index f7fa87e..4119e0c 100644 --- a/examples/redis-minimal/src/app/layout.tsx +++ b/examples/redis-minimal/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Navigation } from "@/components/Navigation"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +14,9 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Next.js Cache Handler Examples", + description: + "Examples demonstrating Next.js caching functionalities with Redis cache handler", }; export default function RootLayout({ @@ -27,6 +29,7 @@ export default function RootLayout({ + {children} diff --git a/examples/redis-minimal/src/app/page.tsx b/examples/redis-minimal/src/app/page.tsx index 695eb59..f7c0edf 100644 --- a/examples/redis-minimal/src/app/page.tsx +++ b/examples/redis-minimal/src/app/page.tsx @@ -1,9 +1,84 @@ +import Link from "next/link"; +import { ExampleLayout } from "@/components/ExampleLayout"; + +const examples = [ + { + href: "/examples/fetch-tags", + title: "Fetch with Tags", + description: + "Demonstrates fetch caching with tags and time-based revalidation. Shows how to use cache tags for selective cache invalidation.", + features: [ + "Time-based revalidation (24 hours)", + "Cache tags for selective invalidation", + "Clear cache button to test tag revalidation", + ], + }, + { + href: "/examples/isr/blog/1", + title: "ISR with Static Params", + description: + "Incremental Static Regeneration with generateStaticParams. Pages are statically generated at build time and regenerated on demand.", + features: [ + "Static generation at build time", + "On-demand regeneration", + "Time-based revalidation (1 hour)", + ], + }, + { + href: "/examples/static-params/cache", + title: "Static Params Test", + description: + "Tests static params generation with dynamic routes. Shows how static params work with revalidation.", + features: [ + "Static params generation", + "Dynamic params support", + "Short revalidation period (5 seconds)", + ], + }, +]; + export default async function Home() { return ( -
- {new Date().toISOString()} -
+ +
+
+ {examples.map((example) => ( + +

+ {example.title} +

+

+ {example.description} +

+
    + {example.features.map((feature, idx) => ( +
  • + + {feature} +
  • + ))} +
+ + ))} +
+
+

+ Note: All examples use Redis as the cache handler. + Make sure Redis is running and configured in your environment + variables. +

+
+
+
); } - -export const revalidate = 3600; diff --git a/examples/redis-minimal/src/app/ppr-example/Example.tsx b/examples/redis-minimal/src/app/ppr-example/Example.tsx deleted file mode 100644 index 748d2dd..0000000 --- a/examples/redis-minimal/src/app/ppr-example/Example.tsx +++ /dev/null @@ -1,39 +0,0 @@ -export async function Example({ - searchParams, -}: { - searchParams: Promise<{ characterId: string }>; -}) { - const characterId = (await searchParams).characterId; - let name: string; - try { - const characterResponse = await fetch( - `https://api.sampleapis.com/futurama/characters/${characterId}`, - { - next: { - revalidate: 86400, // 24 hours in seconds - tags: ["futurama"], - }, - } - ); - const character = await characterResponse.json(); - name = character.name.first; - } catch (error) { - console.error("Error fetching character data:", error); - return ( -
- An error occurred during fetch -
- ); - } - - return ( -
-

Name: {name}

- {new Date().toISOString()} -
- ); -} - -export async function Skeleton() { - return
Loading...
; -} diff --git a/examples/redis-minimal/src/app/ppr-example/page.tsx b/examples/redis-minimal/src/app/ppr-example/page.tsx deleted file mode 100644 index 391a001..0000000 --- a/examples/redis-minimal/src/app/ppr-example/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Suspense } from "react"; -import { Example, Skeleton } from "./Example"; - -export default function Page({ - searchParams, -}: { - searchParams: Promise<{ characterId: string }>; -}) { - return ( -
-

This will be prerendered

-
-

This will be dynamic

- }> - - -
- ); -} - -export const revalidate = 3600; diff --git a/examples/redis-minimal/src/app/static-params-test/[testName]/page copy.tsx b/examples/redis-minimal/src/app/static-params-test/[testName]/page copy.tsx deleted file mode 100644 index 938e4a4..0000000 --- a/examples/redis-minimal/src/app/static-params-test/[testName]/page copy.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -export const dynamicParams = true; - -export const revalidate = 5; - -export default async function TestPage({ - params, -}: { - params: Promise<{ testName: string }>; -}) { - const { testName } = await params; - return
{testName}
; -} - -export async function generateStaticParams() { - return [{ testName: "cache" }]; -} diff --git a/examples/redis-minimal/src/app/static-params-test/[testName]/page.tsx b/examples/redis-minimal/src/app/static-params-test/[testName]/page.tsx deleted file mode 100644 index 938e4a4..0000000 --- a/examples/redis-minimal/src/app/static-params-test/[testName]/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -export const dynamicParams = true; - -export const revalidate = 5; - -export default async function TestPage({ - params, -}: { - params: Promise<{ testName: string }>; -}) { - const { testName } = await params; - return
{testName}
; -} - -export async function generateStaticParams() { - return [{ testName: "cache" }]; -} diff --git a/examples/redis-minimal/src/components/ClearCacheButton.tsx b/examples/redis-minimal/src/components/ClearCacheButton.tsx new file mode 100644 index 0000000..95c0b4f --- /dev/null +++ b/examples/redis-minimal/src/components/ClearCacheButton.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useState } from "react"; + +export function ClearCacheButton({ + tag, + label = "Clear Cache", +}: { + tag: string; + label?: string; +}) { + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + + const handleClear = async () => { + setLoading(true); + setMessage(null); + try { + const response = await fetch(`/api/cache?tag=${tag}`); + const text = await response.text(); + setMessage(text); + setTimeout(() => { + window.location.reload(); + }, 500); + } catch (error) { + setMessage("Error clearing cache"); + } finally { + setLoading(false); + } + }; + + return ( +
+ + {message && ( + + {message} + + )} +
+ ); +} + diff --git a/examples/redis-minimal/src/components/CodeBlock.tsx b/examples/redis-minimal/src/components/CodeBlock.tsx new file mode 100644 index 0000000..8968a40 --- /dev/null +++ b/examples/redis-minimal/src/components/CodeBlock.tsx @@ -0,0 +1,8 @@ +export function CodeBlock({ children }: { children: React.ReactNode }) { + return ( +
+      {children}
+    
+ ); +} + diff --git a/examples/redis-minimal/src/components/ExampleLayout.tsx b/examples/redis-minimal/src/components/ExampleLayout.tsx new file mode 100644 index 0000000..769697d --- /dev/null +++ b/examples/redis-minimal/src/components/ExampleLayout.tsx @@ -0,0 +1,33 @@ +export function ExampleLayout({ + children, + title, + description, + actions, +}: { + children: React.ReactNode; + title: string; + description: string; + actions?: React.ReactNode; +}) { + return ( +
+
+
+
+
+

+ {title} +

+ {actions &&
{actions}
} +
+

{description}

+
+
+ {children} +
+
+
+
+ ); +} + diff --git a/examples/redis-minimal/src/components/InfoCard.tsx b/examples/redis-minimal/src/components/InfoCard.tsx new file mode 100644 index 0000000..84aff50 --- /dev/null +++ b/examples/redis-minimal/src/components/InfoCard.tsx @@ -0,0 +1,17 @@ +export function InfoCard({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+
{children}
+
+ ); +} + diff --git a/examples/redis-minimal/src/components/Navigation.tsx b/examples/redis-minimal/src/components/Navigation.tsx new file mode 100644 index 0000000..68f522d --- /dev/null +++ b/examples/redis-minimal/src/components/Navigation.tsx @@ -0,0 +1,92 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const examples = [ + { href: "/", label: "Home", description: "Overview of all examples" }, + { + href: "/examples/fetch-tags", + label: "Fetch with Tags", + description: "Demonstrates fetch caching with tags and revalidation", + }, + { + href: "/examples/isr/blog/1", + label: "ISR with Static Params", + description: "Incremental Static Regeneration example", + }, + { + href: "/examples/static-params/cache", + label: "Static Params Test", + description: "Testing static params generation", + }, +]; + +export function Navigation() { + const pathname = usePathname(); + + const isActive = (href: string) => { + if (!pathname) return false; + if (href === "/") { + return pathname === "/"; + } + return pathname.startsWith(href); + }; + + return ( + + ); +} diff --git a/examples/redis-minimal/src/types/futurama.ts b/examples/redis-minimal/src/types/futurama.ts new file mode 100644 index 0000000..d753446 --- /dev/null +++ b/examples/redis-minimal/src/types/futurama.ts @@ -0,0 +1,22 @@ +export interface FuturamaCharacter { + id: number; + name: { + first: string; + middle?: string; + last: string; + full: string; + }; + age?: string; + images: { + headShot?: string; + main?: string; + }; + gender?: string; + species?: string; + homePlanet?: string; + occupation?: string; + sayings?: string[]; + createdAt?: string; + updatedAt?: string; +} +