Compare commits

...

11 Commits

13 changed files with 194 additions and 15 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
app/.pnpm-store app/.pnpm-store
app/node_modules app/node_modules
app/dist app/dist
app/public/sitemap.xml
**/*~lock* **/*~lock*

View File

@ -70,7 +70,7 @@ export function PrevNextLinks() {
// In case the next page is the same as the current page (in subitems), // 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. // we need to skip it to get the correct next page.
if (nextPage.href === urlPathname) { if (nextPage?.href === urlPathname) {
nextPage = allLinks[linkIndex + 2] || null; nextPage = allLinks[linkIndex + 2] || null;
} }

View File

@ -3,6 +3,7 @@ import type { Theme } from "@/contexts/ThemeContext";
import { createHandler } from "@universal-middleware/fastify"; import { createHandler } from "@universal-middleware/fastify";
import { telefuncHandler } from "./server/telefunc-handler"; import { telefuncHandler } from "./server/telefunc-handler";
import { vikeHandler } from "./server/vike-handler"; import { vikeHandler } from "./server/vike-handler";
import { sitemap } from "./services/Sitemap";
import fastifyCookie from "@fastify/cookie"; import fastifyCookie from "@fastify/cookie";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname } from "node:path"; import { dirname } from "node:path";
@ -34,6 +35,8 @@ declare global {
async function startServer() { async function startServer() {
const app = Fastify(); const app = Fastify();
sitemap.generateSitemap();
app.register(fastifyCookie, { app.register(fastifyCookie, {
secret: "todo", secret: "todo",
hook: "onRequest", hook: "onRequest",

View File

@ -133,7 +133,7 @@ export function doesLinkSubitemExist(link: NavigationLink, subitemHref: string):
return link.subitems.some((subitem) => subitem.href === subitemHref); 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 currentUrl = `/${namespace}/${href}`.replace(/\/+/g, "/").replace(/\/$/, "");
const foundLink = navigation const foundLink = navigation

View File

@ -1,6 +1,7 @@
{ {
"scripts": { "scripts": {
"dev": "tsx ./fastify-entry.ts", "dev": "tsx ./fastify-entry.ts",
"dev:sitemap": "tsx --watch ./sitemap.ts",
"build": "vike build", "build": "vike build",
"preview": "cross-env NODE_ENV=production tsx ./fastify-entry.ts", "preview": "cross-env NODE_ENV=production tsx ./fastify-entry.ts",
"lint": "eslint .", "lint": "eslint .",

View File

@ -4,7 +4,7 @@ const routeRegex = /^\/docs\/(.*)$/;
export function route(pageContext: PageContext) { export function route(pageContext: PageContext) {
if (pageContext.urlPathname === "/docs") { if (pageContext.urlPathname === "/docs") {
return { routeParams: { key: "documentations" } }; return { routeParams: { key: "index" } };
} }
const match = pageContext.urlPathname.match(routeRegex); const match = pageContext.urlPathname.match(routeRegex);

View File

@ -12,11 +12,8 @@ export type Data = Awaited<ReturnType<typeof data>>;
export async function data(_pageContext: PageContext) { export async function data(_pageContext: PageContext) {
const config = useConfig(); const config = useConfig();
const doc = await docsService.getDoc("docs", "index"); const doc = await docsService.getDoc("root");
if (!doc) throw render(404);
if (!doc) {
throw render(404);
}
const readingTimeObject = readingTime(doc.content, 300, "fr"); const readingTimeObject = readingTime(doc.content, 300, "fr");

View File

@ -109,16 +109,19 @@ class DocsService {
public async fetchDocs() { public async fetchDocs() {
const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`); const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`);
const data = await Promise.all( const data = await Promise.all(
docs.map((doc) => { docs.map((doc) => {
const content = fs.readFileSync(doc, "utf-8"); const content = fs.readFileSync(doc, "utf-8");
const extension = path.extname(doc).slice(1) as DocExtension; const extension = path.extname(doc).slice(1) as DocExtension;
const key = doc let key = doc
.replace(DocsService.DOCS_PATH, "") .replace(DocsService.DOCS_PATH, "")
.replace(`page.${extension}`, "") .replace(`page.${extension}`, "")
.replace(`.${extension}`, "") .replace(`.${extension}`, "")
.replace(/\/$/g, ""); .replace(/\/$/g, "");
if (key === "") key = "/root";
const ast = Markdoc.parse(content); const ast = Markdoc.parse(content);
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1]; const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', ""); 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<DocData | undefined> {
try { try {
await this.fetchDocs(); await this.fetchDocs();
const doc = this.getFromCache(`/${namespace}/${key}`); let doc: DocData | undefined;
if (!doc) { if (namespace === "root" || key === "index") {
throw new Error("Doc not found"); doc = this.getFromCache(`/${namespace}`);
} else {
doc = this.getFromCache(`/${namespace}/${key}`);
} }
if (!doc) throw new Error("Doc not found");
return doc; return doc;
} catch (error) { } catch (error) {
console.error("Error fetching docs:", 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 { try {
await this.fetchDocs(); await this.fetchDocs();
const docs = Array.from(this.cache.keys()).filter((key) => key.startsWith(`/${namespace}`)); const docs = Array.from(this.cache.keys()).filter((key) => key.startsWith(`/${namespace}`));

166
app/services/Sitemap.ts Normal file
View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>`;
this.sitemap += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap-image/1.1">`;
}
private appendSitemap(): void {
this.sitemap += `</urlset>`;
}
private addSitemapElement(url: SitemapElement): void {
this.sitemap += `<url>`;
this.sitemap += `<loc>${url.location}</loc>`;
this.sitemap += `<lastmod>${url.lastmod || this.lastModified}</lastmod>`;
this.sitemap += `<priority>${url.priority}</priority>`;
this.sitemap += `</url>`;
}
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();

3
app/sitemap.ts Normal file
View File

@ -0,0 +1,3 @@
import { sitemap } from "./services/Sitemap";
sitemap.generateSitemap();