diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..296c6fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# .gitignore: + +# macOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db + +# Linux +*~ + +# Node.js +node_modules/ + +# Python +*.pyc +*.pyo +*.pyd +__pycache__/ +venv/ + +# Java +*.class +*.jar +*.war +*.ear +target/ + +# Build directories +/dist/ +/build/ +/out/ + +/ IDEs and Editors +.vscode/ +.idea/ +*.swp +*.swo + +# Log files +*.log + +# Environment files +.env + +# Miscellaneous +*.bak +*.tmp +*.swp +*.swo + +# Compiled binaries +*.exe +*.out +*.app +*.o +*.a +*.so + +# Docker +*.docker/ + +/ Coverage directories +.coverage/ diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/.gitkeep +++ /dev/null diff --git a/package-lock.json b/package-lock.json index 0412cfa..95c56aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "express": "^5.1.0", "express-handlebars": "^8.0.2", "express-rate-limit": "^7.5.0", + "gray-matter": "^4.0.3", + "hbs": "^4.2.0", "helmet": "^8.1.0", "marked": "^15.0.11", "morgan": "^1.10.0", @@ -87,6 +89,15 @@ "node": ">= 8" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -460,6 +471,19 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -540,6 +564,18 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -569,6 +605,12 @@ "node": ">= 0.8" } }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "license": "Apache2" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -716,6 +758,21 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -770,6 +827,41 @@ "node": ">= 0.4" } }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "license": "MIT", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/hbs/node_modules/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==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/helmet": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", @@ -840,6 +932,15 @@ "node": ">=8" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -906,6 +1007,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -1349,6 +1472,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -1530,6 +1666,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1635,6 +1777,15 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1728,6 +1879,15 @@ "node": ">= 0.8" } }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c53eb37..d3130d2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "express": "^5.1.0", "express-handlebars": "^8.0.2", "express-rate-limit": "^7.5.0", + "gray-matter": "^4.0.3", + "hbs": "^4.2.0", "helmet": "^8.1.0", "marked": "^15.0.11", "morgan": "^1.10.0", diff --git a/posts/2025/05/example.md b/posts/2025/05/example.md new file mode 100644 index 0000000..7b0cfb1 --- /dev/null +++ b/posts/2025/05/example.md @@ -0,0 +1,20 @@ +--- +title: "Example Blog Post" +date: "2025-05-15" +author: "Jason Poage" +--- + +# Welcome to My Blog + +This is a sample blog post written in **Markdown**. + +## Features + +- Easy to write +- Converts to clean HTML +- Supports **bold**, *italic*, and `inline code` + +## Code Example + +```js +console.log("Hello, world!"); diff --git a/posts/error.md b/posts/error.md deleted file mode 100644 index a99841c..0000000 --- a/posts/error.md +++ /dev/null @@ -1,5 +0,0 @@ -# Error {{statusCode}} - -{{message}} - -Please try again later. diff --git a/posts/example.md b/posts/example.md deleted file mode 100644 index 7b0cfb1..0000000 --- a/posts/example.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: "Example Blog Post" -date: "2025-05-15" -author: "Jason Poage" ---- - -# Welcome to My Blog - -This is a sample blog post written in **Markdown**. - -## Features - -- Easy to write -- Converts to clean HTML -- Supports **bold**, *italic*, and `inline code` - -## Code Example - -```js -console.log("Hello, world!"); diff --git a/src/api/posts.js b/src/api/posts.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/api/posts.js diff --git a/src/app.js b/src/app.js index 8660e1f..8cc7662 100644 --- a/src/app.js +++ b/src/app.js @@ -1,16 +1,18 @@ +// src/app.js const express = require("express"); -const { engine: exphbs } = require("express-handlebars"); +const exphbs = require("express-handlebars"); const setupMiddleware = require("./middleware"); +const { registerHelpers } = require("./utils/hbsHelpers"); const app = express(); -app.engine( - "handlebars", - exphbs({ - layoutsDir: "src/views/layouts", - partialsDir: "src/views/partials", - defaultLayout: "main", - }) -); +const hbs = exphbs.create({ + layoutsDir: "src/views/layouts", + partialsDir: "src/views/partials", + defaultLayout: "main", +}); +registerHelpers(hbs); + +app.engine("handlebars", hbs.engine); app.set("view engine", "handlebars"); app.set("views", "./src/views"); diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index d734e30..ac1fc4a 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -1,6 +1,4 @@ const path = require("path"); -const fs = require("fs/promises"); -const marked = require("marked"); module.exports = async (err, req, res, next) => { const statusCode = err.statusCode ?? 500; @@ -19,21 +17,9 @@ console.error(err); } - // Render markdown error page or fallback text - try { - const markdownPath = path.resolve(__dirname, "../views/error.md"); - const mdContent = await fs.readFile(markdownPath, "utf-8"); - const htmlContent = marked(mdContent.replace("{{message}}", message)); - - res - .status(statusCode) - .render("error", { content: htmlContent, statusCode, message }); - } catch { - // fallback plain HTML if markdown or template fails - res.status(statusCode).render("error", { - content: `

Error ${statusCode}

${message}

`, - statusCode, - message, - }); - } + res.status(statusCode).render("pages/error", { + statusCode, + message, + content: "", // remove markdown HTML injection, keep content empty or static partial if needed + }); }; diff --git a/src/presentation/postComponent.js b/src/presentation/postComponent.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/presentation/postComponent.js diff --git a/src/presentation/render.js b/src/presentation/render.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/presentation/render.js diff --git a/src/routes/index.js b/src/routes/index.js index 4b1edca..feef405 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,18 +4,44 @@ const { marked } = require("marked"); const fs = require("fs"); const path = require("path"); +const matter = require("gray-matter"); -router.get("/", (req, res) => { - res.render("home", { title: "Blog Home", content: "Welcome to the blog." }); +const getBaseContext = require("../utils/baseContext"); +const getPostsMenu = require("../services/postsService"); +const { formatMonth } = require("../utils/formatMonth"); + +router.get("/post/:year/:month/:name", (req, res) => { + const { year, month, name } = req.params; + const mdPath = path.join(__dirname, "../../posts", year, month, `${name}.md`); + + fs.readFile(mdPath, "utf8", async (err, fileContent) => { + if (err) return res.status(404).send("Post not found"); + + const menu = await getPostsMenu(path.join(__dirname, "../../posts")); + const { data: frontmatter, content } = matter(fileContent); + const htmlContent = marked(content); + const context = getBaseContext({ + title: frontmatter.title, + date: frontmatter.date, + author: frontmatter.author, + content: htmlContent, + years: menu, // pass the built menu here + formatMonth, // pass formatter to template + }); + res.render("pages/post", context); + }); }); -router.get("/post/:name", (req, res) => { - const mdPath = path.join(__dirname, "../posts", `${req.params.name}.md`); - console.log(mdPath); - fs.readFile(mdPath, "utf8", (err, data) => { - if (err) return res.status(404).send("Post not found"); - const htmlContent = marked(data); - res.render("post", { content: htmlContent }); +router.get("/", async (req, res) => { + const menu = await getPostsMenu(path.join(__dirname, "../../posts")); + + const context = getBaseContext({ + title: "Blog Home", + content: "Welcome to the blog.", + years: menu, // pass the built menu here + formatMonth, // pass formatter to template }); + + res.render("pages/home", context); }); module.exports = router; diff --git a/src/services/postsService.js b/src/services/postsService.js new file mode 100644 index 0000000..378926d --- /dev/null +++ b/src/services/postsService.js @@ -0,0 +1,41 @@ +const path = require("path"); +const fs = require("fs").promises; + +async function getPostsMenu(baseDir) { + const years = (await fs.readdir(baseDir, { withFileTypes: true })).filter( + (dirent) => dirent.isDirectory() && /^\d{4}$/.test(dirent.name) + ); + + const menu = []; + + for (const yearDir of years) { + const yearPath = path.join(baseDir, yearDir.name); + const months = await fs.readdir(yearPath, { withFileTypes: true }); + const monthsData = []; + + for (const monthDir of months.filter((d) => d.isDirectory())) { + const monthPath = path.join(yearPath, monthDir.name); + const files = await fs.readdir(monthPath); + + const posts = files + .filter((f) => f.endsWith(".md")) + .map((f) => { + const slug = f.replace(/\.md$/, ""); + return { + slug, + title: slug.replace(/-/g, " "), + year: yearDir.name, + month: monthDir.name, + }; + }); + + monthsData.push({ month: monthDir.name, posts }); + } + + menu.push({ year: yearDir.name, months: monthsData }); + } + + return menu; +} + +module.exports = getPostsMenu; diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js new file mode 100644 index 0000000..8f6e147 --- /dev/null +++ b/src/utils/baseContext.js @@ -0,0 +1,15 @@ +// src/utils/baseContext.js +function getBaseContext(overrides = {}) { + return Object.assign( + { + siteOwner: "Jason Poage", + navLinks: [ + { href: "/", label: "Home" }, + { href: "/about", label: "About" }, + { href: "/contact", label: "Contact" }, + ], + }, + overrides + ); +} +module.exports = getBaseContext; diff --git a/src/utils/formatMonth.js b/src/utils/formatMonth.js new file mode 100644 index 0000000..2d87582 --- /dev/null +++ b/src/utils/formatMonth.js @@ -0,0 +1,22 @@ +const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +function formatMonth(monthStr) { + const monthIndex = parseInt(monthStr, 10) - 1; + if (monthIndex < 0 || monthIndex > 11) return monthStr; + return monthNames[monthIndex]; +} + +module.exports = { formatMonth }; diff --git a/src/utils/hbsHelpers.js b/src/utils/hbsHelpers.js new file mode 100644 index 0000000..3ae0da8 --- /dev/null +++ b/src/utils/hbsHelpers.js @@ -0,0 +1,24 @@ +// src/utils/hbsHelpers.js +function registerHelpers(hbs) { + hbs.handlebars.registerHelper("formatMonth", function (monthStr) { + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + const index = parseInt(monthStr, 10) - 1; + if (index < 0 || index > 11) return monthStr; + return monthNames[index]; + }); +} + +module.exports = { registerHelpers }; diff --git a/src/views/home.handlebars b/src/views/home.handlebars deleted file mode 100644 index 7ec336f..0000000 --- a/src/views/home.handlebars +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to the Blog

-

This is the homepage.

diff --git a/src/views/layouts/main.handlebars b/src/views/layouts/main.handlebars index c480ba2..d14fb3f 100644 --- a/src/views/layouts/main.handlebars +++ b/src/views/layouts/main.handlebars @@ -1,11 +1,21 @@ - - - - - {{title}} - - - {{{body}}} - + + + + + {{title}} + + + {{> headers}} + +
+ + +
+ {{{body}}} +
+
+ diff --git a/src/views/pages/error.handlebars b/src/views/pages/error.handlebars index 4f2f73f..6d367e8 100644 --- a/src/views/pages/error.handlebars +++ b/src/views/pages/error.handlebars @@ -1,10 +1,3 @@ - - - - - Error {{statusCode}} - - - {{{content}}} - - +

Error {{statusCode}}

+

{{message}}

+{{{content}}} diff --git a/src/views/pages/home.handlebars b/src/views/pages/home.handlebars new file mode 100644 index 0000000..06523a7 --- /dev/null +++ b/src/views/pages/home.handlebars @@ -0,0 +1,6 @@ +
+

{{title}}

+
+

{{content}}

+
+
diff --git a/src/views/pages/post.handlebars b/src/views/pages/post.handlebars new file mode 100644 index 0000000..5248b4c --- /dev/null +++ b/src/views/pages/post.handlebars @@ -0,0 +1,3 @@ +
+ {{{content}}} +
diff --git a/src/views/partials/headers.handlebars b/src/views/partials/headers.handlebars index 58c488a..01fc49a 100644 --- a/src/views/partials/headers.handlebars +++ b/src/views/partials/headers.handlebars @@ -1 +1,10 @@ -
+
+
+

{{siteOwner}}

+ +
+
diff --git a/src/views/partials/postsMenu.handlebars b/src/views/partials/postsMenu.handlebars new file mode 100644 index 0000000..4504f99 --- /dev/null +++ b/src/views/partials/postsMenu.handlebars @@ -0,0 +1,15 @@ +{{#each years}} + + +{{/each}} diff --git a/src/views/post.handlebars b/src/views/post.handlebars deleted file mode 100644 index c37be88..0000000 --- a/src/views/post.handlebars +++ /dev/null @@ -1,3 +0,0 @@ -
- {{{content}}} -
\ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..7d70a75 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,164 @@ +/* Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Base styles */ +html, body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; + color: #212529; + min-height: 100vh; + line-height: 1.6; +} + +/* Container utility */ +.container { + max-width: 800px; + margin: 0 auto; + padding: 1rem 1.5rem; +} + +/* Header */ +header { + background-color: #2c3e50; + color: #ecf0f1; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +header .container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + padding: 1rem 0; +} + +header h1 { + font-size: 1.8rem; + font-weight: 700; +} + +header nav { + display: flex; + gap: 1rem; +} + +header nav a { + color: #ecf0f1; + text-decoration: none; + font-weight: 600; + padding: 0.3rem 0.6rem; + border-radius: 3px; + transition: background-color 0.2s ease-in-out; +} + +header nav a:hover, +header nav a:focus { + background-color: #34495e; +} + +/* Layout */ +.layout { + display: flex; + max-width: 1200px; + margin: 2rem auto; + padding: 0 1.5rem; + gap: 2rem; +} + +/* Sidebar */ +.sidebar { + width: 260px; + background-color: #ffffff; + border-right: 1px solid #e0e0e0; + padding: 1.5rem; + font-size: 0.95rem; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} +/* .sidebar nav .menu-year { + font-weight: bold; + margin-bottom: 1rem; + font-size: 1.1rem; +} */ +.sidebar .menu-year { + font-weight: 700; + font-size: 1.2rem; + color: #333333; + margin-bottom: 1rem; + border-bottom: 2px solid #2c3e50; + padding-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.sidebar nav ul { + list-style: none; /* Remove default bullets */ + padding-left: 0; /* Remove indent */ +} + +.sidebar .menu-month { + margin-bottom: 1rem; + font-weight: 600; + color: #555555; +} + +.sidebar .menu-month ul { + list-style: none; + padding-left: 1rem; + margin-top: 0.3rem; +} + +.sidebar .menu-month li a { + color: #2c3e50; + text-decoration: none; + display: block; + padding: 0.2rem 0; + transition: color 0.2s ease-in-out; +} + +.sidebar .menu-month li a:hover { + color: #1a73e8; + text-decoration: underline; +} +/* Main content */ +main.container { + flex: 1; + background-color: #ffffff; + padding: 2rem; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); +} + +main h2 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + border-bottom: 2px solid #2c3e50; + padding-bottom: 0.3rem; +} + +main article p { + font-size: 1rem; + color: #444; +} + +/* Responsive */ +@media (max-width: 900px) { + .layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 1rem; + } + + main.container { + margin: 0; + } +} diff --git a/static/style.css b/static/style.css deleted file mode 100644 index e69de29..0000000 --- a/static/style.css +++ /dev/null