To use this proxy you will (obviously) need an origin server that will be providing the original HTML documents of your SPA.
One way to set this up is like the following:
+-------------------------------------------------------------------------------------------------+
| CDN / reverse proxy server |
| https://my-app.somewhere |
| Splits the incoming traffic: |
| |
| /api/... images/stylesheets/ pages (html documents) |
| | other static assets | |
| | | | |
+-----|--------------------------------------|-------------------|--------------------------------+
| | |
| | |
| | |
v | v
+----------------------------------------+ | +----------------------------------+
| API server | | | spa-ssr-proxy |
| https://api.origin.my-app.somewhere | | | |
| | | | https://ssr.my-app.somewhere |
| serves /api/... requests | | | |
| | | | Proxies requests, executes JS, |
+----------------------------------------+ | | and serves the generated HTML |
| | | |
| +----|-----------------------------+
| |
v v
+----------------------------------------+
|Static origin server |
| |
|https://static.origin.my-app.somwhere |
| |
| serves all static assets: |
| - images |
| - stylesheets |
| - fonts |
| - javascript |
| - html |
| = etc |
| |
+----------------------------------------+
Static origin server
-- you deploy your web-app assets to this server (the app should be able to work when served from this server)API server
-- your back-end (API), if anyspa-ssr-proxy
-- this proxyCDN / reverse proxy server
-- the server your users will be actually served by
CDN / reverse proxy server
needs to be configured to split the traffic:
- API requests should be routed to your API server
- images/stylesheets and other static assets should be routed to the
Static origin server
(to which you deploy your static app). - URLs that are pages (paths that are handled by your SPA) should be forwarded to the
spa-ssr-proxy
.
When spa-ssr-proxy
receives a request, it replaces the domain name with the domain of Static origin server
(specified in the configuration), and loads that URL in a headless browser. After the scripts are executed the
response is served back to the user. User's browser then loads all the assets and runs your SPA.
spa-ssr-proxy
can be configured to cache the rendered pages (by URL). If caching is disabled, all requests will be
rendered in the headless browser.
spa-ssr-proxy
can be configured to pre-render a set of pages (only makes sense with caching enabled). This way
even the first user (or Google) will see the response quickly.
There is an admin endpoint to clear the caches (and optionally re-run pre-rendering):
POST https://ssr.my-app.somewhere/__ssr/admin/clear-cache?pre-render
Authorization: Bearer ${access-key}
Access key if specified in the configuration. pre-render
parameter is optional, if not set -- pre-rendering will not
be re-run after clearing the cache.
When the page is rendered inside the headless browser, we need a way to know if your app has finished rendering the page.
For that, spa-ssr-proxy
will first wait for all HTTP connections to be closed.
After that, it will wait for an element with a particular CSS selector to appear in the dom.
It's configured by pageConfig.waitSelector
.
The value in the template is title#title
- which means the page is considered rendered as soon as there is a
<title>
element with id=title
(you can tweak your APP to do that when it knows it has finished rendering).
For the pre-render (which will work the fastest if the history
API is used to navigate through the specified list of pages, as the headless browser will not re-load/re-run the whole scrip) we need a way to clear the above "completion signal". This is specified by pageConfig.resetSelectorScript
, in the template it's set to this:
document.head.querySelector('title').removeAttribute('id')
Which does what we want -- removes the id
attribute from the <title>
element, so when your app sets it again,
the page is considered to be rendered.
If order to navigate while pre-rendering, you need to tell the spa-ssr-proxy
how to do that. This is configured by
pageConfig.navigateScript
.
In the template it's set to
window.routeTo('${url}')
(${url}
will be replaced by the page URL).
For this to work, you'll need to set window.routeTo = (url) => { do the things to route to ${url}, like pushState(null, null, url) }
in your app.
Or you can specify a different navigateScript
.
There is a number of other configuration options to control the way your app is run in the headless browser (like aborting requests for CSS/images/fonts or other resources that you don't want to be accessed from within the headless browser).
A few options allow to control the verbosity of the spa-ssr-proxy
log.
A brief description is below.
(config.template.json
)
{
"cacheResponses": true, // whether or not to cache the responses
// if set to false pages will be requested and rendered on every request
"prerender": true, // if set to true the specified pages will be pre-rendered (and cached, if cacheResponses=true) on boot
"target": "https://origin-server", // the origin where the requests will be proxied
"adminAccessKey": "secret-access-key", // access key for admin access (to clear the cache/re-render)
"pageConfig": {
"waitSelector": "title#title", // see the explanation above
"resetSelectorScript": "document.head.querySelector('title').removeAttribute('id')",
// see the explanation below
"navigateScript": "window.routeTo('${url}')", // see the explanation below
"logErrors": true, // if true, will log page errors to the console
"logConsole": false, // if true, will log all page console output to the console
"logResponses": false, // if true, will log hxr responses to the console
"logFailedRequests": true, // if true, will log failed xhr requests, unless intentionally aborted
"abortResourceRequests": true, // intentionally abort requests to images, stylesheets and fonts
"requestBlacklist": [ // intentionally block requests with URL matching any of the regexes in the list
"regex1", // for example "\/video\/" -- will block all xhr requests that have "video" in the url
"regex2",
// ...
]
},
"prerenderPaths": [ // the pages to be pre-rendered on boot (if prerender=true)
"/page-1",
"/page-2/page-3"
]
}
To run the spa-ssr-proxy
in docker, first, prepare the config.json
(use the provided config.template.json
as a starting point).
Then, run the docker container:
docker run --rm -it -v (pwd)/config.json:/home/node/spa-ssr-proxy/config.json -p 30090:8080 yurique/spa-ssr-proxy:latest
-v (pwd)/config.json:/home/node/spa-ssr-proxy/config.json
-- mounts yourconfig.json
file into the working dir of the node app-p 30090:8080
--spa-ssr-proxy
binds to port30090
, this binds it to port8080
on the host machine (can be any port that works for you)--rm
-- to remove the container after it shuts down-it
-- if you want it to be run in foreground (andctrl-c
it)
After this, you will need to set up the other parts of the setup.
For local development/testing, the following should work:
nginx
as a CDN / reverse proxy server
:
upstream backend {
server 127.0.0.1:30070;
}
upstream frontend {
server 127.0.0.1:30080; // might be a webpack dev-server
}
upstream ssr {
server 127.0.0.1:8080;
}
server {
server_name http://static.origin.my-app.local;
# this is needed as inside the headless browser your app will believe it's running
# at static.origin.my-app.local, and if you send API requests to /api/.. this will handle it
# the same rule will be below in the ssr server
#
# Alternatively, you can run you API on a separate domain (like api.my-app.local)
location ~ /api/ {
proxy_pass http://backend;
}
location / {
proxy_pass http://frontend;
}
}
server {
server_name http://my-app.local;
location ~ /api/ {
proxy_pass http://saa-backend;
}
location / {
proxy_pass http://ssr;
}
}
# if you run your API server on a separate domain
server {
server_name http://api.my-app.local;
location / {
proxy_pass http://backend;
}
}
[[ work in progress ]]