diff --git a/README.md b/README.md index 02e321a..e942068 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,269 @@ If you have a production web site running WordPress, you can see how its content ## Nginx configuration -**TODO** +Let us look at the [Nginx configuration file](https://github.com/chung-leong/relaks-wordpress-example/blob/master/server/nginx/default.conf). The first two lines tell Nginx where to place cached responses, how large the cache should be (1 GB), and for how long inactive entries are kept until they're deleted (7 days): + +``` +proxy_cache_path /var/cache/nginx/data keys_zone=data:10m max_size=1g inactive=7d; +proxy_temp_path /var/cache/nginx/tmp; +``` + +[`proxy_cache_path`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path) is specified without `levels` so that files are stored in a flat directory structure. This makes it easier to scan the cache. [`proxy_temp_path`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_temp_path) is set to the a location on the same volume as the cache so Nginx can move files into it with a move operation. + +The following section configures reverse-proxying for the WordPress admin page: + +``` +location ~ ^/wp-* { + proxy_pass http://wordpress; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass_header Set-Cookie; + proxy_redirect off; +} +``` + +The following section controls Nginx's interaction with Node: + +``` +location / { + proxy_pass http://node; + proxy_set_header Host $http_host; + proxy_cache data; + proxy_cache_key $uri$is_args$args; + proxy_cache_min_uses 1; + proxy_cache_valid 400 404 1m; + proxy_hide_header Cache-Control; + proxy_ignore_headers Vary; + + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Expose-Headers X-WP-Total; + add_header Cache-Control "public,max-age=0"; + add_header X-Cache-Date $upstream_http_date; + add_header X-Cache-Status $upstream_cache_status; +} +``` + +We select the cache zone we defined earlier with the [`proxy_cache`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache) directive. We set the cache key using [`proxy_cache_key`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_key). The MD5 hash of the path plus the query string will be the name used to save each cached server response. With the [`proxy_cache_min_uses`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_min_uses) directive we tell Nginx to start caching on the very first request. With the [`proxy_cache_valid`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid) directive we ask Nginx to cache error responses for one minute. + +As we're going to add the `Cache-Control` header, we want strip off the one from Node.js first using [`proxy_hide_header`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_hide_header). The [`proxy_ignore_headers`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers) directive, meanwhile, is there to keep Nginx from creating separate cache entries when requests to the same URL have different `Accept-Encoding` headers (additional compression scheme, for example). + +The first two headers we add are there to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). The last two `X-Cache-*` headers are there for debugging purpose. They let us figure out whether a request has resulted in a cache hit when we examine it using the browser's development tools: + +![Chrome Dev Tools](docs/img/dev-tool-x-cache.png) ## Back-end JavaScript -**TODO** +* [HTML page generation](#html-page-generation) +* [JSON data retrieval](#json-data-retrieval) +* [Purge request handling](#purge-request-handling) +* [Timestamp handling](#timestamp-handling) + +### HTML page generation + +```javascript +async function handlePageRequest(req, res, next) { + try { + let path = req.url; + if (path === '/favicon.ico') { + // while the HTML template contains a link tag that suppress the + // loading of favicon.ico, the browser could ask for it still if + // the page fails to load + let err = new Error('File not found'); + err.status = 404; + throw err; + } + let noJS = (req.query.js === '0'); + let target = (req.isSpider() || noJS) ? 'seo' : 'hydrate'; + let page = await PageRenderer.generate(path, target); + if (target === 'seo') { + // not caching content generated for SEO + res.set({ 'X-Accel-Expires': 0 }); + } else { + res.set({ 'Cache-Control': CACHE_CONTROL }); + + // remember the URLs used by the page + pageDependencies[path] = page.sourceURLs; + } + res.type('html').send(page.html); + } catch (err) { + next(err); + } +} +``` + +```javascript +async function generate(path, target) { + console.log(`Regenerating page: ${path}`); + // retrieve cached JSON through Nginx + let host = NGINX_HOST; + // create a fetch() that remembers the URLs used + let sourceURLs = []; + let fetch = (url, options) => { + if (url.startsWith(host)) { + sourceURLs.push(url.substr(host.length)); + options = addHostHeader(options); + } + return CrossFetch(url, options); + }; + let options = { host, path, target, fetch }; + let rootNode = await FrontEnd.render(options); + let appHTML = ReactDOMServer.renderToString(rootNode); + let htmlTemplate = await FS.readFileAsync(HTML_TEMPLATE, 'utf-8'); + let html = htmlTemplate.replace(``, appHTML); + if (target === 'hydrate') { + // add