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();