1---
2title: Static Rendering
3description: Learn how to render routes to static HTML and CSS files with Expo Router.
4hideFromSearch: true
5---
6
7import { Terminal } from '~/ui/components/Snippet';
8import { FileTree } from '~/ui/components/FileTree';
9
10> This guide requires `[email protected]` or higher — everything listed here is still experimental. To use these features, you may need to [use Expo CLI on `main`](https://github.com/expo/expo/tree/main/packages/%40expo/cli#contributing).
11
12To enable Search Engine Optimization (SEO) on the web you must statically render your app. This guide will walk you through the process of statically rendering your Expo Router app.
13
14## Setup
15
16Start by enabling static rendering in the project's [app config](/versions/latest/config/app/) (only available when `web.bundler` is set to `metro`):
17
18```json app.json
19{
20  "expo": {
21    "web": {
22      "bundler": "metro",
23      "output": "static"
24    }
25  }
26}
27```
28
29Next, if you have a **metro.config.js** in your project, ensure it extends `expo/metro-config` as shown below:
30
31```js metro.config.js
32// Learn more: https://docs.expo.dev/guides/customizing-metro/
33const { getDefaultConfig } = require('expo/metro-config');
34
35/** @type {import('expo/metro-config').MetroConfig} */
36const config = getDefaultConfig(__dirname, {
37  // Additional features...
38});
39
40module.exports = config;
41```
42
43Finally, start the development server:
44
45<Terminal cmd={['$ npx expo start']} />
46
47## Production
48
49To bundle your static website for production, run the following command:
50
51<Terminal cmd={['$ npx expo export --platform web']} />
52
53This will create a **dist** directory with your statically rendered website. You can test this production build locally by running the following command and opening the linked URL in your browser:
54
55<Terminal cmd={['$ npx serve dist']} />
56
57This project can be deployed to almost every hosting service. Note that this is not a single-page application, nor does it contain a custom server API. This means dynamic routes (**app/[id].tsx**) will not arbitrarily work. You may need to build a serverless function to handle dynamic routes.
58
59## Dynamic Routes
60
61The `static` output will generate HTML files for each route. This means dynamic routes (**app/[id].tsx**) will not work out of the box. You can generate known routes ahead of time using the `generateStaticParams` function.
62
63```js app/blog/[id].tsx
64import { Text } from 'react-native';
65import { useLocalSearchParams } from 'expo-router';
66
67export async function generateStaticParams() {
68  const posts = await getPosts();
69  // Return an array of params to generate static HTML files for.
70  // Each entry in the array will be a new page.
71  return posts.map(post => ({ id: post.id }));
72}
73
74export default function Page() {
75  const { id } = useLocalSearchParams();
76
77  return <Text>Post {id}</Text>;
78}
79```
80
81This will output a file for each post in the **dist** directory. For example, if the `generateStaticParams` method returned `[{ id: "alpha" }, { id: "beta" }]`, the following files would be generated:
82
83<FileTree files={['dist/blog/alpha.html', 'dist/blog/beta.html']} />
84
85## Root HTML
86
87By default, every page is wrapped with some small HTML boilerplate, this is known as the **root HTML**.
88
89You can customize the root HTML file by creating an **app/+html.tsx** file in your project. This file exports a React component that only ever runs in Node.js, which means global CSS cannot be imported inside of it. The component will wrap all routes in the **app** directory. This is useful for adding global `<head>` elements or disabling body scrolling.
90
91> **Note**: Global context providers should go in the [Root Layout](/router/advanced/root-layout) component, not the Root HTML component.
92
93```js app/+html.tsx
94import { ScrollViewStyleReset } from 'expo-router/html';
95
96// This file is web-only and used to configure the root HTML for every
97// web page during static rendering.
98// The contents of this function only run in Node.js environments and
99// do not have access to the DOM or browser APIs.
100export default function Root({ children }: { children: React.ReactNode }) {
101  return (
102    <html lang="en">
103      <head>
104        <meta charSet="utf-8" />
105        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
106
107        {/*
108          This viewport disables scaling which makes the mobile website act more like a native app.
109          However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
110            <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
111        */}
112        <meta
113          name="viewport"
114          content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
115        />
116        {/*
117          Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
118          However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
119        */}
120        <ScrollViewStyleReset />
121
122        {/* Add any additional <head> elements that you want globally available on web... */}
123      </head>
124      <body>{children}</body>
125    </html>
126  );
127}
128```
129
130- The `children` prop comes with the root `<div id="root" />` tag included inside.
131- The JavaScript scripts are appended after the static render.
132- React Native web styles are statically injected automatically.
133- Global CSS should not be imported into this file. Instead, use the [Root Layout](/router/advanced/root-layout) component.
134- Browser APIs like `window.location` are unavailable in this component as it only runs in Node.js during static rendering.
135
136### `expo-router/html`
137
138The exports from `expo-router/html` are related to the Root HTML component.
139
140- `ScrollViewStyleReset`: Root style-reset for full-screen [React Native web apps](https://necolas.github.io/react-native-web/docs/setup/#root-element) with a root `<ScrollView />` should use the following styles to ensure native parity.
141
142## Meta Tags
143
144You can add meta tags to your pages with the [`<Head />`](/docs/features/head) module from `expo-router`:
145
146```js app/about.tsx
147import Head from 'expo-router/head';
148import { Text } from 'react-native';
149
150export default function Page() {
151  return (
152    <>
153      <Head>
154        <title>My Blog Website</title>
155        <meta name="description" content="This is my blog." />
156      </Head>
157      <Text>About my blog</Text>
158    </>
159  );
160}
161```
162
163The head elements can be updated dynamically using the same API. However, it's useful for SEO to have static head elements rendered ahead of time.
164
165## Static Files
166
167Expo CLI supports a root **public** directory that gets copied to the **dist** folder during static rendering. This is useful for adding static files like images, fonts, and other assets.
168
169<FileTree
170  files={['public/favicon.ico', 'public/logo.png', 'public/.well-known/apple-app-site-association']}
171/>
172
173These files will be copied to the **dist** folder during static rendering:
174
175<FileTree
176  files={[
177    'dist/index.html',
178    'dist/favicon.ico',
179    'dist/logo.png',
180    'dist/.well-known/apple-app-site-association',
181    'dist/_expo/static/js/index-xxx.js',
182    'dist/_expo/static/css/index-xxx.css',
183  ]}
184/>
185
186> **info** **Web only**: Static assets can be accessed in runtime code using relative paths. For example, the **logo.png** can be accessed at `/logo.png`:
187
188```js app/index.tsx
189import { Image } from 'react-native';
190
191export default function Page() {
192  return <Image source={{ uri: '/logo.png' }} />;
193}
194```
195
196## FAQ
197
198### How do I add a custom server?
199
200As of Expo Router v2 there is no prescriptive way to add a custom server. You can use any server you want. However, you will need to handle dynamic routes yourself. You can use the `generateStaticParams` function to generate static HTML files for known routes.
201
202In future, there will be a server API, and a new `web.output` mode which will generate a project that will (amongst other things) support dynamic routes.
203
204## Server-side Rendering
205
206Rendering at request-time (SSR) is not supported in `web.output: 'static'`. This will likely be added in a future version of Expo Router.
207
208### Where can I deploy statically rendered websites?
209
210You can deploy your statically rendered website to any static hosting service. Here are some popular options:
211
212- [Netlify](https://www.netlify.com/)
213- [Cloudflare Pages](https://pages.cloudflare.com/)
214- [AWS Amplify](https://aws.amazon.com/amplify/)
215- [Vercel](https://vercel.com/)
216- [GitHub Pages](https://pages.github.com/)
217- [Render](https://render.com/)
218- [Surge](https://surge.sh/)
219
220> Note: You don't need to add Single-Page Application styled redirects to your static hosting service. The static website is not a single-page application. It is a collection of static HTML files.
221