diff --git a/.gitignore b/.gitignore index d8fc90b..68d7568 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ app/.pnpm-store app/node_modules app/dist +app/public/sitemap.xml **/*~lock* \ No newline at end of file diff --git a/app/components/syntax/PrevNextLinks.tsx b/app/components/syntax/PrevNextLinks.tsx index 5cabe56..77940dd 100644 --- a/app/components/syntax/PrevNextLinks.tsx +++ b/app/components/syntax/PrevNextLinks.tsx @@ -70,7 +70,7 @@ export function PrevNextLinks() { // In case the next page is the same as the current page (in subitems), // we need to skip it to get the correct next page. - if (nextPage.href === urlPathname) { + if (nextPage?.href === urlPathname) { nextPage = allLinks[linkIndex + 2] || null; } diff --git a/app/data/certifications/index/page.md b/app/data/certifications/page.md similarity index 100% rename from app/data/certifications/index/page.md rename to app/data/certifications/page.md diff --git a/app/data/docs/documentations/page.md b/app/data/docs/page.md similarity index 100% rename from app/data/docs/documentations/page.md rename to app/data/docs/page.md diff --git a/app/data/docs/index/page.md b/app/data/page.md similarity index 100% rename from app/data/docs/index/page.md rename to app/data/page.md diff --git a/app/fastify-entry.ts b/app/fastify-entry.ts index 2127835..dcbac11 100644 --- a/app/fastify-entry.ts +++ b/app/fastify-entry.ts @@ -3,6 +3,7 @@ import type { Theme } from "@/contexts/ThemeContext"; import { createHandler } from "@universal-middleware/fastify"; import { telefuncHandler } from "./server/telefunc-handler"; import { vikeHandler } from "./server/vike-handler"; +import { sitemap } from "./services/Sitemap"; import fastifyCookie from "@fastify/cookie"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; @@ -34,6 +35,8 @@ declare global { async function startServer() { const app = Fastify(); + sitemap.generateSitemap(); + app.register(fastifyCookie, { secret: "todo", hook: "onRequest", diff --git a/app/lib/navigation.ts b/app/lib/navigation.ts index a74b5e2..a970b4c 100644 --- a/app/lib/navigation.ts +++ b/app/lib/navigation.ts @@ -133,7 +133,7 @@ export function doesLinkSubitemExist(link: NavigationLink, subitemHref: string): return link.subitems.some((subitem) => subitem.href === subitemHref); } -export function findNavigationLink(namespace: string, href: string): NavigationLink | undefined { +export function findNavigationLink(namespace: string, href?: string): NavigationLink | undefined { const currentUrl = `/${namespace}/${href}`.replace(/\/+/g, "/").replace(/\/$/, ""); const foundLink = navigation diff --git a/app/package.json b/app/package.json index a6e5eba..58e2aca 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,7 @@ { "scripts": { "dev": "tsx ./fastify-entry.ts", + "dev:sitemap": "tsx --watch ./sitemap.ts", "build": "vike build", "preview": "cross-env NODE_ENV=production tsx ./fastify-entry.ts", "lint": "eslint .", diff --git a/app/pages/docs/+route.ts b/app/pages/docs/+route.ts index 6d0b182..1552b94 100644 --- a/app/pages/docs/+route.ts +++ b/app/pages/docs/+route.ts @@ -4,7 +4,7 @@ const routeRegex = /^\/docs\/(.*)$/; export function route(pageContext: PageContext) { if (pageContext.urlPathname === "/docs") { - return { routeParams: { key: "documentations" } }; + return { routeParams: { key: "index" } }; } const match = pageContext.urlPathname.match(routeRegex); diff --git a/app/pages/index/+data.ts b/app/pages/index/+data.ts index 1cc024d..d985aa8 100644 --- a/app/pages/index/+data.ts +++ b/app/pages/index/+data.ts @@ -12,11 +12,8 @@ export type Data = Awaited>; export async function data(_pageContext: PageContext) { const config = useConfig(); - const doc = await docsService.getDoc("docs", "index"); - - if (!doc) { - throw render(404); - } + const doc = await docsService.getDoc("root"); + if (!doc) throw render(404); const readingTimeObject = readingTime(doc.content, 300, "fr"); diff --git a/app/services/DocsService.ts b/app/services/DocsService.ts index a54ecef..53a534f 100644 --- a/app/services/DocsService.ts +++ b/app/services/DocsService.ts @@ -109,16 +109,19 @@ class DocsService { public async fetchDocs() { const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`); + const data = await Promise.all( docs.map((doc) => { const content = fs.readFileSync(doc, "utf-8"); const extension = path.extname(doc).slice(1) as DocExtension; - const key = doc + let key = doc .replace(DocsService.DOCS_PATH, "") .replace(`page.${extension}`, "") .replace(`.${extension}`, "") .replace(/\/$/g, ""); + if (key === "") key = "/root"; + const ast = Markdoc.parse(content); const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1]; const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', ""); @@ -149,23 +152,28 @@ class DocsService { }; } - public async getDoc(namespace: "docs" | "certifications", key: string) { + public async getDoc(namespace: "root" | "docs" | "certifications", key?: string): Promise { try { await this.fetchDocs(); - const doc = this.getFromCache(`/${namespace}/${key}`); + let doc: DocData | undefined; - if (!doc) { - throw new Error("Doc not found"); + if (namespace === "root" || key === "index") { + doc = this.getFromCache(`/${namespace}`); + } else { + doc = this.getFromCache(`/${namespace}/${key}`); } + if (!doc) throw new Error("Doc not found"); + return doc; } catch (error) { console.error("Error fetching docs:", error); - return null; + + return undefined; } } - public async getUrls(namespace: "docs" | "certifications") { + public async getUrls(namespace: "root" | "docs" | "certifications") { try { await this.fetchDocs(); const docs = Array.from(this.cache.keys()).filter((key) => key.startsWith(`/${namespace}`)); diff --git a/app/services/Sitemap.ts b/app/services/Sitemap.ts new file mode 100644 index 0000000..f36a283 --- /dev/null +++ b/app/services/Sitemap.ts @@ -0,0 +1,166 @@ +import { navigation } from "@/lib/navigation"; +import path from "path"; +import fs from "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 dataPath = path.join(__dirname, "data"); + private readonly sitemapPath = path.join(__dirname, "public", "sitemap.xml"); + private readonly lastModified = new Date().toISOString(); + private readonly baseUrl = getBaseUrl(); + + private urls: SitemapElement[] = []; + private sitemap: string = ""; + + 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) { + const jsxHref = ["/politique-de-confidentialite", "/mentions-legales"]; + const isJsxFile = jsxHref.includes(href); + + if (isJsxFile) { + return path.join(this.pagesPath, href.replace("/", ""), "+Page.tsx"); + } + + return path.join(this.dataPath, href.replace("/", ""), "page.md"); + } + + private loadSubitems(subitems: (typeof navigation)[number]["links"][number]["subitems"]): void { + subitems.forEach((subitem) => { + 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 { + section.links.forEach((link) => { + if (link.subitems.length > 0) { + return this.loadSubitems(link.subitems); + } + + 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 generateSitemap(): void { + console.log("Generating sitemap..."); + + this.resetMemory(); + this.loadUrls(); + this.buildSitemap(); + this.saveSitemap(); + + console.log("Sitemap generated successfully."); + } +} + +export const sitemap = Sitemap.getInstance(); diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..e31308f --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,3 @@ +import { sitemap } from "./services/Sitemap"; + +sitemap.generateSitemap();