aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Powers <markppowers0@gmail.com>2020-11-15 21:43:36 -0600
committerMark Powers <markppowers0@gmail.com>2020-11-15 21:43:36 -0600
commit63668ca89f055b2233a8aa6caf4213853918d68d (patch)
tree0d9bf673daaadba36b70a3b7651a1c46c074e938
parent104d9fd314d6f8650c8e032164c7db00c869eea7 (diff)
Switch to handlebars for templating
-rw-r--r--package-lock.json33
-rw-r--r--package.json1
-rw-r--r--src/css/mobile-styles.css23
-rw-r--r--src/css/styles.css31
-rw-r--r--src/html/admin.html3
-rw-r--r--src/index.js3
-rw-r--r--src/server.js90
-rw-r--r--src/templates.js115
-rw-r--r--src/templates/blog-single.html23
-rw-r--r--src/templates/blog.html22
-rw-r--r--src/templates/bread.html23
-rw-r--r--src/templates/feed.html24
-rw-r--r--src/templates/footer.html14
-rw-r--r--src/templates/index.html20
-rw-r--r--src/templates/misc.html (renamed from src/html/misc.html)1
-rw-r--r--src/templates/navigation.html10
-rw-r--r--src/templates/projects.html (renamed from src/html/projects.html)1
-rw-r--r--src/templates/tags.html22
18 files changed, 303 insertions, 156 deletions
diff --git a/package-lock.json b/package-lock.json
index 1f39823..8f43397 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -458,6 +458,18 @@
"assert-plus": "^1.0.0"
}
},
+ "handlebars": {
+ "version": "4.7.6",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
+ "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
+ "requires": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.0",
+ "source-map": "^0.6.1",
+ "uglify-js": "^3.1.4",
+ "wordwrap": "^1.0.0"
+ }
+ },
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -807,6 +819,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
+ "neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+ },
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -1075,6 +1092,11 @@
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
"integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
"sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
@@ -1179,6 +1201,12 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
+ "uglify-js": {
+ "version": "3.11.6",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.6.tgz",
+ "integrity": "sha512-oASI1FOJ7BBFkSCNDZ446EgkSuHkOZBuqRFrwXIKWCoXw8ZXQETooTQjkAcBS03Acab7ubCKsXnwuV2svy061g==",
+ "optional": true
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1235,6 +1263,11 @@
"@types/node": "*"
}
},
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+ },
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
diff --git a/package.json b/package.json
index 8012d6a..4fab4bf 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.4",
"express": "^4.17.1",
+ "handlebars": "^4.7.6",
"jsonwebtoken": "^8.5.1",
"marked": "^1.1.1",
"minimist": ">=0.2.1",
diff --git a/src/css/mobile-styles.css b/src/css/mobile-styles.css
new file mode 100644
index 0000000..36553cc
--- /dev/null
+++ b/src/css/mobile-styles.css
@@ -0,0 +1,23 @@
+@media screen and (min-width: 900px) {
+ body {
+ width: 100%;
+ /* margin: auto; */
+ }
+ .card-img {
+ display: flex;
+ flex-flow: row wrap;
+ align-content: flex-start;
+ }
+ .card img {
+ width: 400px;
+ }
+ form input[type=text], input[type=password], input[type=email] {
+ max-width: 600px;
+ }
+ form textarea {
+ max-width: 600px;
+ }
+ form input[type=submit] {
+ margin-left: 0px;
+ }
+} \ No newline at end of file
diff --git a/src/css/styles.css b/src/css/styles.css
index 522b007..f406104 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -11,8 +11,9 @@
body {
- width: 100%;
- margin: 0px;
+ width: 80%;
+ margin: auto;
+ /* margin: 0px; */
background-color: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@@ -93,7 +94,7 @@ a {
width: 100%;
}
.feed {
- width: 80%;
+ /* width: 80%; */
min-width: 200px;
margin: auto;
display: flex;
@@ -109,30 +110,6 @@ p {
margin-top: 0;
}
-@media screen and (min-width: 900px) {
- body {
- width: 100%;
- /* margin: auto; */
- }
- .card-img {
- display: flex;
- flex-flow: row wrap;
- align-content: flex-start;
- }
- .card img {
- width: 400px;
- }
- form input[type=text], input[type=password], input[type=email] {
- max-width: 600px;
- }
- form textarea {
- max-width: 600px;
- }
- form input[type=submit] {
- margin-left: 0px;
- }
-}
-
.titlebar {
padding: 5px;
border-top: 1px solid var(--light-text);
diff --git a/src/html/admin.html b/src/html/admin.html
index c6683c3..3eaa60a 100644
--- a/src/html/admin.html
+++ b/src/html/admin.html
@@ -32,6 +32,7 @@
</head>
<body>
+ <noscript>Please enable Javascript!</noscript>
<h1>
<a class="navigation" href="/" title="marks.kitchen">&lt;</a>
Admin
@@ -135,4 +136,4 @@
</div>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/src/index.js b/src/index.js
index 62d8bb5..4b9a3c6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -100,8 +100,9 @@ function setUpModels(){
}
const models = setUpModels();
+const templates = require('./templates');
sync();
-server.setUpRoutes(models, jwtFunctions, database);
+server.setUpRoutes(models, jwtFunctions, database, templates.setUpTemplates());
server.listen(config.port);
diff --git a/src/server.js b/src/server.js
index 7be1237..e82fc12 100644
--- a/src/server.js
+++ b/src/server.js
@@ -87,7 +87,7 @@ async function constructFeedFromType(models, postType){
return constructFeed(posts)
}
-async function constructSinglePost(models, postType, postId){
+async function formatPostsforSingle(models, postType, postId){
var posts = await models.posts.findAll({
where: {
type: postType,
@@ -96,11 +96,25 @@ async function constructSinglePost(models, postType, postId){
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
-
- return constructFeed(posts)
+ posts.forEach(post => {
+ post.createdAt = post.createdAt.toString().substring(0, 10)
+ })
+ return posts
+}
+
+async function formatPostsForType(models, postType){
+ var posts = await models.posts.findAll({
+ where: { type: postType }, order: [['createdAt', 'DESC']]
+ });
+ posts = posts.map(x => x.get({ plain: true }));
+ await addImagesAndTagsToPosts(models, posts)
+ posts.forEach(post => {
+ post.createdAt = post.createdAt.toString().substring(0, 10)
+ })
+ return posts;
}
-function setUpRoutes(models, jwtFunctions, database) {
+function setUpRoutes(models, jwtFunctions, database, templates) {
// Authentication routine
server.use(function (req, res, next) {
if (req.path.toLowerCase().startsWith("/admin")) {
@@ -142,41 +156,25 @@ function setUpRoutes(models, jwtFunctions, database) {
})
server.get('/', async (req, res) => {
- var html = []
- html.push(templates["index"]["pre"])
- html.push(templates["titlebar"])
- html.push(await constructFeedFromType(models, "index"))
- html.push(templates["footer"])
- html.push(templates["index"]["post"])
-
- res.status(200).send(html.join(""))
+ let posts = await formatPostsForType(models, "index")
+ let body = templates["index"]({posts});
+ res.status(200).send(body)
})
server.get('/bread', async (req, res) => {
- var html = []
- html.push(templates["bread"]["pre"])
- html.push(await constructFeedFromType(models, "bread"))
- html.push(templates["footer"])
- html.push(templates["bread"]["post"])
-
- res.status(200).send(html.join(""))
+ let posts = await formatPostsForType(models, "bread")
+ let body = templates["bread"]({posts});
+ res.status(200).send(body)
})
server.get('/blog', async (req, res) => {
- var html = []
- html.push(templates["blog"]["pre"])
- html.push(await constructFeedFromType(models, "blog"))
- html.push(templates["footer"])
- html.push(templates["blog"]["post"])
-
- res.status(200).send(html.join(""))
+ let posts = await formatPostsForType(models, "blog")
+ let body = templates["blog"]({posts});
+ res.status(200).send(body)
})
server.get('/post/:type/:id', async (req, res) => {
- var html = []
- html.push(templates["blog"]["pre"])
- html.push(await constructSinglePost(models, req.params.type, req.params.id))
- html.push(templates["footer"])
- html.push(templates["blog"]["post"])
-
- res.status(200).send(html.join(""))
+ let posts = await formatPostsforSingle(models, req.params.type, req.params.id)
+ let date = posts[0].createdAt;
+ let body = templates["blog-single"]({posts, date});
+ res.status(200).send(body)
})
server.get('/tags/:name', async (req, res) => {
const { name } = req.params;
@@ -189,15 +187,12 @@ function setUpRoutes(models, jwtFunctions, database) {
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
+ posts.forEach(post => {
+ post.createdAt = post.createdAt.toString().substring(0, 10)
+ })
- var html = []
- html.push(templates["blog"]["pre"])
- html.push(`<h1>#${name}</h1>`)
- html.push(await constructFeed(posts))
- html.push(templates["footer"])
- html.push(templates["blog"]["post"])
-
- res.status(200).send(html.join(""))
+ let body = templates["tags"]({posts, name})
+ res.status(200).send(body)
})
server.get('/admin', (req, res) => res.sendFile(__dirname + "/html/admin.html"));
@@ -206,14 +201,23 @@ function setUpRoutes(models, jwtFunctions, database) {
server.get('/email-success', (req, res) => res.sendFile(__dirname + "/html/email-success.html"))
server.get('/feed', (req, res) => res.sendFile(__dirname + "/html/feed.html"));
server.get('/essay', (req, res) => res.sendFile(__dirname + "/html/essay.html"));
- server.get('/misc', (req, res) => res.sendFile(__dirname + "/html/misc.html"));
+ // server.get('/misc', (req, res) => res.sendFile(__dirname + "/html/misc.html"));
server.get('/word-square', (req, res) => res.sendFile(__dirname + "/html/word-square.html"));
server.get('/chess', (req, res) => res.sendFile(__dirname + "/html/chess.html"));
server.get('/admin/chess', async (req, res, next) => res.sendFile(__dirname + "/html/chess.html"));
- server.get('/projects', (req, res) => res.sendFile(__dirname + "/html/projects.html"));
+ // server.get('/projects', (req, res) => res.sendFile(__dirname + "/html/projects.html"));
server.get('/zines', (req, res) => res.sendFile(__dirname + "/public/zines.html"));
server.use('/static', express.static(__dirname + '/public'))
+ server.get('/misc', async (req, res) => {
+ let body = templates["misc"]();
+ res.status(200).send(body)
+ })
+ server.get('/projects', async (req, res) => {
+ let body = templates["projects"]();
+ res.status(200).send(body)
+ })
+
server.get('/wordsquares/best', async (req, res, next) => {
var best = await database.query("select words, name from wordsquares where best = 1", { type: database.QueryTypes.SELECT })
res.status(200).send({ best: best });
diff --git a/src/templates.js b/src/templates.js
index b748853..293f0e4 100644
--- a/src/templates.js
+++ b/src/templates.js
@@ -1,86 +1,33 @@
-module.exports = {
- titlebar: `<nav class="titlebar">
- <div>
- <a href="bread" class="btn btn-primary">Bread</a>
- <a href="blog" class="btn btn-primary">Blog</a>
- <!-- <a href="essay" class="btn btn-primary">Essays</a> (Hello inspector, this page exists, but just isn't very interesting, so I'm removing the link) -->
- <a href="https://games.marks.kitchen" class="btn btn-primary">Games</a>
- <a href="email" class="btn btn-primary">Email</a>
- <a href="misc" class="btn btn-primary">Misc</a>
-</div>
-</nav>`,
- footer: `<footer>
- <div>Mark Powers (<span class="email">mark</span>) &#169; 2020 </div>
- <div>
- <a href="/feed.xml">RSS feed</a>
- <span class="spacer"></span>
- <a href="https://github.com/Mark-Powers">GitHub</a>
- <span class="spacer"></span>
- <a href="https://git.marks.kitchen/mark">Gitea</a>
- <span class="spacer"></span>
- <a href="https://fosstodon.org/@markp">Mastodon</a>
- <span class="spacer"></span>
- <br>
- </div>
-</footer>`,
+const fs = require('fs');
+const path = require('path');
+const handlebars = require("handlebars");
+
+function loadTemplate(templates, name, filepath){
+ const templateContent = fs.readFileSync(filepath).toString()
+ templates[name] = handlebars.compile(templateContent);
+}
+
+function loadPartial(name, filepath){
+ handlebars.registerPartial(name, fs.readFileSync(filepath).toString());
+}
+
+function setUpTemplates(){
+ loadPartial("navigation", path.join(__dirname, "templates/navigation.html"))
+ loadPartial("footer", path.join(__dirname, "templates/footer.html"))
+ loadPartial("feed", path.join(__dirname, "templates/feed.html"))
- index: {
- pre: `<!doctype html>
- <html lang="en">
-
- <head>
- <title>Mark's Kitchen</title>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <link rel="stylesheet" type="text/css" href="/css/styles.css">
- <link rel="shortcut icon" href="/favicon.ico">
- <link rel="alternate" type="application/rss+xml" title="RSS Feed for marks.kitchen" href="/feed.xml" />
- </head>
-
- <body>
- <h1>Welcome to Mark's Kitchen</h1>`,
- post: `</body>
- </html>`
- },
- bread: {
- pre: `<!doctype html>
- <html lang="en">
-
- <head>
- <title>Mark's Kitchen - Bread</title>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <link rel="stylesheet" type="text/css" href="/css/styles.css">
- <link rel="shortcut icon" href="/favicon.ico">
- </head>
-
- <body>
- <h1>
- <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
- Bread
- </h1>
- Some highlights (and lowlights) of breadmaking`,
- post: `</body>
- </html>`
- },
- blog: {
- pre: `<!doctype html>
- <html lang="en">
-
- <head>
- <title>Mark's Kitchen - Blog</title>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <link rel="stylesheet" type="text/css" href="/css/styles.css">
- <link rel="shortcut icon" href="/favicon.ico">
- </head>
-
- <body>
- <h1>
- <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
- Blog
- </h1>`,
- post: `</body>
- </html>`
- }
+ let templates = {};
+ loadTemplate(templates, "index", path.join(__dirname, 'templates/index.html'))
+ loadTemplate(templates, "bread", path.join(__dirname, 'templates/bread.html'))
+ loadTemplate(templates, "blog", path.join(__dirname, 'templates/blog.html'))
+ loadTemplate(templates, "blog-single", path.join(__dirname, 'templates/blog-single.html'))
+ loadTemplate(templates, "tags", path.join(__dirname, 'templates/tags.html'))
+ loadTemplate(templates, "misc", path.join(__dirname, 'templates/misc.html'))
+ loadTemplate(templates, "projects", path.join(__dirname, 'templates/projects.html'))
+ return templates
+}
+
+
+module.exports = {
+ setUpTemplates
} \ No newline at end of file
diff --git a/src/templates/blog-single.html b/src/templates/blog-single.html
new file mode 100644
index 0000000..8622007
--- /dev/null
+++ b/src/templates/blog-single.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Kitchen - Blog</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <link rel="shortcut icon" href="/favicon.ico">
+</head>
+
+<body>
+ <h1>
+ <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
+ <a class="navigation" href="/blog" title="blog">&lt;</a>
+ Post from {{date}}
+ </h1>
+ {{> navigation}}
+ {{> feed}}
+ {{> footer}}
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/templates/blog.html b/src/templates/blog.html
new file mode 100644
index 0000000..40b86f5
--- /dev/null
+++ b/src/templates/blog.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Kitchen - Blog</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <link rel="shortcut icon" href="/favicon.ico">
+</head>
+
+<body>
+ <h1>
+ <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
+ Blog
+ </h1>
+ {{> navigation}}
+ {{> feed}}
+ {{> footer}}
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/templates/bread.html b/src/templates/bread.html
new file mode 100644
index 0000000..b5d02ed
--- /dev/null
+++ b/src/templates/bread.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Kitchen - Bread</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <link rel="shortcut icon" href="/favicon.ico">
+</head>
+
+<body>
+ <h1>
+ <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
+ Bread
+ </h1>
+ <p>Some highlights (and lowlights) of breadmaking</p>
+ {{> navigation}}
+ {{> feed}}
+ {{> footer}}
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/templates/feed.html b/src/templates/feed.html
new file mode 100644
index 0000000..2158f3e
--- /dev/null
+++ b/src/templates/feed.html
@@ -0,0 +1,24 @@
+<div class="feed">
+ {{#each posts}}
+ <div class="card">
+ <p class="card-text">{{{this.description}}}</p>
+ <div class="card-img">
+ {{#each this.images}}
+ <span>
+ <a href="/{{this}}"><img src="/{{this}}"></a>
+ </span>
+ {{/each}}
+ </div>
+ <p class="date">
+ <a href="/post/{{this.type}}/{{this.id}}">
+ {{this.createdAt}}
+ </a>
+ <span>
+ {{#each this.tags}}
+ <a class="tag" href="/tags/{{this}}">{{this}}</a>
+ {{/each}}
+ </span>
+ </p>
+ </div>
+ {{/each}}
+</div> \ No newline at end of file
diff --git a/src/templates/footer.html b/src/templates/footer.html
new file mode 100644
index 0000000..617b5fa
--- /dev/null
+++ b/src/templates/footer.html
@@ -0,0 +1,14 @@
+<footer>
+ <div>Mark Powers (<span class="email">mark</span>) &#169; 2020 </div>
+ <div>
+ <a href="/feed.xml">RSS feed</a>
+ <span class="spacer"></span>
+ <a href="https://github.com/Mark-Powers">GitHub</a>
+ <span class="spacer"></span>
+ <a href="https://git.marks.kitchen">Git Repos</a>
+ <span class="spacer"></span>
+ <a href="https://fosstodon.org/@markp">Mastodon</a>
+ <span class="spacer"></span>
+ <br>
+ </div>
+</footer> \ No newline at end of file
diff --git a/src/templates/index.html b/src/templates/index.html
new file mode 100644
index 0000000..efb7369
--- /dev/null
+++ b/src/templates/index.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Kitchen</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <link rel="shortcut icon" href="/favicon.ico">
+ <link rel="alternate" type="application/rss+xml" title="RSS Feed for marks.kitchen" href="/feed.xml" />
+</head>
+
+<body>
+ <h1>Welcome to Mark's Kitchen</h1>
+ {{> navigation}}
+ {{> feed}}
+ {{> footer}}
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/html/misc.html b/src/templates/misc.html
index 6ab0a15..9fb376a 100644
--- a/src/html/misc.html
+++ b/src/templates/misc.html
@@ -15,6 +15,7 @@
<a class="navigation" href="/" title="marks.kitchen">&lt;</a>
Miscellany
</h1>
+ {{> navigation}}
<div>
<a href="projects" class="btn btn-primary">Projects</a>
</div>
diff --git a/src/templates/navigation.html b/src/templates/navigation.html
new file mode 100644
index 0000000..6bb10f1
--- /dev/null
+++ b/src/templates/navigation.html
@@ -0,0 +1,10 @@
+<nav class="titlebar">
+ <div>
+ <a href="/" class="btn btn-primary">Home</a>
+ <a href="/bread" class="btn btn-primary">Bread</a>
+ <a href="/blog" class="btn btn-primary">Blog</a>
+ <a href="https://games.marks.kitchen" class="btn btn-primary">Games</a>
+ <a href="/email" class="btn btn-primary">Email</a>
+ <a href="/misc" class="btn btn-primary">Misc</a>
+ </div>
+</nav> \ No newline at end of file
diff --git a/src/html/projects.html b/src/templates/projects.html
index fef817f..cf3361a 100644
--- a/src/html/projects.html
+++ b/src/templates/projects.html
@@ -15,6 +15,7 @@
<a class="navigation" href="/misc" title="marks.kitchen/misc">&lt;</a>
Projects
</h1>
+ {{> navigation}}
<h2>A collection of my projects, mostly programming</h2>
<div id="feed" class="feed projects">
<div class="card">
diff --git a/src/templates/tags.html b/src/templates/tags.html
new file mode 100644
index 0000000..31858e8
--- /dev/null
+++ b/src/templates/tags.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Kitchen - Tags</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <link rel="shortcut icon" href="/favicon.ico">
+</head>
+
+<body>
+ <h1>
+ <a class="navigation" href="/" title="marks.kitchen">&lt;</a>
+ Tag {{name}}
+ </h1>
+ {{> navigation}}
+ {{> feed}}
+ {{> footer}}
+</body>
+
+</html> \ No newline at end of file