Created: 2026-02-23
Updated: 2026-02-23
Project: My Cove (Next.js 16 frontend + Strapi CMS backend) Date: 2026-02-23
Migrated protected content routes from fully dynamic (server-rendered on every request) to static/ISR rendering, while enforcing authentication through a proxy middleware layer instead of inside individual data-fetching functions. The result is significantly reduced server load and faster page delivery for the majority of page visits.
All content routes were marked ƒ Dynamic in the Next.js build output:
ƒ /mini-blog
ƒ /post
ƒ /post/[id]
ƒ /search
Root cause: every data-fetching function called auth() to retrieve the user's session
before making any API request. Because auth() reads a request-time cookie, Next.js
correctly classified every page as dynamic — even pages whose content is identical for
all authenticated users.
Created proxy.ts (the Next.js 16 replacement for middleware.ts) to handle all
authentication checks at the edge, before any page renders.
Protected routes: all routes except / and /login.
Unauthenticated users are redirected to /.
Matcher config: all /api/* routes are excluded from the matcher so that API
route handlers (e.g. /api/auth/[...nextauth]) manage their own responses and return
proper JSON errors rather than HTML redirects.
matcher: ["/((?!api|next/static|_next/image|favicon\\.ico).)"]
auth() removed from server-side data fetchingWith authentication enforced at the proxy layer, individual page data-fetchers no longer
need to verify the session. The Authorization header and all auth() / early-return
guards were stripped from:
/app/post/page.tsx/app/post/[id]/page.tsx/app/search/page.tsxThis is what allows Next.js to treat these fetches as cacheable.
export const revalidate = 3600; // 1-hour stale-while-revalidate
Added to /post and /post/[id]. Cache expires after 1 hour; Next.js revalidates in
the background on the next request after expiry, keeping the served page fresh without
blocking the user.
generateStaticParams added to /post/[id]All post detail pages are now pre-rendered at build time by fetching the list of post IDs from Strapi and generating a static HTML file for each. New posts published after a build are served dynamically on first visit and then cached automatically.
The function is wrapped in a try-catch so a network failure at build time degrades gracefully to on-demand rendering rather than crashing the build.
Route (app) Revalidate Expire
┌ ○ /
├ ○ /not-found
├ ƒ /api/auth/[...nextauth]
├ ○ /login
├ ƒ /mini-blog
├ ○ /post 1h 1y
├ ● /post/[id] 1h 1y
│ ├ /post/1
│ └ [+12 more paths]
└ ƒ /search
ƒ Proxy (Middleware)
○ Static
● SSG (generateStaticParams)
ƒ Dynamic
Centralising auth in the proxy layer gives a single, auditable enforcement point. It also decouples what data to fetch from whether the user is allowed to be here — the data layer becomes stateless with respect to auth, which is what enables caching.
The trade-off: individual pages lose the ability to branch on auth state during the server render (e.g. showing different content for admins vs. regular users). For this application that is not a requirement — all authenticated users see the same content.
Posts are authored content that changes occasionally. Full static (revalidate = false)
would require a redeploy to surface new posts. ISR with a 1-hour window means new content
appears automatically within an hour of being published in Strapi, with zero deployment
overhead.
/mini-blog stayed DynamicThe /mini-blogs Strapi endpoint requires a user JWT to return data (HTTP 403 otherwise).
ISR revalidation runs as a background server job with no user session — there is no cookie
or JWT available during cache regeneration. Forcing ISR here would result in the cache
always being regenerated with empty data.
Resolution path: if the Strapi Public role is granted find permission on mini-blogs,
the page can be converted to ISR using the same pattern as /post. Until then, dynamic
rendering is the only correct strategy.
/search stayed DynamicsearchParams (URL query parameters) are inherently request-time data. Next.js cannot
cache a page whose output depends on arbitrary query string values. Dynamic is the only
viable rendering mode here regardless of auth approach.
During implementation, static pre-rendering of /post/[id] surfaced latent bugs that were
previously hidden behind the early-return auth guard:
collection.attributes.posts?.data || [] — Strapi can return null for an empty
relation rather than { data: [] }, crashing .data access.post.attributes.categories?.data || [] — same pattern for categories.post.attributes.tags?.data || [] — same pattern for tags.These were fixed with optional chaining + empty-array fallbacks. The crashes only appeared
at build time (static generation) because previously a missing session caused getPost to
return null before reaching this code.
| File | Change |
|---|---|
proxy.ts |
New — auth enforcement middleware (replaces deprecated middleware.ts) |
app/post/page.tsx |
Removed auth(), removed JWT header, added revalidate = 3600 |
app/post/[id]/page.tsx |
Removed auth(), added generateStaticParams, added revalidate = 3600, defensive optional chaining |
app/search/page.tsx |
Removed auth() and JWT header (stays dynamic) |
app/mini-blog/page.tsx |
Unchanged from original (kept dynamic with auth()) |
components/mini-blog/MiniBlogClient.tsx |
Unchanged from original |
components/mini-blog/NewMiniBlog.tsx |
Unchanged from original |