Server rendering: rules for theme code
What changes for theme code that renders on the server: shared module scope, time, server-side fetch, SSR assets, pricing mode and bundle limits.
On this page
Every storefront page renders its theme fragments (header, footer, sections, blocks, theme pages, checkout and account templates) on the server, and the browser then hydrates the same markup. This applies to every store and every theme.
Most theme code needs no change. The rules below cover what behaves differently from a single browser tab.
Module scope is shared by every shopper
Your modules are loaded once and then render pages for every shopper of the store at the same time. Anything
at module scope (a Map used as a cache, an array you push to, a let counter) is seen by the next shopper's
render, possibly in the middle of yours.
- Keep per-request state in React state, props or context.
- A module-scope cache is fine for store-level public data (a CMS response every shopper sees), never for anything derived from the shopper: cart, customer, locale choice, search.
- The contexts the platform hands you (settings, messages, the theme document) are frozen; mutating them throws.
The theme check warns (module-state) when a module-scope container or let is changed from inside a function.
It is a warning, not an error: a store-level cache is legitimate, and only you know which one you wrote.
Read the time inside components
Time read at module scope is stale: Date.now(), performance.now() and new Date() there return a moment long
before the current render. A module-scope "new" badge, countdown or cache timestamp is wrong for every later
shopper.
Read the time inside the component or loader that needs it. The theme check warns (module-state) on clock reads
at module scope.
Events inside a fragment
Each fragment hydrates as its own React root. A handler inside a fragment that calls e.stopPropagation() also
hides the event from listeners the platform attaches to document (analytics, the cart drawer's outside click,
keyboard shortcuts). Stop propagation only when you mean to hide the event from the whole page.
A slow fetch renders its fallback
Server fetches must be fast. When one is slow, the page does not wait for it: every Suspense boundary still
waiting is sent in its fallback state, and the browser renders it after hydration. Your design still renders,
and only the waiting part is late.
- Put data you fetch on the server behind a
Suspenseboundary with a meaningful fallback. - A fetch that times out rejects with a typed error that the nearest boundary catches.
- A host that keeps failing is skipped for a while, so its boundaries fall back at once.
Server-side fetch
fetch() during server rendering reaches the public internet:
https:on port 443 andhttp:on port 80, to public host names. Private networks, IP literals and local names are unreachable.- Your own request headers are sent,
cookieandauthorizationincluded, so you can call your API with your key. - The status and the response headers reach your code. Nothing from a server-side response reaches the shopper's headers or cookies.
network.allow
List the hosts your theme calls in theme.json to restrict it to them:
{
"network": { "allow": ["cms.example.com", "*.cdn.example.org"] }
}example.com matches only itself. *.example.com matches every name below it, but not example.com itself.
Without network.allow the theme may call any public host. When a publish would let the theme reach a host the
published version could not (a new host, or dropping the list altogether), the merchant must confirm the new hosts
on the theme page before it goes live. A narrower list, or a first list, needs no confirmation.
Reading bundle files during SSR: assets.ssr
import data from './stores.json' works: the build inlines the file into your code.
For text files you want to read at render time instead, list them in theme.json:
{
"assets": { "ssr": ["src/data/stores.json", "src/content/faq.md"] }
}- Only
.json,.txt,.md,.svgand.csvfiles, at most 256 KiB each, paths inside the theme. - During server rendering,
fetch('/theme-assets/assets/src/content/faq.md')answers from the theme, and anything not listed is refused. This path answers only on the server. In the browser, import the file instead.
Pricing: inline or islands
theme.json may set "pricing": "inline" (the default) or "pricing": "islands".
Every price a theme receives is a PriceView from @poltura/theme-sdk/price: a ref the
storefront fills a price into, and resolved, the worked-out amounts. Draw it with Price,
PriceRange, CompareAtPrice, DiscountBadge or UnitPrice; read the amounts with
usePrice(price) only when code must compute with them.
inline: everyPriceViewarrives resolved.islands: on the home page, category pages,/productsand the header's featured search products, eachPriceViewcarries only itsref,usePricereturns null, and the price parts show live prices filled in by the storefront. The product page keeps its prices resolved in both modes. Chooseislandsonly if your components never compute with resolved amounts on those pages.
The default theme renders islands. A theme whose code is still the default theme's builds with
islands without declaring it; once you change its code it builds inline unless your
theme.json says otherwise.
Bundle limits
The server half of a built theme must fit these limits, or the build fails with the error
code THEME_GRAPH_LIMITS:
- at most 256 modules (the server
.jsfiles plus theassets.ssrfiles); - at most 1 MiB per module;
- at most 8 path segments and 200 characters per file path, each segment starting with a letter or digit.
An invalid assets.ssr, network.allow or pricing fails the build with the error code
THEME_SETTINGS_INVALID, and the theme check reports it against theme.json.
Next
- Chrome stable classes: restyle the platform's header, cart and chat parts from CSS.
- Theme assets and media: ship images and fonts with the theme.