A WooCommerce category page with a product filter can’t be statically generated or ISR’d in Next.js, because filter state lives in search params, and Next.js treats any route reading search params as dynamic by definition. A developer who shipped exactly this architecture summed up the lesson in one line: “Category filter + search params pages must be fully dynamic.” The Store API’s collection-data endpoint supplies the facet counts a filter needs — it doesn’t remove that rendering-strategy constraint.
Why can’t a filtered category page use ISR in Next.js?
Incremental Static Regeneration pre-renders a page and serves the same HTML to every visitor until the revalidation window passes. A filter changes what that page shows per request — ?color=blue&size=l is a different result set than ?color=red — so there is no single cached page to regenerate; there are as many possible results as there are filter combinations. A developer building a headless WooCommerce storefront with Next.js reported hitting this directly, in a public account of the build: “that category page with filters can not be ISR or SSR it has to be dynamic because of the searchparams… Next js treat any page with search params as dynamic.” The products list page in the same stack can still use ISR, because its content doesn’t depend on a query parameter; the filtered view of that same list cannot.
If you are searching headless woocommerce next.js filter or woocommerce store api facets, this is the wall: the Store API gives you the data, but Next.js’s own rendering model decides that any route reading that data through search params renders fresh, on the server, on every request. That’s a correct, unavoidable trade-off in Next.js’s App Router — not a bug to work around, a cost to budget for.
What does the Store API actually give you for building facets?
WooCommerce’s own documentation for GET /wc/store/v1/products/collection-data describes it as data for “a collection of products, for example, the min and max price in a collection of products (ignoring pagination). This is used by blocks for product filtering widgets, since counts are based on the product catalog being viewed.” A request can ask for calculate_price_range, calculate_attribute_counts (per taxonomy, with an “or” or “and” query type), calculate_rating_counts, calculate_taxonomy_counts, and stock status counts, and gets back the aggregate numbers for whichever product subset the rest of the request’s filters already narrowed down to. That’s exactly the shape a Next.js facet sidebar needs: counts that update per selection, not a fixed list fetched once.
What it doesn’t give you is a rendering strategy. Calling collection-data from a Next.js Server Component inside a dynamic route works the same way any server-side fetch does — it runs on every request to that route, because the route itself is dynamic. The endpoint is correctly built for its job; the architecture decision about how often it gets called per visitor sits entirely on the Next.js side.
How do developer guides suggest splitting the two APIs?
A published developer guide to building a headless WooCommerce store with Next.js and the Store API draws the split along a clear line: “The REST API handles server-side data fetching during build time or SSR (product listings, category pages, individual product data). The Store API handles client-side interactions (add to cart, update quantities, apply coupons, checkout).” Cart and checkout work through the Store API’s nonce-based session system specifically because that system “does not require API keys, which makes them safe to call directly from the browser” — a deliberate security boundary, not an incidental one.
That guide’s own example shop page sets export const revalidate = 60 on an unfiltered product listing — static generation at build time, refreshed every 60 seconds, the fast path ISR enables. The moment that same route starts reading a category or attribute value from searchParams to filter the grid, it crosses into the dynamic-rendering territory described above, and the 60-second ISR window no longer applies to it: a filtered view has to be computed per request, because a cached static page can only ever represent one combination of filters, not all of them.
What did a real headless build learn the hard way?
The same developer’s account, posted after finishing the project, lists lessons that go beyond the filter route itself. On session handling: “Sessions: client-side only, otherwise you lose ISR/SSR” — mixing server-held session state into otherwise-static pages reintroduces the same dynamic-everywhere problem filters cause, just through a different door. On request volume: “GraphQL request count can explode in production — caching is mandatory,” paired with a specific infrastructure fix that made the difference: “set GraphQL product requests to use GET so Varnish can cache them,” plus Cloudflare in front with rules to bypass session endpoints specifically, so the cache layer isn’t accidentally caching per-user cart or login state alongside public product data.
The stack that got there combined WPGraphQL, WPGraphQL Smart Cache, a paid WooGraphQL build for session handling, and backend hosting on Cloudways with Varnish — a materially larger infrastructure footprint than a standard WooCommerce + theme setup, built specifically to make the dynamic parts of a headless storefront fast enough.
Is headless WooCommerce + Next.js worth it for filtering specifically?
The same developer’s own verdict, after finishing: “I finished the project and learned a lot, but honestly — I wouldn’t recommend headless WooCommerce for client projects. Shopify’s GraphQL setup is way smoother with better documentation.” That’s one build, not a universal rule, but it’s a data point worth weighing before choosing the architecture specifically to solve a filtering problem: the caching, session, and rendering work needed to make filtered category pages fast in a headless Next.js frontend is its own substantial project, separate from and in addition to whatever filtering problem motivated going headless in the first place.
How does InstantFilter’s architecture compare for this specific problem?
InstantFilter isn’t a headless architecture and doesn’t solve the same problem the same way — it’s worth being precise about that rather than claiming a direct swap. A WordPress-rendered WooCommerce category page stays a standard cacheable page because InstantFilter’s filter click never becomes a route change or a search param: the archive loads as static, server-rendered HTML, and after hydration the filter runs against the already-downloaded codebook entirely in the browser. There is no dynamic route to mark, no search-param-triggered server render per filter combination, and no facet-counting request to cache or rate-limit, because the facet counts are computed client-side from data already in the browser.
That trade-off cuts the other way too: InstantFilter runs inside WordPress, not as a decoupled Next.js frontend, so it isn’t the answer for a team that specifically needs a headless architecture for reasons beyond filtering — a separate frontend team, a non-WordPress design system, or a multi-channel storefront sharing one backend. For a team whose headless project exists mainly because WooCommerce’s native category pages felt slow, it’s worth asking whether the filtering problem alone justifies the session-handling and caching work described above, or whether it’s solvable without leaving WordPress’s rendering model at all. The unfiltered shop page in a headless build can still use ISR exactly as the developer guide above describes; the question is only ever about the filtered view layered on top of it, and that’s the specific layer where the two architectures solve the same user-facing goal in structurally different ways.
Keep going
Headless rendering is one way to handle the dynamic-route problem. These pages cover the rest of the architecture.
- AJAX versus frontend-first filtering architecture
- What filter URLs do to crawl budget
- The Store API endpoint behind this page's facets
- Why WooCommerce filters get slow
- Pricing and trial