Skip to content

Skills · frontend

rendering-strategy

Decide route by route how each page of a web app is rendered — static at build (SSG), static with revalidation (ISR, stale-while-revalidate), server-rendered per request (SSR, streaming), client-rendered (CSR, SPA), at the edge, or a mix with islands and server components — from how fresh each route's data must be, whether it is personalised, whether it must be indexed or shared, what it costs per request at the stated traffic and how much JavaScript it needs, with the cache keys, invalidation events and failure modes of each choice. Use when the user asks "should this be SSR or static", "SSR vs SSG vs ISR", "how should each page be rendered", "do we need SSR for SEO", "which pages can we cache", or is choosing rendering modes in Next.js, Nuxt, Remix, React Router, SvelteKit or Astro.

Pack frontendTier standardJob Build and review softwarev0.1.0

npx skilldrop-cli install rendering-strategy
Other ways to install

With the sibling skills it hands off to:

npx skilldrop-cli install rendering-strategy --with-related

The whole pack it belongs to:

npx skilldrop-cli install --pack frontend

By hand, from a clone of the repo:

cp -R packs/frontend/skills/rendering-strategy ~/.claude/skills/

In Claude Code, with the frontend plugin installed, invoke it as:

/frontend:rendering-strategy

Try it

A realistic prompt from the skill's acceptance evals:

We're rebuilding our outdoor gear shop in Next.js and the team wants SSR on every page. Everything we know is in fernhill-app.md. How should each route be rendered? Keep it to what we need to decide.

What a good result looks like

Example output

Worked example: SaaS marketing site, docs, status page and app (SvelteKit) · the skill's own worked example, source

Input

We're moving Tallyhook (invoicing SaaS) to one SvelteKit app on Node behind a CDN. Routes: / and /pricing (marketing, edited a few times a month, the pricing table shows the visitor's currency from their country); /docs/** (about 500 pages, markdown in the repo, deployed on merge); /changelog (one entry a week); /status (must show an incident within 60 seconds, and has to work when the app is down); /app/** (the logged-in product). Traffic a month: marketing and changelog 300,000 views, docs 450,000, /app 900,000. We were going to set ssr = true everywhere and be done. How should each route render?

Output

Don't render everything per request: only /app needs per-user pages, and even there the choice is client- or server-rendered behind no shared cache. Everything public is prerendered or cached, and the status page must not depend on the app at all.

RouteModeWhyInvalidated byWhen its source fails
/, /pricingprerendered; currency picked by a CDN-geo cache key or a small client fragmentsame for everyone in a country, indexed, shareddeploy (content is in the repo) [assumption]page serves; currency falls back to USD [assumption]
/docs/**prerendered (export const prerender = true on the docs layout), csr = false on pages with no widgetssame for everyone, indexed, changes only on mergedeploy on mergeserved from the CDN
/changelogprerenderedweekly change, indexeddeployserved from the CDN
/statusstatic page on separate hosting, polling a status JSON with a 30-second TTLmust work when the app is down; 60-second freshnessthe status JSON updates; the page never needs a rebuildpage loads; shows the last status with its time
/app/**ssr = false (client-rendered SPA) or SSR with private, no-storeper user, never indexed—spinner, then an error state with retry

Traps found

  • /status inside the same SvelteKit app goes down with it. Host it apart from the app.
  • Currency from geo makes /pricing vary per country: put the country in the CDN cache key (a few entries), not Vary: Cookie (one entry per visitor).
  • prerender = true on the root layout would reach /app too; set it on the public layouts only.

Cost. SSR on every route: 1,650,000 renders a month. This plan: public pages render at build only (500-odd pages per deploy); /app at most 900,000 if server-rendered, 0 if client-rendered.

To confirm

  • How often marketing pages are edited outside a deploy (if a CMS edits them, add an on-demand rebuild on its publish event).
  • The fallback currency.

How it's checked

Measured against plain Claude Code (2026-10-23): it met 78% of its checks, against 45% without it, and a blind judge preferred it. How skills are checked

40 assertions across 5 acceptance evals, and 9 trigger queries. Every skill's checks

What the output must do
  • Opens with the answer, including that SSR on every page is not needed, and gives a decision for each route (home, listing, product detail, cart, account, blog) rather than one mode for the app
  • Says SEO needs the content in the HTML of the first response, which static or revalidated pages provide, so the home, listing, blog and product content do not need per-request SSR
  • Keeps the personalised header off the rendering decision: the greeting and cart count load as a client or edge fragment on cached pages, and it flags that reading the session cookie in the root layout (or force-dynamic there) makes every route dynamic
  • Does not rely on time-based revalidation for the 10-second stock requirement: keeps the product page cached, fetches stock separately from a short-lived source, and says checkout or add-to-cart must re-check stock
  • Names what the product page shows when the inventory API is slow or times out during a drop (a timeout and a fallback), rather than letting the whole page wait
  • Names invalidation events and the pages they touch: the PIM webhook revalidates the product page and its category pages, the CMS publish webhook revalidates the post and the blog index; and states that cart and account are never stored in a shared cache
  • Compares renders or server load under SSR everywhere with the proposed mix using the file's traffic figures, with arithmetic that matches them
  • Does not invent traffic, freshness requirements, versions or services not in the file; anything else is tagged [assumption] or listed under To confirm
  • Answers in the first lines that the whole app does not need server rendering: the public pages need HTML at first byte and the logged-in app can stay client-rendered
  • Splits the decision by route group (marketing, pricing, docs, logged-in app) rather than one mode for everything
  • Explains the blank link preview as the Open Graph tags not being in the initial HTML, because preview bots do not run JavaScript, and that prerendering the public pages fixes it
  • Recommends prerendering (static at build) or revalidation for marketing, pricing and docs rather than per-request SSR, since they are the same for every visitor
  • Says how the prerendered pages get updated (rebuild on deploy or on a content-change event)
  • Stays framework-neutral or offers options, and does not present one specific meta-framework's API as required
  • Does not invent traffic, page counts or update frequencies; anything it needs is asked for or tagged [assumption]
  • Is a chat-length answer, not a long design document
  • Flags the swr rule on /account/** as caching personalised pages in a shared server cache, so one reader's account page can be served to another, and replaces it with no shared caching (ssr: false or private, no-store)
  • Does not keep per-request SSR for the results pages: they are the same for everyone and a minute of staleness is acceptable, so they get a short swr or isr window (or a cached shell with a short-TTL results endpoint)
  • Uses the election-night traffic to show what per-request SSR on results would cost against a cached window, with arithmetic that matches the file
  • Keeps article pages cached and moves the paywall meter to a client-side (or edge) fragment read from the cookie, instead of rendering every article per request
  • Flags that prerender: true on topic pages freezes them until the next deploy while 150 articles a day are published, and replaces it with revalidation or on-demand invalidation on the CMS webhook
  • Names invalidation events: article publish or correction purges that article plus its topic pages and the home page
  • Names what results pages show if the results feed stalls (the last good copy, with an updated-at time)
  • Gives settings in Nuxt routeRules terms only, and does not invent traffic, versions or hosting details beyond the file
  • Answers the question directly with a rendering mode for each of the seven routes in routes.md (/, /recipes/[slug], /category/[name], /search?q=, /account/saved, /account/settings, /print/[slug]), e.g. as a table, before or alongside the explanation.
  • Flags that the current setup (nitro preset 'static', files in a bucket behind a CDN, no servers) cannot run SSR or ISR, so any per-request or revalidating route needs a server/edge runtime; either recommends that hosting change explicitly or gives a static-only path (prerender plus rebuild on the Larder webhook) and states the trade-off.
  • Uses build-notes.txt to estimate that prerendering all ~42,000 recipes at the observed rate (1,200 in 6 min) takes about 3.5 hours (~210 min), and therefore recommends prerendering a subset and generating the rest on demand/incrementally rather than a full rebuild per CMS edit.
  • Keeps /recipes/[slug] shared-cacheable (static or revalidated) and moves the per-user Save heart to a client-side island/fetch; does not make the whole recipe page per-request SSR or vary its cache on the user's login just for the heart.
  • Explains that WhatsApp and similar link-preview crawlers do not run JavaScript, so title, description and og:image must be in the initial HTML, and points out that [slug].vue fetches the recipe and calls useHead inside onMounted, so that fetch must move to server-side data loading (useAsyncData/useFetch) - turning ssr on alone will not fix previews.
  • Ties invalidation to real events from the input: the Larder publish/edit/unpublish webhook for recipe (and affected category) pages and the daily 06:00 editor swap for the home page, and treats the ~3,000 reviews/day rating as tolerating a short revalidation window or a client fetch rather than requiring SSR.
  • Marks /search and /account/* as not for indexing or shared caching (client-rendered or private per-request), and gives /print/[slug] a canonical to /recipes/[slug] or noindex as duplicate content.
  • Invents nothing not in the input: no traffic figures, costs, recipe counts, build times, hosting providers, or Larder/FindIt API features beyond what the files state; anything needing the network (e.g. testing a URL with a preview debugger) is given as a command or step for the user to run.
  • Answers both questions directly: gives a rendering mode for each route or route group in routes.txt (/, /pricing, /customers/:slug, /docs/*, /blog/:slug, /app/* pages, api/*) and gives a clear yes/no on whether SSR-everything is a cost problem.
  • Computes cost from traffic.md: public pages 2.4M x 0.22 s x 1 GB = ~528,000 GB-s = ~$9.50 plus ~$0.96 invocations, about $10-11/month (and/or the app's ~2.52M navigations adding roughly another $11), and concludes compute cost is not a strong argument against SSR - the stronger reasons are cold-start latency (p95 900 ms) and cacheability.
  • Flags as the top-priority issue that src/hooks.server.ts sets 'cache-control: public, max-age=300' on every response, which would let the CDN serve one customer's authenticated /app pages (dashboard, billing) to another; says app and API responses must be private/no-store and the header must be set per route, not globally.
  • Notices that the root src/routes/+layout.server.ts returns locals.user for every page, so public pages depend on the session cookie and cannot be prerendered or shared-cached; recommends moving the user load into the app layout and rendering the nav's Log in/Open dashboard state on the client (or from a cheap check) for public pages.
  • Handles /pricing's per-country currency (x-country header -> USD/EUR/GBP): prerender or cache one variant per currency with country/currency in the cache key, resolve at the edge, or pick currency on the client - and warns that a single prerendered or shared-cached pricing page would show one currency to everyone.
  • Recommends prerendering docs (~900 repo markdown pages), blog (~250) and customers (40) at build, e.g. with SvelteKit's per-route export const prerender = true, with deploy as the invalidation event since the content lives in the repo.
  • Keeps /app/dashboard (live usage metrics) and other /app pages per-request or client-fetched behind login, never shared-cached, and notes that SvelteKit client navigations still hit functions for load data.
  • Invents nothing not in the input: no Stratus features, prices, traffic numbers, adapters or routes beyond the files (adapter-auto is noted as-is; any platform capability it can't verify offline is stated as something to check), and the answer is sized for a slide or two plus notes rather than a long essay.
When it should and shouldn't activate

Should activate:

  • Should our product pages be SSR or static with ISR? Stock changes a lot.
  • The team wants to server-render the whole site for SEO. Is that actually needed?
  • Help me pick SSG, ISR, SSR or CSR for each route of our Nuxt app
  • Our SPA's pages don't show up in Google and link previews are blank. What should we render on the server?
  • Which of our SvelteKit pages can we prerender and cache at the CDN, and which have to be dynamic because they're logged in?

Should leave to another skill:

  • Write the design doc for the new storefront: services, data stores and rollout plan
  • Write an ADR for our decision to move from Create React App to Next.js
  • Set p95 latency and availability targets for the checkout API
  • Here's our Lighthouse report: LCP is 4.1s on mobile. What's slowing the page down?

Hands off to

Related skills

design-doc

Source

SKILL.md · the whole folder on GitHub.