From f719d23be1c9f860cf13f7cbfb519c25c1a07cee Mon Sep 17 00:00:00 2001 From: GauthierWebDev Date: Tue, 22 Apr 2025 11:17:55 +0200 Subject: [PATCH] feat: Add functionality to generate sitemap --- app/build-sitemap.ts | 3 + app/package.json | 4 +- app/public/robots.txt | 3 + app/services/Sitemap.ts | 167 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 app/build-sitemap.ts create mode 100644 app/public/robots.txt create mode 100644 app/services/Sitemap.ts diff --git a/app/build-sitemap.ts b/app/build-sitemap.ts new file mode 100644 index 0000000..e31308f --- /dev/null +++ b/app/build-sitemap.ts @@ -0,0 +1,3 @@ +import { sitemap } from "./services/Sitemap"; + +sitemap.generateSitemap(); diff --git a/app/package.json b/app/package.json index 7033f39..04b058e 100755 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,9 @@ { "scripts": { "dev": "bun ./fastify-entry.ts", - "build": "cross-env DEBUG=vike:error,vike:log vike build", + "build": "bun run build:sitemap && bun run build:server", + "build:server": "cross-env DEBUG=vike:error,vike:log vike build", + "build:sitemap": "cross-env NODE_ENV=production bun ./build-sitemap.ts", "preview": "cross-env NODE_ENV=production bun ./fastify-server.ts", "production": "bun run build && bun run preview", "lint": "biome lint --write .", diff --git a/app/public/robots.txt b/app/public/robots.txt new file mode 100644 index 0000000..ac03453 --- /dev/null +++ b/app/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: +Sitemap: https://memento-dev.fr/sitemap.xml \ No newline at end of file diff --git a/app/services/Sitemap.ts b/app/services/Sitemap.ts new file mode 100644 index 0000000..4c39db1 --- /dev/null +++ b/app/services/Sitemap.ts @@ -0,0 +1,167 @@ +import { navigation } from "@/libs/navigation"; +import path from "node:path"; +import fs from "node:fs"; + +const __dirname = path.resolve(); + +const getBaseUrl = () => { + if (process.env.NODE_ENV === "production") { + return "https://memento-dev.fr"; + } + + return `http://localhost:${process.env.PORT || 3000}`; +}; + +type SitemapElement = { + location: string; + lastmod: string; + priority: string; +}; + +class Sitemap { + private readonly pagesPath = path.join(__dirname, "pages"); + private readonly sitemapPath = path.join(__dirname, "public", "sitemap.xml"); + private readonly lastModified = new Date().toISOString(); + private readonly baseUrl = getBaseUrl(); + + private urls: SitemapElement[] = []; + private sitemap = ""; + + private static instance: Sitemap; + + private constructor() {} + + public static getInstance(): Sitemap { + if (!Sitemap.instance) { + Sitemap.instance = new Sitemap(); + } + return Sitemap.instance; + } + + private resetMemory(): void { + this.sitemap = ""; + this.urls = []; + } + + private prependSitemap(): void { + this.sitemap = ``; + this.sitemap += ``; + } + + private appendSitemap(): void { + this.sitemap += ""; + } + + private addSitemapElement(url: SitemapElement): void { + this.sitemap += ""; + this.sitemap += `${url.location}`; + this.sitemap += `${url.lastmod || this.lastModified}`; + this.sitemap += `${url.priority}`; + this.sitemap += ""; + } + + private buildSitemap(): void { + this.prependSitemap(); + this.urls.forEach(this.addSitemapElement.bind(this)); + this.appendSitemap(); + } + + private saveSitemap(): void { + fs.writeFileSync(this.sitemapPath, this.sitemap, "utf8"); + } + + private loadPriority(href: string): string { + const isRootUrl = ["/", ""].includes(href); + + if (isRootUrl) return "1.0"; + const countOfSlashes = (href.match(/\//g) || []).length; + return (1 - countOfSlashes * 0.1).toFixed(1); + } + + private loadLastModified(stat?: fs.Stats): string { + return stat ? stat.mtime.toISOString() : this.lastModified; + } + + private getFileServerLocation(href: string) { + if (href === "/") return path.join(this.pagesPath, "index", "+Page.mdx"); + return path.join(this.pagesPath, href.replace("/", ""), "+Page.mdx"); + } + + private loadSubitems( + subitems: (typeof navigation)[number]["links"][number]["subitems"], + ): void { + for (const subitem of subitems) { + const fileLocation = this.getFileServerLocation(subitem.href); + let fileDetails: fs.Stats | undefined; + try { + fileDetails = fs.statSync(fileLocation); + } catch (error) { + console.error(`Error loading file for ${subitem.href}:`, error); + } + + const priority = this.loadPriority(subitem.href); + const lastmod = this.loadLastModified(fileDetails); + const location = `${this.baseUrl}${subitem.href}`; + + this.urls.push({ + location, + lastmod, + priority, + }); + } + } + + private loadSection(section: (typeof navigation)[number]): void { + for (const link of section.links) { + if (link.subitems.length > 0) { + this.loadSubitems(link.subitems); + return; + } + + const fileLocation = this.getFileServerLocation(link.href); + let fileDetails: fs.Stats | undefined; + + try { + fileDetails = fs.statSync(fileLocation); + } catch (error) { + console.error(`Error loading file for ${link.href}:`, error); + } + + const priority = this.loadPriority(link.href); + const lastmod = this.loadLastModified(fileDetails); + const location = `${this.baseUrl}${link.href}`; + + this.urls.push({ + location, + lastmod, + priority, + }); + } + } + + private loadUrls(): void { + navigation.forEach(this.loadSection.bind(this)); + + this.urls = Array.from(new Set(this.urls)).sort((a, b) => { + return a.location.localeCompare(b.location); + }); + } + + public getSitemap(): string { + if (!this.sitemap) this.generateSitemap(); + return this.sitemap; + } + + public generateSitemap(): void { + console.log("Generating sitemap..."); + + this.resetMemory(); + this.loadUrls(); + this.buildSitemap(); + this.saveSitemap(); + + console.log("Sitemap generated successfully."); + } +} + +export const sitemap = Sitemap.getInstance();