Ajout page Tags pour recherche articles par sujet

- Créer page Tags unifiée affichant tous les tags des articles blog et docs
- Mettre à jour tags dans toutes les pages pour être plus pertinents et recherchables
This commit is contained in:
Tellsanguis 2025-12-03 11:57:18 +01:00
parent 87a2508769
commit 20fcabaf46
23 changed files with 683 additions and 10 deletions

View file

@ -2,7 +2,7 @@
slug: bienvenue
title: Bienvenue sur TellServ Tech Blog
authors: [tellserv]
tags: [introduction, blog]
tags: [homelab, devops, infrastructure, cloud]
---
# Bienvenue sur TellServ Tech Blog

View file

@ -2,7 +2,7 @@
slug: stockage-distribue-proxmox-ha
title: "Choisir sa technologie de stockage distribué pour un cluster Proxmox HA"
authors: [tellserv]
tags: [proxmox, stockage, ha, linstor, drbd, ceph, zfs, homelab]
tags: [proxmox, high-availability, linstor, drbd, ceph, zfs, storage, clustering, homelab]
---
# Choisir sa technologie de stockage distribué pour un cluster Proxmox HA

View file

@ -2,7 +2,7 @@
slug: linstor-drbd-opentofu-problemes
title: "Déploiement avec OpenTofu sur LINSTOR DRBD : le début des problèmes"
authors: [tellserv]
tags: [homelab, proxmox, linstor, drbd, opentofu, terraform, iac, gitops, kubernetes]
tags: [opentofu, terraform, iac, infrastructure-as-code, proxmox, linstor, drbd, automation, devops]
date: 2025-11-26
---

View file

@ -2,7 +2,7 @@
slug: reduire-taille-disque-vm-clonezilla
title: Réduire la taille du disque d'une VM avec Clonezilla
authors: [tellserv]
tags: [proxmox, clonezilla, vm, stockage, linstor-drbd]
tags: [proxmox, clonezilla, virtualization, storage, linstor, drbd, disk-management]
image: /img/blog/2025-11-30-reduire-disque-vm/clonezilla-logo.svg
---

View file

@ -2,7 +2,7 @@
slug: ssh-freeze-mtu-mss-clamping
title: Freeze de session SSH, MTU et MSS clamping
authors: [tellserv]
tags: [openwrt, gretap, ssh, mtu, networking]
tags: [openwrt, networking, ssh, mtu, mss-clamping, tunneling, gretap, troubleshooting]
image: /img/blog/2025-12-02-ssh-freeze-mtu/freeze_session_ssh.png
---

View file

@ -1,5 +1,6 @@
---
sidebar_position: 3
tags: [docker, docker-compose, conteneurisation, homelab]
---
# Docker et Docker Compose

View file

@ -1,5 +1,6 @@
---
sidebar_position: 2
tags: [ansible, automation, iac, configuration-management, homelab]
---
# Playbooks Ansible

View file

@ -1,5 +1,6 @@
---
sidebar_position: 4
tags: [traefik, docker, reverse-proxy, ssl, crowdsec, homelab]
---
# Traefik - Reverse Proxy moderne

View file

@ -1,5 +1,6 @@
---
sidebar_position: 3
tags: [proxmox, cluster, ha, homelab]
---
# Cluster 3 noeuds Proxmox : un véritable Homelab HA

View file

@ -1,5 +1,6 @@
---
sidebar_position: 2
tags: [proxmox, kubernetes, k3s, ha, homelab]
---
# Première version : le Homelab "HA" monomachine (projet initial)

View file

@ -1,6 +1,7 @@
---
sidebar_position: 2
title: Backhaul WiFi Mesh avec 802.11s
tags: [openwrt, wifi, mesh-networking, wireless, networking]
---
# Backhaul WiFi Mesh avec 802.11s

View file

@ -1,6 +1,7 @@
---
sidebar_position: 3
title: Tunnels GREtap pour VLANs
tags: [openwrt, gretap, vlan, networking, tunneling]
---
# Tunnels GREtap pour VLANs à travers le mesh

View file

@ -23,6 +23,7 @@ const config: Config = {
plugins: [
'docusaurus-plugin-image-zoom',
'./plugins/docusaurus-plugin-unified-tags',
],
title: 'TellServ Tech Blog',
@ -125,6 +126,7 @@ const config: Config = {
label: 'Documentation',
},
{to: '/blog', label: 'Blog', position: 'left'},
{to: '/tags', label: 'Tags', position: 'left'},
{
type: 'localeDropdown',
position: 'right',

View file

@ -51,5 +51,41 @@
"homepage.feature3.description": {
"message": "Sharing experiences and solutions with the community.",
"description": "Description of feature 3 (knowledge sharing) on the homepage"
},
"tags.page.title": {
"message": "Tags",
"description": "Tags page title"
},
"tags.page.description": {
"message": "Browse content by tags",
"description": "Tags page meta description"
},
"tags.page.heading": {
"message": "Tags",
"description": "Tags page main heading"
},
"tags.filtered.title": {
"message": "{count} posts tagged with \"{tagName}\"",
"description": "Filtered tag page title"
},
"tags.filtered.description": {
"message": "Browse all content tagged with {tagName}",
"description": "Filtered tag page meta description"
},
"tags.filtered.heading": {
"message": "{count} posts tagged with \"{tagName}\"",
"description": "Filtered tag page heading"
},
"tags.viewAll": {
"message": "View All Tags",
"description": "View all tags link"
},
"tags.notFound.title": {
"message": "Tag Not Found",
"description": "Tag not found title"
},
"tags.notFound.description": {
"message": "The tag you are looking for does not exist.",
"description": "Tag not found description"
}
}

View file

@ -2,7 +2,7 @@
slug: bienvenue
title: Welcome to TellServ Tech Blog
authors: [tellserv]
tags: [introduction, blog]
tags: [homelab, devops, infrastructure, cloud]
---
# Welcome to TellServ Tech Blog

View file

@ -2,7 +2,7 @@
slug: stockage-distribue-proxmox-ha
title: "Choosing a Distributed Storage Technology for a Proxmox HA Cluster"
authors: [tellserv]
tags: [proxmox, storage, ha, linstor, drbd, ceph, zfs, homelab]
tags: [proxmox, high-availability, linstor, drbd, ceph, zfs, storage, clustering, homelab]
---
# Choosing a Distributed Storage Technology for a Proxmox HA Cluster

View file

@ -2,7 +2,7 @@
slug: linstor-drbd-opentofu-problemes
title: "Deploying with OpenTofu on LINSTOR DRBD: the beginning of problems"
authors: [tellserv]
tags: [homelab, proxmox, linstor, drbd, opentofu, terraform, iac, gitops, kubernetes]
tags: [opentofu, terraform, iac, infrastructure-as-code, proxmox, linstor, drbd, automation, devops]
date: 2025-11-26
---

View file

@ -2,7 +2,7 @@
slug: reduire-taille-disque-vm-clonezilla
title: Reducing VM Disk Size with Clonezilla
authors: [tellserv]
tags: [proxmox, clonezilla, vm, storage, linstor-drbd]
tags: [proxmox, clonezilla, virtualization, storage, linstor, drbd, disk-management]
image: /img/blog/2025-11-30-reduire-disque-vm/clonezilla-logo.svg
---

View file

@ -2,7 +2,7 @@
slug: ssh-freeze-mtu-mss-clamping
title: SSH Session Freeze, MTU and MSS Clamping
authors: [tellserv]
tags: [openwrt, gretap, ssh, mtu, networking]
tags: [openwrt, networking, ssh, mtu, mss-clamping, tunneling, gretap, troubleshooting]
image: /img/blog/2025-12-02-ssh-freeze-mtu/freeze_session_ssh.png
---

View file

@ -0,0 +1,132 @@
const path = require('path');
const fs = require('fs-extra');
module.exports = function pluginUnifiedTags(context, options) {
return {
name: 'docusaurus-plugin-unified-tags',
async allContentLoaded({actions, allContent}) {
const {addRoute} = actions;
const {setGlobalData} = actions;
const blogPlugin = allContent?.['docusaurus-plugin-content-blog'];
const docsPlugin = allContent?.['docusaurus-plugin-content-docs'];
const blogContent = blogPlugin?.default;
const docsContent = docsPlugin?.default;
const tagsMap = new Map();
if (blogContent?.blogPosts) {
blogContent.blogPosts.forEach((post) => {
if (post.metadata.tags) {
post.metadata.tags.forEach((tag) => {
const tagKey = tag.label.toLowerCase();
if (!tagsMap.has(tagKey)) {
tagsMap.set(tagKey, {
label: tag.label,
permalink: tag.permalink,
count: 0,
items: []
});
}
const tagData = tagsMap.get(tagKey);
tagData.count++;
tagData.items.push({
type: 'blog',
title: post.metadata.title,
permalink: post.metadata.permalink,
date: post.metadata.date,
formattedDate: post.metadata.formattedDate,
description: post.metadata.description,
authors: post.metadata.authors,
tags: post.metadata.tags,
readingTime: post.metadata.readingTime
});
});
}
});
}
if (docsContent?.loadedVersions) {
docsContent.loadedVersions.forEach((version) => {
version.docs.forEach((doc) => {
if (doc.tags) {
doc.tags.forEach((tag) => {
const tagKey = tag.label.toLowerCase();
if (!tagsMap.has(tagKey)) {
tagsMap.set(tagKey, {
label: tag.label,
permalink: tag.permalink,
count: 0,
items: []
});
}
const tagData = tagsMap.get(tagKey);
tagData.count++;
tagData.items.push({
type: 'doc',
title: doc.title,
permalink: doc.permalink,
description: doc.description,
tags: doc.tags
});
});
}
});
});
}
const allTags = Array.from(tagsMap.values()).sort((a, b) =>
a.label.localeCompare(b.label)
);
const tagsByLetter = {};
allTags.forEach((tag) => {
const firstLetter = tag.label.charAt(0).toUpperCase();
if (!tagsByLetter[firstLetter]) {
tagsByLetter[firstLetter] = [];
}
tagsByLetter[firstLetter].push(tag);
});
const globalData = {
allTags,
tagsByLetter,
tagsMap: Object.fromEntries(
Array.from(tagsMap.entries()).map(([key, value]) => [
key,
{
...value,
items: value.items.sort((a, b) => {
if (a.type === 'blog' && b.type === 'blog') {
return new Date(b.date) - new Date(a.date);
}
if (a.type === 'doc' && b.type === 'doc') {
return a.title.localeCompare(b.title);
}
return a.type === 'blog' ? -1 : 1;
})
}
])
)
};
setGlobalData(globalData);
const locale = context.i18n.currentLocale;
const baseUrl = locale === context.i18n.defaultLocale ? '/' : `/${locale}/`;
Array.from(tagsMap.entries()).forEach(([tagKey, tagData]) => {
addRoute({
path: `${baseUrl}tags/${tagKey}`,
component: '@site/src/theme/TagPage',
exact: true,
props: {
tagKey: tagKey,
},
});
});
}
};
};

86
src/pages/tags/index.tsx Normal file
View file

@ -0,0 +1,86 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {usePluginData} from '@docusaurus/useGlobalData';
import Layout from '@theme/Layout';
import Translate, {translate} from '@docusaurus/Translate';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
interface Tag {
label: string;
permalink: string;
count: number;
}
interface TagsByLetter {
[letter: string]: Tag[];
}
interface UnifiedTagsData {
allTags: Tag[];
tagsByLetter: TagsByLetter;
}
export default function TagsPage(): JSX.Element {
const pluginData = usePluginData('docusaurus-plugin-unified-tags') as UnifiedTagsData;
console.log('Plugin data:', pluginData);
if (!pluginData || !pluginData.tagsByLetter) {
return (
<Layout>
<div className="container margin-vert--lg">
<p>No tags data available</p>
</div>
</Layout>
);
}
const {tagsByLetter} = pluginData;
const letters = Object.keys(tagsByLetter).sort();
return (
<Layout
title={translate({
id: 'tags.page.title',
message: 'Tags',
description: 'Tags page title',
})}
description={translate({
id: 'tags.page.description',
message: 'Browse content by tags',
description: 'Tags page meta description',
})}>
<div className="container margin-vert--lg">
<Heading as="h1" className={styles.tagsPageTitle}>
<Translate id="tags.page.heading" description="Tags page main heading">
Tags
</Translate>
</Heading>
<div className={styles.tagsContainer}>
{letters.map((letter) => (
<div key={letter} className={styles.letterSection}>
<Heading as="h2" className={styles.letterHeading}>
{letter}
</Heading>
<div className={styles.tagsRow}>
{tagsByLetter[letter].map((tag) => (
<Link
key={tag.label}
to={`/tags/${tag.label.toLowerCase().replace(/\s+/g, '-')}`}
className={styles.tagLink}>
<span className={styles.tagLabel}>{tag.label}</span>
<span className={styles.tagCount}>{tag.count}</span>
</Link>
))}
</div>
</div>
))}
</div>
</div>
</Layout>
);
}

View file

@ -0,0 +1,264 @@
.tagsPageTitle {
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
}
.tagsContainer {
max-width: 1200px;
margin: 0 auto;
}
.letterSection {
margin-bottom: 3rem;
}
.letterHeading {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--ifm-color-emphasis-300);
}
.tagsRow {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1.5rem;
}
.tagLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--ifm-color-emphasis-100);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
text-decoration: none;
color: var(--ifm-font-color-base);
transition: all 0.2s ease;
clip-path: polygon(10px 0%, 100% 0%, calc(100% - 10px) 100%, 0% 100%);
}
.tagLink:hover {
background: var(--ifm-color-primary);
color: white;
border-color: var(--ifm-color-primary);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-decoration: none;
}
[data-theme='dark'] .tagLink {
background: var(--ifm-color-emphasis-200);
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .tagLink:hover {
background: var(--ifm-color-primary);
border-color: var(--ifm-color-primary);
}
.tagLabel {
font-weight: 500;
font-size: 0.95rem;
}
.tagCount {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 6px;
background: var(--ifm-color-emphasis-300);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: var(--ifm-font-color-base);
}
.tagLink:hover .tagCount {
background: rgba(255, 255, 255, 0.3);
color: white;
}
[data-theme='dark'] .tagCount {
background: var(--ifm-color-emphasis-500);
}
.tagHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.tagTitle {
margin: 0;
font-size: 2rem;
}
.viewAllTagsLink {
color: var(--ifm-color-primary);
text-decoration: none;
font-weight: 500;
padding: 0.5rem 1rem;
border: 1px solid var(--ifm-color-primary);
border-radius: 6px;
transition: all 0.2s ease;
}
.viewAllTagsLink:hover {
background: var(--ifm-color-primary);
color: white;
text-decoration: none;
}
.itemsList {
display: flex;
flex-direction: column;
gap: 2rem;
max-width: 900px;
margin: 0 auto;
}
.itemCard {
padding: 2rem;
background: var(--ifm-color-emphasis-100);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
transition: all 0.2s ease;
}
.itemCard:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--ifm-color-primary);
}
[data-theme='dark'] .itemCard {
background: var(--ifm-color-emphasis-200);
border-color: var(--ifm-color-emphasis-400);
}
.itemTitle {
margin: 0 0 1rem 0;
font-size: 1.75rem;
}
.itemTitle a {
color: var(--ifm-font-color-base);
text-decoration: none;
}
.itemTitle a:hover {
color: var(--ifm-color-primary);
text-decoration: none;
}
.itemMeta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
color: var(--ifm-color-emphasis-700);
}
[data-theme='dark'] .itemMeta {
color: var(--ifm-color-emphasis-600);
}
.itemDate,
.itemAuthors,
.itemReadingTime {
display: flex;
align-items: center;
}
.itemDate::before {
content: '📅';
margin-right: 0.3rem;
}
.itemAuthors::before {
content: '✍️';
margin-right: 0.3rem;
}
.itemReadingTime::before {
content: '⏱️';
margin-right: 0.3rem;
}
.itemDescription {
margin: 1rem 0;
line-height: 1.6;
color: var(--ifm-color-emphasis-800);
}
[data-theme='dark'] .itemDescription {
color: var(--ifm-color-emphasis-700);
}
.itemTags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.itemTag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: var(--ifm-color-emphasis-200);
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 4px;
font-size: 0.85rem;
text-decoration: none;
color: var(--ifm-font-color-base);
transition: all 0.2s ease;
}
.itemTag:hover {
background: var(--ifm-color-primary-light);
border-color: var(--ifm-color-primary);
color: white;
text-decoration: none;
}
[data-theme='dark'] .itemTag {
background: var(--ifm-color-emphasis-300);
}
@media (max-width: 768px) {
.tagsPageTitle {
font-size: 2rem;
}
.letterHeading {
font-size: 1.5rem;
}
.tagHeader {
flex-direction: column;
align-items: flex-start;
}
.tagTitle {
font-size: 1.5rem;
}
.itemCard {
padding: 1.5rem;
}
.itemTitle {
font-size: 1.5rem;
}
}

146
src/theme/TagPage.tsx Normal file
View file

@ -0,0 +1,146 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {usePluginData} from '@docusaurus/useGlobalData';
import Layout from '@theme/Layout';
import Translate, {translate} from '@docusaurus/Translate';
import Heading from '@theme/Heading';
import styles from '../pages/tags/styles.module.css';
interface TagItem {
type: 'blog' | 'doc';
title: string;
permalink: string;
date?: string;
formattedDate?: string;
description?: string;
authors?: any[];
tags?: any[];
readingTime?: number;
}
interface TagData {
label: string;
permalink: string;
count: number;
items: TagItem[];
}
interface TagsMapData {
[tagKey: string]: TagData;
}
interface UnifiedTagsData {
tagsMap: TagsMapData;
}
interface TagPageProps {
tagKey: string;
}
export default function TagPage(props: TagPageProps): JSX.Element {
const {tagsMap} = usePluginData('docusaurus-plugin-unified-tags') as UnifiedTagsData;
const tagData = tagsMap[props.tagKey];
if (!tagData) {
return (
<Layout>
<div className="container margin-vert--lg">
<Heading as="h1">Tag Not Found</Heading>
<p>The tag you are looking for does not exist.</p>
<Link to="/tags">View All Tags</Link>
</div>
</Layout>
);
}
const {label, count, items} = tagData;
return (
<Layout
title={translate(
{
id: 'tags.filtered.title',
message: '{count} posts tagged with "{tagName}"',
description: 'Filtered tag page title',
},
{count, tagName: label}
)}
description={translate(
{
id: 'tags.filtered.description',
message: 'Browse all content tagged with {tagName}',
description: 'Filtered tag page meta description',
},
{tagName: label}
)}>
<div className="container margin-vert--lg">
<div className={styles.tagHeader}>
<Heading as="h1" className={styles.tagTitle}>
<Translate
id="tags.filtered.heading"
description="Filtered tag page heading"
values={{count, tagName: label}}>
{'{count} posts tagged with "{tagName}"'}
</Translate>
</Heading>
<Link to="/tags" className={styles.viewAllTagsLink}>
<Translate id="tags.viewAll" description="View all tags link">
View All Tags
</Translate>
</Link>
</div>
<div className={styles.itemsList}>
{items.map((item, idx) => (
<article key={idx} className={styles.itemCard}>
<Heading as="h2" className={styles.itemTitle}>
<Link to={item.permalink}>{item.title}</Link>
</Heading>
{item.type === 'blog' && (
<div className={styles.itemMeta}>
{item.formattedDate && (
<time dateTime={item.date} className={styles.itemDate}>
{item.formattedDate}
</time>
)}
{item.authors && item.authors.length > 0 && (
<span className={styles.itemAuthors}>
{item.authors.map((author, i) => (
<span key={i}>
{i > 0 && ', '}
{author.name}
</span>
))}
</span>
)}
{item.readingTime && (
<span className={styles.itemReadingTime}>
{Math.ceil(item.readingTime)} min read
</span>
)}
</div>
)}
{item.description && (
<p className={styles.itemDescription}>{item.description}</p>
)}
{item.tags && item.tags.length > 0 && (
<div className={styles.itemTags}>
{item.tags.map((tag, i) => (
<Link key={i} to={`/tags/${tag.label.toLowerCase()}`} className={styles.itemTag}>
{tag.label}
</Link>
))}
</div>
)}
</article>
))}
</div>
</div>
</Layout>
);
}