From a57bf3607bd7b56cd57b749d09904b26e5962302 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 12 Apr 2026 15:59:37 -0400 Subject: [PATCH] docs(posts): add API and Next.js integration documentation Add three documentation files for the posts module: - `api.md`: public API reference (list, slug, categories, images) - `admin-api.md`: admin API reference with all CRUD endpoints - `integration.md`: Next.js integration examples with code snippets --- src/modules/posts/docs/admin-api.md | 19 ++++ src/modules/posts/docs/api.md | 108 +++++++++++++++++++++ src/modules/posts/docs/integration.md | 125 +++++++++++++++++++++++++ src/modules/posts/docs/programmatic.md | 114 ++++++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 src/modules/posts/docs/admin-api.md create mode 100644 src/modules/posts/docs/api.md create mode 100644 src/modules/posts/docs/integration.md create mode 100644 src/modules/posts/docs/programmatic.md diff --git a/src/modules/posts/docs/admin-api.md b/src/modules/posts/docs/admin-api.md new file mode 100644 index 0000000..78eda05 --- /dev/null +++ b/src/modules/posts/docs/admin-api.md @@ -0,0 +1,19 @@ +# API d'administration — Module Posts + +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 | diff --git a/src/modules/posts/docs/api.md b/src/modules/posts/docs/api.md new file mode 100644 index 0000000..50be9d7 --- /dev/null +++ b/src/modules/posts/docs/api.md @@ -0,0 +1,108 @@ +# API publique — Module Posts + +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} +``` diff --git a/src/modules/posts/docs/integration.md b/src/modules/posts/docs/integration.md new file mode 100644 index 0000000..5def351 --- /dev/null +++ b/src/modules/posts/docs/integration.md @@ -0,0 +1,125 @@ +# Intégration Next.js — Module Posts + +## 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}`] : [], + }, + }; +} +``` diff --git a/src/modules/posts/docs/programmatic.md b/src/modules/posts/docs/programmatic.md new file mode 100644 index 0000000..f106fd2 --- /dev/null +++ b/src/modules/posts/docs/programmatic.md @@ -0,0 +1,114 @@ +# Usage programmatique — Module Posts + +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'); + } +} +```