From a2ed7d67d2f1f08fd9167e9abc07616c3acd41c0 Mon Sep 17 00:00:00 2001 From: Chung Leong Date: Sat, 26 Jan 2019 11:15:27 +0100 Subject: [PATCH] Added sections to README (issue #1). Added back-end services diagram. --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++- docs/img/services.png | Bin 0 -> 21014 bytes 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 docs/img/services.png diff --git a/README.md b/README.md index e2209b5..679a964 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,65 @@ +Zero-latency WordPress Front-end +================================ +In this example, we're going to build a Zero-latency front-end for WordPress. When a visitor clicks on a link, a story will instantly appear. No hourglass. No spinner. No blank page. We'll accomplish this by aggressively prefetching data in our client-side code. At the same time, we're going to employ server-side rendering (SSR) to minimize time-to-first-impression. + +This is a complex example with many moving parts. It's definitely not for beginners. You should already be familiar with technologies involved: [React](https://reactjs.org/), [Nginx caching](https://www.nginx.com/blog/nginx-caching-guide/), and of course [WordPress](https://wordpress.org/) itself. + +* [Live demo](#live-demo) +* [Server-side rendering](#server-side-rendering) +* [Back-end services](#back-end-services) +* [Uncached page access](#uncached-page-access) +* [Cached page access](#cached-page-access) +* [Cache invalidation](#cache-invalidation) +* [Getting started](#getting-started) +* [Docker Compose configuration](#docker-compose-configuration) +* [Nginx configuration](#nginx-configuration) +* [Back-end JavaScript](#back-end-javaScript) +* [Front-end JavaScript](#front-end-javaScript) +* [Cordova deployment](#cordova-deployment) +* [Final words](#final-words) + +## Live demo + +**TODO** + +## Server-side rendering + +Isomorphic React components are capable of rendering on a web server as well as in a web browser. A primary purpose of server-side rendering (SSR) is search engine optimization. Another is to mask JavaScript loading time. Rather than displaying a spinner or progress bar, we render the front-end on the server and send that to the browser. Effectively, we're using the front-end's own appearance as its loading screen. + +The following animation depicts how an SSR-augmented single-page web-site works. Click on it if you wish to view it as separate images. + +[![Server-side rendering](docs/img/ssr.gif)](docs/ssr.md) + +While the SSR HTML is not backed by JavaScript, it does have functional hyperlinks. If the visitor clicks on a link before the JavaScript bundle is loaded, he'll end up at another SSR page. As the server has immediate access to both code and data, it can generate this page very quickly. It's also possible that the page exists in the server-side cache, in which case it'll be sent even sooner. + +## Back-end services + +Our back-end consists of three services: WordPress itself, Nginx, and Node.js. The following diagram shows how contents of various types move between them: + +![Back-end services](docs/img/services.png) + +Note how Nginx does not fetch JSON data directly from WordPress. Instead, data goes through Node first. This detour is due mainly to WordPress not attaching [e-tags](https://en.wikipedia.org/wiki/HTTP_ETag) to JSON responses. Without e-tags the browser cannot perform cache validation (i.e. conditional request → 304 not modified). Passing the data through Node also gives us a chance to strip out unnecessary fields. Finally, it lets us compress the data prior to sending it to Nginx. Size reduction means more contents will fit in the cache. It also saves Nginx from having to gzip the same data over and over again. + +Node will request JSON data from Nginx when it runs the front-end code. If the data isn't found in the cache, Node will end up serving its own request. This round-trip will result in Nginx caching the JSON data. We want that to happen since the browser will soon be requesting the same data (since it's running the same front-end code). + +## Uncached page access + +The following animation shows what happens when the browser requests a page and Nginx's cache is empty. Click on it to view it as separate images. + [![Uncached page access](docs/img/uncached.gif)](docs/uncached.md) +## Cached page access + +The following animation shows how page requests are handled once contents (both HTML and JSON) are cached. This is what happens most of the time. + [![Cached page access](docs/img/cached.gif)](docs/cached.md) +## Cache invalidation + +**TODO** + +## Getting started + 1. Run docker-compose up -d 2. Go to http://localhost:8000/wp-admin/ 3. Enter site info @@ -12,5 +70,28 @@ 8. Search for, install, and activate "Proxy Cache Purge" plugin 9. Search for, install, and activate "FakerPress" plugin - docker exec server_wordpress_1 php -r "echo gethostbyname('node');" + +## Docker Compose configuration + +**TODO** + +## Nginx configuration + +**TODO** + +## Back-end JavaScript + +**TODO** + +## Front-end JavaScript + +**TODO** + +## Cordova deployment + +**TODO** + +## Final words + +**TODO** diff --git a/docs/img/services.png b/docs/img/services.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc799bb433471d05befd0d1a0586f97068d70a5 GIT binary patch literal 21014 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