Tag: Headless CMS

  • Caching in the Next.js App Router: Which Layer Is Answering?

    Caching in the Next.js App Router: Which Layer Is Answering?

    “I changed the API URL and the site still shows the old posts.” If you have built a Next.js site on top of a headless CMS, you have probably said something like this. The App Router caches aggressively and in several layers, and most confusing bugs come from not knowing which layer is answering. This is the mental model I use.

    Layer 1: environment variables are read at start or build time

    Values in .env.local are loaded when the dev server starts or when you build. Editing the file while the server runs changes nothing until you restart. Also check which folder the server runs from: each project folder (and each git worktree) reads its own env file.

    Anything prefixed NEXT_PUBLIC_ is inlined into the browser bundle at build time, so it is both public and frozen. Secrets such as API tokens must never use that prefix.

    Layer 2: pages rendered at build time

    Routes that can be pre-rendered are generated during next build. With generateStaticParams, every slug you return becomes static HTML. That makes pages instant, but it also means:

    • The CMS must be reachable from the build machine.
    • Content changes only appear after revalidation or a rebuild.
    • Config read at build time, such as allowed image hosts in next.config.js, needs a rebuild to change.

    Layer 3: the data cache

    For data that does not come from a plain fetch, such as an axios client or an SDK, unstable_cache wraps a function and stores its result on the server:

    import { unstable_cache } from "next/cache";
    
    export const getPosts = unstable_cache(
      async () => fetchPostsFromCms(),
      ["cms-posts", API_BASE, String(CATEGORY_ID)],
      { revalidate: 120 }
    );

    Two parts deserve attention:

    • Key parts. The array is part of the cache key. Include everything that changes the result, such as the API base URL and category ID. If you leave the URL out, switching environments can keep serving the old environment’s data.
    • Revalidate. After the window expires, the next request still gets the cached value while a fresh one is fetched in the background. If that fetch fails, the stale value keeps being served, which is usually what you want for a blog, but can look like “nothing changed”.

    Newer Next.js versions also offer the "use cache" directive with cache life profiles. The concepts are the same: a key, a lifetime and a way to invalidate.

    Layer 4: on-demand invalidation

    Time-based revalidation is a safety net, not a publishing workflow. For instant updates, tag cached data and invalidate the tag when the CMS changes:

    // app/api/revalidate/route.ts
    import { revalidateTag } from "next/cache";
    
    export async function POST(request: Request) {
      const secret = request.headers.get("x-revalidate-secret");
      if (secret !== process.env.REVALIDATE_SECRET) {
        return new Response("Unauthorized", { status: 401 });
      }
      revalidateTag("cms-posts");
      return Response.json({ revalidated: true });
    }

    Point a CMS webhook (for WordPress, a small plugin or a save_post hook) at this route, and published posts show up within seconds without waiting for the timer.

    Layer 5: the browser and the router

    The client-side router keeps recently visited segments in memory, so pressing back can show what you saw a minute ago. A hard refresh rules this layer out quickly when debugging.

    A debugging checklist

    1. Restart the dev server and confirm it runs from the folder whose env file you edited.
    2. Check that the variable is read server-side and not inlined with NEXT_PUBLIC_.
    3. Confirm the cache key includes the values you changed.
    4. Look at the server logs for failed revalidation fetches.
    5. For production, ask whether the page was built statically and needs revalidation or a rebuild.

    Caching is what makes a headless setup feel fast. Knowing which layer is answering is what makes it feel predictable.