Skip to content

Commit 2fd5a69

Browse files
committed
Render title and description tags on server side
1 parent c78bcc3 commit 2fd5a69

File tree

17 files changed

+248
-187
lines changed

17 files changed

+248
-187
lines changed

app/backend.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ if (__DEV__) {
3434

3535
try {
3636
if (httpServer) httpServer.close();
37-
delete require.cache[require.resolve('axios')];
3837
delete require.cache[require.resolve(backendBuildPath)];
3938
const app = require(backendBuildPath).default;
4039
httpServer = app.listen(proxyPort);
@@ -43,13 +42,12 @@ if (__DEV__) {
4342
}
4443
}
4544
});
45+
46+
module.exports = proxy({
47+
target: `http://localhost:${proxyPort}/`,
48+
pathRewrite: { ['^' + apiEndpoint]: '' },
49+
ws: true,
50+
});
4651
} else {
47-
const app = require(backendBuildPath).default;
48-
app.listen(proxyPort);
52+
module.exports = require(backendBuildPath).default;
4953
}
50-
51-
module.exports = proxy({
52-
target: `http://localhost:${proxyPort}/`,
53-
pathRewrite: { ['^' + apiEndpoint]: '' },
54-
ws: true,
55-
});

app/frontend.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
const express = require('express');
2+
const history = require('connect-history-api-fallback');
3+
const path = require('path');
4+
const fs = require('fs');
5+
const url = require('url');
6+
const packageJson = require('../package');
27

38
const {
49
__DEV__,
510
frontendSrcPath,
611
frontendBuildPath,
712
} = require('../environment');
813

14+
const app = express();
15+
app.use(history());
16+
917
if (__DEV__) {
10-
const path = require('path');
1118
const webpack = require('webpack');
1219
const webpackDev = require('webpack-dev-middleware');
1320
const webpackHot = require('webpack-hot-middleware');
14-
1521
const webpackConfig = require('../webpack.frontend.config.js');
16-
1722
const compiler = webpack(webpackConfig);
18-
const app = express();
19-
2023
app.use(express.static(path.resolve(frontendSrcPath, 'static')));
2124
app.use(webpackDev(compiler, {
2225
stats: {
@@ -25,8 +28,25 @@ if (__DEV__) {
2528
},
2629
}));
2730
app.use(webpackHot(compiler));
28-
29-
module.exports = app;
3031
} else {
31-
module.exports = express.static(frontendBuildPath);
32-
}
32+
const { hierarchy } = require('./backend'); // TODO: Hmm...
33+
app.get('/index.html', (req, res, next) => {
34+
const [, categoryKey, algorithmKey] = url.parse(req.originalUrl).pathname.split('/');
35+
let { title, description } = packageJson;
36+
const algorithm = hierarchy.find(categoryKey, algorithmKey);
37+
if (algorithm) {
38+
title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
39+
description = algorithm.description;
40+
}
41+
42+
const filePath = path.resolve(frontendBuildPath, 'index.html');
43+
fs.readFile(filePath, 'utf8', (err, data) => {
44+
if (err) next(err);
45+
const result = data.replace(/\$TITLE/g, title).replace(/\$DESCRIPTION/g, description);
46+
res.send(result);
47+
});
48+
});
49+
app.use(express.static(frontendBuildPath));
50+
}
51+
52+
module.exports = app;

app/index.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const compression = require('compression');
2-
const history = require('connect-history-api-fallback');
31
const express = require('express');
42
const app = express();
53

@@ -21,8 +19,6 @@ app.use((req, res, next) => {
2119
}
2220
});
2321
app.use(apiEndpoint, backend);
24-
app.use(history());
25-
app.use(compression());
2622
app.use(frontend);
2723

2824
module.exports = app;

package-lock.json

Lines changed: 0 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "algorithm-visualizer",
33
"version": "2.0.0",
4+
"title": "Algorithm Visualizer",
45
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
56
"scripts": {
67
"dev": "NODE_ENV=development ./bin/www",
@@ -25,18 +26,17 @@
2526
},
2627
"license": "MIT",
2728
"dependencies": {
28-
"express": "^4.15.4",
29-
"morgan": "^1.9.1",
30-
"cookie-parser": "^1.4.3",
31-
"body-parser": "^1.18.2",
32-
"bluebird": "latest",
3329
"axios": "latest",
34-
"fs-extra": "^6.0.1",
30+
"bluebird": "latest",
31+
"body-parser": "^1.18.2",
32+
"connect-history-api-fallback": "^1.5.0",
33+
"cookie-parser": "^1.4.3",
34+
"express": "^4.15.4",
3535
"express-github-webhook": "^1.0.6",
36-
"uuid": "^3.2.1",
37-
"compression": "latest",
38-
"connect-history-api-fallback": "^1.3.0",
39-
"http-proxy-middleware": "^0.18.0"
36+
"fs-extra": "^6.0.1",
37+
"http-proxy-middleware": "^0.18.0",
38+
"morgan": "^1.9.1",
39+
"uuid": "^3.2.1"
4040
},
4141
"devDependencies": {
4242
"@fortawesome/fontawesome": "^1.1.5",

src/backend/common/hierarchy.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Hierarchy } from '/models';
2+
import path from 'path';
3+
4+
const repoPath = path.resolve(__dirname, '..', 'public', 'algorithms');
5+
const hierarchy = new Hierarchy(repoPath);
6+
7+
export default hierarchy;

src/backend/common/util.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Promise from 'bluebird';
22
import child_process from 'child_process';
33
import path from 'path';
44
import fs from 'fs-extra';
5+
import removeMarkdown from 'remove-markdown';
56

67
const execute = (command, cwd, { stdout = process.stdout, stderr = process.stderr } = {}) => new Promise((resolve, reject) => {
78
if (!cwd) return reject(new Error('CWD Not Specified'));
@@ -21,19 +22,31 @@ const spawn = (command, args, cwd, { stdout = process.stdout, stderr = process.s
2122
if (stderr) child.stderr.pipe(stderr);
2223
});
2324

24-
const createKey = name => name.toLowerCase().replace(/ /g, '-');
25+
const createKey = name => name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
2526

2627
const isDirectory = dirPath => fs.lstatSync(dirPath).isDirectory();
2728

2829
const listFiles = dirPath => fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.'));
2930

3031
const listDirectories = dirPath => listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
3132

33+
const getDescription = files => {
34+
const readmeFile = files.find(file => file.name === 'README.md');
35+
if (!readmeFile) return '';
36+
const lines = readmeFile.content.split('\n');
37+
lines.shift();
38+
while (lines.length && !lines[0].trim()) lines.shift();
39+
let descriptionLines = [];
40+
while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
41+
return removeMarkdown(descriptionLines.join(' '));
42+
};
43+
3244
export {
3345
execute,
3446
spawn,
3547
createKey,
3648
isDirectory,
3749
listFiles,
3850
listDirectories,
51+
getDescription,
3952
};

src/backend/controllers/algorithms.js

Lines changed: 12 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,17 @@
11
import express from 'express';
2-
import Promise from 'bluebird';
32
import fs from 'fs-extra';
4-
import path from 'path';
5-
import { NotFoundError } from '/common/error';
6-
import { GitHubApi } from '/apis';
7-
import { createKey, execute, listDirectories, listFiles } from '/common/util';
3+
import { execute } from '/common/util';
84
import webhook from '/common/webhook';
5+
import hierarchy from '/common/hierarchy';
6+
import { NotFoundError } from '../common/error';
97

108
const router = express.Router();
119

12-
const repoPath = path.resolve(__dirname, '..', 'public', 'algorithms');
13-
const getPath = (...args) => path.resolve(repoPath, ...args);
14-
15-
const cacheFile = (categoryName, algorithmName, fileName) => {
16-
const filePath = getPath(categoryName, algorithmName, fileName);
17-
const content = fs.readFileSync(filePath, 'utf-8');
18-
return {
19-
name: fileName,
20-
path: filePath,
21-
content,
22-
contributors: [],
23-
toJSON: () => fileName,
24-
};
25-
};
26-
27-
const cacheAlgorithm = (categoryName, algorithmName) => {
28-
const algorithmKey = createKey(algorithmName);
29-
const algorithmPath = getPath(categoryName, algorithmName);
30-
const files = listFiles(algorithmPath).map(fileName => cacheFile(categoryName, algorithmName, fileName));
31-
return {
32-
key: algorithmKey,
33-
name: algorithmName,
34-
files,
35-
};
36-
};
37-
38-
const cacheCategory = categoryName => {
39-
const categoryKey = createKey(categoryName);
40-
const categoryPath = getPath(categoryName);
41-
const algorithms = listDirectories(categoryPath).map(algorithmName => cacheAlgorithm(categoryName, algorithmName));
42-
return {
43-
key: categoryKey,
44-
name: categoryName,
45-
algorithms,
46-
};
47-
};
48-
49-
const cacheCommitAuthors = (page = 1, commitAuthors = {}) => {
50-
const per_page = 100;
51-
return GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {
52-
per_page,
53-
page,
54-
}).then(commits => {
55-
commits.forEach(({ sha, commit, author }) => {
56-
if (!author) return;
57-
const { login, avatar_url } = author;
58-
commitAuthors[sha] = { login, avatar_url };
59-
});
60-
if (commits.length < per_page) {
61-
return commitAuthors;
62-
} else {
63-
return cacheCommitAuthors(page + 1, commitAuthors);
64-
}
65-
});
66-
};
67-
68-
const cacheContributors = (files, commitAuthors) => Promise.each(files, file => {
69-
return execute(`git --no-pager log --follow --no-merges --format="%H" "${file.path}"`, getPath(), { stdout: null })
70-
.then(stdout => {
71-
const output = stdout.toString().replace(/\n$/, '');
72-
const shas = output.split('\n').reverse();
73-
const contributors = [];
74-
for (const sha of shas) {
75-
const author = commitAuthors[sha];
76-
if (author && !contributors.find(contributor => contributor.login === author.login)) {
77-
contributors.push(author);
78-
}
79-
}
80-
file.contributors = contributors;
81-
});
82-
});
83-
84-
const cacheCategories = () => {
85-
const categories = listDirectories(getPath()).map(cacheCategory);
86-
87-
const files = [];
88-
categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
89-
cacheCommitAuthors().then(commitAuthors => cacheContributors(files, commitAuthors));
90-
91-
return categories;
92-
};
93-
94-
let categories = [];
9510
const downloadCategories = () => (
96-
fs.pathExistsSync(repoPath) ?
97-
execute(`git fetch && git reset --hard origin/master`, repoPath) :
98-
execute(`git clone https://github.com/algorithm-visualizer/algorithms.git ${repoPath}`, __dirname)
99-
).then(() => categories = cacheCategories());
11+
fs.pathExistsSync(hierarchy.path) ?
12+
execute(`git fetch && git reset --hard origin/master`, hierarchy.path) :
13+
execute(`git clone https://github.com/algorithm-visualizer/algorithms.git ${hierarchy.path}`, __dirname)
14+
).then(() => hierarchy.refresh());
10015

10116
downloadCategories().catch(console.error);
10217

@@ -110,30 +25,23 @@ webhook.on('algorithms', event => {
11025

11126
router.route('/')
11227
.get((req, res, next) => {
113-
res.json({ categories });
28+
res.json(hierarchy);
11429
});
11530

11631
router.route('/:categoryKey/:algorithmKey')
11732
.get((req, res, next) => {
11833
const { categoryKey, algorithmKey } = req.params;
119-
120-
const category = categories.find(category => category.key === categoryKey);
121-
if (!category) return next(new NotFoundError());
122-
const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
34+
const algorithm = hierarchy.find(categoryKey, algorithmKey);
12335
if (!algorithm) return next(new NotFoundError());
124-
125-
const categoryName = category.name;
126-
const algorithmName = algorithm.name;
127-
const files = algorithm.files.map(({ name, content, contributors }) => ({ name, content, contributors }));
128-
res.json({ algorithm: { categoryKey, categoryName, algorithmKey, algorithmName, files } });
36+
res.json({ algorithm });
12937
});
13038

13139
router.route('/sitemap.txt')
13240
.get((req, res, next) => {
13341
const urls = [];
134-
categories.forEach(category => category.algorithms.forEach(algorithm => {
42+
hierarchy.iterate((category, algorithm) => {
13543
urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
136-
}));
44+
});
13745
res.set('Content-Type', 'text/plain');
13846
res.send(urls.join('\n'));
13947
});

src/backend/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as controllers from '/controllers';
77
import { ClientError, ForbiddenError, NotFoundError, UnauthorizedError } from '/common/error';
88
import webhook from '/common/webhook';
99
import { spawn } from '/common/util';
10+
import hierarchy from '/common/hierarchy';
1011

1112
const app = express();
1213
app.use(morgan('tiny'));
@@ -29,6 +30,7 @@ app.use((err, req, res, next) => {
2930
res.send(err.message);
3031
console.error(err);
3132
});
33+
app.hierarchy = hierarchy;
3234

3335
const rootPath = path.resolve(__dirname, '..', '..');
3436
webhook.on('algorithm-visualizer', event => {

0 commit comments

Comments
 (0)