How-to
Single-page application
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/*.yamlin/etc/scalws/scalws.yaml. - The built application in a directory outside
/home, for example/srv/spa/dist, readable by groupscalws.
Precompressed files
With 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:
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
(.htaccess support).
/srv/spa/dist/.htaccess:
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
sudo scalwsctl site add spa.example.com --root /srv/spa/dist --htaccessThen add precompressed:
# /etc/scalws/sites.d/spa.example.com.yaml
domain: spa.example.com
root: /srv/spa/dist
static: {precompressed: true}
htaccess: true
https_redirect: trueThe .htaccess files are looked up below the site’s
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:
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:
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/api(/|$)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]Check it works
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-EncodingCommon problems
- Client-side routes answer 404:
htaccess: trueis missing, the.htaccessis not belowroot, or it is not readable byscalws. Edits are picked up within 2 seconds. - No
Content-Encoding:precompressedis not set, the client did not send a matchingAccept-Encoding, or the.br/.gzfile is older than the original (rebuild, then compress again). - A missing asset returns
index.htmlwith200: 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 theRewriteCond %{REQUEST_URI} !^/api(/|$)line shown above. - Only
GETandHEADwork: the static runtime serves nothing else; send other methods to an API route.
See also: applications: static, static SPA example.