Choosing Between Static and Server Rendering Per Route

Rendering-mode arguments usually happen at the project level and stay unresolved, because the honest answer differs per route. The procedure below resolves it per route in about twenty minutes, produces a written decision, and keeps islands out of the argument entirely β€” activation is orthogonal, as rendering modes for islands sets out. What follows is the classification work that makes the choice mechanical.

Prerequisites

Classifying Every Value on One Route A product route's values sorted into three columns. Build-time constants include the title, description, images and specification table. Shared time-varying values include price, stock band and review count. Per-visitor values include the greeting, saved state and loyalty tier. The middle column determines the revalidation window and the right column determines what must sit below the personalisation line. Every value goes in exactly one column BUILD-TIME title Β· description images Β· specification breadcrumb Β· related links changes on deploy SHARED, VARYING price Β· promotion badge stock band Β· review count delivery estimate sets the revalidation window PER VISITOR greeting Β· saved state loyalty tier Β· basket recently viewed must leave the cached document Values that only reach island props still count β€” they ship inside the document and inherit its cacheability.

Diagnostic Steps

Step 1 β€” Inventory what the route displays

Open the route and write down every value a visitor can see, including values that appear only inside island props. The props matter as much as the visible markup, because a serialised prop is bytes in the document and is cached with it. A typical product route produces a list of fifteen to twenty entries; a listing route produces fewer than you expect, because most of the page is one repeated shape.

Step 2 β€” Classify each value into one of three buckets

# route-decision.md β€” kept next to the route, not in a wiki
route: /product/[id]

build-time      title, description, images, spec table, breadcrumb
shared-varying  price (max staleness: 5 min), stock band (5 min), review count (1 h)
per-visitor     greeting, saved flag, loyalty price, recently viewed

decision: incremental, revalidate = 300s
personalised:   /_fragment/personal-offer  (private, no-store)
reason:         price is the tightest shared constraint at 5 minutes;
                everything per-visitor is already below the line.

Disagreements surface at this step and they are productive. β€œIs the stock band per visitor?” is a real question β€” if warehouse allocation differs by region, the answer is yes and the value moves right, which changes the whole decision.

Step 3 β€” Draw the personalisation line and route what is below it

Every per-visitor value becomes either a separate fragment request or a fetch performed by an island after activation. Prefer the fragment when the value must be visible without JavaScript, and the island fetch when it is genuinely enhancement. Both keep the document shared, which is the entire objective. The mechanics of the fragment are covered in using Astro server islands for personalised content.

Step 4 β€” Take the tightest freshness constraint as the window

If price may be five minutes stale and review count may be an hour stale, the window is five minutes. If any shared value tolerates zero staleness, it is not shared-varying β€” it is per-request, and it belongs below the line with the personalised values.

Step 5 β€” Write the decision into the route file

A three-line comment above the route’s mode export, naming the window and the constraint that produced it. This is the artefact that survives; the ticket does not.

From Classification to Mode Three questions resolve the mode. If no shared value varies between deploys, the route is static. If shared values vary on a schedule and can tolerate a window, the route is incremental with that window. If any shared value tolerates no staleness, the route is server-rendered β€” or, better, that value is moved below the personalisation line and the route stays incremental. Three questions, in order Does any shared value change between deploys? no β†’ nothing to revalidate static Can every shared value tolerate a window of staleness? yes β†’ take the tightest one incremental, window = tightest Does a shared value tolerate no staleness at all? first try moving it below the line β€” it is usually not really shared server-rendered the last resort, not the default

Verification

Three checks confirm the decision was implemented rather than merely written down.

The two-session diff. Request the route with two different session cookies and diff the documents. Any difference must be a fragment you deliberately placed below the line. This is the check that catches a leak before a visitor does.

The header check. Request the route and read the cache headers that actually arrive. Framework defaults, host defaults and CDN rules interact, and the header you set is not reliably the header that ships. Confirm the document is public with the window you chose, and that the personalised fragment is private and no-store.

The freshness check. Change a shared value at the source, then poll the route until it updates. The observed lag should approximate the window. If it never updates, revalidation is not wired to anything β€” a common outcome when the framework option was set but the platform serves from a different cache layer.

What Each Check Catches Three checks and the specific defect each one detects. The two-session diff catches personal data inside a shared document. The header check catches a configuration that never reached the cache layer. The freshness check catches revalidation that is configured but not connected, where a value updates at the source and never appears on the page. CHECK CATCHES two-session diff personal data inside a document a shared cache will store header check a policy that was configured but never reached the cache layer freshness check revalidation that is enabled but connected to nothing The Decision Record That Outlives the Ticket A short committed note beside the route captures the mode, the window, the constraint that produced it and the endpoint serving personalised content. Each field answers a question the next engineer would otherwise have to reverse-engineer from behaviour. Four fields, kept next to the route mode: incremental Β· the choice itself window: 300 s Β· derived from the tightest shared value reason: price tolerates 5 minutes Β· why, not just what personalised: /_fragment/offer, private no-store

Troubleshooting

What if one value on the route needs to be fresh every request?

Move that value below the personalisation line rather than making the whole route dynamic. A live stock indicator, a rate or a queue position can be rendered by a small per-request fragment or fetched by its island after activation, leaving the document itself cacheable. Making an entire route dynamic for one number is the most common and most expensive over-correction in this area.

How do I decide the revalidation window?

Ask what happens if a visitor acts on a value that is N minutes old. If the answer is nothing, the window can be long. If the answer involves a promise you cannot keep β€” an item that is actually sold out, a price you will not honour β€” the value does not belong in a cached document at all. The window is a statement about acceptable wrongness, and writing it down that way makes it a product decision rather than a guess.

Should the decision ever be revisited?

Yes, whenever the route gains a value it did not have. Adding a greeting to a cached page is a one-line change that quietly makes the page uncacheable or, worse, leaks one visitor's data to another. A comment in the route file naming the mode and the reason gives the next author the context to notice, which no amount of documentation elsewhere achieves.

← Back to Rendering Modes for Islands