diff --git a/README.md b/README.md index 8915a9c..2d260db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## Description -A boilerplate for `ExpressJS` using `TypeScript` +A boilerplate for `ExpressJS` using `TypeScript` and `HandleBars` as a templating engine, this is most useful +for people that wish to create a website that is server-side rendered using nodejs, in my opinion that's a great idea +it's super manageable when you have a nice project layout ( such as this one ). ## Dev Running ```bash @@ -11,4 +13,4 @@ nodemon ./src/app.ts ```bash # Run the following command in the root of the folder npm build -``` +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f9a1724..93520ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,12 @@ "@types/serve-static": "*" } }, + "@types/express-handlebars": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-3.1.0.tgz", + "integrity": "sha512-Bn6j/tfhAnZEAbMtcNUFk6ESu1I6PE2pYLbUn1PR1MyNonUuQErlQ71n9DPppHK7uAuMCfgcF0oT28Lh0ej4SQ==", + "dev": true + }, "@types/express-serve-static-core": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", @@ -55,6 +61,24 @@ "@types/range-parser": "*" } }, + "@types/handlebars": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.1.0.tgz", + "integrity": "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==", + "dev": true, + "requires": { + "handlebars": "*" + } + }, + "@types/hbs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/hbs/-/hbs-4.0.1.tgz", + "integrity": "sha512-kbgeYPLGOG8LQhqNlAvMm7vMz6Iu3IaXDEufpkEYT/viTko1ZlIrj+b6lvo4cAIDRkh6eg+JdKVZkriDGTisEw==", + "dev": true, + "requires": { + "handlebars": "^4.1.0" + } + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -103,6 +127,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -120,11 +149,25 @@ "type-is": "~1.6.17" } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -228,6 +271,16 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.2.tgz", + "integrity": "sha512-iGR7HXP+x+SfJQo9m00ocqcr7hU8ZzcssTLE/4wBX+jsqcblO6sFJEbEAEFjiNze3XMz9Y26Zs1WN5Bb4zxivQ==", + "requires": { + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "handlebars": "^4.7.7" + } + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -242,6 +295,11 @@ "unpipe": "~1.0.0" } }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -252,6 +310,50 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "hbs": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.1.2.tgz", + "integrity": "sha512-WfBnQbozbdiTLjJu6P6Wturgvy0FN8xtRmIjmP0ebX9OGQrt+2S6UC7xX0IebHTCS1sXe20zfTzQ7yhjrEvrfQ==", + "requires": { + "handlebars": "4.7.7", + "walk": "2.3.14" + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -272,6 +374,15 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -315,6 +426,19 @@ "mime-db": "1.47.0" } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -325,6 +449,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==" + }, "npm": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/npm/-/npm-7.14.0.tgz", @@ -2118,11 +2247,24 @@ "ee-first": "1.1.1" } }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -2211,6 +2353,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -2230,6 +2377,12 @@ "mime-types": "~2.1.24" } }, + "uglify-js": { + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.8.tgz", + "integrity": "sha512-PvFLMFIQHfIjFFlvAch69U2IvIxK9TNzNWt1SxZGp9JZ/v70yvqIQuiJeVPPtUMOzoNt+aNRDk4wgxb34wvEqA==", + "optional": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2244,6 +2397,24 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "walk": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", + "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", + "requires": { + "foreachasync": "^3.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } } diff --git a/package.json b/package.json index 4ad3286..083443a 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "dependencies": { "dotenv": "^10.0.0", "express": "^4.17.1", + "express-handlebars": "^5.3.2", + "hbs": "^4.1.2", "npm": "^7.14.0" }, "name": "boilerplate", @@ -10,7 +12,10 @@ "devDependencies": { "@types/body-parser": "^1.19.0", "@types/dotenv": "^8.2.0", - "@types/express": "^4.17.11" + "@types/express": "^4.17.11", + "@types/express-handlebars": "^3.1.0", + "@types/handlebars": "^4.1.0", + "@types/hbs": "^4.0.1" }, "scripts": { "build": "rm -rf dist/* && tsc", diff --git a/src/app.ts b/src/app.ts index 29ea38f..8e81c1f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,9 +2,12 @@ import http from 'http'; import express from 'express'; import config from './config/config'; + import exphbs from 'express-handlebars'; + import path from 'path'; // Routes import sampleRoutes from './routes/Sample'; + import Homepage from './routes/Homepage'; //#endregion @@ -13,6 +16,12 @@ const NAMESPACE = `App`; // Setup the expressJS instance const router = express(); +// Set the view engine as Handlebars +router.set('view engine', 'hbs'); + +router.engine("hbs", exphbs({ + extname: 'hbs' +})); // Setup the router to log all activity that is happening router.use((req, res, next) => { @@ -48,13 +57,35 @@ router.use((req, res, next) => { // Routing -router.use('/sample', sampleRoutes); +// Register the public folder where you can serve static/public data +router.use(`/public`, express.static('./src/public/')); + +// Handle the homepage +router.get("/", Homepage); + +// An example API route +router.use('/api', sampleRoutes); + { // Error handling router.use((req, res, next) => { - const error = new Error(`Api Route Not Found`); + // Generate an error + const error = new Error(`Page Not Found`); - return res.status(404).json({ + // Set the response to 404 + res.status(404) + + // Read the request's preffered response type ( default text/html ) + if ( !!req.headers.accept ) { + // Check if HTML is acceptable + console.log(req.headers.accept); + if ( req.headers.accept.includes(`text/html`) ) { + return res.sendFile(`./views/errors/404.html`, { root: __dirname }); + } + } + + // If text/html is not accepted, then simply return a JSON + return res.json({ message: error.message }) diff --git a/src/controllers/Homepage.ts b/src/controllers/Homepage.ts new file mode 100644 index 0000000..f36ba7b --- /dev/null +++ b/src/controllers/Homepage.ts @@ -0,0 +1,26 @@ +import { Request, Response, NextFunction, Router } from 'express'; + +const homepage = ( req:Request, res:Response, next:NextFunction ) => { + + // Make sure that the browser isn't caching this + res.setHeader(`Cache-Control`, `no-cache, must-revalidate`); + res.setHeader(`Pragma`, `no-cache`); + res.setHeader(`Expires`, `Sat, 26 Jul 1997 05:00:00 GMT`); + + // Render the home page + res.render('home', { + // This is a simple variable + title: 'Page Title', + // A simple list example for the loops + listExample: { + 0: {id: 0, name: `ZERO`}, + 1: {id: 1, name: `First`, disabled: true}, + 2: {id: 2, name: `Second`}, + 3: {id: 3, name: `Third`}, + }, + // layout: 'main', // Change this from main to your layout ( from the layouts folder ) if you so wish, it's basically the wrapper of it all + time: new Date().toLocaleDateString() + }); +} + +export default { homepage } \ No newline at end of file diff --git a/src/public/css/.keep b/src/public/css/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/public/img/.keep b/src/public/img/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/public/js/.keep b/src/public/js/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/Homepage.ts b/src/routes/Homepage.ts new file mode 100644 index 0000000..d4da298 --- /dev/null +++ b/src/routes/Homepage.ts @@ -0,0 +1,8 @@ +import express from 'express'; +import controller from '../controllers/Homepage'; + +const router = express.Router(); + +router.get("/", controller.homepage); + +export = router; \ No newline at end of file diff --git a/views/errors/404.html b/views/errors/404.html new file mode 100644 index 0000000..bbccdd5 --- /dev/null +++ b/views/errors/404.html @@ -0,0 +1,327 @@ + + + + + + + + + We've got some trouble | 400 - Bad Request + + + + +
+

Bad Request 400

+

We've been looking far and wide, yet couldn't find what you're looking for 😢 +

+
+ + + + \ No newline at end of file diff --git a/views/home.hbs b/views/home.hbs new file mode 100644 index 0000000..c43319f --- /dev/null +++ b/views/home.hbs @@ -0,0 +1,75 @@ + + +

About

+

Hey there, this is the main body of the content, over here you can see that this file is individual from the other + ones

+

It is completely isolated and has nothing to do with the header or the footer

+
+

Examples

+

Wanna know what date it is? It's --> {{{time}}}

+
+

Example Listing

+

+ This is just some filler text 😊 +

+

+ Sorry about the lack of style, but I really wanted to keep this as vanilla and minimal as possible + whilst still providing a solid example for as many + things as possible.
+ It is quite a bit more complex than the master branch, but it's still super useful! +

+

+ Here's an example of how to use for-loops, if you are new I think this will come in handy, I know that + it was quite a bother for me to find a working example, the following works amazingly well with this + project setup. +

+ + + + + + + + + + + {{#each listExample as | row |}} + {{!-- Sadly, handlebars can not handle Javascript inside of these little --}} + {{!-- brackets, so you have to create helpers, it's quite simple but annoying. --}} + + + + + {{/each}} + + +
#Title
{{row.id}}{{row.name}}
\ No newline at end of file diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs new file mode 100644 index 0000000..af65cbe --- /dev/null +++ b/views/layouts/main.hbs @@ -0,0 +1,9 @@ +{{> header}} +{{{body}}} + + + +{{> footer}} \ No newline at end of file diff --git a/views/partials/footer.hbs b/views/partials/footer.hbs new file mode 100644 index 0000000..9e3b677 --- /dev/null +++ b/views/partials/footer.hbs @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/views/partials/header.hbs b/views/partials/header.hbs new file mode 100644 index 0000000..7feb93e --- /dev/null +++ b/views/partials/header.hbs @@ -0,0 +1,19 @@ + + + + {{!-- Configuration Tags --}} + + + + + {{!-- How long for the cached page to be available for --}} + + {{!-- Some public information of your website --}} + + + + + {{!-- The title of your website --}} + Example Website + + \ No newline at end of file