1
0
mirror of https://github.com/trambarhq/relaks-wordpress-example.git synced 2025-09-03 05:02:34 +02:00

Refactored routing code.

Implemented periodic freshness check (issue #8).
Implemented proper cache purge (issue #10).
Implemented compression in Node side.
Implemented JSON retrieval through Node.
This commit is contained in:
Chung Leong
2019-01-21 22:03:44 +01:00
parent e03bddcb6b
commit ddea511ee7
30 changed files with 485 additions and 316 deletions

View File

@@ -2,14 +2,11 @@
2. Go to http://localhost:8000/wp-admin/ 2. Go to http://localhost:8000/wp-admin/
3. Enter site info 3. Enter site info
4. Log in 4. Log in
5. Go to Plugins page 5. Go to Settings > Permalinks
6. Search for, install, and activate "Demo Data Creator" plugin 6. Select a scheme other than "Plain" (to enable clean JSON URLs)
7. Search for, install, and activate "Proxy Cache Purge" plugin 7. Go to Plugins page
8. Go to Tools > Demo Data Creator 8. Search for, install, and activate "Proxy Cache Purge" plugin
9. Create demo users 9. Search for, install, and activate "FakerPress" plugin
10. Create demo categories
11. Create demo pages
12. Create demo posts docker exec server_wordpress_1 php -r "echo gethostbyname('node');"
13. Create demo comments
14. Go to Settings > Permalinks
15. Select a scheme other than "Plain" (to enable cleaner JSON URLs)

47
package-lock.json generated
View File

@@ -1413,10 +1413,9 @@
} }
}, },
"bluebird": { "bluebird": {
"version": "3.5.1", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
"dev": true
}, },
"bn.js": { "bn.js": {
"version": "4.11.8", "version": "4.11.8",
@@ -1893,19 +1892,24 @@
"dev": true "dev": true
}, },
"compressible": { "compressible": {
"version": "2.0.14", "version": "2.0.15",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz",
"integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==",
"dev": true,
"requires": { "requires": {
"mime-db": ">= 1.34.0 < 2" "mime-db": ">= 1.36.0 < 2"
},
"dependencies": {
"mime-db": {
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
}
} }
}, },
"compression": { "compression": {
"version": "1.7.3", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz",
"integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==",
"dev": true,
"requires": { "requires": {
"accepts": "~1.3.5", "accepts": "~1.3.5",
"bytes": "3.0.0", "bytes": "3.0.0",
@@ -6638,15 +6642,15 @@
} }
}, },
"relaks": { "relaks": {
"version": "1.1.8", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/relaks/-/relaks-1.1.8.tgz", "resolved": "https://registry.npmjs.org/relaks/-/relaks-1.1.9.tgz",
"integrity": "sha512-bu9mI7qEvKdPMJ+9wykM1TryRORvX+0CC0LSEIo9zglSgRbMDVdFHQP41mOafA7JWSkYWN8BjvFoVILIcYwGGw==", "integrity": "sha512-FY3pBgbTS25+/mv4Q8plNOBivUlZCN5KScBgxUprmUPk+clcOnptSi3x8GjRBoWdV2/bx89XzzCP4fApdJ1kNA==",
"dev": true "dev": true
}, },
"relaks-event-emitter": { "relaks-event-emitter": {
"version": "0.0.1", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/relaks-event-emitter/-/relaks-event-emitter-0.0.1.tgz", "resolved": "https://registry.npmjs.org/relaks-event-emitter/-/relaks-event-emitter-0.0.6.tgz",
"integrity": "sha512-h2O9+308vjxkR9YTt3pJYwwVr0JpHi4A6mK7KHBXqhigWbG1gXAZ0O6bHV6E8HjWY3A16zMTqtotGq+T8xtSMw==", "integrity": "sha512-scNhLQeH7v1q+CRJFRWVYDUI4kES+P4suQzoW9bbayrwhpbSm5oZtofins8MDRLh+Hp55QEUmoIkfb1NGlljgw==",
"dev": true "dev": true
}, },
"relaks-harvest": { "relaks-harvest": {
@@ -6656,12 +6660,12 @@
"dev": true "dev": true
}, },
"relaks-route-manager": { "relaks-route-manager": {
"version": "0.0.18", "version": "0.0.20",
"resolved": "https://registry.npmjs.org/relaks-route-manager/-/relaks-route-manager-0.0.18.tgz", "resolved": "https://registry.npmjs.org/relaks-route-manager/-/relaks-route-manager-0.0.20.tgz",
"integrity": "sha512-Srxe5KaKtIbyrtckTwoNY8/j2A4DTRGB+DUN7vVaguiPxqygY2tDLc0dYcUAcoDxoq/iHKNFu6/wGZM3t2duqw==", "integrity": "sha512-OQ51fvpI18S/kgBhpo77xjcBYWwE62TTEHLTMVfAb38m+HkXtWM6o/FNZrKP0N+7vJg5muTsh5DfTuN73XdQVA==",
"dev": true, "dev": true,
"requires": { "requires": {
"relaks-event-emitter": "0.0.1" "relaks-event-emitter": "0.0.6"
} }
}, },
"relateurl": { "relateurl": {
@@ -6822,8 +6826,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"dev": true
}, },
"safe-regex": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",

View File

@@ -40,9 +40,9 @@
"react": "^16.6.3", "react": "^16.6.3",
"react-html-parser": "^2.0.2", "react-html-parser": "^2.0.2",
"regenerator-runtime": "^0.12.0", "regenerator-runtime": "^0.12.0",
"relaks": "^1.1.8", "relaks": "^1.1.9",
"relaks-harvest": "^0.0.3", "relaks-harvest": "^0.0.3",
"relaks-route-manager": "0.0.18", "relaks-route-manager": "0.0.20",
"sass-loader": "^6.0.5", "sass-loader": "^6.0.5",
"uglifyjs-webpack-plugin": "^0.4.6", "uglifyjs-webpack-plugin": "^0.4.6",
"webpack": "^3.1.0", "webpack": "^3.1.0",
@@ -51,6 +51,8 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.6.3", "@fortawesome/fontawesome-free": "^5.6.3",
"bluebird": "^3.5.3",
"compression": "^1.7.3",
"cross-fetch": "^2.2.2", "cross-fetch": "^2.2.2",
"dnscache": "^1.0.1", "dnscache": "^1.0.1",
"express": "^4.16.3", "express": "^4.16.3",

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,6 @@
<title>Relaks WordPress Example</title> <title>Relaks WordPress Example</title>
<link href="/styles.css" rel="stylesheet"></head> <link href="/styles.css" rel="stylesheet"></head>
<body> <body>
<div id="app-container"><!--APP--></div> <div id="react-container"><!--REACT--></div>
<script type="text/javascript" src="/app.js"></script></body> <script type="text/javascript" src="/front-end.js"></script></body>
</html> </html>

View File

@@ -24,10 +24,13 @@ A:link, A:visited {
background-color: #66023c; background-color: #66023c;
overflow: hidden; overflow: hidden;
color: #cccccc; } color: #cccccc; }
.side-nav A:link, .side-nav A:visited { .side-nav A {
color: #cccccc; } opacity: 0.5; }
.side-nav A:link:hover, .side-nav A:visited:hover { .side-nav A:link, .side-nav A:visited {
color: #eeccdd; } opacity: 1;
color: #cccccc; }
.side-nav A:link:hover, .side-nav A:visited:hover {
color: #eeccdd; }
.side-nav .archive LI { .side-nav .archive LI {
margin-top: 0.1em; margin-top: 0.1em;
margin-bottom: 0.1em; } margin-bottom: 0.1em; }
@@ -85,6 +88,7 @@ A:link, A:visited {
.top-nav .page-bar A:link:hover, .top-nav .page-bar A:visited:hover { .top-nav .page-bar A:link:hover, .top-nav .page-bar A:visited:hover {
color: #eecccc; } color: #eecccc; }
.top-nav .page-bar .button { .top-nav .page-bar .button {
flex: 0 0 auto;
padding-left: 0.5em; padding-left: 0.5em;
padding-right: 0.5em; padding-right: 0.5em;
border-right: 1px solid rgba(204, 204, 204, 0.25); } border-right: 1px solid rgba(204, 204, 204, 0.25); }
@@ -138,9 +142,13 @@ A:link, A:visited {
max-width: 60em; } max-width: 60em; }
.page-container .page .meta, .page-container .post .meta { .page-container .page .meta, .page-container .post .meta {
float: right; float: right;
text-align: right; } text-align: right;
margin-left: 1em;
margin-top: 0.4em; }
.page-container .page .meta .author, .page-container .post .meta .author { .page-container .page .meta .author, .page-container .post .meta .author {
margin-top: 0.25em; } margin-top: 0.25em; }
.page-container .page .post-list-view .excerpt, .page-container .post .post-list-view .excerpt {
margin-top: -0.7em; }
.page-container .page .comments, .page-container .post .comments { .page-container .page .comments, .page-container .post .comments {
font-size: 0.9em; font-size: 0.9em;
padding-left: 1.5em; } padding-left: 1.5em; }
@@ -154,6 +162,8 @@ A:link, A:visited {
vertical-align: middle; } vertical-align: middle; }
.page-container .page .comments .replies, .page-container .post .comments .replies { .page-container .page .comments .replies, .page-container .post .comments .replies {
padding-left: 1.5em; } padding-left: 1.5em; }
.page-container .page IMG, .page-container .post IMG {
max-width: 100%; }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
.page-container { .page-container {

View File

@@ -16,6 +16,8 @@ services:
depends_on: depends_on:
- db - db
image: wordpress:latest image: wordpress:latest
volumes:
- wp_content:/var/www/html/wp-content
environment: environment:
WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress WORDPRESS_DB_USER: wordpress
@@ -45,4 +47,5 @@ services:
restart: always restart: always
volumes: volumes:
db_data: db_data:
wp_content:
web_cache: web_cache:

View File

@@ -1,22 +1,22 @@
const FS = require('fs'); const Bluebird = require('bluebird');
const FS = Bluebird.promisifyAll(require('fs'));
const OS = require('os'); const OS = require('os');
const Express = require('express'); const Express = require('express');
const CrossFetch = require('cross-fetch'); const Compression = require('compression');
const DNSCache = require('dnscache');
const Crypto = require('crypto');
const SpiderDetector = require('spider-detector') const SpiderDetector = require('spider-detector')
const DNSCache = require('dnscache');
const CrossFetch = require('cross-fetch');
const ReactDOMServer = require('react-dom/server'); const ReactDOMServer = require('react-dom/server');
const ClientApp = require('./client/app'); const FrontEnd = require('./client/front-end');
const NginxCache = require('./nginx-cache');
// enable DNS caching // enable DNS caching
let dnsCache = DNSCache({ enable: true, ttl: 300, cachesize: 100 }); let dnsCache = DNSCache({ enable: true, ttl: 300, cachesize: 100 });
const basePath = `/`;
const perPage = 10; const perPage = 10;
const serverPort = 80; const serverPort = 80;
const wordpressHost = process.env.WORDPRESS_HOST; const wordpressHost = process.env.WORDPRESS_HOST;
const nginxHost = process.env.NGINX_HOST; const nginxHost = process.env.NGINX_HOST;
const nginxCache = process.env.NGINX_CACHE;
let wordpressIP; let wordpressIP;
dnsCache.lookup(wordpressHost, (err, result) => { dnsCache.lookup(wordpressHost, (err, result) => {
@@ -27,14 +27,40 @@ dnsCache.lookup(wordpressHost, (err, result) => {
let app = Express(); let app = Express();
app.set('json spaces', 2); app.set('json spaces', 2);
app.use(Compression())
app.use(SpiderDetector.middleware()); app.use(SpiderDetector.middleware());
app.use(`/`, Express.static(`${__dirname}/www`)); app.use(`/`, Express.static(`${__dirname}/www`));
app.get('/.mtime', handleTimestampRequest); app.get('/.mtime', handleTimestampRequest);
app.get('/json/*', handleJSONRequest);
app.get(`/*`, handlePageRequest); app.get(`/*`, handlePageRequest);
app.purge(`/*`, handlePurgeRequest); app.purge(`/*`, handlePurgeRequest);
app.use(handleError); app.use(handleError);
app.listen(serverPort); app.listen(serverPort);
let pageDependencies = {};
async function handleJSONRequest(req, res, next) {
try {
let path = `/wp-json/${req.url.substr(6)}`;
let url = `http://${wordpressHost}${path}`;
let sres = await CrossFetch(url);
let text = await sres.text();
res.send(text);
} catch (err) {
next(err);
}
}
function handleTimestampRequest(req, res, next) {
try {
let now = new Date;
let ts = now.toISOString();
res.type('text').send(ts);
} catch (err) {
next(err);
}
}
async function handlePageRequest(req, res, next) { async function handlePageRequest(req, res, next) {
try { try {
let host = `http://${nginxHost}`; let host = `http://${nginxHost}`;
@@ -52,10 +78,10 @@ async function handlePageRequest(req, res, next) {
return CrossFetch(url, options); return CrossFetch(url, options);
}; };
let options = { host, path, target, fetch }; let options = { host, path, target, fetch };
let rootNode = await ClientApp.render(options); let rootNode = await FrontEnd.render(options);
let appHTML = ReactDOMServer.renderToString(rootNode); let appHTML = ReactDOMServer.renderToString(rootNode);
let indexHTMLPath = `${__dirname}/client/index.html`; let indexHTMLPath = `${__dirname}/client/index.html`;
let html = await replaceHTMLComment(indexHTMLPath, 'APP', appHTML); let html = await replaceHTMLComment(indexHTMLPath, 'REACT', appHTML);
if (target === 'hydrate') { if (target === 'hydrate') {
// add <noscript> tag to redirect to SEO version // add <noscript> tag to redirect to SEO version
@@ -66,20 +92,8 @@ async function handlePageRequest(req, res, next) {
} }
res.type('html').send(html); res.type('html').send(html);
recordDependencies(path, sourceURLs); // save the URLs that the page depends on
} catch (err) { pageDependencies[path] = sourceURLs.map(addTrailingSlash);
next(err);
}
}
function handleTimestampRequest(req, res, next) {
try {
let now = new Date;
let ts = now.toISOString();
res.type('text').send(ts);
let path = req.url;
recordDependencies(path, '*');
} catch (err) { } catch (err) {
next(err); next(err);
} }
@@ -92,125 +106,86 @@ function handleError(err, req, res, next) {
console.error(err); console.error(err);
} }
async function handlePurgeRequest(req, res) { function handlePurgeRequest(req, res) {
let remoteIP = req.connection.remoteAddress; let remoteIP = req.connection.remoteAddress;
if (remoteIP === wordpressIP) { if (remoteIP === wordpressIP) {
let url = req.url; let url = req.url;
let method = req.headers['x-purge-method']; let method = req.headers['x-purge-method'];
await purgeCachedFile(url, method); purgeCachedFile(url, method);
let pattern = (method === 'regex') ? new RegExp(url) : url;
let isJSON;
if (pattern instanceof RegExp) {
isJSON = pattern.test('/wp-json');
} else {
isJSON = pattern.startsWith('/wp-json');
}
if (isJSON) {
await purgeDependentPages(pattern);
}
} }
res.end(); res.end();
} }
let pageDependencies = {};
function recordDependencies(url, sourceURLs) {
if (sourceURLs instanceof Array) {
sourceURLs = sourceURLs.map(removeTrailingSlash);
}
pageDependencies[url] = sourceURLs;
}
async function purgeDependentPages(host, pattern) {
for (let [ url, sourceURLs ] of Object.entries(pageDependencies)) {
let match = false;
if (sourceURLs === '*') {
match = true;
} else if (pattern instanceof RegExp) {
match = sourceURLs.some((sourceURL) => {
return pattern.test(sourceURL);
});
} else {
let url = removeTrailingSlash(pattern);
if (sourceURLs.indexOf(url)) {
match = true;
}
}
if (match) {
delete pageDependencies[pageURL];
await purgeCachedFile(pageURL);
}
}
}
async function purgeCachedFile(url, method) { async function purgeCachedFile(url, method) {
console.log(`Purging: ${url}`); let pattern, isJSON;
if (method === 'regex') { if (method === 'default' && url.startsWith('/wp-json/')) {
// delete everything let path = url.substr(9);
let files = await new Promise((resolve, reject) => { let m = /^(\w+\/\w+\/(\w+)\/)(\d+)\/$/.exec(path);
FS.readdir(nginxCache, (err, files) => { if (m) {
if (!err) { let folderPath = m[1];
resolve(files); let folderType = m[2];
} else { pattern = new RegExp(`^/json/${folderPath}.*`);
resolve([]); }
} } else if (method === 'regex' && url === '.*') {
}); pattern = /.*/;
}); }
let isMD5 = /^[0-9a-f]{32}$/; if (!pattern) {
for (let file of files) { return;
if (isMD5.test(file)) { }
await unlinkFile(`${nginxCache}/${file}`); let purged = await NginxCache.purge(pattern);
for (let [ pageURL, sourceURLs ] of Object.entries(pageDependencies)) {
let affected = false;
for (let jsonURL of purged) {
jsonURL = addTrailingSlash(jsonURL);
if (sourceURLs.indexOf(jsonURL)) {
affected = true;
break;
} }
} }
} else { if (affected) {
let hash = Crypto.createHash('md5').update(url); delete pageDependencies[pageURL];
let md5 = hash.digest("hex"); await NginxCache.purge(pageURL);
await unlinkFile(`${nginxCache}/${md5}`); }
} }
} await NginxCache.purge('/.mtime');
async function unlinkFile(path) {
console.log(`Unlinking ${path}`);
await new Promise((resolve, reject) => {
FS.unlink(path, (err) => {
if (!err) {
resolve(true);
} else {
resolve(false);
}
});
});
} }
async function replaceHTMLComment(path, comment, newElement) { async function replaceHTMLComment(path, comment, newElement) {
let text = await new Promise((resolve, reject) => { let text = await FS.readFileAsync(path, 'utf-8');
FS.readFile(path, 'utf-8', (err, text) => {
if (!err) {
resolve(text);
} else {
reject(err);
}
});
});
return text.replace(`<!--${comment}-->`, newElement).replace(`<!--${comment}-->`, newElement); return text.replace(`<!--${comment}-->`, newElement).replace(`<!--${comment}-->`, newElement);
} }
/** /**
* Remove trailing slash from URL * Add trailing slash to URL
* *
* @param {String} url * @param {String} url
* *
* @return {String} * @return {String}
*/ */
function removeTrailingSlash(url) { function addTrailingSlash(url) {
var lc = url.charAt(url.length - 1); let qi = url.indexOf('?');
if (lc === '/') { if (qi === -1) {
url = url.substr(0, url.length - 1); qi = url.length;
}
let lc = url.charAt(qi - 1);
if (lc !== '/') {
url = url.substr(0, qi) + '/' + url.substr(qi);
} }
return url; return url;
} }
[ './index', './nginx-cache', './client/front-end' ].forEach((path) => {
let fullPath = require.resolve(path);
FS.watchFile(fullPath, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
console.log('Restarting');
process.exit(0);
}
});
});
NginxCache.purge(/.*/);
process.on('unhandledRejection', (err) => { process.on('unhandledRejection', (err) => {
console.error(err); console.error(err);
}); });

102
server/nginx-cache.js Normal file
View File

@@ -0,0 +1,102 @@
const Bluebird = require('bluebird');
const FS = Bluebird.promisifyAll(require('fs'));
const Crypto = require('crypto');
const nginxCache = process.env.NGINX_CACHE;
async function purge(pattern) {
console.log(`Purging: ${pattern}`);
let purged = [];
if (typeof(pattern) === 'string') {
let url = pattern;
let hash = Crypto.createHash('md5').update(url);
let md5 = hash.digest('hex');
let success = await removeCacheEntry({ url, md5 });
if (success) {
purged.push(url);
}
} else if (pattern instanceof RegExp) {
let cacheEntries = await loadCacheEntries();
for (let cacheEntry of cacheEntries) {
if (pattern.test(cacheEntry.url)) {
let success = await removeCacheEntry(cacheEntry);
if (success) {
purged.push(cacheEntry.url);
}
}
}
}
return purged;
}
const isMD5 = /^[0-9a-f]{32}$/;
let cacheEntriesPromise = null;
async function loadCacheEntries() {
if (!cacheEntriesPromise) {
cacheEntriesPromise = loadCacheEntriesUncached();
}
let entries = await cacheEntriesPromise;
cacheEntriesPromise = null;
return entries;
}
async function loadCacheEntriesUncached() {
let files = await FS.readdirAsync(nginxCache);
let entries = [];
for (let file of files) {
if (isMD5.test(file)) {
let entry = await loadCacheEntry(file);
if (entry) {
entries.push(entry);
}
}
}
return entries;
}
let cacheEntryCache = {};
async function loadCacheEntry(md5) {
try {
let path = `${nginxCache}/${md5}`;
let { mtime } = await FS.statAsync(path);
let entry = cacheEntryCache[md5];
if (!entry || entry.mtime !== mtime) {
let url = await loadCacheEntryKey(path);
entry = cacheEntryCache[md5] = { url, mtime, md5 };
}
return entry;
} catch (err) {
delete cacheEntryCache[md5];
return null;
}
}
async function loadCacheEntryKey(path) {
let fd = await FS.openAsync(path, 'r');
let buf = Buffer.alloc(1024);
let bytesRead = await FS.readAsync(fd, buf, 0, 1024, 0);
let si = buf.indexOf('KEY:');
let ei = buf.indexOf('\n', si);
if (si !== -1 && ei !== -1) {
let s = buf.toString('utf-8', si + 4, ei).trim();;
return s;
} else {
throw new Error('Unable to find key');
}
}
async function removeCacheEntry(entry) {
try {
delete cacheEntryCache[entry.md5];
await FS.unlinkAsync(`${nginxCache}/${entry.md5}`);
console.log(`Purged: ${entry.url}`);
return true;
} catch (err){
return false;
}
}
exports.purge = purge;

View File

@@ -1,14 +1,3 @@
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_min_length 256;
proxy_cache_path /var/cache/nginx/data keys_zone=data:10m max_size=1g; proxy_cache_path /var/cache/nginx/data keys_zone=data:10m max_size=1g;
proxy_temp_path /var/cache/nginx/tmp; proxy_temp_path /var/cache/nginx/tmp;
@@ -16,12 +5,6 @@ server {
listen 80; listen 80;
server_name _; server_name _;
location ~ ^/wp-json {
proxy_pass http://wordpress;
proxy_set_header Host $http_host;
include /etc/nginx/conf.d/inc/caching.conf;
}
location ~ ^/wp-* { location ~ ^/wp-* {
proxy_pass http://wordpress; proxy_pass http://wordpress;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
@@ -36,7 +19,19 @@ server {
location / { location / {
proxy_pass http://node; proxy_pass http://node;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
include /etc/nginx/conf.d/inc/caching.conf; proxy_buffering on;
proxy_cache data;
proxy_cache_key $uri$is_args$args;
proxy_cache_min_uses 1;
proxy_cache_valid 200 301 302 120m;
proxy_cache_valid 404 1m;
proxy_hide_header Cache-Control;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
add_header Cache-Control "public,max-age=0";
add_header X-Cache-Date $upstream_http_date;
add_header X-Cache-Status $upstream_cache_status;
add_header Access-Control-Allow-Origin *;
} }
} }

View File

@@ -1,16 +0,0 @@
proxy_buffering on;
proxy_cache data;
proxy_cache_key $uri$is_args$args;
proxy_cache_min_uses 1;
proxy_cache_valid 200 301 302 120m;
proxy_cache_valid 404 1m;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Set-Cookie;
proxy_hide_header Access-Control-Allow-Origin;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
add_header Cache-Control "public,must-revalidate";
add_header X-Cache-Date $upstream_http_date;
add_header X-Cache-Status $upstream_cache_status;
add_header Access-Control-Allow-Origin *;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,10 +24,13 @@ A:link, A:visited {
background-color: #66023c; background-color: #66023c;
overflow: hidden; overflow: hidden;
color: #cccccc; } color: #cccccc; }
.side-nav A:link, .side-nav A:visited { .side-nav A {
color: #cccccc; } opacity: 0.5; }
.side-nav A:link:hover, .side-nav A:visited:hover { .side-nav A:link, .side-nav A:visited {
color: #eeccdd; } opacity: 1;
color: #cccccc; }
.side-nav A:link:hover, .side-nav A:visited:hover {
color: #eeccdd; }
.side-nav .archive LI { .side-nav .archive LI {
margin-top: 0.1em; margin-top: 0.1em;
margin-bottom: 0.1em; } margin-bottom: 0.1em; }
@@ -85,6 +88,7 @@ A:link, A:visited {
.top-nav .page-bar A:link:hover, .top-nav .page-bar A:visited:hover { .top-nav .page-bar A:link:hover, .top-nav .page-bar A:visited:hover {
color: #eecccc; } color: #eecccc; }
.top-nav .page-bar .button { .top-nav .page-bar .button {
flex: 0 0 auto;
padding-left: 0.5em; padding-left: 0.5em;
padding-right: 0.5em; padding-right: 0.5em;
border-right: 1px solid rgba(204, 204, 204, 0.25); } border-right: 1px solid rgba(204, 204, 204, 0.25); }
@@ -138,9 +142,13 @@ A:link, A:visited {
max-width: 60em; } max-width: 60em; }
.page-container .page .meta, .page-container .post .meta { .page-container .page .meta, .page-container .post .meta {
float: right; float: right;
text-align: right; } text-align: right;
margin-left: 1em;
margin-top: 0.4em; }
.page-container .page .meta .author, .page-container .post .meta .author { .page-container .page .meta .author, .page-container .post .meta .author {
margin-top: 0.25em; } margin-top: 0.25em; }
.page-container .page .post-list-view .excerpt, .page-container .post .post-list-view .excerpt {
margin-top: -0.7em; }
.page-container .page .comments, .page-container .post .comments { .page-container .page .comments, .page-container .post .comments {
font-size: 0.9em; font-size: 0.9em;
padding-left: 1.5em; } padding-left: 1.5em; }
@@ -154,6 +162,8 @@ A:link, A:visited {
vertical-align: middle; } vertical-align: middle; }
.page-container .page .comments .replies, .page-container .post .comments .replies { .page-container .page .comments .replies, .page-container .post .comments .replies {
padding-left: 1.5em; } padding-left: 1.5em; }
.page-container .page IMG, .page-container .post IMG {
max-width: 100%; }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
.page-container { .page-container {

View File

@@ -15,7 +15,7 @@ class FrontEnd extends PureComponent {
super(props); super(props);
let { routeManager, dataSource } = this.props; let { routeManager, dataSource } = this.props;
this.state = { this.state = {
route: new Route(routeManager), route: new Route(routeManager, dataSource),
wp: new Wordpress(dataSource, props.ssr), wp: new Wordpress(dataSource, props.ssr),
sideNavCollapsed: true, sideNavCollapsed: true,
topNavCollapsed: false, topNavCollapsed: false,
@@ -97,7 +97,8 @@ class FrontEnd extends PureComponent {
* @param {RelaksRouteManagerEvent} evt * @param {RelaksRouteManagerEvent} evt
*/ */
handleRouteChange = (evt) => { handleRouteChange = (evt) => {
this.setState({ route: new Route(evt.target) }); let { dataSource } = this.props;
this.setState({ route: new Route(evt.target, dataSource) });
} }
/** /**

View File

@@ -6,6 +6,6 @@
<title>Relaks WordPress Example</title> <title>Relaks WordPress Example</title>
</head> </head>
<body> <body>
<div id="app-container"><!--APP--></div> <div id="react-container"><!--REACT--></div>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,12 @@
import { delay } from 'bluebird';
import { createElement } from 'react'; import { createElement } from 'react';
import { hydrate, render } from 'react-dom'; import { hydrate, render } from 'react-dom';
import { FrontEnd } from 'front-end'; import { FrontEnd } from 'front-end';
import { routes, setPageType } from 'routing'; import { Route, routes } from 'routing';
import WordpressDataSource from 'wordpress-data-source'; import WordpressDataSource from 'wordpress-data-source';
import RouteManager from 'relaks-route-manager'; import RouteManager from 'relaks-route-manager';
import { harvest } from 'relaks-harvest'; import { harvest } from 'relaks-harvest';
import Relaks from 'relaks'; import Relaks, { plant } from 'relaks';
const pageBasePath = ''; const pageBasePath = '';
@@ -18,7 +19,7 @@ if (typeof(window) === 'object') {
host = 'http://192.168.0.56:8000'; host = 'http://192.168.0.56:8000';
} }
let dataSource = new WordpressDataSource({ let dataSource = new WordpressDataSource({
baseURL: `${host}/wp-json`, baseURL: `${host}/json`,
}); });
dataSource.activate(); dataSource.activate();
@@ -29,32 +30,50 @@ if (typeof(window) === 'object') {
preloadingDelay: 2000, preloadingDelay: 2000,
}); });
routeManager.addEventListener('beforechange', (evt) => { routeManager.addEventListener('beforechange', (evt) => {
evt.postponeDefault(setPageType(dataSource, evt.params)); let route = new Route(routeManager, dataSource);
evt.postponeDefault(route.setPageType(evt.params));
}); });
routeManager.activate(); routeManager.activate();
await routeManager.start(); await routeManager.start();
let appContainer = document.getElementById('app-container'); let container = document.getElementById('react-container');
if (!appContainer) {
throw new Error('Unable to find app element in DOM');
}
// expect SSR unless we're running in dev-server // expect SSR unless we're running in dev-server
if (!(process.env.NODE_ENV !== 'production' && process.env.WEBPACK_DEV_SERVER)) { if (!(process.env.NODE_ENV !== 'production' && process.env.WEBPACK_DEV_SERVER)) {
let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' }); let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
let seeds = await harvest(ssrElement, { seeds: true }); let seeds = await harvest(ssrElement, { seeds: true });
Relaks.set('seeds', seeds); plant(seeds);
hydrate(ssrElement, appContainer); hydrate(ssrElement, container);
} }
let appElement = createElement(FrontEnd, { dataSource, routeManager }); let csrElement = createElement(FrontEnd, { dataSource, routeManager });
render(appElement, appContainer); render(csrElement, container);
// check for changes periodically
let mtimeURL = `${host}/.mtime`;
let mtimeLast;
for (;;) {
try {
let res = await fetch(mtimeURL);
let mtime = await res.text();
if (mtime !== mtimeLast) {
if (mtimeLast) {
console.log('changed');
dataSource.invalidate();
}
mtimeLast = mtime;
}
} catch (err) {
}
await delay(10 * 1000);
}
} }
window.addEventListener('load', initialize); window.addEventListener('load', initialize);
} else { } else {
async function serverSideRender(options) { async function serverSideRender(options) {
let dataSource = new WordpressDataSource({ let dataSource = new WordpressDataSource({
baseURL: `${options.host}/wp-json`, baseURL: `${options.host}/json`,
fetchFunc: options.fetch, fetchFunc: options.fetch,
}); });
dataSource.activate(); dataSource.activate();
@@ -64,7 +83,8 @@ if (typeof(window) === 'object') {
basePath: pageBasePath, basePath: pageBasePath,
}); });
routeManager.addEventListener('beforechange', (evt) => { routeManager.addEventListener('beforechange', (evt) => {
evt.postponeDefault(setPageType(dataSource, evt.params)); let route = new Route(routeManager, dataSource);
evt.postponeDefault(route.setPageType(evt.params));
}); });
routeManager.activate(); routeManager.activate();
await routeManager.start(options.path); await routeManager.start(options.path);

View File

@@ -31,7 +31,7 @@ class PagePageSync extends PureComponent {
static displayName = 'PagePageSync'; static displayName = 'PagePageSync';
render() { render() {
let { route, page, parentPages, childPages } = this.props; let { route, page, parentPages, childPages, transform } = this.props;
let trail = []; let trail = [];
let parents = []; let parents = [];
if (parentPages) { if (parentPages) {
@@ -48,7 +48,7 @@ class PagePageSync extends PureComponent {
return ( return (
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PageView page={page} /> <PageView page={page} transform={route.transformLink} />
<PageList route={route} pages={childPages} parentPages={parents} /> <PageList route={route} pages={childPages} parentPages={parents} />
</div> </div>
); );

View File

@@ -64,7 +64,7 @@ class PostPageSync extends PureComponent {
return ( return (
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PostView category={category} post={post} author={author} /> <PostView category={category} post={post} author={author} transform={route.transformLink} />
<CommentSection comments={comments} /> <CommentSection comments={comments} />
</div> </div>
); );

View File

@@ -1,12 +1,15 @@
let _ = require('lodash'); let _ = require('lodash');
class Route { class Route {
constructor(routeManager) { constructor(routeManager, dataSource) {
this.routeManager = routeManager; this.routeManager = routeManager;
this.name = routeManager.name; this.name = routeManager.name;
this.params = routeManager.params; this.params = routeManager.params;
this.history = routeManager.history; this.history = routeManager.history;
this.url = routeManager.url; this.url = routeManager.url;
this.dataSource = dataSource;
this.pageLinkRegExp = null;
this.imageLinkRegExp = null;
} }
change(url, options) { change(url, options) {
@@ -21,6 +24,106 @@ class Route {
return this.routeManager.find('page', params); return this.routeManager.find('page', params);
} }
} }
async setPageType(params) {
let slugs = params.slugs;
if (slugs.length > 0) {
let slugType1 = await this.getSlugType(slugs[0]);
if (slugType1 === 'page') {
params.pageType = 'page';
params.pageSlug = _.last(slugs);
params.parentPageSlugs = _.slice(slugs, 0, -1);
} else if (slugType1 === 'category') {
if (slugs.length === 1) {
params.pageType = 'category';
params.categorySlug = slugs[0];
} else if (slugs.length === 2) {
params.pageType = 'post';
params.categorySlug = slugs[0];
params.postSlug = slugs[1];
}
} else if (slugType1 === 'archive') {
if (slugs.length === 1) {
params.pageType = 'archive';
params.monthSlug = slugs[0];
} else if (slugs.length === 2) {
let slugType2 = await this.getSlugType(slugs[1]);
if (slugType2 === 'category') {
params.pageType = 'archive';
params.monthSlug = slugs[0];
params.categorySlug = slugs[1];
} else {
params.pageType = 'post';
params.monthSlug = slugs[0];
params.postSlug = slugs[1];
}
} else if (slugs.length === 3) {
params.pageType = 'post';
params.monthSlug = slugs[0];
params.categorySlug = slugs[1];
params.postSlug = slugs[2];
}
}
}
if (!params.pageType) {
if (params.search !== undefined) {
params.pageType = 'search';
} else {
params.pageType = 'welcome';
}
}
}
async getSlugType(slug) {
let options = { minimum: '100%' };
let pages = await this.dataSource.fetchList('/wp/v2/pages/?parent=0', options);
if (_.some(pages, { slug })) {
return 'page';
}
let categories = await this.dataSource.fetchList('/wp/v2/categories/', options);
if (_.some(categories, { slug })) {
return 'category';
}
if (/^\d{4}\-\d{2}$/.test(slug)) {
return 'archive';
}
}
async preloadPage(params) {
try {
if (params.postSlug) {
this.dataSource.fetchOne('/wp/v2/posts/', params.postSlug);
} else if (params.pageSlug) {
this.dataSource.fetchOne('/wp/v2/pages/', params.pageSlug);
}
} catch (err) {
}
}
transformLink = (node) => {
if (node.type === 'tag' && node.name === 'a') {
if (this.pageLinkRegExp) {
let m = this.pageLinkRegExp.exec(node.attribs.href);
if (m) {
let categorySlug = m[1];
let postSlug = m[3];
node.attribs.href = `/${categorySlug}/${postSlug}/`;
delete node.attribs.target;
this.preloadPage({ categorySlug, postSlug });
return;
}
}
if (this.imageLinkRegExp) {
let m = this.imageLinkRegExp.exec(node.attribs.href);
if (m) {
if (!node.attribs.target) {
node.attribs.target = '_blank';
}
return;
}
}
}
}
} }
let routes = { let routes = {
@@ -50,68 +153,7 @@ let routes = {
}, },
}; };
async function setPageType(dataSource, params) { export {
let slugs = params.slugs; Route,
if (slugs.length > 0) { routes,
let slugType1 = await getSlugType(dataSource, slugs[0]); };
if (slugType1 === 'page') {
params.pageType = 'page';
params.pageSlug = _.last(slugs);
params.parentPageSlugs = _.slice(slugs, 0, -1);
} else if (slugType1 === 'category') {
if (slugs.length === 1) {
params.pageType = 'category';
params.categorySlug = slugs[0];
} else if (slugs.length === 2) {
params.pageType = 'post';
params.categorySlug = slugs[0];
params.postSlug = slugs[1];
}
} else if (slugType1 === 'archive') {
if (slugs.length === 1) {
params.pageType = 'archive';
params.monthSlug = slugs[0];
} else if (slugs.length === 2) {
let slugType2 = await getSlugType(dataSource, slugs[1]);
if (slugType2 === 'category') {
params.pageType = 'archive';
params.monthSlug = slugs[0];
params.categorySlug = slugs[1];
} else {
params.pageType = 'post';
params.monthSlug = slugs[0];
params.postSlug = slugs[1];
}
} else if (slugs.length === 3) {
params.pageType = 'post';
params.monthSlug = slugs[0];
params.categorySlug = slugs[1];
params.postSlug = slugs[2];
}
}
}
if (!params.pageType) {
if (params.search !== undefined) {
params.pageType = 'search';
} else {
params.pageType = 'welcome';
}
}
}
async function getSlugType(dataSource, slug) {
let options = { minimum: '100%' };
let pages = await dataSource.fetchList('/wp/v2/pages/?parent=0', options);
if (_.some(pages, { slug })) {
return 'page';
}
let categories = await dataSource.fetchList('/wp/v2/categories/', options);
if (_.some(categories, { slug })) {
return 'category';
}
if (/^\d{4}\-\d{2}$/.test(slug)) {
return 'archive';
}
}
export { Route, routes, setPageType };

View File

@@ -249,6 +249,10 @@ A {
padding-left: 1.5em; padding-left: 1.5em;
} }
} }
IMG {
max-width: 100%;
}
} }
} }

View File

@@ -3,11 +3,8 @@ import ReactHtmlParser from 'react-html-parser';
class HTML extends PureComponent { class HTML extends PureComponent {
render() { render() {
let { text } = this.props; let { text, transform } = this.props;
let options = {}; let options = { transform };
if (transformFunc) {
options.transform = transformFunc;
}
return ReactHtmlParser(text, options); return ReactHtmlParser(text, options);
} }
} }
@@ -16,17 +13,11 @@ if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
HTML.propTypes = { HTML.propTypes = {
text: PropTypes.string, text: PropTypes.string,
transform: PropTypes.func,
}; };
} }
let transformFunc = null;
function setTransformFunction(f) {
transformFunc = f;
}
export { export {
HTML as default, HTML as default,
HTML, HTML,
setTransformFunction,
}; };

View File

@@ -8,7 +8,7 @@ class PageView extends PureComponent {
static displayName = 'PageView'; static displayName = 'PageView';
render() { render() {
let { page } = this.props; let { page, transform } = this.props;
let title = _.get(page, 'title.rendered', ''); let title = _.get(page, 'title.rendered', '');
let content = _.get(page, 'content.rendered', ''); let content = _.get(page, 'content.rendered', '');
let date = _.get(page, 'modified_gmt'); let date = _.get(page, 'modified_gmt');
@@ -21,7 +21,9 @@ class PageView extends PureComponent {
<div className="date">{date}</div> <div className="date">{date}</div>
</div> </div>
<h1><HTML text={title} /></h1> <h1><HTML text={title} /></h1>
<div className="content"><HTML text={content} /></div> <div className="content">
<HTML text={content} transform={transform}/>
</div>
</div> </div>
); );
} }
@@ -32,6 +34,7 @@ if (process.env.NODE_ENV !== 'production') {
PageView.propTypes = { PageView.propTypes = {
category: PropTypes.object, category: PropTypes.object,
transform: PropTypes.func,
}; };
} }

View File

@@ -8,7 +8,7 @@ class PostView extends PureComponent {
static displayName = 'PostView'; static displayName = 'PostView';
render() { render() {
let { category, post, author } = this.props; let { category, post, author, transform } = this.props;
let title = _.get(post, 'title.rendered', ''); let title = _.get(post, 'title.rendered', '');
let content = _.get(post, 'content.rendered', ''); let content = _.get(post, 'content.rendered', '');
let date = _.get(post, 'date_gmt'); let date = _.get(post, 'date_gmt');
@@ -23,7 +23,9 @@ class PostView extends PureComponent {
<div className="author">{name}</div> <div className="author">{name}</div>
</div> </div>
<h1><HTML text={title} /></h1> <h1><HTML text={title} /></h1>
<div className="content"><HTML text={content} /></div> <div className="content">
<HTML text={content} transform={transform} />
</div>
</div> </div>
); );
} }
@@ -36,6 +38,7 @@ if (process.env.NODE_ENV !== 'production') {
category: PropTypes.object, category: PropTypes.object,
post: PropTypes.object, post: PropTypes.object,
author: PropTypes.object, author: PropTypes.object,
transform: PropTypes.func,
}; };
} }

View File

@@ -1662,11 +1662,11 @@ prototype.delete = function(url) {
*/ */
prototype.request = function(url, options, token, waitForAuthentication) { prototype.request = function(url, options, token, waitForAuthentication) {
var _this = this; var _this = this;
if (!options) {
options = {};
}
if (token) { if (token) {
var keyword = this.options.authorizationKeyword; var keyword = this.options.authorizationKeyword;
if (!options) {
options = {};
}
if (!options.headers) { if (!options.headers) {
options.headers = {}; options.headers = {};
} }

View File

@@ -17,7 +17,7 @@ var clientConfig = {
output: { output: {
path: Path.resolve('./server/www'), path: Path.resolve('./server/www'),
publicPath: '/', publicPath: '/',
filename: 'app.js', filename: 'front-end.js',
}, },
resolve: { resolve: {
extensions: [ '.js', '.jsx' ], extensions: [ '.js', '.jsx' ],
@@ -81,7 +81,7 @@ var serverConfig = {
output: { output: {
path: Path.resolve('./server/client'), path: Path.resolve('./server/client'),
publicPath: '/', publicPath: '/',
filename: 'app.js', filename: 'front-end.js',
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
}, },
resolve: clientConfig.resolve, resolve: clientConfig.resolve,