From 66314481a0286620598b6daf611b3862d8401ab8 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 12 Apr 2026 15:59:11 -0400 Subject: [PATCH] docs(posts): simplify README by removing verbose examples and details --- src/modules/posts/README.md | 424 +----------------------------------- 1 file changed, 5 insertions(+), 419 deletions(-) diff --git a/src/modules/posts/README.md b/src/modules/posts/README.md index 55df62b..7dbeb8f 100644 --- a/src/modules/posts/README.md +++ b/src/modules/posts/README.md @@ -6,21 +6,8 @@ Types de contenus configurables via variables d'environnement. Chaque projet dé ## Configuration -### Variables d'environnement - Copier les variables de [`.env.example`](.env.example) dans votre `.env`. -```bash -# Liste des types (séparés par |, en minuscules) -# Format optionnel avec label : cle:Label -ZEN_MODULE_POSTS_TYPES=blogue:Blogue|cve:CVE|emploi:Emplois - -# Champs par type : nom:type|nom:type|... -ZEN_MODULE_POSTS_TYPE_BLOGUE=title:title|slug:slug|category:category|date:date|resume:text|content:markdown|image:image -ZEN_MODULE_POSTS_TYPE_CVE=title:title|slug:slug|cve_id:text|severity:text|date:datetime|description:markdown|tags:relation:tag -ZEN_MODULE_POSTS_TYPE_TAG=title:title|slug:slug -``` - Si aucun label n'est fourni (`ZEN_MODULE_POSTS_TYPES=blogue`), le nom affiché sera la clé avec la première lettre en majuscule. ### Types de champs @@ -40,29 +27,6 @@ Si aucun label n'est fourni (`ZEN_MODULE_POSTS_TYPES=blogue`), le nom affiché s Chaque type doit avoir au moins un champ `title` et un champ `slug`. -**`date` ou `datetime` ?** - -- `date` stocke `"2026-03-14"`. Suffisant pour un billet de blogue ou un événement. -- `datetime` stocke `"2026-03-14T10:30:00.000Z"`. Nécessaire pour les CVE ou tout contenu avec une heure précise. - -### Champ `relation` - -Le champ `relation` associe plusieurs posts d'un autre type (many-to-many). - -```bash -ZEN_MODULE_POSTS_TYPE_ACTUALITE=title:title|slug:slug|date:datetime|resume:text|content:markdown|source:relation:source|tags:relation:tag -ZEN_MODULE_POSTS_TYPE_TAG=title:title|slug:slug -ZEN_MODULE_POSTS_TYPE_SOURCE=title:title|slug:slug -``` - -- Le nom avant `relation` (`source`, `tags`) est le nom du champ dans le formulaire et dans la réponse API. -- La valeur après `relation` (`source`, `tag`) est le type cible. -- La sélection est multiple avec recherche en temps réel. -- Les relations sont stockées dans `zen_posts_relations`. -- Dans l'API, les relations sont retournées comme un tableau d'objets avec tous les champs du post lié. - -### Images - Si un type utilise le champ `image`, configurer le stockage Zen dans le `.env` principal : `ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`. --- @@ -71,14 +35,6 @@ Si un type utilise le champ `image`, configurer le stockage Zen dans le `.env` p Les tables sont créées automatiquement avec `npx zen-db init`. -| Table | Description | -|---|---| -| `zen_posts` | Posts de tous les types (champs personnalisés dans `data JSONB`) | -| `zen_posts_category` | Catégories par type (créée si un champ `category` est défini) | -| `zen_posts_relations` | Relations entre posts (créée si un champ `relation` est défini) | - -Tous les champs personnalisés sont dans la colonne `data JSONB`. Ajouter ou retirer un champ dans le `.env` ne nécessite aucune migration SQL. - --- ## Interface d'administration @@ -89,382 +45,12 @@ Tous les champs personnalisés sont dans la colonne `data JSONB`. Ajouter ou ret | Créer un post | `/admin/posts/{type}/new` | | Modifier un post | `/admin/posts/{type}/edit/{id}` | | Liste des catégories | `/admin/posts/{type}/categories` | -| Créer une catégorie | `/admin/posts/{type}/categories/new` | -| Modifier une catégorie | `/admin/posts/{type}/categories/edit/{id}` | --- -## API publique +## Documentation -Pas d'authentification requise. - -### Config - -``` -GET /zen/api/posts/config -``` - -Retourne la liste de tous les types configurés avec leurs champs. - -### Liste de posts - -``` -GET /zen/api/posts/{type} -``` - -| Paramètre | Défaut | Description | -|---|---|---| -| `page` | `1` | Page courante | -| `limit` | `20` | Résultats par page | -| `category_id` | — | Filtrer par catégorie | -| `sortBy` | `created_at` | Trier par (nom de champ du type) | -| `sortOrder` | `DESC` | `ASC` ou `DESC` | -| `withRelations` | `false` | `true` pour inclure les champs relation | - -`withRelations=true` exécute une requête SQL supplémentaire par post. Garder un `limit` raisonnable (20 maximum). Sur une page de détail, préférer `/posts/{type}/{slug}` qui charge toujours les relations. - -**Réponse sans relations (défaut) :** - -```json -{ - "success": true, - "posts": [ - { - "id": 1, - "post_type": "actualite", - "slug": "faille-critique-openssh", - "title": "Faille critique dans OpenSSH", - "date": "2026-03-14T10:30:00.000Z", - "resume": "Une faille critique...", - "image": "blog/1234567890-image.webp", - "created_at": "2026-03-14T12:00:00Z", - "updated_at": "2026-03-14T12:00:00Z" - } - ], - "total": 42, - "totalPages": 3, - "page": 1, - "limit": 20 -} -``` - -**Réponse avec `withRelations=true` :** - -```json -{ - "success": true, - "posts": [ - { - "id": 1, - "slug": "faille-critique-openssh", - "title": "Faille critique dans OpenSSH", - "date": "2026-03-14T10:30:00.000Z", - "source": [ - { "id": 3, "slug": "cert-fr", "post_type": "source", "title": "CERT-FR" } - ], - "tags": [ - { "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" }, - { "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" } - ] - } - ] -} -``` - -### Post par slug - -``` -GET /zen/api/posts/{type}/{slug} -``` - -Les relations sont toujours incluses sur un post individuel. - -### Catégories - -``` -GET /zen/api/posts/{type}/categories -``` - -Retourne les catégories actives du type (pour alimenter un filtre). - -### Images - -Les clés d'image s'utilisent avec la route de stockage : - -```jsx -{post.title} -``` - ---- - -## Intégration Next.js - -### Liste de posts - -```js -// app/actualites/page.js -export default async function ActualitesPage() { - const res = await fetch( - `${process.env.NEXT_PUBLIC_URL}/zen/api/posts/actualite?limit=10&sortBy=date&sortOrder=DESC` - ); - const { posts, total, totalPages } = await res.json(); - - return ( - - ); -} -``` - -### Liste avec relations - -```js -// app/actualites/page.js -export default async function ActualitesPage() { - const res = await fetch( - `${process.env.NEXT_PUBLIC_URL}/zen/api/posts/actualite?limit=10&withRelations=true` - ); - const { posts } = await res.json(); - - return ( - - ); -} -``` - -### Page de détail - -```js -// app/actualites/[slug]/page.js -export default async function ActualiteDetailPage({ params }) { - const res = await fetch( - `${process.env.NEXT_PUBLIC_URL}/zen/api/posts/actualite/${params.slug}` - ); - const { post } = await res.json(); - - if (!post) notFound(); - - return ( -
-

{post.title}

- - - - {post.source?.[0] && ( -

Source : {post.source[0].title}

- )} - - {post.tags?.length > 0 && ( -
- {post.tags.map(tag => ( - {tag.title} - ))} -
- )} - - {post.image && } -
{post.content}
-
- ); -} -``` - -### Métadonnées SEO dynamiques - -```js -// app/actualites/[slug]/page.js -export async function generateMetadata({ params }) { - const res = await fetch( - `${process.env.NEXT_PUBLIC_URL}/zen/api/posts/actualite/${params.slug}` - ); - const { post } = await res.json(); - if (!post) return {}; - - return { - title: post.title, - description: post.resume, - openGraph: { - title: post.title, - description: post.resume, - images: post.image ? [`/zen/api/storage/${post.image}`] : [], - }, - }; -} -``` - ---- - -## Ajouter ou modifier un type - -**Ajouter un type :** modifier uniquement le `.env`, pas besoin de redémarrer la base. - -```bash -# Avant -ZEN_MODULE_POSTS_TYPES=cve:CVE|actualite:Actualités - -# Après -ZEN_MODULE_POSTS_TYPES=cve:CVE|actualite:Actualités|evenement:Événements -ZEN_MODULE_POSTS_TYPE_EVENEMENT=title:title|slug:slug|date:datetime|location:text|description:markdown|image:image -``` - -Redémarrer le serveur. Les tables ne changent pas (les nouveaux champs utilisent le JSONB existant). - -**Modifier les champs d'un type existant :** mettre à jour la variable `ZEN_MODULE_POSTS_TYPE_*` et redémarrer. Les posts existants conservent leurs données en JSONB, même si un champ est retiré de la config. - ---- - -## Utilisation programmatique - -Les fonctions CRUD sont importables côté serveur. Idéal pour les cron jobs, scripts d'import ou fetchers automatisés. - -### Fonctions disponibles - -```js -import { - createPost, // Créer un post - updatePost, // Modifier un post - getPostBySlug, // Chercher par slug - getPostByField, // Chercher par n'importe quel champ JSONB - upsertPost, // Créer ou mettre à jour (idempotent) - getPosts, // Liste avec pagination - deletePost, // Supprimer -} from '@zen/core/modules/posts/crud'; -``` - -### `upsertPost(postType, rawData, uniqueField)` - -Crée le post s'il n'existe pas, le met à jour sinon. - -- `postType` : le type de post (`'cve'`, `'actualite'`...) -- `rawData` : les données du post (mêmes champs que pour `createPost`) -- `uniqueField` : le champ de déduplication (`'slug'` par défaut) - -Retourne `{ post, created: boolean }`. - -### Champs `relation` dans `rawData` - -Les champs `relation` reçoivent un **tableau d'IDs** de posts existants. - -```js -// Correct -{ tags: [7, 8, 12], source: [3] } - -// Incorrect -{ tags: ['openssh', 'vuln'], source: { id: 3 } } -``` - -Si les posts liés n'existent pas encore, les créer d'abord avec `upsertPost` puis utiliser leurs IDs. - -### Exemple : fetcher de CVE - -```js -// src/cron/fetch-cves.js -import { upsertPost } from '@zen/core/modules/posts/crud'; - -export async function fetchAndImportCVEs() { - const response = await fetch('https://api.example.com/cves/recent'); - const { cves } = await response.json(); - - const results = { created: 0, updated: 0, errors: 0 }; - - for (const cve of cves) { - try { - // Résoudre les relations : s'assurer que les tags existent - const tagIds = []; - for (const tagName of (cve.tags || [])) { - const { post: tag } = await upsertPost('tag', { title: tagName }, 'slug'); - tagIds.push(tag.id); - } - - // Upsert du CVE, dédupliqué sur cve_id - const { created } = await upsertPost('cve', { - title: cve.title, - cve_id: cve.id, - severity: cve.severity, - score: String(cve.cvssScore), - product: cve.affectedProduct, - date: cve.publishedAt, - description: cve.description, - tags: tagIds, - }, 'cve_id'); - - created ? results.created++ : results.updated++; - } catch (err) { - console.error(`[CVE import] Error for ${cve.id}:`, err.message); - results.errors++; - } - } - - console.log(`[CVE import] Done — created: ${results.created}, updated: ${results.updated}, errors: ${results.errors}`); - return results; -} -``` - -### Exemple : fetcher d'actualités avec source - -```js -import { upsertPost } from '@zen/core/modules/posts/crud'; - -export async function fetchAndImportActualites(sourceName, articles) { - // S'assurer que la source existe - const { post: source } = await upsertPost('source', { title: sourceName }, 'slug'); - - for (const article of articles) { - await upsertPost('actualite', { - title: article.title, - date: article.publishedAt, - resume: article.summary, - content: article.content, - source: [source.id], - tags: [], - }, 'slug'); - } -} -``` - ---- - -## API d'administration - -Authentification requise. - -| Méthode | Route | Description | -|---|---|---| -| `GET` | `/zen/api/admin/posts/config` | Config complète de tous les types | -| `GET` | `/zen/api/admin/posts/search?type={type}&q={query}` | Recherche pour le sélecteur de relation | -| `GET` | `/zen/api/admin/posts/posts?type={type}` | Liste des posts d'un type | -| `GET` | `/zen/api/admin/posts/posts?type={type}&withRelations=true` | Liste avec relations | -| `GET` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Post par ID (relations toujours incluses) | -| `POST` | `/zen/api/admin/posts/posts?type={type}` | Créer un post | -| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Modifier un post | -| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Supprimer un post | -| `POST` | `/zen/api/admin/posts/upload-image` | Upload d'image | -| `GET` | `/zen/api/admin/posts/categories?type={type}` | Liste des catégories | -| `POST` | `/zen/api/admin/posts/categories?type={type}` | Créer une catégorie | -| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Modifier une catégorie | -| `DELETE` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Supprimer une catégorie | +- [API publique](docs/api.md) — endpoints, paramètres, réponses JSON +- [API d'administration](docs/admin-api.md) — routes authentifiées +- [Intégration Next.js](docs/integration.md) — liste, détail, SEO +- [Usage programmatique](docs/programmatic.md) — `upsertPost`, cron jobs, imports