# Server rendering: rules for theme code (/themes/server-rendering)

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 [#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 [#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 [#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 [#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 [#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` [#networkallow]

List the hosts your theme calls in `theme.json` to restrict it to them:

```json
{
  "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` [#reading-bundle-files-during-ssr-assetsssr]

`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`:

```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` [#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 [#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 [#next]

- [Chrome stable classes](/themes/chrome-classes): restyle the platform's header, cart and chat
  parts from CSS.
- [Theme assets and media](/themes/theme-assets): ship images and fonts with the theme.
