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:
parent
87a2508769
commit
20fcabaf46
23 changed files with 683 additions and 10 deletions
86
src/pages/tags/index.tsx
Normal file
86
src/pages/tags/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
264
src/pages/tags/styles.module.css
Normal file
264
src/pages/tags/styles.module.css
Normal 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
146
src/theme/TagPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue