In an Expo Router project, what do the web.output values single, static and server produce, and which suits a marketing site?
answer
- unset means single
- one index.html needs rewrites
- HTML per route at export
- generateStaticParams for [id] pages
- server adds dist/server for +api
basics
~20 ssingle 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{
"expo": {
"scheme": "habits",
"web": {
"output": "static"
}
}
}go deeper
Recall the three values of web.output and that single is the default when the field is unset.
Explain what each export writes into dist, why single needs rewrites and hides content from crawlers, and why dynamic routes need generateStaticParams under static.
Tie the choice to hosting: static fits any bucket, server needs a runtime for dist/server, and switching modes changes the deployment contract.
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