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
This commit is contained in:
2026-04-12 15:59:37 -04:00
parent 66314481a0
commit a57bf3607b
4 changed files with 366 additions and 0 deletions
+19
View File
@@ -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 |
+108
View File
@@ -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
<img src={`/zen/api/storage/${post.image}`} alt={post.title} />
```
+125
View File
@@ -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 (
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/actualites/${post.slug}`}>{post.title}</a>
<p>{post.resume}</p>
{post.image && <img src={`/zen/api/storage/${post.image}`} alt="" />}
</li>
))}
</ul>
);
}
```
---
## 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 (
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/actualites/${post.slug}`}>{post.title}</a>
{post.source?.[0] && (
<span>Source : {post.source[0].title}</span>
)}
<div>
{post.tags?.map(tag => (
<a key={tag.id} href={`/tags/${tag.slug}`}>{tag.title}</a>
))}
</div>
</li>
))}
</ul>
);
}
```
---
## 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 (
<article>
<h1>{post.title}</h1>
<time dateTime={post.date}>
{new Date(post.date).toLocaleString('fr-FR')}
</time>
{post.source?.[0] && (
<p>Source : <a href={`/sources/${post.source[0].slug}`}>{post.source[0].title}</a></p>
)}
{post.tags?.length > 0 && (
<div>
{post.tags.map(tag => (
<a key={tag.id} href={`/tags/${tag.slug}`}>{tag.title}</a>
))}
</div>
)}
{post.image && <img src={`/zen/api/storage/${post.image}`} alt="" />}
<div>{post.content}</div>
</article>
);
}
```
---
## 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}`] : [],
},
};
}
```
+114
View File
@@ -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');
}
}
```