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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
tags: [docker, docker-compose, conteneurisation, homelab]
|
||||
---
|
||||
|
||||
# Docker et Docker Compose
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
tags: [ansible, automation, iac, configuration-management, homelab]
|
||||
---
|
||||
|
||||
# Playbooks Ansible
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
tags: [traefik, docker, reverse-proxy, ssl, crowdsec, homelab]
|
||||
---
|
||||
|
||||
# Traefik - Reverse Proxy moderne
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
tags: [proxmox, cluster, ha, homelab]
|
||||
---
|
||||
|
||||
# Cluster 3 noeuds Proxmox : un véritable Homelab HA
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
tags: [proxmox, kubernetes, k3s, ha, homelab]
|
||||
---
|
||||
|
||||
# Première version : le Homelab "HA" monomachine (projet initial)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
132
plugins/docusaurus-plugin-unified-tags/index.js
Normal file
132
plugins/docusaurus-plugin-unified-tags/index.js
Normal 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
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