# Configuration Sveltia CMS. # Source de vérité du schéma : src/content.config.ts (collections `blog`, # `projects`, `prompts`, `skills`, `artefacts`). # Tableau de bord (plan 20, R2) : titre de l'app et de l'onglet, logo de la # page d'entrée, de l'en-tête (bouton « Visit Live Site ») et favicon. # `public/admin/logo.svg` redessine la tuile 28×28 du logo de l'en-tête du site # (src/components/Header.astro) aux couleurs accent/accentSoft de global.css. app_title: benCat · Studio logo: src: /admin/logo.svg show_in_header: true backend: name: github repo: bendevcat/bencat.dev branch: main # Plan 22 (T2, R10) : messages de commit conventionnels. Substitution de # Sveltia 0.259 (`git/shared/commits.js`) : `{{collection}}` = libellé # SINGULIER de la collection (`label_singular` : Article, Projet, Prompt, # Skill — aucun marqueur n'existe pour son nom), `{{slug}}` = dossier de # l'entrée, `{{path}}` = premier fichier ; « +N » est ajouté quand N autres # fichiers suivent (une image jointe à la sauvegarde, par ex.). Le dépôt de # test (`/admin/?test-repo`) ignore ces messages et `testRepoConfig` retire # tout le backend : seul un vrai commit GitHub les montre. commit_messages: create: 'content({{collection}}): create "{{slug}}"' update: 'content({{collection}}): update "{{slug}}"' delete: 'content({{collection}}): delete "{{slug}}"' uploadMedia: 'content(media): upload "{{path}}"' deleteMedia: 'content(media): delete "{{path}}"' # Mode brouillon par branche (Preview Workflow, Sveltia 0.241) : enregistrer écrit sur une branche # `cms/…` avec sa pull request, sans toucher au site. La page de flux liste ces brouillons et les # publie. `direct_publish` garde « Enregistrer et publier » pour une correction immédiate. publish_mode: type: preview direct_publish: true # Repli média global. Sveltia EXIGE cette clé (sans elle : « The media folder is # not defined ») et l'onglet « global » du sélecteur reste TOUJOURS activé, même # pour une collection entry-relative — impossible à masquer dans 0.175.1. Il # pointe donc dans `src/`, où la résolution Vite d'Astro sait suivre le chemin : # une couverture choisie depuis cet onglet donne `/src/assets/uploads/`, # qu'`image()` importe et optimise. Un chemin hors de `src/` casserait le build. media_folder: src/assets/uploads public_folder: /src/assets/uploads # Plan 22 (T2, R8) : toute image matricielle téléversée (PNG, JPEG, GIF, AVIF, # HEIC, WebP) est convertie dans le navigateur, AVANT tout backend # (`assets/process.js` `processFile`) : WebP, 1600 px de large au plus (jamais # agrandie), qualité 80 (défaut Sveltia : 85). Puis `oversized = fichier # converti > max_file_size` : au-delà de 1 Mio, le fichier est refusé avec un # avis, rien n'est écrit. Le SVG n'est pas touché (`raster_image` seulement). media_libraries: default: config: max_file_size: 1048576 transformations: raster_image: format: webp quality: 80 width: 1600 output: # Défaut Sveltia = false : sans ça, un champ optionnel laissé vide sort en # chaîne vide / null et casse la validation Zod côté Astro (spec R4). omit_empty_optional_fields: true # Plan 22 (F2 ; D151) : dossiers ASCII pour les nouvelles entrées. Le `{{slug}}` # d'une nouvelle entrée = son titre passé par `slugify` de Sveltia 0.259 # (`common/slug/index.js`, options globales `slug`, schéma `SlugOptions`) : # `clean_accents` translittère (`@sindresorhus/transliterate` : é → e, œ → oe, # tirets → -), puis `encoding: ascii` change tout caractère hors # `[A-Za-z0-9_~-]` (apostrophe typographique, guillemets, emoji…) en # séparateur `-`, et `lowercase` met en minuscules (défaut, rendu explicite). # « Essai création à la une » → `essai-creation-a-la-une` (sans ces options : # `essai-création-à-la-une`). Les dossiers existants ne changent pas : seul le # slug d'une entrée créée ou dupliquée est calculé ainsi. # Garde : src/lib/cms-config.test.ts (slugify de Sveltia exécuté tel quel). slug: encoding: ascii clean_accents: true lowercase: true # Listes des collections (plan 20, T3) — gardées par src/lib/cms-navigation.test.ts. # Comportements Sveltia 0.259 lus dans ses sources (npm/index.js.map) : # - `icon` : nom d'une icône Material Symbols Outlined (police livrée) (R4). # - `summary` : `{{champ}}` affiche la valeur telle que l'éditeur la voit (le # LIBELLÉ d'un select) ; `date('…')` et `ternary('a','b')` lisent la valeur # brute ; une clé absente donne '' avant toute transformation. D'où les # séparateurs portés par `ternary` : rien ne s'affiche quand le champ manque (R6). # - `thumbnail` : un champ image nommé, ou `false` pour aucune vignette (R8). # - `sortable_fields.default` : tri d'une liste sans réglage de vue enregistré (R10). # - `view_filters` : `eq` compare aussi la forme texte (`true` = 'true') ; `ne` # garde une entrée SANS le champ ; `empty: true` = clé absente, null, '' ou [] # (R12). Un filtre, groupe ou tri sur un champ non déclaré est une ERREUR de # config. Toutes les collections ont un champ `draft` depuis le skill `site` # (C10). # - `view_groups` : avec `pattern`, groupe par le premier match de la regex sur # la valeur brute ; sans, par la valeur (R14). # - `preview_path` (plan 20, T4, R16) : lien « Afficher sur le site en ligne ». # Avec `path: '{{slug}}/index'`, `{{slug}}` vaut le NOM DU DOSSIER de l'entrée # (`getSlug`), c.-à-d. son id Astro : le lien donne `/blog/`, jamais # `/blog//index`. Base = origine courante (pas de `site_url`, D135) : # localhost en dev, bencat.dev en prod. Sans barre finale : Astro # (trailingSlash « ignore ») sert les deux formes en dev et en preview, Caddy # (file_server) redirige vers `//`. Un brouillon n'a pas de page : son lien # vise l'URL qu'il aura une fois publié, 404 d'ici là (D134). collections: - name: blog label: Articles label_singular: Article description: Articles du blog — src/content/blog/{slug}/index.md # R4 : icône de la barre latérale. icon: article folder: src/content/blog path: '{{slug}}/index' # R16 : « Afficher sur le site en ligne » → /blog/ (voir plus haut). preview_path: /blog/{{slug}} extension: md format: yaml-frontmatter create: true # Suppression permise (Plan 19, R15 — lève D08). Depuis Sveltia 0.259, # supprimer une entrée retire son slug de chaque champ relation qui la # cite (ici `relatedPosts` des projets), DANS LE MÊME COMMIT : aucun # renvoi mort ne reste pour `assertEntriesResolved`. Sveltia ne refuse la # suppression que si un champ ainsi vidé est `required` ou passe sous son # `min` — nos quatre relations sont optionnelles et sans `min` : les # garder ainsi (test « garde les quatre relations optionnelles »). delete: true slug: '{{slug}}' identifier_field: title # R6 : titre · date · catégorie, 📝 si brouillon, ⭐ si mis en avant. summary: "{{title}} · {{pubDate | date('YYYY-MM-DD')}} · {{category}}{{draft | ternary(' 📝','')}}{{featured | ternary(' ⭐','')}}" # R8 : vignette = couverture. thumbnail: cover # Vue tableau (0.238) et édition en masse d'un brouillon, d'une catégorie… bulk_edit: true table_columns: - title - { field: "{{pubDate | date('YYYY-MM-DD')}}", label: Date } - category - draft - aiUsage # R10 : plus récent d'abord. sortable_fields: fields: [pubDate, updatedDate, title] default: { field: pubDate, direction: descending } # R12 : un filtre par condition, valeurs tirées du schéma Zod. view_filters: - { name: drafts, label: Brouillons, field: draft, eq: true } - { name: published, label: Publiés, field: draft, ne: true } - { name: featured, label: Mis en avant, field: featured, eq: true } - { name: no-cover, label: Sans couverture, field: cover, empty: true } - { name: ai-partial, label: IA partielle, field: aiUsage, eq: partial } - { name: ai-full, label: IA totale, field: aiUsage, eq: full } # R14 : année (4 premiers chiffres de pubDate) et catégorie. view_groups: - { name: year, label: Année, field: pubDate, pattern: '^\d{4}' } - { name: category, label: Catégorie, field: category } # Médias à côté de l'article : Astro optimise les images co-localisées. media_folder: '' public_folder: '' fields: - { name: title, label: Titre, widget: string } - { name: description, label: Description, widget: text, hint: Résumé affiché sur les cartes et dans le flux RSS } # Corps juste après le résumé : on écrit sans dérouler les métadonnées. # Plan 23 (T4, D152) : blocs `:::` dans le menu Insert du corps des # articles et, depuis le plan 24 (D02), des projets (Encadré, Terminal, # Carte, Vidéo — composants enregistrés par # src/admin/blocks/editorComponents.ts). Sans cette # liste, Sveltia 0.259 active `code-block`, `image` ET tout composant # enregistré sur chaque champ markdown. `allow_nested_components` : # aucun bloc dans le contenu d'un encadré (le champ markdown d'un # composant hérite l'option du champ parent). - name: body label: Contenu widget: markdown editor_components: [code-block, image, table, hr, encadre, terminal, carte, video] allow_nested_components: false # Plan 22 (T2, R1) : `{{now}}` = heure de création au format du champ # (`draft/defaults.js`), pour une NOUVELLE entrée. Une entrée existante # sans pubDate recevrait l'heure d'ouverture : cmsFrontmatter.ts refuse # ce cas (pubDate est requis, chaque article le déclare). - { name: pubDate, label: Date de publication, widget: datetime, format: 'YYYY-MM-DDTHH:mm:ssZ', default: '{{now}}' } - { name: updatedDate, label: Date de mise à jour, widget: datetime, format: 'YYYY-MM-DDTHH:mm:ssZ', required: false } - { name: category, label: Catégorie, widget: select, options: [Actus, DevOps, Outils, Sécurité, Geekerie, Tutos, IA] } - { name: tags, label: Tags, widget: list, required: false, default: [] } - { name: cover, label: Image de couverture, widget: image, required: false, choose_url: false } - { name: coverAlt, label: Texte alternatif de la couverture, widget: string, required: false } - name: aiUsage label: Transparence IA widget: select required: false options: - { label: '✍️ 100% humain', value: none } - { label: '🤝 Co-créé avec IA', value: partial } - { label: '🤖 Généré par IA', value: full } # Plan 22 (T2, R1) : toute nouvelle entrée naît brouillon. Sveltia remplit # aussi de ce défaut un champ ABSENT d'une entrée existante (à # l'ouverture, `populateDefaultValue`) : chaque fichier déclare donc # `draft` (garde de src/lib/cmsFrontmatter.test.ts, plan 22 T1). - { name: draft, label: Brouillon, widget: boolean, required: false, default: true } - { name: featured, label: Mis en avant, widget: boolean, required: false, default: false } - name: relatedProjects label: Projets liés widget: relation required: false multiple: true collection: projects value_field: '{{slug}}' search_fields: [title] display_fields: [title] - name: projects label: Projets label_singular: Projet description: Fiches projets — src/content/projects/{slug}/index.md # R4 : icône de la barre latérale. icon: rocket_launch folder: src/content/projects path: '{{slug}}/index' # R16 : « Afficher sur le site en ligne » → /projets/ (voir plus haut). preview_path: /projets/{{slug}} extension: md format: yaml-frontmatter create: true # Suppression permise (R15) : Sveltia 0.259 retire le slug du projet des # `relatedProjects` des articles dans le même commit — voir `blog`. delete: true slug: '{{slug}}' identifier_field: title # R6 : titre · statut, 📝 si brouillon, ⭐ si mis en avant. summary: "{{title}} · {{status}}{{draft | ternary(' 📝','')}}{{featured | ternary(' ⭐','')}}" # R8 : vignette = couverture. thumbnail: cover # R10 : projet le plus récent d'abord. sortable_fields: fields: [startDate, title] default: { field: startDate, direction: descending } # R12 : un filtre par valeur de PROJECT_STATUSES, libellé de la puce du # site (src/lib/projectStatus.ts). Brouillons / Publiés comme les autres # collections (skill `site`, C10). view_filters: - { name: drafts, label: Brouillons, field: draft, eq: true } - { name: published, label: Publiés, field: draft, ne: true } - { name: status-actif, label: actif, field: status, eq: actif } - { name: status-wip, label: wip, field: status, eq: wip } - { name: status-archive, label: archivé, field: status, eq: archivé } # R14 : par statut. view_groups: - { name: status, label: Statut, field: status } # Médias à côté de la fiche : Astro optimise les images co-localisées # (même stratégie que `blog`, prouvée en production au Plan 2). media_folder: '' public_folder: '' fields: - { name: title, label: Titre, widget: string } - { name: description, label: Description, widget: text, hint: Résumé affiché sur les cartes } # Pot de départ (src/lib/deprecated.ts) : liste d'un élément, un seul bouton # sur une entrée normale. L'ajouter marque l'entrée deprecated. - name: deprecated label: Pot de départ label_singular: pot de départ widget: list required: false max: 1 hint: 'Je ne l’utilise plus : ajouter ce bloc marque l’entrée deprecated et la range dans /deprecated/' summary: '{{on}}' fields: - { name: on, label: Deprecated le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, hint: 'Date du retrait' } - { name: since, label: Utilisé depuis, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Début de la durée de vie affichée' } - { name: gave, label: Ce qu’il m’a apporté, widget: text, required: false, hint: 'Premier bloc du discours' } - { name: why, label: Pourquoi je l’ai lâché, widget: text, hint: 'Obligatoire' } - { name: kept, label: Ce que je garde, widget: text, required: false, hint: 'Troisième bloc du discours' } - { name: by, label: Ce qui le remplace, widget: text, required: false, hint: 'Quatrième bloc du discours' } - { name: review, label: À réexaminer le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Date où la décision sera revue' } - name: status label: Statut widget: select default: actif options: [actif, wip, archivé] - { name: startDate, label: Date de début, widget: datetime, format: 'YYYY-MM-DDTHH:mm:ssZ', required: false } - { name: stack, label: Stack technique, widget: list, required: false, default: [], hint: Une techno par entrée — alimente le filtre de /projets } # Plan 15 (D104) : miroir de `stackRoles` dans src/content.config.ts. Les # deux sous-champs sont requis ; `name` reprend une entrée de `stack`. - name: stackRoles label: Rôles des technos label_singular: Rôle widget: list required: false hint: 'Rôle affiché sous la techno dans l’onglet Stack — à citer mot pour mot depuis le titre, la description ou le contenu' fields: - { name: name, label: Techno (telle qu’écrite dans la stack), widget: string } - { name: role, label: Rôle, widget: string } - { name: tags, label: Tags, widget: list, required: false, default: [] } - { name: cover, label: Image de couverture, widget: image, required: false, choose_url: false } - { name: coverAlt, label: Texte alternatif de la couverture, widget: string, required: false } # Plan 15 (D102) : seulement si le dépôt du projet a un fichier LICENSE. - { name: license, label: Licence, widget: string, required: false, hint: 'Identifiant tel qu’écrit dans le LICENSE du dépôt (ex. MIT) ; ligne masquée si vide' } # Motif ancré des deux côtés (voir D03) : `^https?://` seul n'est pas ancré à # droite dans le validateur Sveltia 0.175.1 (RegExp.test non ancré = simple # préfixe), donc `https://` ou `http://exa mple.com` passaient le CMS et # cassaient `astro build` (ZodError sur `z.string().url()`) après le commit. # Partie hôte resserrée (voir D10, Plan 4, fix round 1) : la première version # de ce motif ([^\s/]+ pour l'hôte) acceptait encore un port invalide — # `https://github.com:bendevcat/x`, `http://exemple.fr:99999`, # `https://exemple.fr:abc`, `http://[` — que `z.string().url()` rejette tous. # Resserrée une seconde fois (fix round 2) : `@ ? # % | < > ^` manquaient # de la classe d'hôte, laissant passer `https://@`, `http://?`, `https://#`, # `https://exemple.fr%20/`, `https://ex|emple.fr`, etc., que Zod rejette # aussi. # Resserrée une troisième fois (fix round 3) : `\s` couvre espace/tab/saut # de ligne mais pas les autres caractères de contrôle — un octet NUL (ou # tout `\x00`–`\x1f`/`\x7f`) dans l'hôte passait le motif alors que Zod le # rejette. Résidu théorique (personne ne tape un NUL dans un champ texte), # mais le test affirme « jamais plus laxiste » : ça doit rester vrai. # L'hôte exclut désormais `: @ ? # % | < > ^ [ ] \x00-\x1f \x7f`, et un port # explicite (`:NNNNN`) doit être un entier 0–65535. Motif établi # empiriquement contre `z.string().url()` (cartésien de cas générés, pas # une liste choisie à la main) — voir cms-config.test.ts. # Écarts connus et assumés, motif plus STRICT que Zod (jamais l'inverse) : # littéraux IPv6 (`https://[::1]/`), userinfo (`user:pass@hôte`), port vide # (`https://hôte:/`) et port à zéros non significatifs (`:00080`) — tous # acceptés par Zod, tous refusés ici. Voir le test dédié dans # cms-config.test.ts qui encode cet écart explicitement, comme pour `ftp://`. - { name: repoUrl, label: URL du dépôt, widget: string, required: false, pattern: ['^https?://[^\s\x00-\x1f\x7f/:@?#%|<>^\[\]]+(:(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4}))?(/[^\s]*)?$', 'Doit être une URL complète et valide, commençant par http:// ou https:// (ex. https://github.com/mon-projet)'] } - { name: demoUrl, label: URL de la démo, widget: string, required: false, pattern: ['^https?://[^\s\x00-\x1f\x7f/:@?#%|<>^\[\]]+(:(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4}))?(/[^\s]*)?$', 'Doit être une URL complète et valide, commençant par http:// ou https:// (ex. https://mon-projet.exemple.fr)'] } # Plan 24 (R3) : onglet Code, des fichiers réels du dépôt du projet # recopiés tels quels, chacun avec la phrase qui dit pourquoi il est montré. # Plan 21 (R15, R17 ; D139, D140) : widget `code` de Sveltia 0.259 — éditeur # monospace coloré par Shiki. `output_code_only: true` : la valeur reste une # chaîne, lue et réécrite telle quelle (sinon objet `{code, lang}`, refusé par # Zod) ; langue fixe, sans sélecteur. Ouvrir une entrée n'écrit rien : Save # reste désactivé tant que le brouillon n'est pas modifié (toolbar.svelte, # `!modified`) ; une vraie modification réécrit tout le frontmatter (plan 19). - name: codeFiles label: Fichiers de code label_singular: Fichier widget: list required: false hint: 'Fichiers réels du dépôt, recopiés tels quels ; l’onglet Code est omis s’il n’y en a aucun' fields: - { name: path, label: Chemin dans le dépôt du projet, widget: string, hint: 'Ex. .github/workflows/deploy.yml ; affiché en tête de la fenêtre' } - { name: why, label: Pourquoi ce fichier, widget: string, hint: 'Une phrase, d’après le contenu du fichier' } - { name: code, label: Code, widget: code, output_code_only: true, allow_language_selection: false, default_language: yaml } # Plan 24 (R3) : onglet Journal, une entrée par étape datée. - name: journal label: Journal label_singular: Étape widget: list required: false hint: 'Étapes datées du projet ; l’onglet Journal est omis s’il n’y en a aucune' fields: - { name: date, label: Date, widget: datetime, format: 'YYYY-MM-DD', time_format: false } - { name: title, label: Titre, widget: string } - { name: note, label: Note, widget: string, required: false } - { name: version, label: Version, widget: string, required: false, hint: 'Telle qu’affichée dans la carte (ex. v1.6.0)' } - { name: draft, label: Brouillon, widget: boolean, required: false, default: true } - { name: featured, label: Mis en avant, widget: boolean, required: false, default: false } - name: relatedPosts label: Articles liés widget: relation required: false multiple: true collection: blog value_field: '{{slug}}' search_fields: [title] display_fields: [title] # Plan 24 (D02) : les mêmes blocs `:::` que le corps des articles, pour # que le corps d'une fiche projet les porte comme blocs éditables. - name: body label: Contenu widget: markdown editor_components: [code-block, image, table, hr, encadre, terminal, carte, video] allow_nested_components: false - name: prompts label: Prompts label_singular: Prompt description: Bibliothèque de prompts — src/content/prompts/{slug}/index.md # R4 : icône de la barre latérale. icon: terminal folder: src/content/prompts path: '{{slug}}/index' # R16 : « Afficher sur le site en ligne » → /prompts/ (voir plus haut). preview_path: /prompts/{{slug}} extension: md format: yaml-frontmatter create: true # Suppression permise (R15) : Sveltia 0.259 retire le slug du prompt des # `relatedPrompts` des skills dans le même commit — voir `blog`. delete: true slug: '{{slug}}' identifier_field: title # R6 : titre · format (libellé du select) · outil, « v » si # renseignée, 📝 si brouillon. summary: "{{title}} · {{format}} · {{tool}}{{version | ternary(' v','')}}{{version}}{{draft | ternary(' 📝','')}}" # R8 : pas de couverture, pas de vignette. thumbnail: false # R10 : dernière mise à jour d'abord (une entrée sans `updated` n'a pas de date). sortable_fields: fields: [updated, title] default: { field: updated, direction: descending } # R12 : Brouillons / Publiés, puis un filtre par valeur de PROMPT_FORMATS. view_filters: - { name: drafts, label: Brouillons, field: draft, eq: true } - { name: published, label: Publiés, field: draft, ne: true } - { name: fiches, label: Fiches, field: format, eq: fiche } - { name: guides, label: Guides, field: format, eq: guide } # R14 : par outil. view_groups: - { name: tool, label: Outil, field: tool } media_folder: '' public_folder: '' fields: - { name: title, label: Titre, widget: string } - { name: description, label: Description, widget: text, hint: Résumé affiché sur les cartes } # Pot de départ (src/lib/deprecated.ts) : liste d'un élément, un seul bouton # sur une entrée normale. L'ajouter marque l'entrée deprecated. - name: deprecated label: Pot de départ label_singular: pot de départ widget: list required: false max: 1 hint: 'Je ne l’utilise plus : ajouter ce bloc marque l’entrée deprecated et la range dans /deprecated/' summary: '{{on}}' fields: - { name: on, label: Deprecated le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, hint: 'Date du retrait' } - { name: since, label: Utilisé depuis, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Début de la durée de vie affichée' } - { name: gave, label: Ce qu’il m’a apporté, widget: text, required: false, hint: 'Premier bloc du discours' } - { name: why, label: Pourquoi je l’ai lâché, widget: text, hint: 'Obligatoire' } - { name: kept, label: Ce que je garde, widget: text, required: false, hint: 'Troisième bloc du discours' } - { name: by, label: Ce qui le remplace, widget: text, required: false, hint: 'Quatrième bloc du discours' } - { name: review, label: À réexaminer le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Date où la décision sera revue' } - name: format label: Format widget: select default: fiche options: - { label: 'Fiche — prompt copiable + notes', value: fiche } - { label: 'Guide — write-up long', value: guide } # Plan 21 (R15, R17 ; D139, D140) : widget `code` de Sveltia 0.259 — éditeur # monospace coloré par Shiki. `output_code_only: true` : la valeur reste une # chaîne, lue et réécrite telle quelle (sinon objet `{code, lang}`, refusé par # Zod) ; langue fixe, sans sélecteur. Ouvrir une entrée n'écrit rien : Save # reste désactivé tant que le brouillon n'est pas modifié (toolbar.svelte, # `!modified`) ; une vraie modification réécrit tout le frontmatter (plan 19). - { name: prompt, label: Texte du prompt, widget: code, output_code_only: true, allow_language_selection: false, default_language: markdown, required: false, hint: 'Le texte copiable en 1 clic. Requis en pratique pour une fiche ; inutile pour un guide. Écrire {nom} là où le lecteur remplit une variable déclarée ci-dessous.' } # Plan 14 (D94) : forme `{name, hint?, default?}` de l'inventaire §11, # miroir de `variables` dans src/content.config.ts. Seul `name` est requis. - name: variables label: Variables label_singular: Variable widget: list required: false hint: 'Emplacements {nom} du prompt que le lecteur peut remplir (Nom = nom sans accolades) ; la carte affiche « N variables »' fields: - { name: name, label: Nom, widget: string } - { name: hint, label: Aide, widget: string, required: false } - { name: default, label: Valeur par défaut, widget: string, required: false } - { name: tool, label: Outil, widget: string, default: Claude, hint: 'Alimente le filtre « outil » de /prompts' } - { name: model, label: Modèle, widget: string, required: false, hint: 'Affiché en pastille sur la fiche du prompt, si renseigné' } - { name: version, label: Version, widget: string, required: false, hint: 'Affichée « v » sur la carte, si renseignée' } # Plan 16 (D111) : miroir de `updated` (z.coerce.date().optional()). - { name: updated, label: Date de mise à jour, widget: datetime, format: 'YYYY-MM-DDTHH:mm:ssZ', required: false, hint: 'Date de la version, affichée sur la fiche du prompt, si renseignée' } - { name: tags, label: Tags, widget: list, required: false, default: [] } # Plan 22 (T2, R1) : toute nouvelle entrée naît brouillon. Sveltia remplit # aussi de ce défaut un champ ABSENT d'une entrée existante (à # l'ouverture, `populateDefaultValue`) : chaque fichier déclare donc # `draft` (garde de src/lib/cmsFrontmatter.test.ts, plan 22 T1). - { name: draft, label: Brouillon, widget: boolean, required: false, default: true } - name: relatedSkills label: Skills liés widget: relation required: false multiple: true collection: skills value_field: '{{slug}}' search_fields: [title] display_fields: [title] # Plan 16 (D110) : facultatif — une fiche peut n'avoir que son prompt. # Plan 23 (T4) : les composants d'avant les blocs, rien de plus. - { name: body, label: Contenu, widget: markdown, required: false, editor_components: [code-block, image, table, hr] } - name: skills label: Skills label_singular: Skill description: Bibliothèque de skills Claude Code — src/content/skills/{slug}/index.md # R4 : icône de la barre latérale. icon: extension folder: src/content/skills path: '{{slug}}/index' # R16 : « Afficher sur le site en ligne » → /skills/ (voir plus haut). preview_path: /skills/{{slug}} extension: md format: yaml-frontmatter create: true # Suppression permise (R15) : Sveltia 0.259 retire le slug du skill des # `relatedSkills` des prompts dans le même commit — voir `blog`. delete: true slug: '{{slug}}' identifier_field: title # R6 : titre · v · licence (chaque partie seulement si # renseignée), 📝 si brouillon. summary: "{{title}}{{version | ternary(' · v','')}}{{version}}{{license | ternary(' · ','')}}{{license}}{{draft | ternary(' 📝','')}}" # R8 : pas de couverture, pas de vignette. thumbnail: false # R10 : ordre alphabétique. sortable_fields: fields: [title] default: { field: title, direction: ascending } # R12 : Brouillons / Publiés. Pas de groupe (R14). view_filters: - { name: drafts, label: Brouillons, field: draft, eq: true } - { name: published, label: Publiés, field: draft, ne: true } media_folder: '' public_folder: '' fields: - { name: title, label: Titre, widget: string } - { name: name, label: Nom technique, widget: string, required: false, hint: 'Slug type SKILL.md (ex. anti-drift-planning)' } - { name: description, label: Description, widget: text, hint: 'Le « when to use » — affiché sur les cartes' } # Pot de départ (src/lib/deprecated.ts) : liste d'un élément, un seul bouton # sur une entrée normale. L'ajouter marque l'entrée deprecated. - name: deprecated label: Pot de départ label_singular: pot de départ widget: list required: false max: 1 hint: 'Je ne l’utilise plus : ajouter ce bloc marque l’entrée deprecated et la range dans /deprecated/' summary: '{{on}}' fields: - { name: on, label: Deprecated le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, hint: 'Date du retrait' } - { name: since, label: Utilisé depuis, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Début de la durée de vie affichée' } - { name: gave, label: Ce qu’il m’a apporté, widget: text, required: false, hint: 'Premier bloc du discours' } - { name: why, label: Pourquoi je l’ai lâché, widget: text, hint: 'Obligatoire' } - { name: kept, label: Ce que je garde, widget: text, required: false, hint: 'Troisième bloc du discours' } - { name: by, label: Ce qui le remplace, widget: text, required: false, hint: 'Quatrième bloc du discours' } - { name: review, label: À réexaminer le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Date où la décision sera revue' } - { name: type, label: Type, widget: string, default: claude-code } - { name: version, label: Version, widget: string, required: false } - { name: license, label: Licence, widget: string, required: false, hint: 'Identifiant lu dans le LICENSE du plugin (ex. MIT)' } # Motif ancré des deux côtés (D03, Plan 3), hôte resserré contre les ports # invalides (D10, Plan 4, fix round 1) — voir le commentaire détaillé sur # `projects.repoUrl` plus haut dans ce fichier ; motif identique partagé # par les champs URL du CMS. - { name: repoUrl, label: URL du dépôt, widget: string, required: false, pattern: ['^https?://[^\s\x00-\x1f\x7f/:@?#%|<>^\[\]]+(:(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4}))?(/[^\s]*)?$', 'Doit être une URL complète et valide, commençant par http:// ou https:// (ex. https://github.com/mon-skill)'] } - { name: installCmd, label: Commande d’installation, widget: string, required: false, hint: 'Affichée dans un bloc copiable sur la page du skill' } # Plan 17 (D115, D116) : miroir des champs facultatifs de la fiche skill # dans src/content.config.ts. Toute valeur est citée du plugin réel # (contrôle local : node scripts/check-skill-sources.mjs). - { name: installNote, label: Note d’installation, widget: string, required: false, hint: 'Affichée sous les étapes de la fenêtre d’installation — citée de la doc du plugin' } - { name: skillCount, label: Nombre de skills, widget: number, value_type: int, min: 0, required: false, hint: 'Nombre de skills/*/SKILL.md du plugin' } - { name: commandCount, label: Nombre de commandes, widget: number, value_type: int, min: 0, required: false, hint: 'Nombre de commands/*.md du plugin' } - name: highlights label: Ce que fait ce skill label_singular: Point widget: list required: false hint: 'Une phrase par point, citée mot pour mot de la doc du plugin ou du contenu ci-dessous' field: { name: highlight, label: Point, widget: string } - name: triggers label: Déclencheurs label_singular: Déclencheur widget: list required: false hint: 'Phrases citées mot pour mot des descriptions SKILL.md ou du README du plugin' field: { name: trigger, label: Déclencheur, widget: string } - name: changelog label: Notes de version label_singular: Version widget: list required: false hint: 'Du plus récent au plus ancien, cité du CHANGELOG du plugin ; la ligne de la version déclarée donne « maj. »' fields: - { name: version, label: Version, widget: string } - { name: date, label: Date, widget: datetime, format: 'YYYY-MM-DD', time_format: false } - { name: text, label: Texte, widget: string } - name: files label: Fichiers du plugin label_singular: Fichier widget: list required: false hint: 'Extrait = premières lignes du fichier, recopiées telles quelles (16 au plus, jamais une adresse e-mail)' fields: - { name: path, label: Chemin (relatif à la racine du plugin), widget: string } - { name: lines, label: Nombre de lignes du fichier, widget: number, value_type: int, min: 1 } # Plan 21 (R15, R17 ; D139, D140) : widget `code` de Sveltia 0.259 — éditeur # monospace coloré par Shiki. `output_code_only: true` : la valeur reste une # chaîne, lue et réécrite telle quelle (sinon objet `{code, lang}`, refusé par # Zod) ; langue fixe, sans sélecteur. Ouvrir une entrée n'écrit rien : Save # reste désactivé tant que le brouillon n'est pas modifié (toolbar.svelte, # `!modified`) ; une vraie modification réécrit tout le frontmatter (plan 19). - { name: excerpt, label: Extrait, widget: code, output_code_only: true, allow_language_selection: false, default_language: markdown } - { name: filesSource, label: Révision des fichiers, widget: string, required: false, hint: 'Commit ou version d’où viennent les fichiers (ex. 3dc3336, 6.4.1)' } - { name: tags, label: Tags, widget: list, required: false, default: [] } # Plan 22 (T2, R1) : toute nouvelle entrée naît brouillon. Sveltia remplit # aussi de ce défaut un champ ABSENT d'une entrée existante (à # l'ouverture, `populateDefaultValue`) : chaque fichier déclare donc # `draft` (garde de src/lib/cmsFrontmatter.test.ts, plan 22 T1). - { name: draft, label: Brouillon, widget: boolean, required: false, default: true } - name: relatedPrompts label: Prompts liés widget: relation required: false multiple: true collection: prompts value_field: '{{slug}}' search_fields: [title] display_fields: [title] # Plan 17 (D115) : facultatif, comme pour `prompts` (D110). # Plan 23 (T4) : les composants d'avant les blocs, rien de plus. - { name: body, label: Contenu, widget: markdown, required: false, editor_components: [code-block, image, table, hr] } # Plan 26 (R19, R20) : une copie d'artifact entre et se remplace par # scripts/artefacts/import.mjs, seul chemin qui passe les gardes. Le CMS édite # le texte et les champs de la fiche, sans créer ni supprimer d'entrée. - name: artefacts label: Artefacts label_singular: Artefact description: Copies d’artifacts — src/content/artefacts/{slug}/index.md, à côté d’artifact.html # R4 : icône de la barre latérale. icon: diamond folder: src/content/artefacts path: '{{slug}}/index' # R16 : « Afficher sur le site en ligne » → /artefacts/ (voir plus haut). preview_path: /artefacts/{{slug}} extension: md format: yaml-frontmatter create: false delete: false slug: '{{slug}}' identifier_field: title # R6 : titre · source · date de la copie, 📝 si brouillon. summary: "{{title}} · {{source}} · {{snapshotDate | date('YYYY-MM-DD')}}{{draft | ternary(' 📝','')}}" # R8 : pas de couverture, pas de vignette. thumbnail: false # R10 : le plus récent d'abord, comme /artefacts. sortable_fields: fields: [pubDate, snapshotDate, title] default: { field: pubDate, direction: descending } # R12 : Brouillons / Publiés, puis un filtre par valeur de ARTEFACT_SOURCES. view_filters: - { name: drafts, label: Brouillons, field: draft, eq: true } - { name: published, label: Publiés, field: draft, ne: true } - { name: perso, label: Perso, field: source, eq: perso } - { name: pro, label: Pro, field: source, eq: pro } # R14 : par source. view_groups: - { name: source, label: Source, field: source } # Pas de `media_folder: ''` : le dossier de l'entrée deviendrait celui de ses # médias, et `artifact.html` y apparaîtrait. Les médias restent ceux du repli global. # Champs dans l'ordre de R1, celui qu'écrit import.mjs : Sveltia réécrit le # frontmatter dans cet ordre, et une entrée importée doit rester canonique (R20). fields: - { name: title, label: Titre, widget: string } - { name: description, label: Description, widget: text, hint: Résumé affiché sur la carte et en tête de la fiche } # Dates seules, comme import.mjs les écrit ; sans `{{now}}` : elles viennent de l'import. - { name: pubDate, label: Date de publication, widget: datetime, format: 'YYYY-MM-DD', time_format: false } - { name: snapshotDate, label: Date de la copie, widget: datetime, format: 'YYYY-MM-DD', time_format: false, hint: 'Affichée sur la fiche, précédée de « Copie du »' } - { name: source, label: Source, widget: select, options: [perso, pro] } - { name: why, label: Pourquoi cet artefact, widget: text, hint: 'Affiché sur la fiche ; la recherche trouve la fiche par ce texte' } - { name: tags, label: Tags, widget: list, required: false, default: [] } - { name: draft, label: Brouillon, widget: boolean, required: false, default: true } - { name: sourceSlug, label: Slug d’origine, widget: string, required: false, hint: 'Interne : jamais affiché sur le site' } - { name: sourceVersion, label: Version d’origine, widget: string, required: false, hint: 'Interne : signale une copie périmée' } # Motif des autres champs URL : voir `projects.repoUrl`. - { name: claudeUrl, label: Lien vers l’original, widget: string, required: false, hint: 'Seulement pour un artifact déjà public', pattern: ['^https?://[^\s\x00-\x1f\x7f/:@?#%|<>^\[\]]+(:(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4}))?(/[^\s]*)?$', 'Doit être une URL complète et valide, commençant par http:// ou https://'] } - { name: clearedOn, label: Caviardage validé le, widget: datetime, format: 'YYYY-MM-DD', time_format: false, required: false, hint: 'Rempli seulement après le caviardage de cette copie ; une entrée pro ne se publie pas sans cette date' } - { name: body, label: Contenu, widget: markdown, required: false, editor_components: [code-block, table, hr] }