docs: translate posts README to French and update language guide
- Rewrite content with clearer structure, adding env variable examples and improving field type descriptions
This commit is contained in:
+1
-2
@@ -6,7 +6,6 @@ Tout ce qui est **code** est en **anglais**, sans exception :
|
|||||||
- Noms de fichiers (sauf dossiers de routes Next.js, voir ci-dessous)
|
- Noms de fichiers (sauf dossiers de routes Next.js, voir ci-dessous)
|
||||||
- Variables, fonctions, classes, composants
|
- Variables, fonctions, classes, composants
|
||||||
- Commentaires dans le code
|
- Commentaires dans le code
|
||||||
- README.md
|
|
||||||
- Props, événements, constantes, types
|
- Props, événements, constantes, types
|
||||||
- Git commit
|
- Git commit
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ Tout ce qui est **code** est en **anglais**, sans exception :
|
|||||||
Tout ce qui est **visible par l'utilisateur** est en **français** :
|
Tout ce qui est **visible par l'utilisateur** est en **français** :
|
||||||
- Textes, titres, descriptions, labels
|
- Textes, titres, descriptions, labels
|
||||||
- Slugs et noms de dossiers qui correspondent à des routes URL
|
- Slugs et noms de dossiers qui correspondent à des routes URL
|
||||||
- Documentations
|
- Documentations, README.md
|
||||||
|
|
||||||
## Messages de commit Git
|
## Messages de commit Git
|
||||||
|
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
# Module System
|
|
||||||
|
|
||||||
Modules are self-contained features that can be enabled/disabled via environment variables.
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/modules/your-module/
|
|
||||||
├── module.config.js # Required — navigation, pages, widgets
|
|
||||||
├── db.js # Database schema (createTables / dropTables)
|
|
||||||
├── crud.js # CRUD operations
|
|
||||||
├── actions.js # Server actions (for public pages)
|
|
||||||
├── metadata.js # SEO metadata generators
|
|
||||||
├── api.js # API route handlers
|
|
||||||
├── cron.config.js # Scheduled tasks
|
|
||||||
├── index.js # Public API re-exports
|
|
||||||
├── .env.example # Environment variable documentation
|
|
||||||
├── admin/ # Admin pages (lazy-loaded)
|
|
||||||
│ └── index.js # Re-exports admin components
|
|
||||||
├── pages/ # Public pages (lazy-loaded)
|
|
||||||
│ └── index.js
|
|
||||||
├── dashboard/ # Dashboard widgets
|
|
||||||
│ ├── statsActions.js
|
|
||||||
│ └── Widget.js
|
|
||||||
└── sub-feature/ # Optional sub-modules (e.g. items/, categories/)
|
|
||||||
├── db.js
|
|
||||||
├── crud.js
|
|
||||||
└── admin/
|
|
||||||
```
|
|
||||||
|
|
||||||
> Not all files are required. Only create what the module actually needs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1 — Create `module.config.js`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { lazy } from 'react';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Module identity
|
|
||||||
name: 'your-module',
|
|
||||||
displayName: 'Your Module',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'Description of your module',
|
|
||||||
|
|
||||||
// Other modules this one depends on (must be enabled too)
|
|
||||||
dependencies: ['clients'],
|
|
||||||
|
|
||||||
// Environment variables this module uses (documentation only)
|
|
||||||
envVars: [
|
|
||||||
'YOUR_MODULE_API_KEY',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Admin navigation — single section object or array of section objects
|
|
||||||
navigation: {
|
|
||||||
id: 'your-module',
|
|
||||||
title: 'Your Module',
|
|
||||||
icon: 'SomeIcon', // String icon name from shared/Icons.js
|
|
||||||
items: [
|
|
||||||
{ name: 'Items', href: '/admin/your-module/items', icon: 'PackageIcon' },
|
|
||||||
{ name: 'New', href: '/admin/your-module/items/new', icon: 'PlusSignIcon' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Admin pages — path → lazy component
|
|
||||||
adminPages: {
|
|
||||||
'/admin/your-module/items': lazy(() => import('./admin/ItemsListPage.js')),
|
|
||||||
'/admin/your-module/items/new': lazy(() => import('./admin/ItemCreatePage.js')),
|
|
||||||
'/admin/your-module/items/edit': lazy(() => import('./admin/ItemEditPage.js')),
|
|
||||||
},
|
|
||||||
|
|
||||||
// (Optional) Custom resolver for dynamic paths not known at build time.
|
|
||||||
// Called before the adminPages map. Return the lazy component or null.
|
|
||||||
pageResolver(path) {
|
|
||||||
const parts = path.split('/').filter(Boolean);
|
|
||||||
// example: /admin/your-module/{type}/list
|
|
||||||
if (parts[2] === 'list') return lazy(() => import('./admin/ItemsListPage.js'));
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public pages — keyed by 'default' (one component handles all public routes)
|
|
||||||
publicPages: {
|
|
||||||
default: lazy(() => import('./pages/YourModulePublicPages.js')),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public route patterns for SEO/route matching (relative to /zen/your-module/)
|
|
||||||
publicRoutes: [
|
|
||||||
{ pattern: ':id', description: 'View item' },
|
|
||||||
{ pattern: ':id/pdf', description: 'PDF viewer' },
|
|
||||||
],
|
|
||||||
|
|
||||||
// Dashboard widgets (lazy-loaded, rendered on the admin dashboard)
|
|
||||||
dashboardWidgets: [
|
|
||||||
lazy(() => import('./dashboard/Widget.js')),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Navigation as multiple sections
|
|
||||||
|
|
||||||
When a module provides several distinct sections (like the `posts` module with one section per post type), set `navigation` to an array:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
navigation: [
|
|
||||||
{ id: 'your-module-foo', title: 'Foo', icon: 'Book02Icon', items: [...] },
|
|
||||||
{ id: 'your-module-bar', title: 'Bar', icon: 'Layers01Icon', items: [...] },
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2 — Create `db.js`
|
|
||||||
|
|
||||||
Every module that uses a database must expose a `createTables` function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { query } from '@hykocx/zen/database';
|
|
||||||
|
|
||||||
export async function createTables() {
|
|
||||||
const created = [];
|
|
||||||
const skipped = [];
|
|
||||||
|
|
||||||
const exists = await query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_name = $1
|
|
||||||
)`, ['zen_your_module']);
|
|
||||||
|
|
||||||
if (!exists.rows[0].exists) {
|
|
||||||
await query(`
|
|
||||||
CREATE TABLE zen_your_module (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
created.push('zen_your_module');
|
|
||||||
} else {
|
|
||||||
skipped.push('zen_your_module');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { created, skipped };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function dropTables() {
|
|
||||||
await query(`DROP TABLE IF EXISTS zen_your_module CASCADE`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Never create migrations.** Instead, provide the SQL to the user so they can run it manually.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3 — Create `.env.example`
|
|
||||||
|
|
||||||
Document every environment variable the module reads:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#################################
|
|
||||||
# MODULE YOUR-MODULE
|
|
||||||
ZEN_MODULE_YOUR_MODULE=false
|
|
||||||
|
|
||||||
ZEN_MODULE_YOUR_MODULE_API_KEY=
|
|
||||||
ZEN_MODULE_YOUR_MODULE_SOME_OPTION=default_value
|
|
||||||
#################################
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4 — Create `cron.config.js` (optional)
|
|
||||||
|
|
||||||
Only needed if the module requires scheduled tasks:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { doSomething } from './reminders.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
jobs: [
|
|
||||||
{
|
|
||||||
name: 'your-module-task',
|
|
||||||
description: 'Description of what this job does',
|
|
||||||
schedule: '*/5 * * * *', // cron expression
|
|
||||||
handler: doSomething,
|
|
||||||
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5 — Register the module in 5 files
|
|
||||||
|
|
||||||
### `modules/modules.registry.js` — add the module name
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const AVAILABLE_MODULES = [
|
|
||||||
'clients',
|
|
||||||
'invoice',
|
|
||||||
'your-module',
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### `modules/modules.pages.js` — import the config
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import yourModuleConfig from './your-module/module.config.js';
|
|
||||||
|
|
||||||
const MODULE_CONFIGS = {
|
|
||||||
// ...existing modules...
|
|
||||||
'your-module': yourModuleConfig,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### `modules/modules.actions.js` — import server actions (if public pages or dashboard widgets)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { yourPublicAction } from './your-module/actions.js';
|
|
||||||
import { getYourModuleDashboardStats } from './your-module/dashboard/statsActions.js';
|
|
||||||
|
|
||||||
export const MODULE_ACTIONS = {
|
|
||||||
// ...existing modules...
|
|
||||||
'your-module': { yourPublicAction },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MODULE_DASHBOARD_ACTIONS = {
|
|
||||||
// ...existing modules...
|
|
||||||
'your-module': getYourModuleDashboardStats,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### `modules/modules.metadata.js` — import metadata generators (if SEO needed)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as yourModuleMetadata from './your-module/metadata.js';
|
|
||||||
|
|
||||||
export const MODULE_METADATA = {
|
|
||||||
// ...existing modules...
|
|
||||||
'your-module': yourModuleMetadata,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### `modules/init.js` — register the database initializer
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { createTables as createYourModuleTables } from './your-module/db.js';
|
|
||||||
|
|
||||||
const MODULE_DB_INITIALIZERS = {
|
|
||||||
// ...existing modules...
|
|
||||||
'your-module': createYourModuleTables,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6 — Enable the module
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ZEN_MODULE_YOUR_MODULE=true
|
|
||||||
```
|
|
||||||
|
|
||||||
The environment variable is derived from the module name: `ZEN_MODULE_` + module name uppercased (hyphens become underscores).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sub-modules
|
|
||||||
|
|
||||||
For complex modules, group related features into sub-directories. Each sub-module follows the same pattern (`db.js`, `crud.js`, `admin/`). The parent `module.config.js` registers all sub-module admin pages, and the parent `index.js` re-exports everything publicly.
|
|
||||||
|
|
||||||
See `src/modules/invoice/` for a complete example with `items/`, `categories/`, `transactions/`, and `recurrences/` sub-modules.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference implementations
|
|
||||||
|
|
||||||
| Module | Features demonstrated |
|
|
||||||
|--------|-----------------------|
|
|
||||||
| `src/modules/invoice/` | Sub-modules, public pages, cron jobs, Stripe, email, PDF, dashboard widgets, metadata |
|
|
||||||
| `src/modules/posts/` | Dynamic config from env vars, `pageResolver`, multiple navigation sections |
|
|
||||||
| `src/modules/clients/` | Simple module, dependencies, no public pages |
|
|
||||||
+171
-248
@@ -1,99 +1,102 @@
|
|||||||
# Posts Module
|
# Module Posts
|
||||||
|
|
||||||
Configurable Custom Post Types via environment variables. Inspired by the WordPress CPT concept: each project declares its own content types (blog, CVE, job, event, etc.) with the fields it needs, without modifying code.
|
Types de contenus configurables via variables d'environnement. Chaque projet déclare ses propres types (blogue, CVE, emploi, événement...) avec les champs dont il a besoin, sans toucher au code.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Configuration
|
||||||
|
|
||||||
- **Multiple post types** in a single module (blog, CVE, job...)
|
### Variables d'environnement
|
||||||
- **Dynamic fields** per type: title, slug, text, markdown, date, datetime, color, category, image, relation
|
|
||||||
- **Generic admin**: forms adapt automatically to the config
|
|
||||||
- **Public API** per type for integration in a Next.js site
|
|
||||||
- **Optional categories** per type (enabled if a `category` field is defined)
|
|
||||||
- **Relations** between types (many-to-many, e.g. CVE → Tags)
|
|
||||||
- **Unique slugs** per type (scoped: `blogue/mon-article` ≠ `cve/mon-article`)
|
|
||||||
|
|
||||||
---
|
Copier les variables de [`.env.example`](.env.example) dans votre `.env`.
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### 1. Environment variables
|
|
||||||
|
|
||||||
Copy variables from [`.env.example`](.env.example) into your `.env`:
|
|
||||||
|
|
||||||
> If no label is provided (`ZEN_MODULE_POSTS_TYPES=blogue`), the display name will be the key with the first letter capitalized (`Blogue`).
|
|
||||||
|
|
||||||
**Optional (images):**
|
|
||||||
|
|
||||||
If one of your types uses the `image` field, configure Zen storage in your main `.env` (`ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`).
|
|
||||||
|
|
||||||
### 2. Available field types
|
|
||||||
|
|
||||||
| Type | `.env` syntax | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `title` | `name:title` | Main text field — auto-generates the slug |
|
|
||||||
| `slug` | `name:slug` | Unique URL slug per type — auto-filled from title |
|
|
||||||
| `text` | `name:text` | Free text area (textarea) |
|
|
||||||
| `markdown` | `name:markdown` | Markdown editor with preview |
|
|
||||||
| `date` | `name:date` | Date picker only (YYYY-MM-DD) |
|
|
||||||
| `datetime` | `name:datetime` | Date **and time** picker (YYYY-MM-DDTHH:MM) |
|
|
||||||
| `color` | `name:color` | Color picker — stores a hex code `#rrggbb` |
|
|
||||||
| `category` | `name:category` | Dropdown linked to the category table |
|
|
||||||
| `image` | `name:image` | Image upload to Zen storage |
|
|
||||||
| `relation` | `name:relation:target_type` | Multi-select to posts of another type |
|
|
||||||
|
|
||||||
> **Rule:** each type must have at least one `title` field and one `slug` field. The `category` field automatically creates the `zen_posts_category` table. The `relation` field automatically creates the `zen_posts_relations` table.
|
|
||||||
|
|
||||||
#### `date` vs `datetime`
|
|
||||||
|
|
||||||
- `date` → stores `"2026-03-14"` — sufficient for blog posts, events
|
|
||||||
- `datetime` → stores `"2026-03-14T10:30:00.000Z"` (ISO 8601, UTC) — needed for CVEs, security bulletins, precise schedules
|
|
||||||
|
|
||||||
### `relation` field — Linking posts together
|
|
||||||
|
|
||||||
The `relation` field associates multiple posts of another type (many-to-many). Example: news posts referencing a source and tags.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ZEN_MODULE_POSTS_TYPE_ACTUALITE=title:title|slug:slug|date:datetime|resume:text|content:markdown|image:image|source:relation:source|tags:relation:tag
|
# 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
|
||||||
|
|
||||||
|
| Type | Syntaxe `.env` | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | `nom:title` | Champ texte principal, génère le slug automatiquement |
|
||||||
|
| `slug` | `nom:slug` | Slug unique par type, pré-rempli depuis le titre |
|
||||||
|
| `text` | `nom:text` | Zone de texte libre |
|
||||||
|
| `markdown` | `nom:markdown` | Éditeur Markdown avec prévisualisation |
|
||||||
|
| `date` | `nom:date` | Sélecteur de date (YYYY-MM-DD) |
|
||||||
|
| `datetime` | `nom:datetime` | Date et heure (ISO 8601, UTC) |
|
||||||
|
| `color` | `nom:color` | Sélecteur de couleur, stocke un code hex `#rrggbb` |
|
||||||
|
| `category` | `nom:category` | Menu déroulant lié à la table des catégories |
|
||||||
|
| `image` | `nom:image` | Upload d'image vers le stockage Zen |
|
||||||
|
| `relation` | `nom:relation:type_cible` | Sélection multiple vers des posts d'un autre type |
|
||||||
|
|
||||||
|
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_TAG=title:title|slug:slug
|
||||||
ZEN_MODULE_POSTS_TYPE_SOURCE=title:title|slug:slug
|
ZEN_MODULE_POSTS_TYPE_SOURCE=title:title|slug:slug
|
||||||
```
|
```
|
||||||
|
|
||||||
- The name before `relation` (`source`, `tags`) is the field name in the form and in the API response
|
- Le nom avant `relation` (`source`, `tags`) est le nom du champ dans le formulaire et dans la réponse API.
|
||||||
- The value after `relation` (`source`, `tag`) is the target post type
|
- La valeur après `relation` (`source`, `tag`) est le type cible.
|
||||||
- Selection is **multiple** (multi-select with real-time search)
|
- La sélection est multiple avec recherche en temps réel.
|
||||||
- Relations are stored in `zen_posts_relations` (junction table)
|
- Les relations sont stockées dans `zen_posts_relations`.
|
||||||
- In the API, relations are returned as an array of objects containing **all fields** of the linked post: `tags: [{ id, slug, title, color, ... }]`
|
- Dans l'API, les relations sont retournées comme un tableau d'objets avec tous les champs du post lié.
|
||||||
|
|
||||||
### 3. Database tables
|
### Images
|
||||||
|
|
||||||
Tables are created automatically with `npx zen-db init`. For reference, here are the module tables:
|
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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base de données
|
||||||
|
|
||||||
|
Les tables sont créées automatiquement avec `npx zen-db init`.
|
||||||
|
|
||||||
| Table | Description |
|
| Table | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `zen_posts` | Posts (all types — custom fields in `data JSONB`) |
|
| `zen_posts` | Posts de tous les types (champs personnalisés dans `data JSONB`) |
|
||||||
| `zen_posts_category` | Categories per type (if a `category` field is defined) |
|
| `zen_posts_category` | Catégories par type (créée si un champ `category` est défini) |
|
||||||
| `zen_posts_relations` | Relations between posts (if a `relation` field is defined) |
|
| `zen_posts_relations` | Relations entre posts (créée si un champ `relation` est défini) |
|
||||||
|
|
||||||
> **Design:** all custom fields are stored in the `data JSONB` column. Adding or removing a field in `.env` requires no SQL migration.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Admin interface
|
## Interface d'administration
|
||||||
|
|
||||||
| Page | URL |
|
| Page | URL |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Post list for a type | `/admin/posts/{type}/list` |
|
| Liste des posts | `/admin/posts/{type}/list` |
|
||||||
| Create a post | `/admin/posts/{type}/new` |
|
| Créer un post | `/admin/posts/{type}/new` |
|
||||||
| Edit a post | `/admin/posts/{type}/edit/{id}` |
|
| Modifier un post | `/admin/posts/{type}/edit/{id}` |
|
||||||
| Category list for a type | `/admin/posts/{type}/categories` |
|
| Liste des catégories | `/admin/posts/{type}/categories` |
|
||||||
| Create a category | `/admin/posts/{type}/categories/new` |
|
| Créer une catégorie | `/admin/posts/{type}/categories/new` |
|
||||||
| Edit a category | `/admin/posts/{type}/categories/edit/{id}` |
|
| Modifier une catégorie | `/admin/posts/{type}/categories/edit/{id}` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Public API (no authentication)
|
## API publique
|
||||||
|
|
||||||
|
Pas d'authentification requise.
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
@@ -101,28 +104,27 @@ Tables are created automatically with `npx zen-db init`. For reference, here are
|
|||||||
GET /zen/api/posts/config
|
GET /zen/api/posts/config
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns the list of all configured types with their fields.
|
Retourne la liste de tous les types configurés avec leurs champs.
|
||||||
|
|
||||||
### Post list
|
### Liste de posts
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /zen/api/posts/{type}
|
GET /zen/api/posts/{type}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Query parameters:**
|
| Paramètre | Défaut | Description |
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `page` | `1` | Current page |
|
| `page` | `1` | Page courante |
|
||||||
| `limit` | `20` | Results per page |
|
| `limit` | `20` | Résultats par page |
|
||||||
| `category_id` | — | Filter by category |
|
| `category_id` | — | Filtrer par catégorie |
|
||||||
| `sortBy` | `created_at` | Sort by (field name of the type) |
|
| `sortBy` | `created_at` | Trier par (nom de champ du type) |
|
||||||
| `sortOrder` | `DESC` | `ASC` or `DESC` |
|
| `sortOrder` | `DESC` | `ASC` ou `DESC` |
|
||||||
| `withRelations` | `false` | `true` to include relation fields in each post |
|
| `withRelations` | `false` | `true` pour inclure les champs relation |
|
||||||
|
|
||||||
> **Performance:** `withRelations=true` runs an additional SQL query per post. Use with a reasonable `limit` (≤ 20). On a detail page, prefer `/posts/{type}/{slug}` which always loads relations.
|
`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) :**
|
||||||
|
|
||||||
**Response without `withRelations` (default):**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -146,7 +148,8 @@ GET /zen/api/posts/{type}
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response with `withRelations=true`:**
|
**Réponse avec `withRelations=true` :**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -160,61 +163,33 @@ GET /zen/api/posts/{type}
|
|||||||
{ "id": 3, "slug": "cert-fr", "post_type": "source", "title": "CERT-FR" }
|
{ "id": 3, "slug": "cert-fr", "post_type": "source", "title": "CERT-FR" }
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
{ "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" },
|
{ "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" },
|
||||||
{ "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" }
|
{ "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Single post by slug
|
### Post par slug
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /zen/api/posts/{type}/{slug}
|
GET /zen/api/posts/{type}/{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
Relations are **always included** on a single post.
|
Les relations sont toujours incluses sur un post individuel.
|
||||||
|
|
||||||
**Response for a news post:**
|
### Catégories
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"post": {
|
|
||||||
"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 a été découverte...",
|
|
||||||
"content": "# Détails\n\n...",
|
|
||||||
"image": "blog/1234567890-image.webp",
|
|
||||||
|
|
||||||
"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é" }
|
|
||||||
],
|
|
||||||
|
|
||||||
"created_at": "2026-03-14T12:00:00Z",
|
|
||||||
"updated_at": "2026-03-14T12:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Categories
|
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /zen/api/posts/{type}/categories
|
GET /zen/api/posts/{type}/categories
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns the list of active categories for the type (to populate a filter).
|
Retourne les catégories actives du type (pour alimenter un filtre).
|
||||||
|
|
||||||
### Images
|
### Images
|
||||||
|
|
||||||
Image keys are built as follows: `/zen/api/storage/{image_field_value}`
|
Les clés d'image s'utilisent avec la route de stockage :
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<img src={`/zen/api/storage/${post.image}`} alt={post.title} />
|
<img src={`/zen/api/storage/${post.image}`} alt={post.title} />
|
||||||
@@ -222,9 +197,9 @@ Image keys are built as follows: `/zen/api/storage/{image_field_value}`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next.js integration examples
|
## Intégration Next.js
|
||||||
|
|
||||||
### News list (without relations)
|
### Liste de posts
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app/actualites/page.js
|
// app/actualites/page.js
|
||||||
@@ -248,7 +223,7 @@ export default async function ActualitesPage() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### News list with tags and source (withRelations)
|
### Liste avec relations
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app/actualites/page.js
|
// app/actualites/page.js
|
||||||
@@ -264,12 +239,10 @@ export default async function ActualitesPage() {
|
|||||||
<li key={post.id}>
|
<li key={post.id}>
|
||||||
<a href={`/actualites/${post.slug}`}>{post.title}</a>
|
<a href={`/actualites/${post.slug}`}>{post.title}</a>
|
||||||
|
|
||||||
{/* Source (array, usually 1 element) */}
|
|
||||||
{post.source?.[0] && (
|
{post.source?.[0] && (
|
||||||
<span>Source : {post.source[0].title}</span>
|
<span>Source : {post.source[0].title}</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags (array of 0..N elements) */}
|
|
||||||
<div>
|
<div>
|
||||||
{post.tags?.map(tag => (
|
{post.tags?.map(tag => (
|
||||||
<a key={tag.id} href={`/tags/${tag.slug}`}>{tag.title}</a>
|
<a key={tag.id} href={`/tags/${tag.slug}`}>{tag.title}</a>
|
||||||
@@ -282,7 +255,7 @@ export default async function ActualitesPage() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### News detail page (relations always included)
|
### Page de détail
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app/actualites/[slug]/page.js
|
// app/actualites/[slug]/page.js
|
||||||
@@ -298,19 +271,14 @@ export default async function ActualiteDetailPage({ params }) {
|
|||||||
<article>
|
<article>
|
||||||
<h1>{post.title}</h1>
|
<h1>{post.title}</h1>
|
||||||
|
|
||||||
{/* datetime field: display with time */}
|
|
||||||
<time dateTime={post.date}>
|
<time dateTime={post.date}>
|
||||||
{new Date(post.date).toLocaleString('fr-FR')}
|
{new Date(post.date).toLocaleString('fr-FR')}
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
{/* Source — array even with a single element */}
|
|
||||||
{post.source?.[0] && (
|
{post.source?.[0] && (
|
||||||
<p>
|
<p>Source : <a href={`/sources/${post.source[0].slug}`}>{post.source[0].title}</a></p>
|
||||||
Source : <a href={`/sources/${post.source[0].slug}`}>{post.source[0].title}</a>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{post.tags?.length > 0 && (
|
{post.tags?.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{post.tags.map(tag => (
|
{post.tags.map(tag => (
|
||||||
@@ -326,46 +294,7 @@ export default async function ActualiteDetailPage({ params }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### CVE detail page
|
### Métadonnées SEO dynamiques
|
||||||
|
|
||||||
```js
|
|
||||||
// app/cve/[slug]/page.js
|
|
||||||
export default async function CVEDetailPage({ params }) {
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_URL}/zen/api/posts/cve/${params.slug}`
|
|
||||||
);
|
|
||||||
const { post } = await res.json();
|
|
||||||
|
|
||||||
if (!post) notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article>
|
|
||||||
<h1>{post.title}</h1>
|
|
||||||
<p>ID : {post.cve_id}</p>
|
|
||||||
<p>Sévérité : {post.severity} — Score : {post.score}</p>
|
|
||||||
<p>Produit : {post.product}</p>
|
|
||||||
|
|
||||||
{/* Disclosure date and time */}
|
|
||||||
<time dateTime={post.date}>
|
|
||||||
{new Date(post.date).toLocaleString('fr-FR')}
|
|
||||||
</time>
|
|
||||||
|
|
||||||
{/* Associated tags */}
|
|
||||||
{post.tags?.length > 0 && (
|
|
||||||
<div>
|
|
||||||
{post.tags.map(tag => (
|
|
||||||
<span key={tag.id}>{tag.title}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>{post.description}</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic SEO metadata
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app/actualites/[slug]/page.js
|
// app/actualites/[slug]/page.js
|
||||||
@@ -390,63 +319,74 @@ export async function generateMetadata({ params }) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Adding a new post type
|
## Ajouter ou modifier un type
|
||||||
|
|
||||||
Edit `.env` only — no database restart needed:
|
**Ajouter un type :** modifier uniquement le `.env`, pas besoin de redémarrer la base.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Before
|
# Avant
|
||||||
ZEN_MODULE_POSTS_TYPES=cve:CVEs|actualite:Actualités
|
ZEN_MODULE_POSTS_TYPES=cve:CVE|actualite:Actualités
|
||||||
|
|
||||||
# After — adding the 'evenement' type
|
# Après
|
||||||
ZEN_MODULE_POSTS_TYPES=cve:CVEs|actualite:Actualités|evenement:Événements
|
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
|
ZEN_MODULE_POSTS_TYPE_EVENEMENT=title:title|slug:slug|date:datetime|location:text|description:markdown|image:image
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart the server. Tables are unchanged (new fields use the existing JSONB).
|
Redémarrer le serveur. Les tables ne changent pas (les nouveaux champs utilisent le JSONB existant).
|
||||||
|
|
||||||
## Modifying fields on an existing type
|
**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.
|
||||||
|
|
||||||
Update the `ZEN_MODULE_POSTS_TYPE_*` variable and restart. Existing posts keep their data in JSONB even if a field is removed from the config — it simply won't appear in the form anymore.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Programmatic usage (importers / fetchers)
|
## Utilisation programmatique
|
||||||
|
|
||||||
CRUD functions are directly importable server-side. No need to go through the HTTP API — ideal for cron jobs, import scripts, or automated fetchers.
|
Les fonctions CRUD sont importables côté serveur. Idéal pour les cron jobs, scripts d'import ou fetchers automatisés.
|
||||||
|
|
||||||
### Available functions
|
### Fonctions disponibles
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {
|
import {
|
||||||
createPost, // Create a post
|
createPost, // Créer un post
|
||||||
updatePost, // Update a post
|
updatePost, // Modifier un post
|
||||||
getPostBySlug, // Find by slug
|
getPostBySlug, // Chercher par slug
|
||||||
getPostByField, // Find by any JSONB field
|
getPostByField, // Chercher par n'importe quel champ JSONB
|
||||||
upsertPost, // Create or update (idempotent, for importers)
|
upsertPost, // Créer ou mettre à jour (idempotent)
|
||||||
getPosts, // List with pagination
|
getPosts, // Liste avec pagination
|
||||||
deletePost, // Delete
|
deletePost, // Supprimer
|
||||||
} from '@hykocx/zen/modules/posts/crud';
|
} from '@hykocx/zen/modules/posts/crud';
|
||||||
```
|
```
|
||||||
|
|
||||||
### `upsertPost(postType, rawData, uniqueField)`
|
### `upsertPost(postType, rawData, uniqueField)`
|
||||||
|
|
||||||
Key function for importers: creates the post if it doesn't exist, updates it otherwise.
|
Crée le post s'il n'existe pas, le met à jour sinon.
|
||||||
|
|
||||||
- `postType`: the post type (`'cve'`, `'actualite'`, etc.)
|
- `postType` : le type de post (`'cve'`, `'actualite'`...)
|
||||||
- `rawData`: post data (same fields as for `createPost`)
|
- `rawData` : les données du post (mêmes champs que pour `createPost`)
|
||||||
- `uniqueField`: the deduplication key field (`'slug'` by default, or `'cve_id'`, etc.)
|
- `uniqueField` : le champ de déduplication (`'slug'` par défaut)
|
||||||
|
|
||||||
Returns `{ post, created: boolean }`.
|
Retourne `{ post, created: boolean }`.
|
||||||
|
|
||||||
### Example — CVE fetcher (cron job)
|
### 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
|
```js
|
||||||
// src/cron/fetch-cves.js
|
// src/cron/fetch-cves.js
|
||||||
import { upsertPost } from '@hykocx/zen/modules/posts/crud';
|
import { upsertPost } from '@hykocx/zen/modules/posts/crud';
|
||||||
|
|
||||||
export async function fetchAndImportCVEs() {
|
export async function fetchAndImportCVEs() {
|
||||||
// 1. Fetch data from an external source
|
|
||||||
const response = await fetch('https://api.example.com/cves/recent');
|
const response = await fetch('https://api.example.com/cves/recent');
|
||||||
const { cves } = await response.json();
|
const { cves } = await response.json();
|
||||||
|
|
||||||
@@ -454,24 +394,24 @@ export async function fetchAndImportCVEs() {
|
|||||||
|
|
||||||
for (const cve of cves) {
|
for (const cve of cves) {
|
||||||
try {
|
try {
|
||||||
// 2. Resolve relations — ensure tags exist
|
// Résoudre les relations : s'assurer que les tags existent
|
||||||
const tagIds = [];
|
const tagIds = [];
|
||||||
for (const tagName of (cve.tags || [])) {
|
for (const tagName of (cve.tags || [])) {
|
||||||
const { post: tag } = await upsertPost('tag', { title: tagName }, 'slug');
|
const { post: tag } = await upsertPost('tag', { title: tagName }, 'slug');
|
||||||
tagIds.push(tag.id);
|
tagIds.push(tag.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Upsert the CVE (deduplicated on cve_id)
|
// Upsert du CVE, dédupliqué sur cve_id
|
||||||
const { created } = await upsertPost('cve', {
|
const { created } = await upsertPost('cve', {
|
||||||
title: cve.title, // title field
|
title: cve.title,
|
||||||
cve_id: cve.id, // text field
|
cve_id: cve.id,
|
||||||
severity: cve.severity, // text field
|
severity: cve.severity,
|
||||||
score: String(cve.cvssScore), // text field
|
score: String(cve.cvssScore),
|
||||||
product: cve.affectedProduct, // text field
|
product: cve.affectedProduct,
|
||||||
date: cve.publishedAt, // datetime field (ISO 8601)
|
date: cve.publishedAt,
|
||||||
description: cve.description, // markdown field
|
description: cve.description,
|
||||||
tags: tagIds, // relation:tag field — array of IDs
|
tags: tagIds,
|
||||||
}, 'cve_id'); // deduplicate on cve_id
|
}, 'cve_id');
|
||||||
|
|
||||||
created ? results.created++ : results.updated++;
|
created ? results.created++ : results.updated++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -485,17 +425,14 @@ export async function fetchAndImportCVEs() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example — News fetcher with source
|
### Exemple : fetcher d'actualités avec source
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { upsertPost, getPostByField } from '@hykocx/zen/modules/posts/crud';
|
import { upsertPost } from '@hykocx/zen/modules/posts/crud';
|
||||||
|
|
||||||
export async function fetchAndImportActualites(sourceName, sourceUrl, articles) {
|
export async function fetchAndImportActualites(sourceName, articles) {
|
||||||
// 1. Ensure the source exists
|
// S'assurer que la source existe
|
||||||
const { post: source } = await upsertPost('source', {
|
const { post: source } = await upsertPost('source', { title: sourceName }, 'slug');
|
||||||
title: sourceName,
|
|
||||||
color: '#3b82f6',
|
|
||||||
}, 'slug');
|
|
||||||
|
|
||||||
for (const article of articles) {
|
for (const article of articles) {
|
||||||
await upsertPost('actualite', {
|
await upsertPost('actualite', {
|
||||||
@@ -503,45 +440,31 @@ export async function fetchAndImportActualites(sourceName, sourceUrl, articles)
|
|||||||
date: article.publishedAt,
|
date: article.publishedAt,
|
||||||
resume: article.summary,
|
resume: article.summary,
|
||||||
content: article.content,
|
content: article.content,
|
||||||
source: [source.id], // relation:source — array of IDs
|
source: [source.id],
|
||||||
tags: [], // relation:tag
|
tags: [],
|
||||||
}, 'slug');
|
}, 'slug');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rules for relation fields in `rawData`
|
|
||||||
|
|
||||||
`relation` fields must receive an **array of IDs** of existing posts:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Correct
|
|
||||||
{ tags: [7, 8, 12], source: [3] }
|
|
||||||
|
|
||||||
// Incorrect — no slugs or objects
|
|
||||||
{ tags: ['openssh', 'vuln'], source: { id: 3 } }
|
|
||||||
```
|
|
||||||
|
|
||||||
If the linked posts don't exist yet, create them first with `upsertPost` then use their IDs.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Admin API (authentication required)
|
## API d'administration
|
||||||
|
|
||||||
These routes require an active admin session.
|
Authentification requise.
|
||||||
|
|
||||||
| Method | Route | Description |
|
| Méthode | Route | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GET` | `/zen/api/admin/posts/config` | Full config for all types |
|
| `GET` | `/zen/api/admin/posts/config` | Config complète de tous les types |
|
||||||
| `GET` | `/zen/api/admin/posts/search?type={type}&q={query}` | Search posts for the relation picker |
|
| `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}` | Post list for a type |
|
| `GET` | `/zen/api/admin/posts/posts?type={type}` | Liste des posts d'un type |
|
||||||
| `GET` | `/zen/api/admin/posts/posts?type={type}&withRelations=true` | List with relations included |
|
| `GET` | `/zen/api/admin/posts/posts?type={type}&withRelations=true` | Liste avec relations |
|
||||||
| `GET` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Post by ID (relations always included) |
|
| `GET` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Post par ID (relations toujours incluses) |
|
||||||
| `POST` | `/zen/api/admin/posts/posts?type={type}` | Create a post |
|
| `POST` | `/zen/api/admin/posts/posts?type={type}` | Créer un post |
|
||||||
| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Update a post |
|
| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Modifier un post |
|
||||||
| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Delete a post |
|
| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Supprimer un post |
|
||||||
| `POST` | `/zen/api/admin/posts/upload-image` | Upload an image |
|
| `POST` | `/zen/api/admin/posts/upload-image` | Upload d'image |
|
||||||
| `GET` | `/zen/api/admin/posts/categories?type={type}` | Category list |
|
| `GET` | `/zen/api/admin/posts/categories?type={type}` | Liste des catégories |
|
||||||
| `POST` | `/zen/api/admin/posts/categories?type={type}` | Create a category |
|
| `POST` | `/zen/api/admin/posts/categories?type={type}` | Créer une catégorie |
|
||||||
| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Update a category |
|
| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Modifier une catégorie |
|
||||||
| `DELETE` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Delete a category |
|
| `DELETE` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Supprimer une catégorie |
|
||||||
|
|||||||
Reference in New Issue
Block a user