plugin-recent-docs
1. Description
Je ne voulais pas mettre en place un blog en plus de la section documentation du site. Cependant, j'aimerais afficher une page des dernières pages modifiées afin que les visiteurs puissent voir facilement les documentations qui ont été mises à jour. Je n'ai pas trouvé de fonction native dans docusaurus et ai donc décidé de me lancer dans le développement d'un plugin docusaurus afin de créer une liste des derniers documents modifiés, classés par ordre chronologique décroissant.
C'est mon premier plugin docusaurus : il marche chez moi mais il n'est pas générique pour fonctionner dans toutes les configurations.
Il manque par exemple les fonctionnalités suivantes :
- pas de support de multilingue i18n
- pas de support des versions de docs
- pas de support des docs Multi-Instance
- pas de support de certains champs spéciaux Frontmatter, pour ne pas afficher les documents dans la liste des derniers fichiers modifiés
- draft: true
- unlisted: true
2. Editeur markdown : Obsidian
J'utilise l'éditeur Markdown Obsidian avec le plugin Linter. En plus de vérifier la syntaxe des fichiers Markdown, ce plugin permet de mettre à jour automatiquement la date de modification du fichier dans les métadonnées Frontmatter.

Dès que j'enregistre mon fichier Markdown, la date de modification est mise à jour automatiquement dans le champ modified: (mais il est possible de choisir un autre champ).

3. Développement du plugin docusaurus 'plugin-last-docs'
Le plugin docusaurus plugin-last-docs va lire ce champ modified: dans les métadonnées Frontmatter de tous les fichiers Markdown du projet, puis les trier de façon chronologique et sauvegarder les informations dans le tableau updatedDocs.
Pour cela, il est nécesaire d'installer le module nodejs gray-matter afin de parser plus facilement les métadonnées Frontmatter des fichiers Markdown.
3.1. Installer le module 'gray-matter'
$ yarn add gray-matter
3.2. Créer le répertoire 'plugin-recent-docs'
- créer le fichier
index.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
module.exports = function pluginRecentDocs(context, options) {
const docsDir = path.resolve(__dirname, '../../docs');
function getCategorySlugs(dir, base = '', map = {}) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relPath = path.join(base, entry.name);
if (entry.isDirectory()) {
getCategorySlugs(fullPath, relPath, map);
} else if (entry.isFile() && entry.name === '_category_.json') {
const json = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
if (json.slug) {
//map[path.dirname(relPath)] = json.slug.toLowerCase();
map[path.dirname(relPath)] = json.slug;
}
}
}
return map;
}
const categorySlugs = getCategorySlugs(docsDir);
function getAllMarkdownFiles(dir, base = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relPath = path.join(base, entry.name);
if (entry.isDirectory()) {
files = files.concat(getAllMarkdownFiles(fullPath, relPath));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(relPath);
}
}
return files;
}
function getPermalinkFromPath(relPath, frontmatterSlug) {
const parts = relPath
.replace(/\.md$/, '')
.split(path.sep)
.map((part, index, arr) => {
const dirPath = arr.slice(0, index + 1).join(path.sep);
const isFile = index === arr.length - 1;
if (isFile) {
if (frontmatterSlug) {
return frontmatterSlug.toLowerCase();
}
// Retirer les chiffres mais conserver les majuscules du nom de fichier
return part.replace(/^\d+-/, '');
}
// Répertoire : slug personnalisé ou normalisé
if (categorySlugs[dirPath]) {
return categorySlugs[dirPath];
}
return part.replace(/^\d+-/, '').toLowerCase();
});
return '/' + parts.join('/');
}
return {
name: 'plugin-recent-docs',
async loadContent() {
const markdownFiles = getAllMarkdownFiles(docsDir);
const docs = [];
for (const relPath of markdownFiles) {
const fullPath = path.join(docsDir, relPath);
const content = fs.readFileSync(fullPath, 'utf-8');
const { data } = matter(content);
if (!data.modified || !data.title) continue;
const permalink = '/homelabasdoc' + getPermalinkFromPath(relPath, data.slug);
docs.push({
title: data.title,
permalink,
lastUpdatedAt: new Date(data.modified).getTime(),
});
}
docs.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
return docs;
},
async contentLoaded({ content, actions }) {
const { setGlobalData } = actions;
setGlobalData({ recentDocs: content });
},
};
};
3.3. Ajouter le plugin au projet docusaurus
- Ajouter le plugin dans le fichier
docusaurus.config.ts
plugins: [
+ path.resolve('./plugins/plugin-recent-docs'),
'@docusaurus/plugin-content-docs',
'@docusaurus/plugin-content-pages',
}
4. Création de la page 'Dernières modifications'
4.1. Créer la page 'recent-docs.js'
- Créer le fichier
recent-docs.js
import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import usePluginData from '@docusaurus/useGlobalData';
import { useState } from 'react';
export default function RecentDocsPage() {
const globalData = usePluginData('plugin-recent-docs');
const docs = globalData['plugin-recent-docs'].default.recentDocs;
const itemsPerPage = 10;
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(docs.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const paginatedDocs = docs.slice(startIndex, startIndex + itemsPerPage);
const goToPrevious = () => setCurrentPage((p) => Math.max(p - 1, 1));
const goToNext = () => setCurrentPage((p) => Math.min(p + 1, totalPages));
return (
<Layout title="Derniers documents modifiés">
<div className="container margin-vert--lg">
<h1>Derniers documents modifiés</h1>
<ul>
{paginatedDocs.map((doc, i) => (
<li key={i}>
<Link to={doc.permalink}>{doc.title}</Link> —{' '}
{new Date(doc.lastUpdatedAt).toLocaleDateString()}
</li>
))}
</ul>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2rem' }}>
<button style={{ padding: '1em 3em', fontSize: '1rem' }} onClick={goToPrevious} disabled={currentPage === 1}>
◀ Précédent
</button>
<span style={{ padding: '1em 5em', fontSize: '1rem'}}>Page {currentPage} / {totalPages}</span>
<button style={{ padding: '1em 3em', fontSize: '1rem'}} onClick={goToNext} disabled={currentPage === totalPages}>
Suivant ▶
</button>
</div>
</div>
</Layout>
);
}
4.2. Ajouter la page dans la barre de navigation
- Ajouter les lignes dans le fichier
docusaurus.config.ts
themeConfig: {
// ...
navbar: {
// ...
items: [
+ {
+ href: '/recent-docs',
+ label: '<Dernières modifications/>',
+ position: 'left',
+ }
],
},
};
5. Test du bon fonctionnement du plugin
5.1. Construire le site web
$ yarn build
5.2. Démarrer le site web en local
$ yarn run start
- Ouvrir le site web : http://localhost:3000/recent-docs

On peut voir que le dernier fichier modifié est bien le fichier plugin-recent-docs 😀