diff --git a/.gitignore b/.gitignore index 88f0e6b..dfa72c6 100755 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ / Coverage directories .coverage/ +*.sqlite3 +*.sqlite diff --git a/public/css/site-map.css b/public/css/site-map.css deleted file mode 100644 index eae67d8..0000000 --- a/public/css/site-map.css +++ /dev/null @@ -1,53 +0,0 @@ -/* Sitemap base styling */ -nav.sitemap { - max-width: 800px; - margin: 2rem auto; - padding: 1rem 2rem; - font-family: sans-serif; - background-color: #f8f9fa; - border: 1px solid #ddd; - border-radius: 8px; -} - -nav.sitemap h1 { - font-size: 1.8rem; - margin-bottom: 1rem; - text-align: center; - color: #333; -} - -/* Top-level list */ -nav.sitemap ul { - list-style-type: none; - padding-left: 0; - margin: 0; -} - -nav.sitemap li { - margin: 0.5rem 0; - position: relative; -} - -/* Links */ -nav.sitemap a { - text-decoration: none; - color: #007acc; - font-weight: 500; -} - -nav.sitemap a:hover { - text-decoration: underline; -} - -/* Nested lists */ -nav.sitemap li > ul { - margin-top: 0.3rem; - margin-left: 1.5rem; - padding-left: 1rem; - border-left: 2px solid #ddd; -} - -nav.sitemap li > ul li::before { - content: "→ "; - color: #999; -} diff --git a/public/css/sitemap.css b/public/css/sitemap.css new file mode 100644 index 0000000..eae67d8 --- /dev/null +++ b/public/css/sitemap.css @@ -0,0 +1,53 @@ +/* Sitemap base styling */ +nav.sitemap { + max-width: 800px; + margin: 2rem auto; + padding: 1rem 2rem; + font-family: sans-serif; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 8px; +} + +nav.sitemap h1 { + font-size: 1.8rem; + margin-bottom: 1rem; + text-align: center; + color: #333; +} + +/* Top-level list */ +nav.sitemap ul { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +nav.sitemap li { + margin: 0.5rem 0; + position: relative; +} + +/* Links */ +nav.sitemap a { + text-decoration: none; + color: #007acc; + font-weight: 500; +} + +nav.sitemap a:hover { + text-decoration: underline; +} + +/* Nested lists */ +nav.sitemap li > ul { + margin-top: 0.3rem; + margin-left: 1.5rem; + padding-left: 1rem; + border-left: 2px solid #ddd; +} + +nav.sitemap li > ul li::before { + content: "→ "; + color: #999; +} diff --git a/src/routes/index.js b/src/routes/index.js index 1f7d8e3..ef04402 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,16 +4,18 @@ const getBaseContext = require("../utils/baseContext"); const analytics = require("./analytics"); +const robots = require("./robots"); router.post("/track", analytics); const contact = require("./contact"); -const site_map = require("./site-map"); +const sitemap = require("./sitemap"); const post = require("./post"); const pages = require("./pages"); +router.use(robots); router.use(contact); -router.use(site_map); +router.use(sitemap); router.use(pages); router.get("/post/:year/:month/:name", post); diff --git a/src/routes/robots.js b/src/routes/robots.js new file mode 100644 index 0000000..5831af9 --- /dev/null +++ b/src/routes/robots.js @@ -0,0 +1,16 @@ +const express = require("express"); +const router = express.Router(); + +router.get("/robots.txt", (req, res) => { + const robotsTxt = ` +User-agent: * +Disallow: + +Sitemap: ${req.protocol}://${req.get("host")}/sitemap.xml +`.trim(); + + res.type("text/plain"); + res.send(robotsTxt); +}); + +module.exports = router; diff --git a/src/routes/site-map.js b/src/routes/site-map.js deleted file mode 100644 index 81931f7..0000000 --- a/src/routes/site-map.js +++ /dev/null @@ -1,13 +0,0 @@ -// src/routes/site-map.js -const express = require("express"); -const router = express.Router(); -const getBaseContext = require("../utils/baseContext"); - -router.get("/site-map", async (req, res) => { - const context = await getBaseContext({ - title: "Site Map", - }); - res.render("pages/site-map.handlebars", context); -}); - -module.exports = router; diff --git a/src/routes/sitemap.js b/src/routes/sitemap.js new file mode 100644 index 0000000..61acad1 --- /dev/null +++ b/src/routes/sitemap.js @@ -0,0 +1,51 @@ +// src/routes/sitemap.js +const express = require("express"); +const router = express.Router(); +const fs = require("fs"); +const path = require("path"); +const Handlebars = require("handlebars"); +const getBaseContext = require("../utils/baseContext"); +const sitemapService = require("../services/sitemapService"); + +// Precompile XML template once +const xmlTplSrc = fs.readFileSync( + path.resolve(__dirname, "../views/pages/sitemap-xml.handlebars"), + "utf-8" +); +const xmlTpl = Handlebars.compile(xmlTplSrc); + +// function flatten(entries, out = []) { +// for (const e of entries) { +// if (e.loc) out.push(e.loc); +// if (Array.isArray(e.children)) flatten(e.children, out); +// } +// return out; +// } + +// HTML sitemap page +router.get("/sitemap", async (req, res) => { + const context = await getBaseContext({ + title: "Site Map", + sitemap: await sitemapService.getCompleteSitemap(), + }); + res.render("pages/sitemap", context); +}); + +// XML sitemap endpoint +router.get("/sitemap.xml", async (req, res) => { + const urls = await sitemapService.getAllUrls(); + const baseUrl = `${req.protocol}://${req.get("host")}`; + + // Format URLs for XML template + const formattedUrls = urls.map((url) => ({ + loc: `${baseUrl}${url.loc}`, + lastmod: url.lastmod, + changefreq: url.changefreq, + priority: url.priority, + })); + + const xml = xmlTpl({ urls: formattedUrls, baseUrl }); + res.type("application/xml").send(xml); +}); + +module.exports = router; diff --git a/src/services/sitemapService.js b/src/services/sitemapService.js new file mode 100644 index 0000000..7e75e5e --- /dev/null +++ b/src/services/sitemapService.js @@ -0,0 +1,91 @@ +// src/services/sitemapService.js +const path = require("path"); +const fs = require("fs").promises; +const getPostsMenu = require("./postsMenuService"); + +class SitemapService { + constructor() { + this.staticSitemapPath = path.resolve( + __dirname, + "../../content/sitemap.json" + ); + this.postsPath = path.join(__dirname, "../../content/posts"); + } + + async getStaticPages() { + try { + const data = await fs.readFile(this.staticSitemapPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + console.warn("Could not load static sitemap.json, using empty array"); + return []; + } + } + + async getBlogPostUrls() { + const menu = await getPostsMenu(this.postsPath); + const urls = []; + + for (const yearData of menu) { + for (const monthData of yearData.months) { + for (const post of monthData.posts) { + urls.push({ + loc: `/blog/${post.year}/${post.month}/${post.slug}`, + lastmod: post.date + ? new Date(post.date).toISOString().split("T")[0] + : null, + changefreq: "monthly", + priority: "0.7", + }); + } + } + } + + return urls; + } + + async getCompleteSitemap() { + const [staticPages, blogUrls] = await Promise.all([ + this.getStaticPages(), + this.getBlogPostUrls(), + ]); + + // Add blog posts as a section in the sitemap + const blogSection = { + title: "Blog Posts", + children: blogUrls.map((url) => ({ + loc: url.loc, + title: url.loc.split("/").pop().replace(/-/g, " "), // Convert slug to title + lastmod: url.lastmod, + changefreq: url.changefreq, + priority: url.priority, + })), + }; + + return [...staticPages, blogSection]; + } + + async getAllUrls() { + const sitemap = await this.getCompleteSitemap(); + return this.flatten(sitemap); + } + + flatten(entries, out = []) { + for (const entry of entries) { + if (entry.loc) { + out.push({ + loc: entry.loc, + lastmod: entry.lastmod, + changefreq: entry.changefreq || "monthly", + priority: entry.priority || "0.5", + }); + } + if (Array.isArray(entry.children)) { + this.flatten(entry.children, out); + } + } + return out; + } +} + +module.exports = new SitemapService(); diff --git a/src/views/pages/site-map.handlebars b/src/views/pages/site-map.handlebars deleted file mode 100644 index 977ff3b..0000000 --- a/src/views/pages/site-map.handlebars +++ /dev/null @@ -1,26 +0,0 @@ -{{#section "styles"}} - -{{/section}} - - diff --git a/src/views/pages/sitemap-xml.handlebars b/src/views/pages/sitemap-xml.handlebars new file mode 100644 index 0000000..ef377fe --- /dev/null +++ b/src/views/pages/sitemap-xml.handlebars @@ -0,0 +1,11 @@ + + + {{#each urls}} + + {{this.loc}} + {{#if this.lastmod}}{{this.lastmod}}{{/if}} + {{#if this.changefreq}}{{this.changefreq}}{{/if}} + {{#if this.priority}}{{this.priority}}{{/if}} + + {{/each}} + diff --git a/src/views/pages/sitemap.handlebars b/src/views/pages/sitemap.handlebars new file mode 100644 index 0000000..dad897e --- /dev/null +++ b/src/views/pages/sitemap.handlebars @@ -0,0 +1,52 @@ +{{!-- views/pages/sitemap.handlebars --}} +{{#section "styles"}} + +{{/section}} + + diff --git a/src/views/partials/footer.handlebars b/src/views/partials/footer.handlebars index 7199ddb..54c5fa3 100644 --- a/src/views/partials/footer.handlebars +++ b/src/views/partials/footer.handlebars @@ -8,7 +8,7 @@