skip to content

In an Expo Router project, what do the web.output values single, static and server produce, and which suits a marketing site?

level: middleimportance: should knowfreq 40%

answer

  1. unset means single
  2. one index.html needs rewrites
  3. HTML per route at export
  4. generateStaticParams for [id] pages
  5. server adds dist/server for +api

basics

~20 s

single exports one index.html for a client-rendered SPA, static renders an HTML file per route at export time, and server adds a server bundle for API routes. A marketing site wants static, or server once it has its own endpoints.

solid answer

~40 s

`web.output` in the app config decides what `npx expo export --platform web` produces. `single`, the default, is one `index.html`: the router resolves the URL on the client, the host must rewrite every path to it, and crawlers see an empty shell. `static` renders each route to its own HTML file at export, so pages carry real content and head tags and any static host works without rewrites; dynamic routes need `generateStaticParams`. `server` also pre-renders pages but adds `dist/server` with the `+api.ts` routes, so it needs EAS Hosting or an `expo-server` adapter. For a marketing site I pick `static`, and switch to `server` when the waitlist posts to our own API route.

code

json · 8 lines
json
{
  "expo": {
    "scheme": "habits",
    "web": {
      "output": "static"
    }
  }
}

go deeper

for a junior

Recall the three values of web.output and that single is the default when the field is unset.

for a middle

Explain what each export writes into dist, why single needs rewrites and hides content from crawlers, and why dynamic routes need generateStaticParams under static.

for a senior

Tie the choice to hosting: static fits any bucket, server needs a runtime for dist/server, and switching modes changes the deployment contract.

for a principal

Decide how much of the web presence belongs in the app's codebase at all, weighing one file tree for app and site against coupling marketing releases to app builds.

## One file tree, three web builds An **Expo Router** project describes its screens as files under `src/app`. When the same project targets the web, the app config's `web.output` field decides what `npx expo export --platform web` writes into the `dist` directory. There are three values, and when the field is unset Expo uses `single`. | `web.output` | What `dist` contains | Hosting needs | Indexable per route? | |---|---|---|---| | `single` | One `index.html` plus the JavaScript bundle | Any static host, with every path rewritten to `/index.html` | No | | `static` | An HTML file per route, rendered at export time | Any static host, no rewrites | Yes | | `server` | `dist/client` (HTML and assets) plus `dist/server` (API routes, routes manifest) | A server runtime | Yes | ## `single`: a single-page application With `single`, the export produces one HTML shell. The browser downloads it, runs the bundle, and Expo Router works out which screen to show from the URL **on the client**. Two consequences: - The host must **rewrite every path to `/index.html`**, or a reload of `/pricing` returns the host's 404 page. - Crawlers and link-preview bots that do not run JavaScript see an empty page, so per-page titles and descriptions are invisible to them. The HTML template comes from `public/index.html`. Expo's migration guide calls this mode "not recommended" for Expo Router sites. ## `static`: HTML for every route With `static`, Expo CLI renders each route to its own HTML file during export (for example `dist/pricing.html`), then the client bundle **hydrates** it, taking over the already-rendered markup so the page becomes interactive. This is the mode for a **marketing site**: - Each page has real content and head tags in its HTML, set per page with the `Head` component from `expo-router/head`. - The output is a collection of HTML files, not an SPA, so no rewrite rules are needed. - **Dynamic segments** such as `src/app/blog/[slug].tsx` produce no page unless the route exports `generateStaticParams`, a build-time function that returns the parameter sets to render. - The root HTML shell can be customised in `src/app/+html.tsx`. - **No server code runs**: `+api.ts` files are not exported, and pages cannot render per request. ## `server`: static pages plus server code With `server`, the export still pre-renders HTML for routes, but it also bundles every **API route** (`+api.ts` files) into `dist/server`. That output needs something to execute it: EAS Hosting (`eas deploy`), or a host that runs the `expo-server` package through one of its adapters. `npx expo serve` runs the export locally. Two further capabilities hang off this mode: 1. **Server rendering**, rendering pages per request instead of at export time, is an **alpha** feature from SDK 55, enabled with the `expo-router` plugin option `unstable_useServerRendering`. 2. `npx expo export --platform web --no-ssg` skips the website and exports only the server code; it is rejected unless `web.output` is `server`. ## Choosing for a marketing site with a waitlist For a habit-tracker app whose web presence is a landing page, a pricing page and a waitlist form, the choice falls out of two questions: 1. **Does the site need to be indexed and previewed well?** Yes, so `single` is out. 2. **Does the form post to your own server code?** If the waitlist posts to a third-party form service, `static` is enough and the site can live on any static host. If it posts to your own `+api.ts` endpoint, choose `server`. In practice many teams start with `static` and move to `server` the day they add their first API route. The switch is a one-line config change, but it changes the **hosting contract**: a static bucket can no longer serve the site on its own. ## Common mistakes - Setting `static` and expecting `[id]` pages to exist for arbitrary ids at runtime; only the ids returned by `generateStaticParams` are generated. - Adding SPA rewrites to a `static` deployment; they are unnecessary and can hide real 404s. - Using `server` output and uploading only `dist/client` to a static host, which silently drops every API route. - Expecting per-page head tags in a `single` build to reach crawlers; they are applied only after the JavaScript runs in the browser. These values and defaults are those of Expo SDK 57 with Expo Router 57.x.

  • With web.output set to static, why does /blog/new-feature return a 404 after deployment?
    Static output writes HTML only for routes known at export time. A dynamic route such as `src/app/blog/[slug].tsx` produces a page only for the parameter sets its `generateStaticParams` function returns. If `new-feature` was not in that list, no file exists for it. Add it to `generateStaticParams` and re-export, or move to `server` output if pages must exist for arbitrary slugs.
  • What breaks if a team deploys only dist/client from a server-output export to a static host?
    The pages and assets load, but every API route disappears, because `+api.ts` handlers live in `dist/server` and need a runtime to execute them. The waitlist form's POST then fails. Server output must be deployed to EAS Hosting or to a host running an `expo-server` adapter that serves `dist/client` and delegates other requests to `dist/server`.

saying these in an interview costs you the question

  • static output generates pages for any [id] at runtime
  • single output gives each route its own indexable HTML file
  • A static export needs SPA rewrites to index.html
  • API routes work on any static host with static output
  • static output needs a Node.js server in production
  • server output means every page renders per request by default