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/
3. Enter site info
4. Log in
5. Go to Plugins page
6. Search for, install, and activate "Demo Data Creator" plugin
7. Search for, install, and activate "Proxy Cache Purge" plugin
8. Go to Tools > Demo Data Creator
9. Create demo users
10. Create demo categories
11. Create demo pages
12. Create demo posts
13. Create demo comments
14. Go to Settings > Permalinks
15. Select a scheme other than "Plain" (to enable cleaner JSON URLs)
5. Go to Settings > Permalinks
6. Select a scheme other than "Plain" (to enable clean JSON URLs)
7. Go to Plugins page
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');"

47
package-lock.json generated
View File

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

View File

@@ -40,9 +40,9 @@
"react": "^16.6.3",
"react-html-parser": "^2.0.2",
"regenerator-runtime": "^0.12.0",
"relaks": "^1.1.8",
"relaks": "^1.1.9",
"relaks-harvest": "^0.0.3",
"relaks-route-manager": "0.0.18",
"relaks-route-manager": "0.0.20",
"sass-loader": "^6.0.5",
"uglifyjs-webpack-plugin": "^0.4.6",
"webpack": "^3.1.0",
@@ -51,6 +51,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.6.3",
"bluebird": "^3.5.3",
"compression": "^1.7.3",
"cross-fetch": "^2.2.2",
"dnscache": "^1.0.1",
"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>
<link href="/styles.css" rel="stylesheet"></head>
<body>
<div id="app-container"><!--APP--></div>
<script type="text/javascript" src="/app.js"></script></body>
<div id="react-container"><!--REACT--></div>
<script type="text/javascript" src="/front-end.js"></script></body>
</html>

View File

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

View File

@@ -16,6 +16,8 @@ services:
depends_on:
- db
image: wordpress:latest
volumes:
- wp_content:/var/www/html/wp-content
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
@@ -45,4 +47,5 @@ services:
restart: always
volumes:
db_data:
wp_content:
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 Express = require('express');
const CrossFetch = require('cross-fetch');
const DNSCache = require('dnscache');
const Crypto = require('crypto');
const Compression = require('compression');
const SpiderDetector = require('spider-detector')
const DNSCache = require('dnscache');
const CrossFetch = require('cross-fetch');
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
let dnsCache = DNSCache({ enable: true, ttl: 300, cachesize: 100 });
const basePath = `/`;
const perPage = 10;
const serverPort = 80;
const wordpressHost = process.env.WORDPRESS_HOST;
const nginxHost = process.env.NGINX_HOST;
const nginxCache = process.env.NGINX_CACHE;
let wordpressIP;
dnsCache.lookup(wordpressHost, (err, result) => {
@@ -27,14 +27,40 @@ dnsCache.lookup(wordpressHost, (err, result) => {
let app = Express();
app.set('json spaces', 2);
app.use(Compression())
app.use(SpiderDetector.middleware());
app.use(`/`, Express.static(`${__dirname}/www`));
app.get('/.mtime', handleTimestampRequest);
app.get('/json/*', handleJSONRequest);
app.get(`/*`, handlePageRequest);
app.purge(`/*`, handlePurgeRequest);
app.use(handleError);
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) {
try {
let host = `http://${nginxHost}`;
@@ -52,10 +78,10 @@ async function handlePageRequest(req, res, next) {
return CrossFetch(url, options);
};
let options = { host, path, target, fetch };
let rootNode = await ClientApp.render(options);
let rootNode = await FrontEnd.render(options);
let appHTML = ReactDOMServer.renderToString(rootNode);
let indexHTMLPath = `${__dirname}/client/index.html`;
let html = await replaceHTMLComment(indexHTMLPath, 'APP', appHTML);
let html = await replaceHTMLComment(indexHTMLPath, 'REACT', appHTML);
if (target === 'hydrate') {
// add <noscript> tag to redirect to SEO version
@@ -66,20 +92,8 @@ async function handlePageRequest(req, res, next) {
}
res.type('html').send(html);
recordDependencies(path, sourceURLs);
} catch (err) {
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, '*');
// save the URLs that the page depends on
pageDependencies[path] = sourceURLs.map(addTrailingSlash);
} catch (err) {
next(err);
}
@@ -92,125 +106,86 @@ function handleError(err, req, res, next) {
console.error(err);
}
async function handlePurgeRequest(req, res) {
function handlePurgeRequest(req, res) {
let remoteIP = req.connection.remoteAddress;
if (remoteIP === wordpressIP) {
let url = req.url;
let method = req.headers['x-purge-method'];
await 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);
}
purgeCachedFile(url, method);
}
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) {
console.log(`Purging: ${url}`);
if (method === 'regex') {
// delete everything
let files = await new Promise((resolve, reject) => {
FS.readdir(nginxCache, (err, files) => {
if (!err) {
resolve(files);
} else {
resolve([]);
}
});
});
let isMD5 = /^[0-9a-f]{32}$/;
for (let file of files) {
if (isMD5.test(file)) {
await unlinkFile(`${nginxCache}/${file}`);
let pattern, isJSON;
if (method === 'default' && url.startsWith('/wp-json/')) {
let path = url.substr(9);
let m = /^(\w+\/\w+\/(\w+)\/)(\d+)\/$/.exec(path);
if (m) {
let folderPath = m[1];
let folderType = m[2];
pattern = new RegExp(`^/json/${folderPath}.*`);
}
} else if (method === 'regex' && url === '.*') {
pattern = /.*/;
}
if (!pattern) {
return;
}
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 {
let hash = Crypto.createHash('md5').update(url);
let md5 = hash.digest("hex");
await unlinkFile(`${nginxCache}/${md5}`);
if (affected) {
delete pageDependencies[pageURL];
await NginxCache.purge(pageURL);
}
}
}
async function unlinkFile(path) {
console.log(`Unlinking ${path}`);
await new Promise((resolve, reject) => {
FS.unlink(path, (err) => {
if (!err) {
resolve(true);
} else {
resolve(false);
}
});
});
await NginxCache.purge('/.mtime');
}
async function replaceHTMLComment(path, comment, newElement) {
let text = await new Promise((resolve, reject) => {
FS.readFile(path, 'utf-8', (err, text) => {
if (!err) {
resolve(text);
} else {
reject(err);
}
});
});
let text = await FS.readFileAsync(path, 'utf-8');
return text.replace(`<!--${comment}-->`, newElement).replace(`<!--${comment}-->`, newElement);
}
/**
* Remove trailing slash from URL
* Add trailing slash to URL
*
* @param {String} url
*
* @return {String}
*/
function removeTrailingSlash(url) {
var lc = url.charAt(url.length - 1);
if (lc === '/') {
url = url.substr(0, url.length - 1);
function addTrailingSlash(url) {
let qi = url.indexOf('?');
if (qi === -1) {
qi = url.length;
}
let lc = url.charAt(qi - 1);
if (lc !== '/') {
url = url.substr(0, qi) + '/' + url.substr(qi);
}
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) => {
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_temp_path /var/cache/nginx/tmp;
@@ -16,12 +5,6 @@ server {
listen 80;
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-* {
proxy_pass http://wordpress;
proxy_set_header Host $http_host;
@@ -36,7 +19,19 @@ server {
location / {
proxy_pass http://node;
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;
overflow: hidden;
color: #cccccc; }
.side-nav A:link, .side-nav A:visited {
color: #cccccc; }
.side-nav A:link:hover, .side-nav A:visited:hover {
color: #eeccdd; }
.side-nav A {
opacity: 0.5; }
.side-nav A:link, .side-nav A:visited {
opacity: 1;
color: #cccccc; }
.side-nav A:link:hover, .side-nav A:visited:hover {
color: #eeccdd; }
.side-nav .archive LI {
margin-top: 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 {
color: #eecccc; }
.top-nav .page-bar .button {
flex: 0 0 auto;
padding-left: 0.5em;
padding-right: 0.5em;
border-right: 1px solid rgba(204, 204, 204, 0.25); }
@@ -138,9 +142,13 @@ A:link, A:visited {
max-width: 60em; }
.page-container .page .meta, .page-container .post .meta {
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 {
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 {
font-size: 0.9em;
padding-left: 1.5em; }
@@ -154,6 +162,8 @@ A:link, A:visited {
vertical-align: middle; }
.page-container .page .comments .replies, .page-container .post .comments .replies {
padding-left: 1.5em; }
.page-container .page IMG, .page-container .post IMG {
max-width: 100%; }
@media only screen and (max-width: 800px) {
.page-container {

View File

@@ -15,7 +15,7 @@ class FrontEnd extends PureComponent {
super(props);
let { routeManager, dataSource } = this.props;
this.state = {
route: new Route(routeManager),
route: new Route(routeManager, dataSource),
wp: new Wordpress(dataSource, props.ssr),
sideNavCollapsed: true,
topNavCollapsed: false,
@@ -97,7 +97,8 @@ class FrontEnd extends PureComponent {
* @param {RelaksRouteManagerEvent} 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>
</head>
<body>
<div id="app-container"><!--APP--></div>
<div id="react-container"><!--REACT--></div>
</body>
</html>

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
let _ = require('lodash');
class Route {
constructor(routeManager) {
constructor(routeManager, dataSource) {
this.routeManager = routeManager;
this.name = routeManager.name;
this.params = routeManager.params;
this.history = routeManager.history;
this.url = routeManager.url;
this.dataSource = dataSource;
this.pageLinkRegExp = null;
this.imageLinkRegExp = null;
}
change(url, options) {
@@ -21,6 +24,106 @@ class Route {
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 = {
@@ -50,68 +153,7 @@ let routes = {
},
};
async function setPageType(dataSource, params) {
let slugs = params.slugs;
if (slugs.length > 0) {
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 };
export {
Route,
routes,
};

View File

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

View File

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

View File

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

View File

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

View File

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