diff --git a/public/css/presentation.css b/public/css/presentation.css new file mode 100644 index 0000000..11fafdb --- /dev/null +++ b/public/css/presentation.css @@ -0,0 +1,115 @@ + html { + font-size: 16px !important; + } + .reveal { + font-size: 1rem !important; + } + + .reveal h1 { + font-size: 3.8rem !important; + line-height: 1.2 !important; + margin-bottom: 0.5em !important; + } + + .reveal h2 { + font-size: 2.5rem !important; + line-height: 1.3 !important; + margin-bottom: 0.8em !important; + } + + .reveal h3 { font-size: 1.8rem !important; } + .reveal h4 { font-size: 1.4rem !important; } + + .reveal p, + .reveal ul li { + font-size: 1.15rem !important; + line-height: 1.6 !important; + margin-bottom: 0.8em !important; + text-align: left !important; + padding: 0 !important; + max-width: 900px !important; + margin-left: auto !important; + margin-right: auto !important; + } + + .slide-content-wrapper { + display: flex !important; + flex-direction: column !important; + justify-content: flex-start !important; + align-items: center !important; + width: 100% !important; + height: calc(100% - 140px) !important; + max-height: calc(100% - 140px) !important; + overflow-y: auto !important; + padding: 0 60px !important; + box-sizing: border-box !important; + } + + .content-block { + width: 100% !important; + max-width: 900px !important; + margin-bottom: 25px !important; + box-sizing: border-box !important; + } + + .content-list ul { + list-style-position: outside !important; + padding-left: 25px !important; + margin-left: 0 !important; + } + .content-list ul li { + margin-bottom: 0.5em !important; + } + + .slide-images { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 25px !important; + margin-top: 25px !important; + padding: 0 !important; + overflow: hidden !important; + } + + .slide-images img { + width: auto !important; + height: auto !important; + max-width: 100% !important; + max-height: 100% !important; + object-fit: contain !important; + display: block !important; + + flex-grow: 1 !important; + flex-shrink: 1 !important; + flex-basis: auto !important; + + min-width: 250px !important; + max-width: 48% !important; + max-height: 40vh !important; + + border: 1px solid rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 8px 16px rgba(0,0,0,0.4) !important; + + cursor: zoom-in; + } + + .reveal img.reveal-lightbox-img { + cursor: pointer !important; + } + + .reveal section[data-state*="Showcase: Error"] .slide-images img, + .reveal section[data-state*="Showcase: Essential"] .slide-images img, + .reveal section[data-state*="Showcase: Documentation"] .slide-images img { + max-width: 31% !important; + max-height: 30vh !important; + } + + .reveal .lightbox { + background: rgba(0, 0, 0, 0.9) !important; + } + .reveal .lightbox-image { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6) !important; + max-width: 90vw !important; + max-height: 90vh !important; + } diff --git a/public/images/about-me.png b/public/images/about-me.png new file mode 100644 index 0000000..49de49b --- /dev/null +++ b/public/images/about-me.png Binary files differ diff --git a/public/images/construction.png b/public/images/construction.png new file mode 100644 index 0000000..53f7ec6 --- /dev/null +++ b/public/images/construction.png Binary files differ diff --git a/public/images/contact.png b/public/images/contact.png new file mode 100644 index 0000000..e870b68 --- /dev/null +++ b/public/images/contact.png Binary files differ diff --git a/public/images/documentation.png b/public/images/documentation.png new file mode 100644 index 0000000..2192004 --- /dev/null +++ b/public/images/documentation.png Binary files differ diff --git a/public/images/error403.png b/public/images/error403.png new file mode 100644 index 0000000..d800015 --- /dev/null +++ b/public/images/error403.png Binary files differ diff --git a/public/images/error404.png b/public/images/error404.png new file mode 100644 index 0000000..dd0f428 --- /dev/null +++ b/public/images/error404.png Binary files differ diff --git a/public/images/error500.png b/public/images/error500.png new file mode 100644 index 0000000..953f162 --- /dev/null +++ b/public/images/error500.png Binary files differ diff --git a/public/images/footer.png b/public/images/footer.png new file mode 100644 index 0000000..bf28e48 --- /dev/null +++ b/public/images/footer.png Binary files differ diff --git a/public/images/home.png b/public/images/home.png new file mode 100644 index 0000000..b07b08a --- /dev/null +++ b/public/images/home.png Binary files differ diff --git a/public/images/logging.png b/public/images/logging.png new file mode 100644 index 0000000..9a0dd6f --- /dev/null +++ b/public/images/logging.png Binary files differ diff --git a/public/images/nav-bar.png b/public/images/nav-bar.png new file mode 100644 index 0000000..59e70e0 --- /dev/null +++ b/public/images/nav-bar.png Binary files differ diff --git a/public/images/newsletter.png b/public/images/newsletter.png new file mode 100644 index 0000000..d04be6d --- /dev/null +++ b/public/images/newsletter.png Binary files differ diff --git a/public/images/sitemap-xml.png b/public/images/sitemap-xml.png new file mode 100644 index 0000000..51342f5 --- /dev/null +++ b/public/images/sitemap-xml.png Binary files differ diff --git a/public/images/sitemap.png b/public/images/sitemap.png new file mode 100644 index 0000000..54ee5c2 --- /dev/null +++ b/public/images/sitemap.png Binary files differ diff --git a/public/images/table-of-contents.png b/public/images/table-of-contents.png new file mode 100644 index 0000000..364949b --- /dev/null +++ b/public/images/table-of-contents.png Binary files differ diff --git a/public/js/presentation.js b/public/js/presentation.js new file mode 100644 index 0000000..0b07290 --- /dev/null +++ b/public/js/presentation.js @@ -0,0 +1,17 @@ +import Reveal from "https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/dist/reveal.esm.js"; +import RevealMarkdown from "https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/plugin/markdown/markdown.esm.js"; +import RevealHighlight from "https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/plugin/highlight/highlight.esm.js"; + +const deck = new Reveal({ + hash: true, + slideNumber: true, + plugins: [RevealMarkdown, RevealHighlight], + embedded: true, + controls: true, + progress: true, + history: true, + center: true, + transition: "slide", +}); + +deck.initialize(); diff --git a/src/constants/securityConstants.js b/src/constants/securityConstants.js index cd0344e..9583ece 100644 --- a/src/constants/securityConstants.js +++ b/src/constants/securityConstants.js @@ -1,6 +1,6 @@ // config/securityConstants.js -module.exports = { +module.exports = ({ nonce }) => ({ LOCALHOST_HOSTNAMES: ["127.0.0.1", "localhost"], HEALTHCHECK_METHOD: "HEAD", HEALTHCHECK_PATH: "/health", @@ -9,8 +9,13 @@ HSTS_MAX_AGE: 63072000, CSP_DIRECTIVES: { defaultSrc: ["'self'"], - scriptSrc: ["'self'", "https://hcaptcha.com"], - styleSrc: ["'self'", "https:"], + scriptSrc: ["'self'", "https://hcaptcha.com", "https://cdn.jsdelivr.net"], + styleSrc: [ + "'self'", + "https:", + "'sha256-huhqpKwGcFswbXjh5F/DueoxnLh3Yh/pg/lNbo+tnLE='", + `'${nonce}'`, + ], imgSrc: [ "'self'", "data:", @@ -21,4 +26,4 @@ objectSrc: ["'none'"], upgradeInsecureRequests: [], }, -}; +}); diff --git a/src/middleware/applyProductionSecurity.js b/src/middleware/applyProductionSecurity.js index b4fcbe6..d9c64cd 100644 --- a/src/middleware/applyProductionSecurity.js +++ b/src/middleware/applyProductionSecurity.js @@ -31,6 +31,29 @@ } next(); }; +const crypto = require("crypto"); + +function generateNonce() { + return crypto.randomBytes(16).toString("base64"); +} + +const securityPolicy = (req, res, next) => { + const nonce = generateNonce(); + res.locals.nonce = nonce; // if templates need it + + helmet.contentSecurityPolicy({ + directives: { + ...CSP_DIRECTIVES, + defaultSrc: ["'self'", baseUrl], + scriptSrc: [ + "'self'", + `'nonce-${nonce}'`, + "https://hcaptcha.com", + "https://cdn.jsdelivr.net", + ], + }, + })(req, res, next); +}; const applyProductionSecurity = [ disablePoweredBy, @@ -39,9 +62,7 @@ // rateLimit middleware can be added here blockLocalhostAccess, helmet.hsts({ maxAge: HSTS_MAX_AGE }), - helmet.contentSecurityPolicy({ - directives: { ...CSP_DIRECTIVES, defaultSrc: ["'self'", baseUrl] }, - }), + securityPolicy, ]; module.exports = applyProductionSecurity; diff --git a/src/routes/index.js b/src/routes/index.js index 253b719..086be87 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -10,6 +10,7 @@ const errorPage = require("./errorPage"); const admin = require("./admin"); const tags = require("./tags"); +const presentation = require("./presentation"); const contact = require("./contact"); const sitemap = require("./sitemap"); @@ -48,11 +49,11 @@ extensions: false, fallthrough: false, setHeaders: (res) => { - // Since GPT's like to remove comments - // let's hard code this in here as a reminder to change the cache timing later - if (stable) { + if (process.env.NODE_ENV == "production") { + // Doesn't expire res.set("Cache-Control", "public, max-age=31536000, immutable"); } else { + // Live long enough for the page to load res.set("Cache-Control", "public, max-age=30, must-revalidate"); } }, @@ -68,6 +69,7 @@ router.use(pages); router.use(rssFeed); router.use(tags); +router.use("/presentation", presentation); router.use("/docs", docs); router.get("/blog/:year/:month/:name", post); diff --git a/src/routes/presentation.js b/src/routes/presentation.js new file mode 100644 index 0000000..ba314c3 --- /dev/null +++ b/src/routes/presentation.js @@ -0,0 +1,46 @@ +// src/routes/presentation.js +const express = require("express"); +const path = require("path"); +const fs = require("fs/promises"); +const yaml = require("js-yaml"); + +const router = express.Router(); +const HttpError = require("../utils/HttpError"); +const { qualifyLink } = require("../utils/qualifyLinks"); +const { baseUrl } = require("../utils/baseUrl"); + +const yamlPath = path.resolve("content/presentation.yaml"); + +router.get("/", async (req, res, next) => { + try { + const fileContent = await fs.readFile(yamlPath, "utf8"); + const data = yaml.load(fileContent); + + // Wrap relative URLs with qualifyLink() + if (data.slides) { + for (const slide of data.slides) { + if (slide.images) { + slide.images = slide.images.map((img) => { + if (img.src && !img.src.match(/^https?:\/\//)) { + img.src = qualifyLink(img.src); + } + return img; + }); + } + } + } + + res.render("pages/presentation", { + layout: "presentation", + slides: data.slides, + title: data.title, + baseUrl, + nonce: res.locals.nonce, + }); + } catch (err) { + req.log.error(err.stack); + next(new HttpError("Failed to load presentation data", 500)); + } +}); + +module.exports = router; diff --git a/src/views/layouts/presentation.handlebars b/src/views/layouts/presentation.handlebars new file mode 100644 index 0000000..23ee7b7 --- /dev/null +++ b/src/views/layouts/presentation.handlebars @@ -0,0 +1,62 @@ + + + +
+ + + + + + + + {{{_sections.styles}}} + +