Compare commits

...

2 Commits

7 changed files with 221 additions and 4 deletions

3
app/build-sitemap.ts Normal file
View File

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

View File

@ -4,7 +4,9 @@ export function buildPublicUrl(pageContext: PageContext, resource: string) {
const { baseUrl } = pageContext; const { baseUrl } = pageContext;
const url = new URL( const url = new URL(
resource, resource,
process.env.NODE_ENV === "production" ? "https://memento-dev.fr" : baseUrl, process.env.NODE_ENV === "production"
? "https://memento-dev.fr"
: baseUrl || "http://localhost:5500",
).toString(); ).toString();
return url; return url;

View File

@ -1,7 +1,9 @@
{ {
"scripts": { "scripts": {
"dev": "bun ./fastify-entry.ts", "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", "preview": "cross-env NODE_ENV=production bun ./fastify-server.ts",
"production": "bun run build && bun run preview", "production": "bun run build && bun run preview",
"lint": "biome lint --write .", "lint": "biome lint --write .",

View File

@ -1,13 +1,23 @@
import { usePageContext } from "vike-solid/usePageContext";
import blurCyanImage from "@/images/blur-cyan.webp"; import blurCyanImage from "@/images/blur-cyan.webp";
import { buildPublicUrl } from "@/buildPublicUrl";
import logoUrl from "@/assets/logo.svg"; import logoUrl from "@/assets/logo.svg";
// https://vike.dev/Head // https://vike.dev/Head
export default function HeadDefault() { export default function HeadDefault() {
const pageContext = usePageContext();
const getCanonicalUrl = () => {
return buildPublicUrl(pageContext, pageContext.urlParsed.pathname);
};
return ( return (
<> <>
<link rel="icon" href={logoUrl} /> <link rel="icon" href={logoUrl} />
<link rel="canonical" href={getCanonicalUrl()} />
<script <script
defer defer
src="https://cloud.umami.is/script.js" src="https://cloud.umami.is/script.js"

View File

@ -1,8 +1,38 @@
import type { OnPageTransitionEndAsync } from "vike/types"; import type { OnPageTransitionEndAsync, PageContext } from "vike/types";
import { buildPublicUrl } from "@/buildPublicUrl";
import NProgress from "nprogress"; import NProgress from "nprogress";
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => { export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
pageContext,
) => {
NProgress.done(); NProgress.done();
NProgress.remove(); NProgress.remove();
updateCanonicalTag(pageContext);
};
const updateCanonicalTag = (pageContext: PageContext) => {
const canonicalTag = findOrCreateCanonicalTag();
const canonicalUrl = buildPublicUrl(
pageContext,
pageContext.urlParsed.pathname,
);
canonicalTag.href = canonicalUrl;
};
const findOrCreateCanonicalTag = () => {
const head = document.head;
let canonicalTag: HTMLLinkElement | null = head.querySelector(
"link[rel=canonical]",
);
if (canonicalTag) return canonicalTag;
canonicalTag = document.createElement("link");
canonicalTag.rel = "canonical";
document.head.appendChild(canonicalTag);
return canonicalTag;
}; };

3
app/public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Disallow:
Sitemap: https://memento-dev.fr/sitemap.xml

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

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>`;
this.sitemap += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
}
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) {
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();