Our Strapi blog uses ISR, can I personalize one section without going fully dynamic?
Strapi feeds a Next.js App Router blog, ISR with revalidate 300. Build serves ~1200 posts, TTFB is great and I want to keep it that way.
Marketing wants a personalized CTA band on posts. I read that fetching Croct content in a server component makes the route dynamic, which would throw away ISR for the whole article.
Is there a pattern that keeps the article body static and personalizes just the CTA band? What is the actual tradeoff, because going fully dynamic for one section is not it
2 answers
Your reading is correct: fetchContent reads the request, so calling it in a server component opts the route into dynamic rendering. Do not call it on routes you want to keep static. There is a whole thread on that behavior: fetchContent forces dynamic rendering.
The underlying constraint is not Croct-specific. ISR serves the same HTML to everyone, so any request-level personalization forces either dynamic rendering or middleware rewrites.
The hybrid pattern for your case: keep the route static and render only the CTA band with the client hook.
'use client';
import {useContent} from '@croct/plug-react';
export function CtaBand() { const content = useContent('post-cta', { fallback: {title: 'Try it free', buttonLabel: 'Start now'}, });
return <Cta {...content} />;}The tradeoff you accept is that one section resolves on the client, so the fallback renders first and the personalized version swaps in. The article body, layout, and everything above the fold stay in the static HTML. For a below-the-fold CTA band that swap is usually invisible; for a hero it would not be, and then you would weigh dynamic rendering instead.
Band is below the fold, so the swap is acceptable. Shipping the hybrid.
Came across this solving the same ISR-plus-CTA problem on a Strapi blog. One addition that helped us verify the tradeoff: since only the CTA band is a client component, we left the post route's revalidate untouched and ran next build to confirm the article routes still show as prerendered, with only the CTA subtree hydrating on the client. TTFB stayed exactly where it was. If you later decide you want the CTA in the static HTML too, the only way there is moving that route to dynamic with fetchContent, but for a below-the-fold band the client hook is the cheaper call.
Small addition: set a sensible fallback in useContent that matches your Strapi default styling exactly. If the fallback and the personalized variant share dimensions, the swap causes zero layout shift, which keeps CLS clean even though the content resolves client side.