diff --git a/content b/content index 94bd7ac..89e7c1c 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit 94bd7aca98671d668ceee710b9509e7ac058f534 +Subproject commit 89e7c1c2a7857140bb29058d7028186f2a3b077e diff --git a/package-lock.json b/package-lock.json index de3105a..92d4db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "dotenv": "^17.2.1", "express": "^5.1.0", "express-handlebars": "^8.0.3", + "express-list-endpoints": "^7.1.1", "express-rate-limit": "^8.0.1", "glob": "^11.0.3", "gray-matter": "^4.0.3", @@ -1836,6 +1837,7 @@ "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -2589,7 +2591,8 @@ "version": "0.0.1464554", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff": { "version": "7.0.0", @@ -2976,6 +2979,15 @@ "node": ">=22.15.0" } }, + "node_modules/express-list-endpoints": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-7.1.1.tgz", + "integrity": "sha512-SA6YHH1r6DrioJ4fFJNqiwu1FweGFqJZO9KBApMzwPosoSGPOX2AW0wiMepOXjojjEXDuP9whIvckomheErbJA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/express-rate-limit": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", @@ -6351,6 +6363,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8397,6 +8410,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", diff --git a/package.json b/package.json index e2edac1..5592f03 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dotenv": "^17.2.1", "express": "^5.1.0", "express-handlebars": "^8.0.3", + "express-list-endpoints": "^7.1.1", "express-rate-limit": "^8.0.1", "glob": "^11.0.3", "gray-matter": "^4.0.3", diff --git a/src/middleware/baseContext.js b/src/middleware/baseContext.js index 36533a4..983405d 100644 --- a/src/middleware/baseContext.js +++ b/src/middleware/baseContext.js @@ -24,7 +24,7 @@ const filteredNavLinks = processMenuLinks( navLinks, isAuthenticated, - req.path + req.path, ); const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); const menu = await getPostsMenu(POSTS_DIR); @@ -60,13 +60,14 @@ res.locals.baseContext = baseContext; res.renderWithBaseContext = (template, overrides = {}) => { - res.render(template, Object.assign({}, baseContext, overrides)); + const context = Object.assign({}, baseContext, overrides); + res.render(template, context); }; res.renderGenericMessage = (overrides = {}) => { res.render( "pages/generic-message", - Object.assign({}, baseContext, overrides) + Object.assign({}, baseContext, overrides), ); }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 4640ffe..030d2b6 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -22,6 +22,7 @@ const analytics = require("../controllers/analyticsControllers"); const httpLogger = require("../utils/structuredLogger"); const cacheUtils = require("./cacheUtils"); +const trace = require("./trace"); function setupApp() { const app = express(); @@ -49,6 +50,7 @@ } app.use(compression()); + app.use(trace); app.use(validateRequestIntegrity); app.use(formatHtml); app.use(redirectMiddleware); diff --git a/src/middleware/trace.js b/src/middleware/trace.js new file mode 100644 index 0000000..0d33633 --- /dev/null +++ b/src/middleware/trace.js @@ -0,0 +1,10 @@ +module.exports = (req, res, next) => { + const start = performance.now(); + res.on("finish", () => { + const duration = performance.now() - start; + console.log( + `${req.method} ${req.originalUrl} - [TOTAL LATENCY]: ${duration.toFixed(2)}ms`, + ); + }); + next(); +}; diff --git a/src/routes/pages.js b/src/routes/pages.js index 21e6217..b28b5bd 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -3,9 +3,11 @@ const router = express.Router(); const ConstructionRoutes = require("../utils/ConstructionRoutes"); const MarkdownRoutes = require("../utils/MarkdownRoutes"); +const HtmlRoutes = require("../utils/htmlRoutes"); const csrfToken = require("../middleware/csrfToken"); const construction = new ConstructionRoutes(); +const html = new HtmlRoutes(); const markdown = new MarkdownRoutes(); if ( @@ -27,8 +29,10 @@ markdown.register("/tools", "tools", "tools"); markdown.register("/about/me", "about-me"); +html.register("/games/word-guesser", "word-guesser"); router.use(construction.getRouter()); +router.use(html.getRouter()); router.use(markdown.getRouter()); module.exports = router; diff --git a/src/routes/pages_1.js b/src/routes/pages_1.js new file mode 100644 index 0000000..21e6217 --- /dev/null +++ b/src/routes/pages_1.js @@ -0,0 +1,34 @@ +// src/routes/pages.js +const express = require("express"); +const router = express.Router(); +const ConstructionRoutes = require("../utils/ConstructionRoutes"); +const MarkdownRoutes = require("../utils/MarkdownRoutes"); +const csrfToken = require("../middleware/csrfToken"); + +const construction = new ConstructionRoutes(); +const markdown = new MarkdownRoutes(); + +if ( + process.env.NODE_ENV === "production" || + process.env.NODE_ENV === "testing" +) { + // construction.register("/newsletter", "Newsletter"); + construction.register("/projects", "Projects"); +} else { + markdown.register("/projects", "projects"); +} +markdown.register("/about/blog", "about-blog"); + +const newsletter = require("./newsletter"); +router.use(newsletter, csrfToken); + +construction.register("/changelog", "Changelog"); +construction.register("/archive", "Archive"); + +markdown.register("/tools", "tools", "tools"); +markdown.register("/about/me", "about-me"); + +router.use(construction.getRouter()); +router.use(markdown.getRouter()); + +module.exports = router; diff --git a/src/utils/htmlRoutes.js b/src/utils/htmlRoutes.js new file mode 100644 index 0000000..117472a --- /dev/null +++ b/src/utils/htmlRoutes.js @@ -0,0 +1,82 @@ +// src/utils/HtmlRoutes.js +const express = require("express"); +const BaseRoute = require("./BaseRoute"); +const fs = require("fs/promises"); +const path = require("path"); +const yaml = require("js-yaml"); +const { baseUrl } = require("./baseUrl.js"); + +class HtmlRoutes extends BaseRoute { + constructor() { + super(); + } + + /** + * @param {string} routePath - The URL path (e.g., '/about') + * @param {string} htmlFile - Filename in content/pages/ (e.g., 'resume' for resume.html) + * @param {string} handlebarsFile - The template to wrap the HTML in + */ + async register(routePath, contentFolder) { + // Use the 'contentFolder' argument to find the directory + const folderPath = path.join( + __dirname, + `../../content/html/${contentFolder}`, + ); + const configPath = path.join(folderPath, `config.yaml`); + + const configRaw = await fs.readFile(configPath, "utf8"); + const pageConfig = yaml.load(configRaw); + + // Fetch the actual HTML file name from the YAML config + const filePath = path.join(folderPath, pageConfig.file); + const htmlContent = await fs.readFile(filePath, "utf8"); + + const assetsDir = path.join( + __dirname, + `../../content/html/${pageConfig.slug}`, + ); + + // The router's endpoint + const assetsRouterPath = path.join(routePath, "assets"); + + // -- The uri + const assetsUri = baseUrl + assetsRouterPath; + + const extraStyles = pageConfig.styles.map( + (stylePath) => assetsUri + "/" + stylePath, + ); + const extraScripts = pageConfig.scripts.map( + (scriptsPath) => assetsUri + "/" + scriptsPath, + ); + + const context = { + title: pageConfig.title || "Default Title", + slug: routePath, + content: htmlContent, + extraStyles: extraStyles || [], + extraScripts: extraScripts || [], + meta: pageConfig.meta || {}, + }; + + console.log("Assets Router Path", assetsRouterPath); + console.log("Assets URI", assetsUri); + console.log("Assets Dir", assetsDir); + console.log("extraScripts", extraScripts); + console.log("extraStyles", extraStyles); + + this.router.use(assetsRouterPath, express.static(assetsDir)); + this.router.get(routePath, async (req, res, next) => { + // Renders the web page on each request + // Moving the logic outside of the function would read contents ahead of time + // Keeping the logic intact would allow editing files without reloading the server + try { + res.renderWithBaseContext(`pages/${pageConfig.page}`, context); + } catch (err) { + err.statusCode = 500; + next(err); + } + }); + } +} + +module.exports = HtmlRoutes; diff --git a/src/utils/logging/trace.js b/src/utils/logging/trace.js new file mode 100644 index 0000000..b18c7d1 --- /dev/null +++ b/src/utils/logging/trace.js @@ -0,0 +1,27 @@ +const fs = require("fs"); +const path = require("path"); + +const logStream = fs.createWriteStream( + path.join(__dirname, "../../trace.log"), + { flags: "a" }, +); + +function trace(label, operation) { + const start = performance.now(); + + // If operation is a promise, handle accordingly + if (operation instanceof Promise) { + return operation.finally(() => { + const duration = (performance.now() - start).toFixed(4); + logStream.write(`[TRACE] ${label}: ${duration}ms\n`); + }); + } + + // Synchronous execution + const result = operation(); + const duration = (performance.now() - start).toFixed(4); + logStream.write(`[TRACE] ${label}: ${duration}ms\n`); + return result; +} + +module.exports = { trace }; diff --git a/src/views/pages/page.handlebars b/src/views/pages/page.handlebars index aa7c863..9032a35 100644 --- a/src/views/pages/page.handlebars +++ b/src/views/pages/page.handlebars @@ -2,11 +2,17 @@ {{#section "styles"}} + {{#each extraStyles}} + + {{/each}} {{/section}} {{#section "scripts"}} + {{#each extraScripts}} + + {{/each}} {{/section}}
diff --git a/src/views/partials/extraStyles.handlebars b/src/views/partials/extraStyles.handlebars new file mode 100644 index 0000000..6d16913 --- /dev/null +++ b/src/views/partials/extraStyles.handlebars @@ -0,0 +1,7 @@ +{{!-- partials/styles.handlebars --}} + +{{#section "styles"}} + {{#each extraStyles}} + + {{/each}} +{{/section}} diff --git a/src/views/partials/scripts.handlebars b/src/views/partials/scripts.handlebars new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/views/partials/scripts.handlebars