# Single-page application

URL: https://scalws.com/docs/howto/spa/
Source: `docs/howto/spa.md` in the ScalWS repository

Serve the build output of a single-page application (React, Vue, Svelte, ...) as static
files, with precompressed assets and client-side routes falling back to `index.html`.

## Prerequisites

* ScalWS installed, with `include: sites.d/*.yaml` in `/etc/scalws/scalws.yaml`.
* The built application in a directory outside `/home`, for example `/srv/spa/dist`,
  readable by group `scalws`.

## Precompressed files

With [`static.precompressed`](https://scalws.com/docs/reference/site-file/#static.precompressed) ScalWS
serves `<file>.br` or `<file>.gz` instead of `<file>` to clients that accept that
encoding, with `Vary: Accept-Encoding`. The compressed file must not be older than the
original. Create them after every build, for example:

```sh
cd /srv/spa/dist
find . -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' -o -name '*.svg' -o -name '*.json' \) \
    -exec brotli -kf {} \; -exec gzip -kf9 {} \;
```

Many bundlers can write the `.br` and `.gz` files themselves.

## The history fallback

The static runtime has no "fall back to `index.html`" setting: a request for
`/settings/profile` looks for that file and answers `404`. Client-side routes are sent to
`index.html` with an `.htaccess` file in the document root, which ScalWS reads with
[`htaccess: true`](https://scalws.com/docs/reference/site-file/#htaccess)
([.htaccess support](https://scalws.com/docs/howto/htaccess/)).

`/srv/spa/dist/.htaccess`:

```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
```

Existing files and directories are served as they are; every other path is rewritten
internally to `/index.html`. `.htaccess` itself is never served (dotfiles answer `404`).
Many build tools copy files from a `public/` folder into the output, so you can keep the
`.htaccess` there.

## The site file

```sh
sudo scalwsctl site add spa.example.com --root /srv/spa/dist --htaccess
```

Then add `precompressed`:

```yaml
# /etc/scalws/sites.d/spa.example.com.yaml
domain: spa.example.com
root: /srv/spa/dist
static: {precompressed: true}
htaccess: true
https_redirect: true
```

The `.htaccess` files are looked up below the site's
[`root`](https://scalws.com/docs/reference/site-file/#root), so `root` must be the build directory (or a
directory above it that holds the `.htaccess`). Static sites work under both
scalws-controller and `scalwsd.service`.

An API on the same domain can be added as a route; see [reverse proxy](https://scalws.com/docs/howto/reverse-proxy/):

```yaml
domain: spa.example.com
root: /srv/spa/dist
static: {precompressed: true}
htaccess: true
routes:
  - path: /api
    runtime: {type: proxy, upstream: "http://127.0.0.1:4000"}
```

`.htaccess` rules run for every request of the site before the route is chosen, and a
rewritten path selects the route again. The fallback rule above would therefore rewrite
`/api/users` (no such file) to `/index.html`. Exclude the API path:

```apache
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/api(/|$)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
```

## Check it works

```sh
curl -sI -H 'Host: spa.example.com' http://127.0.0.1/                    # 200, index.html
curl -sI -H 'Host: spa.example.com' http://127.0.0.1/settings/profile    # 200, index.html
curl -sI -H 'Host: spa.example.com' -H 'Accept-Encoding: br' \
     http://127.0.0.1/assets/app.js | grep -iE '^(content-encoding|vary)'  # br, Accept-Encoding
```

## Common problems

* **Client-side routes answer 404**: `htaccess: true` is missing, the `.htaccess` is not
  below `root`, or it is not readable by `scalws`. Edits are picked up within 2 seconds.
* **No `Content-Encoding`**: `precompressed` is not set, the client did not send a matching
  `Accept-Encoding`, or the `.br`/`.gz` file is older than the original (rebuild, then
  compress again).
* **A missing asset returns `index.html` with `200`**: that is the fallback rule at work
  for every path that is not a file.
* **API requests get `index.html`**: the fallback rule rewrote them before routing; add
  the `RewriteCond %{REQUEST_URI} !^/api(/|$)` line shown above.
* **Only `GET` and `HEAD` work**: the static runtime serves nothing else; send other
  methods to an API route.

See also: [applications: static](https://scalws.com/docs/guide/applications/#static),
[static SPA example](https://scalws.com/docs/guide/applications/#static-single-page-application).
