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

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