seo/sitemap (#18)
All checks were successful
Update Memento Dev on VPS / deploy (push) Successful in 1m16s
All checks were successful
Update Memento Dev on VPS / deploy (push) Successful in 1m16s
Reviewed-on: #18 Co-authored-by: GauthierWebDev <gauthier@gauthierdaniels.fr> Co-committed-by: GauthierWebDev <gauthier@gauthierdaniels.fr>
This commit is contained in:
parent
2def582b00
commit
b29acdc202
3
app/build-sitemap.ts
Normal file
3
app/build-sitemap.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { sitemap } from "./services/Sitemap";
|
||||||
|
|
||||||
|
sitemap.generateSitemap();
|
||||||
@ -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;
|
||||||
|
|||||||
@ -3,14 +3,21 @@ import type { IconProps } from "./Icon";
|
|||||||
|
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { Link } from "./Link";
|
import { Link } from "./Link";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
type QuickLinksProps = {
|
type QuickLinksProps = {
|
||||||
children: JSXElement;
|
children: JSXElement;
|
||||||
|
class?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QuickLinks(props: QuickLinksProps) {
|
export default function QuickLinks(props: QuickLinksProps) {
|
||||||
return (
|
return (
|
||||||
<div class="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2",
|
||||||
|
props.class,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -20,6 +27,7 @@ type QuickLinkProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
href: string;
|
href: string;
|
||||||
|
lastEdited?: Date;
|
||||||
icon: IconProps["icon"];
|
icon: IconProps["icon"];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,13 +37,26 @@ QuickLinks.QuickLink = (props: QuickLinkProps) => (
|
|||||||
<div class="relative overflow-hidden rounded-xl p-6">
|
<div class="relative overflow-hidden rounded-xl p-6">
|
||||||
<Icon icon={props.icon} color="blue" class="h-8 w-8" />
|
<Icon icon={props.icon} color="blue" class="h-8 w-8" />
|
||||||
|
|
||||||
<h2 class="mt-4 font-display text-base text-slate-900">
|
<h2 class="mt-4 font-display text-base text-slate-900 leading-5">
|
||||||
<Link href={props.href}>
|
<Link href={props.href}>
|
||||||
<span class="absolute -inset-px rounded-xl" />
|
<span class="absolute -inset-px rounded-xl" />
|
||||||
{props.title}
|
{props.title}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{props.lastEdited && (
|
||||||
|
<p class="mt-2 mb-4 italic text-xs text-slate-500">
|
||||||
|
<span class="font-semibold">Dernière modification :</span>{" "}
|
||||||
|
<time datetime={props.lastEdited.toISOString()}>
|
||||||
|
{props.lastEdited.toLocaleDateString("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<p class="mt-1 text-sm text-slate-700">{props.description}</p>
|
<p class="mt-1 text-sm text-slate-700">{props.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { JSXElement } from "solid-js";
|
|||||||
|
|
||||||
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
import { LatestDocs } from "@/partials/LatestDocs";
|
||||||
import { clientOnly } from "vike-solid/clientOnly";
|
import { clientOnly } from "vike-solid/clientOnly";
|
||||||
import { clock } from "solid-heroicons/outline";
|
import { clock } from "solid-heroicons/outline";
|
||||||
import { navigation } from "@/libs/navigation";
|
import { navigation } from "@/libs/navigation";
|
||||||
@ -21,21 +22,25 @@ export function DocsLayout(props: DocsLayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main
|
<div class="flex">
|
||||||
id="article-content"
|
<main
|
||||||
class="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16 grow"
|
id="article-content"
|
||||||
>
|
class="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16 grow"
|
||||||
<article>
|
>
|
||||||
<DocsHeader
|
<article>
|
||||||
title={pageContext.exports.frontmatter?.title}
|
<DocsHeader
|
||||||
estimatedReadingTime={pageContext.exports.readingTime?.text}
|
title={pageContext.exports.frontmatter?.title}
|
||||||
/>
|
estimatedReadingTime={pageContext.exports.readingTime?.text}
|
||||||
<Prose>{props.children}</Prose>
|
/>
|
||||||
</article>
|
<Prose>{props.children}</Prose>
|
||||||
<PrevNextLinks />
|
</article>
|
||||||
</main>
|
<PrevNextLinks />
|
||||||
|
</main>
|
||||||
|
|
||||||
<TableOfContents />
|
<TableOfContents />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LatestDocs />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import type { JSXElement } from "solid-js";
|
|||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
import { SmoothScroll } from "@/components/SmoothScroll";
|
import { SmoothScroll } from "@/components/SmoothScroll";
|
||||||
import { HeroSection } from "@/partials/HeroSection";
|
import { HeroSection } from "@/partials/HeroSection";
|
||||||
import { Navigation } from "@/partials/Navigation";
|
|
||||||
import { clientOnly } from "vike-solid/clientOnly";
|
import { clientOnly } from "vike-solid/clientOnly";
|
||||||
|
import { Navigation } from "@/partials/Navigation";
|
||||||
import { Header } from "@/partials/Header";
|
import { Header } from "@/partials/Header";
|
||||||
import { Footer } from "@/partials/Footer";
|
import { Footer } from "@/partials/Footer";
|
||||||
import { DocsLayout } from "./DocsLayout";
|
import { DocsLayout } from "./DocsLayout";
|
||||||
@ -40,7 +40,10 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
|
|||||||
<Navigation />
|
<Navigation />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DocsLayout>{props.children}</DocsLayout>
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<DocsLayout>{props.children}</DocsLayout>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@ -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 .",
|
||||||
|
|||||||
@ -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"
|
||||||
@ -26,6 +36,9 @@ export default function HeadDefault() {
|
|||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<meta property="og:type" content="siteweb" />
|
||||||
|
<meta property="og:url" content={getCanonicalUrl()} />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Lexend:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Lexend:wght@400;500;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|||||||
@ -38,5 +38,10 @@ export async function data(pageContext: PageContext) {
|
|||||||
return {
|
return {
|
||||||
sections: doc?.sections || [],
|
sections: doc?.sections || [],
|
||||||
frontmatter,
|
frontmatter,
|
||||||
|
docs: docCache.orderByLastEdit({
|
||||||
|
limit: 2,
|
||||||
|
includedBasePaths: ["docs", "certifications"],
|
||||||
|
excludedFileNames: [cachePathname],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,60 @@
|
|||||||
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();
|
||||||
|
|
||||||
|
updateCanonicalsTag(pageContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCanonicalsTag = (pageContext: PageContext) => {
|
||||||
|
const canonicalNativeTag = findOrCreateTag<HTMLLinkElement>("link", {
|
||||||
|
rel: "canonical",
|
||||||
|
});
|
||||||
|
const canonicalOGTag = findOrCreateTag<HTMLMetaElement>("meta", {
|
||||||
|
property: "og:url",
|
||||||
|
});
|
||||||
|
const typeOGTag = findOrCreateTag<HTMLMetaElement>("meta", {
|
||||||
|
property: "og:type",
|
||||||
|
});
|
||||||
|
const localOGTag = findOrCreateTag<HTMLMetaElement>("meta", {
|
||||||
|
property: "og:locale",
|
||||||
|
});
|
||||||
|
const siteNameOGTag = findOrCreateTag<HTMLMetaElement>("meta", {
|
||||||
|
property: "og:site_name",
|
||||||
|
});
|
||||||
|
|
||||||
|
const canonicalUrl = buildPublicUrl(
|
||||||
|
pageContext,
|
||||||
|
pageContext.urlParsed.pathname,
|
||||||
|
);
|
||||||
|
|
||||||
|
canonicalNativeTag.setAttribute("href", canonicalUrl);
|
||||||
|
canonicalOGTag.setAttribute("content", canonicalUrl);
|
||||||
|
typeOGTag.setAttribute("content", "website");
|
||||||
|
localOGTag.setAttribute("content", "fr-FR");
|
||||||
|
siteNameOGTag.setAttribute("content", document.title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOrCreateTag = <T>(
|
||||||
|
tagName: string,
|
||||||
|
attributes: Record<string, string>,
|
||||||
|
): T => {
|
||||||
|
const head = document.head;
|
||||||
|
let tag: HTMLElement | null = head.querySelector(tagName);
|
||||||
|
if (tag) return tag as T;
|
||||||
|
|
||||||
|
tag = document.createElement(tagName);
|
||||||
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
|
tag.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.appendChild(tag);
|
||||||
|
|
||||||
|
return tag as T;
|
||||||
};
|
};
|
||||||
|
|||||||
31
app/partials/LatestDocs.tsx
Normal file
31
app/partials/LatestDocs.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { Data } from "@/pages/+data";
|
||||||
|
|
||||||
|
import QuickLinks from "@/components/QuickLinks";
|
||||||
|
import { useData } from "vike-solid/useData";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
|
||||||
|
export function LatestDocs() {
|
||||||
|
const data = useData<Data>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class="bg-violet-200 rounded-md p-4 m-4 lg:m-6">
|
||||||
|
<h2 class="font-display text-3xl tracking-tight text-slate-900 text-center">
|
||||||
|
Dernières documentations
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<QuickLinks class="!mb-0 !mt-6">
|
||||||
|
<For each={data.docs}>
|
||||||
|
{(doc) => (
|
||||||
|
<QuickLinks.QuickLink
|
||||||
|
title={doc.frontmatter?.title || ""}
|
||||||
|
description={doc.frontmatter?.description || ""}
|
||||||
|
lastEdited={doc.lastEdit}
|
||||||
|
href={doc.filePath}
|
||||||
|
icon="presets"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</QuickLinks>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -27,7 +27,9 @@ function NavigationItem(props: NavigationItemProps) {
|
|||||||
return props.section.links.some((link) => checkIsLinkActive(link));
|
return props.section.links.some((link) => checkIsLinkActive(link));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isOpened, setIsOpened] = createSignal(checkIsActive());
|
const [isOpened, setIsOpened] = createSignal(
|
||||||
|
checkIsActive() || pageContext.urlPathname === "/",
|
||||||
|
);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// If the current URL path is the same as the link's href, set isOpened to true
|
// If the current URL path is the same as the link's href, set isOpened to true
|
||||||
|
|||||||
3
app/public/robots.txt
Normal file
3
app/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
Sitemap: https://memento-dev.fr/sitemap.xml
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { FlexSearchData } from "./FlexSearchService";
|
|
||||||
import type { TableOfContents } from "@/remarkHeadingId";
|
|
||||||
import type { Node } from "@markdoc/markdoc";
|
import type { Node } from "@markdoc/markdoc";
|
||||||
|
|
||||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||||
@ -16,6 +14,8 @@ export type SectionCache = {
|
|||||||
frontmatter?: SectionFrontmatter;
|
frontmatter?: SectionFrontmatter;
|
||||||
markdocNode: Node;
|
markdocNode: Node;
|
||||||
sections: DocSection[];
|
sections: DocSection[];
|
||||||
|
lastEdit: Date;
|
||||||
|
filePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocSection = {
|
export type DocSection = {
|
||||||
@ -32,6 +32,14 @@ type SectionFrontmatter = {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OrderConfig = {
|
||||||
|
limit: number;
|
||||||
|
includedBasePaths: string[];
|
||||||
|
excludedBasePaths: string[];
|
||||||
|
includedFileNames: string[];
|
||||||
|
excludedFileNames: string[];
|
||||||
|
};
|
||||||
|
|
||||||
class DocCache {
|
class DocCache {
|
||||||
private static readonly pagesDir = path.join(__dirname, "pages");
|
private static readonly pagesDir = path.join(__dirname, "pages");
|
||||||
private static instance: DocCache | null = null;
|
private static instance: DocCache | null = null;
|
||||||
@ -162,12 +170,17 @@ class DocCache {
|
|||||||
.replace(/\+Page\.md(x)?$/, "")
|
.replace(/\+Page\.md(x)?$/, "")
|
||||||
.replace(/\/+/, "/")
|
.replace(/\/+/, "/")
|
||||||
.replace(/\/$/, "");
|
.replace(/\/$/, "");
|
||||||
|
const lastEdit = new Date(
|
||||||
|
(await fs.stat(path.join(DocCache.pagesDir, file))).mtime,
|
||||||
|
);
|
||||||
|
|
||||||
this.cache.set(filePath, {
|
this.cache.set(filePath, {
|
||||||
content,
|
content,
|
||||||
frontmatter,
|
frontmatter,
|
||||||
markdocNode,
|
markdocNode,
|
||||||
sections: this.getTableOfContents(markdocNode),
|
sections: this.getTableOfContents(markdocNode),
|
||||||
|
lastEdit,
|
||||||
|
filePath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +220,84 @@ class DocCache {
|
|||||||
return this.cache.get(file);
|
return this.cache.get(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public orderByLastEdit(customConfig?: Partial<OrderConfig>): SectionCache[] {
|
||||||
|
const config: OrderConfig = {
|
||||||
|
excludedBasePaths: [],
|
||||||
|
includedBasePaths: [],
|
||||||
|
excludedFileNames: [],
|
||||||
|
includedFileNames: [],
|
||||||
|
limit: 0,
|
||||||
|
...customConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfIncludedBasePath = (doc: SectionCache) => {
|
||||||
|
if (config.includedBasePaths.length > 0) {
|
||||||
|
return config.includedBasePaths.some((basePath) => {
|
||||||
|
return doc.filePath.startsWith(basePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfExcludedBasePaths = (doc: SectionCache) => {
|
||||||
|
if (config.excludedBasePaths.length > 0) {
|
||||||
|
return !config.excludedBasePaths.some((basePath) => {
|
||||||
|
return doc.filePath.startsWith(basePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfIncludedFileNames = (doc: SectionCache) => {
|
||||||
|
if (config.includedFileNames.length > 0) {
|
||||||
|
return config.includedFileNames.some((fileName) => {
|
||||||
|
return doc.filePath.includes(fileName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfExcludedFileNames = (doc: SectionCache) => {
|
||||||
|
if (config.excludedFileNames.length > 0) {
|
||||||
|
return !config.excludedFileNames.some((fileName) => {
|
||||||
|
return doc.filePath === fileName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedDocs = Array.from(this.cache.values())
|
||||||
|
.sort((a, b) => b.lastEdit.getTime() - a.lastEdit.getTime())
|
||||||
|
.filter((doc) => {
|
||||||
|
const isIncluded = [
|
||||||
|
checkIfIncludedBasePath(doc),
|
||||||
|
checkIfExcludedBasePaths(doc),
|
||||||
|
checkIfIncludedFileNames(doc),
|
||||||
|
checkIfExcludedFileNames(doc),
|
||||||
|
].every((check) => check === true);
|
||||||
|
|
||||||
|
// DEBUG
|
||||||
|
// if (!isIncluded) {
|
||||||
|
// console.group(doc.filePath);
|
||||||
|
// console.log("includedBasePaths", checkIfIncludedBasePath(doc));
|
||||||
|
// console.log("excludedBasePaths", checkIfExcludedBasePaths(doc));
|
||||||
|
// console.log("includedFileNames", checkIfIncludedFileNames(doc));
|
||||||
|
// console.log("excludedFileNames", checkIfExcludedFileNames(doc));
|
||||||
|
// console.groupEnd();
|
||||||
|
// }
|
||||||
|
|
||||||
|
return isIncluded;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.limit > 0) return sortedDocs.slice(0, config.limit);
|
||||||
|
|
||||||
|
return sortedDocs;
|
||||||
|
}
|
||||||
|
|
||||||
public waitingForCache(timeout: 20000): Promise<void> {
|
public waitingForCache(timeout: 20000): Promise<void> {
|
||||||
const timer = hrtime();
|
const timer = hrtime();
|
||||||
|
|
||||||
|
|||||||
167
app/services/Sitemap.ts
Normal file
167
app/services/Sitemap.ts
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user