feat: Add functionality to display latest documentation

This commit is contained in:
Gauthier Daniels 2025-04-22 13:10:56 +02:00
parent a2acd6be8f
commit f5f797eea7
6 changed files with 138 additions and 19 deletions

View File

@ -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"];
}; };
@ -36,6 +44,19 @@ QuickLinks.QuickLink = (props: QuickLinkProps) => (
</Link> </Link>
</h2> </h2>
{props.lastEdited && (
<p class="-mt-2 italic mb-2 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>

View File

@ -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,6 +22,7 @@ export function DocsLayout(props: DocsLayoutProps) {
return ( return (
<> <>
<div class="flex">
<main <main
id="article-content" 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" 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"
@ -36,6 +38,9 @@ export function DocsLayout(props: DocsLayoutProps) {
</main> </main>
<TableOfContents /> <TableOfContents />
</div>
<LatestDocs />
</> </>
); );
} }

View File

@ -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,8 +40,11 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
<Navigation /> <Navigation />
</div> </div>
</div> </div>
<div class="flex flex-col">
<DocsLayout>{props.children}</DocsLayout> <DocsLayout>{props.children}</DocsLayout>
</div> </div>
</div>
<Footer /> <Footer />
</SmoothScroll> </SmoothScroll>

View File

@ -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],
}),
}; };
} }

View 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-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>
);
}

View File

@ -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,47 @@ 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 sortedDocs = Array.from(this.cache.values())
.sort((a, b) => b.lastEdit.getTime() - a.lastEdit.getTime())
.filter((doc) => {
if (config.includedBasePaths.length > 0) {
return config.includedBasePaths.some((basePath) => {
return doc.filePath.startsWith(basePath);
});
}
if (config.excludedBasePaths.length > 0) {
return !config.excludedBasePaths.some((basePath) => {
return doc.filePath.startsWith(basePath);
});
}
if (config.includedFileNames.length > 0) {
return config.includedFileNames.some((fileName) => {
return doc.filePath.includes(fileName);
});
}
if (config.excludedFileNames.length > 0) {
return !config.excludedFileNames.some((fileName) => {
return doc.filePath.includes(fileName);
});
}
return true;
});
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();