From bbaea0278115b42a2e677d699dfbfc1b501d0616 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 13:04:14 +0000 Subject: [PATCH 01/53] =?UTF-8?q?feat:=20syst=C3=A8me=20d'articles,=20aute?= =?UTF-8?q?urs,=20labels=20avec=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduit un nouveau système de gestion du contenu en remplacement des pages statiques. Aucun impact sur l'apparence visuelle du site. Nouveau système : - /content/articles/{id}/ : article avec meta.json, fr.md, en.md, assets/ - /content/authors/{discord-id}.json : profil auteur (fetch Discord API si "DISCORD") - /content/labels/{id}.json : labels/catégories Nouvelles URLs : - /articles/{id}/ avec redirect depuis /{id}/ - /collections/{id}/ avec redirect depuis /{id}/ - /authors/{id}/ avec redirect depuis /{id}/ Internationalisation : - Détection automatique de la langue (navigator.language + localStorage) - Switcher FR/EN dans la top app bar - apply-lang.ts inliné dans pour éviter le flash https://claude.ai/code/session_015nQmfMhLpMNmMhE7a9bBWi --- CLAUDE.md | 190 ++++++++++++ content/articles/v1-changelog/en.md | 24 ++ content/articles/v1-changelog/fr.md | 24 ++ content/articles/v1-changelog/meta.json | 13 + content/authors/123456789012345678.json | 10 + content/labels/change-logs.json | 8 + docs/architecture.md | 40 +++ site/eleventy.config.cjs | 16 + site/package-lock.json | 6 +- site/package.json | 6 +- site/site/_data/articles.js | 47 +++ site/site/_data/authors.js | 68 +++++ site/site/_data/i18n.js | 37 +++ site/site/_data/labels.js | 13 + site/site/_includes/default.html | 22 ++ site/site/articles/article.njk | 99 +++++++ site/site/authors/author.njk | 97 ++++++ site/site/collections/collection.njk | 67 +++++ site/site/css/article.css | 377 ++++++++++++++++++++++++ site/site/redirect-articles.njk | 19 ++ site/site/redirect-authors.njk | 19 ++ site/site/redirect-labels.njk | 19 ++ site/src/inline/apply-lang.ts | 8 + vercel.json | 25 +- 24 files changed, 1248 insertions(+), 6 deletions(-) create mode 100644 CLAUDE.md create mode 100644 content/articles/v1-changelog/en.md create mode 100644 content/articles/v1-changelog/fr.md create mode 100644 content/articles/v1-changelog/meta.json create mode 100644 content/authors/123456789012345678.json create mode 100644 content/labels/change-logs.json create mode 100644 docs/architecture.md create mode 100644 site/site/_data/articles.js create mode 100644 site/site/_data/authors.js create mode 100644 site/site/_data/i18n.js create mode 100644 site/site/_data/labels.js create mode 100644 site/site/articles/article.njk create mode 100644 site/site/authors/author.njk create mode 100644 site/site/collections/collection.njk create mode 100644 site/site/css/article.css create mode 100644 site/site/redirect-articles.njk create mode 100644 site/site/redirect-authors.njk create mode 100644 site/site/redirect-labels.njk create mode 100644 site/src/inline/apply-lang.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..faa9dce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,190 @@ +# Moddy Docs — CLAUDE.md + +## Vue d'ensemble + +Site de documentation pour le bot Discord **Moddy**, construit avec **Eleventy (11ty)** + **Lit.js** + **Material Web Components**. + +Repo racine : `/home/user/docs/` +Projet Eleventy : `/home/user/docs/site/` + +--- + +## Structure du projet + +``` +/ +├── content/ # Contenu éditorial (articles, auteurs, labels) +│ ├── articles/ +│ │ └── {article-id}/ +│ │ ├── meta.json # Métadonnées de l'article +│ │ ├── fr.md # Contenu français +│ │ ├── en.md # Contenu anglais +│ │ └── assets/ # Images et ressources +│ ├── authors/ +│ │ └── {discord-id}.json # Profil auteur +│ └── labels/ +│ └── {label-id}.json # Label/catégorie +│ +├── docs/ # Documentation legacy (pages /about/) +│ ├── quick-start.md +│ ├── intro.md +│ └── Legal/ +│ +├── site/ # Projet Eleventy +│ ├── src/ # TypeScript source (composants Lit) +│ ├── site/ # Input Eleventy (templates Nunjucks) +│ │ ├── _includes/ # Layouts HTML +│ │ ├── _data/ # Data files (articles.js, authors.js, labels.js, i18n.js) +│ │ ├── articles/ # Template pagination articles → /articles/{id}/ +│ │ ├── collections/ # Template pagination labels → /collections/{id}/ +│ │ ├── authors/ # Template pagination auteurs → /authors/{id}/ +│ │ ├── about/ # Pages docs legacy +│ │ └── css/ # Feuilles de style +│ ├── eleventy-helpers/ # Plugins/filtres Eleventy custom +│ ├── scripts/ # Scripts de build +│ └── eleventy.config.cjs # Config Eleventy +│ +└── vercel.json # Config déploiement Vercel +``` + +--- + +## Architecture du contenu + +### Articles (`/content/articles/{id}/`) + +**`meta.json`** — Métadonnées : +```json +{ + "title": "Titre de l'article", + "id": "slug-article", + "authors": ["discord-id-1"], + "date": "2024-01-15", + "chapeau": { "fr": "Résumé FR", "en": "Summary EN" }, + "banner": null, + "seo_level": 1, + "labels": ["label-id"] +} +``` + +**Niveaux SEO :** +- `1` = Référencement normal +- `2` = Accessible via recherche ou lien label/auteur uniquement (`noindex`) +- `3` = Accessible uniquement via lien direct (`noindex, nofollow`) + +**`fr.md` / `en.md`** — Contenu Markdown par langue. + +**`assets/`** — Ressources (images, SVG…). Accessibles à `/articles/{id}/assets/`. + +--- + +### Auteurs (`/content/authors/{discord-id}.json`) + +```json +{ + "id": "123456789012345678", + "username": "DISCORD", + "avatar": "DISCORD", + "avatar_decoration": "DISCORD", + "post": "Product Manager", + "bio": "…", + "banner_color": "#5793f2", + "links": [ + { "name": "Twitter", "icon": "", "url": "https://…" } + ] +} +``` + +La valeur `"DISCORD"` dans `username`, `avatar`, ou `avatar_decoration` déclenche un fetch automatique vers `https://api.moddy.app/users/{id}` au **build time**. + +**API Moddy** : `GET https://api.moddy.app/users/{user_id}` +- Retourne `avatar_url`, `global_name`, `avatar_decoration_data.asset_url`, etc. +- Cache 5 minutes côté API. + +--- + +### Labels (`/content/labels/{id}.json`) + +```json +{ + "id": "change-logs", + "name": { "fr": "Changelogs", "en": "Changelogs" }, + "color": "#4caf50" +} +``` + +--- + +## URLs + +| Type | URL | Redirect depuis | +|------|-----|-----------------| +| Article | `/articles/{article-id}/` | `/{article-id}/` | +| Collection | `/collections/{label-id}/` | `/{label-id}/` | +| Auteur | `/authors/{discord-id}/` | `/{discord-id}/` | + +Les redirects courts sont générés statiquement via des templates Eleventy pagination (`redirect-articles.njk`, `redirect-labels.njk`, `redirect-authors.njk`). + +--- + +## Internationalisation (i18n) + +- Langues supportées : **fr** (défaut), **en** +- Détection automatique via `navigator.language` + `localStorage` (`moddy-lang`) +- Appliqué immédiatement au `` via `src/inline/apply-lang.ts` (inliné dans le HTML) +- Switcher dans la top app bar (boutons FR / EN) + +### Affichage du contenu multilingue + +Tout contenu conditionnel utilise l'attribut `data-lang-content` : +```html +Bonjour +Hello +``` + +CSS dans `/css/article.css` : +```css +[data-lang-content] { display: none; } +:root[data-lang="fr"] [data-lang-content="fr"] { display: revert; } +:root[data-lang="en"] [data-lang-content="en"] { display: revert; } +``` + +--- + +## Build + +```bash +cd site +npm install --legacy-peer-deps + +# Développement (watch) +npm run serve:dev + +# Production +npm run build:prod +``` + +**Pipeline Wireit :** +1. `build:copy-docs` — Copie `/docs/*.md` → `site/about/` +2. `build:dev:ts` — TypeScript → `/lib` (ou `/build` en prod) +3. `build:dev:eleventy` — Eleventy lit `site/site/` + `../content/` → `/_dev` + +--- + +## Data files Eleventy + +Situés dans `site/site/_data/` : +- `articles.js` — Lit `/content/articles/`, parse les markdown, retourne le tableau trié par date +- `authors.js` — Lit `/content/authors/`, fetch Discord API si `"DISCORD"` présent +- `labels.js` — Lit `/content/labels/` +- `i18n.js` — Traductions UI (fr/en) + +--- + +## Conventions + +- Les IDs d'articles et labels sont des slugs kebab-case (ex: `v1-changelog`, `change-logs`) +- Les IDs d'auteurs sont les IDs Discord (entiers 64-bit en string) +- Les bannières peuvent être `null`, une URL, ou un chemin relatif aux assets +- Ne jamais modifier les fichiers dans `_dev/` ou `_prod/` (générés au build) +- Ne pas modifier les fichiers copiés dans `site/site/about/` (générés depuis `/docs/`) diff --git a/content/articles/v1-changelog/en.md b/content/articles/v1-changelog/en.md new file mode 100644 index 0000000..99c482f --- /dev/null +++ b/content/articles/v1-changelog/en.md @@ -0,0 +1,24 @@ +# v1.0 Moddy Changelog + +Welcome to the first official Moddy changelog! Here are the new features in version 1.0. + +## New Features + +### Slash Commands +- `/setup` — Configure Moddy on your server in a few steps. +- `/help` — Displays the list of available commands. +- `/info` — Information about the bot and the server. + +### Moderation +- Automatic log system for member joins and leaves. +- Moderation commands: `/kick`, `/ban`, `/mute`. + +## Improvements + +- Response time reduced by 40%. +- Better handling of Discord API errors. + +## Bug Fixes + +- Fixed a crash when using `/setup` on servers without roles. +- Fixed embed display on mobile. diff --git a/content/articles/v1-changelog/fr.md b/content/articles/v1-changelog/fr.md new file mode 100644 index 0000000..e11d890 --- /dev/null +++ b/content/articles/v1-changelog/fr.md @@ -0,0 +1,24 @@ +# v1.0 Moddy Changelog + +Bienvenue dans le premier changelog officiel de Moddy ! Voici les nouveautés de la version 1.0. + +## Nouvelles fonctionnalités + +### Commandes slash +- `/setup` — Configure Moddy sur votre serveur en quelques étapes. +- `/help` — Affiche la liste des commandes disponibles. +- `/info` — Informations sur le bot et le serveur. + +### Modération +- Système de logs automatiques pour les entrées et sorties de membres. +- Commandes de modération : `/kick`, `/ban`, `/mute`. + +## Améliorations + +- Temps de réponse réduit de 40%. +- Meilleure gestion des erreurs API Discord. + +## Corrections de bugs + +- Correction d'un crash lors de l'utilisation de `/setup` sur des serveurs sans rôles. +- Correction de l'affichage des embeds sur mobile. diff --git a/content/articles/v1-changelog/meta.json b/content/articles/v1-changelog/meta.json new file mode 100644 index 0000000..2a8b2ce --- /dev/null +++ b/content/articles/v1-changelog/meta.json @@ -0,0 +1,13 @@ +{ + "title": "v1.0 Moddy Changelog", + "id": "v1-changelog", + "authors": ["123456789012345678"], + "date": "2024-01-15", + "chapeau": { + "fr": "Voici le changelog de la v1.0 de Moddy.", + "en": "Here is the changelog for Moddy v1.0." + }, + "banner": null, + "seo_level": 1, + "labels": ["change-logs"] +} diff --git a/content/authors/123456789012345678.json b/content/authors/123456789012345678.json new file mode 100644 index 0000000..0247f8b --- /dev/null +++ b/content/authors/123456789012345678.json @@ -0,0 +1,10 @@ +{ + "id": "123456789012345678", + "username": "DISCORD", + "avatar": "DISCORD", + "avatar_decoration": "DISCORD", + "post": "Product Manager", + "bio": "Bonjour ! Je suis le product manager de Moddy.", + "banner_color": "#5793f2", + "links": [] +} diff --git a/content/labels/change-logs.json b/content/labels/change-logs.json new file mode 100644 index 0000000..1c65f43 --- /dev/null +++ b/content/labels/change-logs.json @@ -0,0 +1,8 @@ +{ + "id": "change-logs", + "name": { + "fr": "Changelogs", + "en": "Changelogs" + }, + "color": "#4caf50" +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6813091 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,40 @@ + + +# Architecture du système de contenu + +## Vue d'ensemble + +Le site Moddy Docs utilise un système de contenu structuré basé sur des fichiers JSON et Markdown dans `/content/`. + +## Dossier `/content/` + +``` +content/ +├── articles/ # Articles du blog / documentation +│ └── {article-id}/ +│ ├── meta.json # Métadonnées +│ ├── fr.md # Contenu français +│ ├── en.md # Contenu anglais +│ └── assets/ # Ressources statiques +├── authors/ # Profils auteurs +│ └── {discord-id}.json +└── labels/ # Labels/catégories + └── {label-id}.json +``` + +## Flux de données au build + +1. Les data files Eleventy (`_data/*.js`) lisent `/content/` au démarrage du build. +2. Les fichiers Markdown sont transformés en HTML via `markdown-it`. +3. Les auteurs avec `"DISCORD"` déclenchent un fetch vers `api.moddy.app`. +4. Eleventy génère les pages via pagination depuis ces données. + +## Internationalisation + +La langue active est stockée dans `localStorage` et appliquée comme attribut `data-lang` sur ``. Le CSS affiche/masque le bon contenu en conséquence. + +Voir `CLAUDE.md` pour les détails complets. diff --git a/site/eleventy.config.cjs b/site/eleventy.config.cjs index 193cbe3..796da63 100644 --- a/site/eleventy.config.cjs +++ b/site/eleventy.config.cjs @@ -47,6 +47,22 @@ module.exports = function (eleventyConfig) { // Add this for 11ty's --watch flag eleventyConfig.addWatchTarget(`./${jsDir}/**/*.js`); + // Watch content directory for articles, authors, labels + eleventyConfig.addWatchTarget('../content/**/*'); + + // Pass through article assets (only the assets/ subdirectories) + const contentDir = require('path').resolve(__dirname, '../content/articles'); + const fsSync = require('fs'); + if (fsSync.existsSync(contentDir)) { + fsSync.readdirSync(contentDir).forEach((articleId) => { + const assetsPath = require('path').join(contentDir, articleId, 'assets'); + if (fsSync.existsSync(assetsPath)) { + eleventyConfig.addPassthroughCopy({ + [assetsPath]: `articles/${articleId}/assets`, + }); + } + }); + } // install shortcodes inlineCss(eleventyConfig, DEV); diff --git a/site/package-lock.json b/site/package-lock.json index 877e1fa..7929520 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1,11 +1,11 @@ { - "name": "material-web-catalog", + "name": "moddy-docs", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "material-web-catalog", + "name": "moddy-docs", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { @@ -43,7 +43,7 @@ "wireit": "^0.13.0" }, "engines": { - "node": "20.x.x" + "node": ">=20.0.0" } }, "node_modules/@11ty/dependency-tree": { diff --git a/site/package.json b/site/package.json index a3463e7..b33808f 100644 --- a/site/package.json +++ b/site/package.json @@ -34,7 +34,8 @@ "site", "lib", "eleventy-helpers", - "eleventy.config.cjs" + "eleventy.config.cjs", + "../content/**/*" ], "output": [ "_dev" @@ -79,7 +80,8 @@ "site", "build", "eleventy-helpers", - "eleventy.config.cjs" + "eleventy.config.cjs", + "../content/**/*" ], "output": [ "_prod/" diff --git a/site/site/_data/articles.js b/site/site/_data/articles.js new file mode 100644 index 0000000..9ca10c2 --- /dev/null +++ b/site/site/_data/articles.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const path = require('path'); +const markdownIt = require('markdown-it'); + +const md = markdownIt({ html: true, breaks: false, linkify: true }); +const CONTENT_DIR = path.resolve(__dirname, '../../../content/articles'); + +module.exports = async function () { + if (!fs.existsSync(CONTENT_DIR)) return []; + + const articles = []; + + for (const articleId of fs.readdirSync(CONTENT_DIR)) { + const articleDir = path.join(CONTENT_DIR, articleId); + if (!fs.statSync(articleDir).isDirectory()) continue; + + const metaPath = path.join(articleDir, 'meta.json'); + if (!fs.existsSync(metaPath)) continue; + + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); + const content = {}; + + for (const lang of ['fr', 'en']) { + const langPath = path.join(articleDir, `${lang}.md`); + if (fs.existsSync(langPath)) { + const raw = fs.readFileSync(langPath, 'utf8'); + content[lang] = md.render(raw); + } + } + + // Resolve assets path for passthrough + const assetsDir = path.join(articleDir, 'assets'); + const hasAssets = fs.existsSync(assetsDir); + + articles.push({ + ...meta, + content, + hasAssets, + assetsPath: hasAssets ? `/articles/${meta.id}/assets/` : null, + }); + } + + // Sort by date descending + articles.sort((a, b) => new Date(b.date) - new Date(a.date)); + + return articles; +}; diff --git a/site/site/_data/authors.js b/site/site/_data/authors.js new file mode 100644 index 0000000..1beb521 --- /dev/null +++ b/site/site/_data/authors.js @@ -0,0 +1,68 @@ +const fs = require('fs'); +const path = require('path'); + +const CONTENT_DIR = path.resolve(__dirname, '../../../content/authors'); +const API_BASE = 'https://api.moddy.app/users'; +const DISCORD_PLACEHOLDER = 'DISCORD'; + +async function fetchDiscordUser(userId) { + try { + const res = await fetch(`${API_BASE}/${userId}`); + if (!res.ok) return null; + return await res.json(); + } catch { + return null; + } +} + +function needsDiscordFetch(author) { + return ( + author.username === DISCORD_PLACEHOLDER || + author.avatar === DISCORD_PLACEHOLDER || + author.avatar_decoration === DISCORD_PLACEHOLDER + ); +} + +module.exports = async function () { + if (!fs.existsSync(CONTENT_DIR)) return []; + + const authors = []; + + for (const file of fs.readdirSync(CONTENT_DIR)) { + if (!file.endsWith('.json')) continue; + + const filePath = path.join(CONTENT_DIR, file); + const author = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + if (needsDiscordFetch(author)) { + const discordData = await fetchDiscordUser(author.id); + if (discordData) { + if (author.username === DISCORD_PLACEHOLDER) { + author.username = discordData.global_name || discordData.username; + } + if (author.avatar === DISCORD_PLACEHOLDER) { + author.avatar = discordData.avatar_url || null; + } + if (author.avatar_decoration === DISCORD_PLACEHOLDER) { + author.avatar_decoration = + discordData.avatar_decoration_data?.asset_url || null; + } + // Store extra Discord data + author._discord = { + badges: discordData.badges || [], + accent_color: discordData.accent_color, + }; + } else { + // Fallback: clear DISCORD placeholders + if (author.username === DISCORD_PLACEHOLDER) author.username = author.id; + if (author.avatar === DISCORD_PLACEHOLDER) author.avatar = null; + if (author.avatar_decoration === DISCORD_PLACEHOLDER) + author.avatar_decoration = null; + } + } + + authors.push(author); + } + + return authors; +}; diff --git a/site/site/_data/i18n.js b/site/site/_data/i18n.js new file mode 100644 index 0000000..be53982 --- /dev/null +++ b/site/site/_data/i18n.js @@ -0,0 +1,37 @@ +module.exports = { + supportedLangs: ['fr', 'en'], + defaultLang: 'fr', + + ui: { + fr: { + siteTitle: 'Moddy Docs', + home: 'Accueil', + articles: 'Articles', + collections: 'Collections', + authors: 'Auteurs', + searchPlaceholder: 'Rechercher…', + langSwitchLabel: 'Langue', + readMore: 'Lire la suite', + by: 'Par', + publishedOn: 'Publié le', + labels: 'Labels', + backToArticles: '← Retour aux articles', + noContent: 'Contenu non disponible dans cette langue.', + }, + en: { + siteTitle: 'Moddy Docs', + home: 'Home', + articles: 'Articles', + collections: 'Collections', + authors: 'Authors', + searchPlaceholder: 'Search…', + langSwitchLabel: 'Language', + readMore: 'Read more', + by: 'By', + publishedOn: 'Published on', + labels: 'Labels', + backToArticles: '← Back to articles', + noContent: 'Content not available in this language.', + }, + }, +}; diff --git a/site/site/_data/labels.js b/site/site/_data/labels.js new file mode 100644 index 0000000..bb87679 --- /dev/null +++ b/site/site/_data/labels.js @@ -0,0 +1,13 @@ +const fs = require('fs'); +const path = require('path'); + +const CONTENT_DIR = path.resolve(__dirname, '../../../content/labels'); + +module.exports = function () { + if (!fs.existsSync(CONTENT_DIR)) return []; + + return fs + .readdirSync(CONTENT_DIR) + .filter((f) => f.endsWith('.json')) + .map((f) => JSON.parse(fs.readFileSync(path.join(CONTENT_DIR, f), 'utf8'))); +}; diff --git a/site/site/_includes/default.html b/site/site/_includes/default.html index 276595b..d165265 100644 --- a/site/site/_includes/default.html +++ b/site/site/_includes/default.html @@ -25,6 +25,9 @@ {% inlinejs "inline/apply-saved-theme.js" %} + + {% inlinejs "inline/apply-lang.js" %} +