Developers
Start building
Themes/Server rendering: rules for theme code

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 Suspense boundary 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 and http: on port 80, to public host names. Private networks, IP literals and local names are unreachable.
  • Your own request headers are sent, cookie and authorization included, 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, .svg and .csv files, 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: every PriceView arrives resolved.
  • islands: on the home page, category pages, /products and the header's featured search products, each PriceView carries only its ref, usePrice returns null, and the price parts show live prices filled in by the storefront. The product page keeps its prices resolved in both modes. Choose islands only 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 .js files plus the assets.ssr files);
  • 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

Last updated