Recette : CMS de blog
Créez un backend de blog avec :
- Des articles avec contenu Markdown et images de couverture
- Des auteurs avec profils
- Des catégories avec une relation many-to-many
Collections
Section intitulée « Collections »import { defineCollection } from "@rebasepro/cms-types";
export const authorsCollection = defineCollection({ slug: "authors", name: "Authors", singularName: "Author", table: "authors", properties: { name: { type: "string", name: "Name", validation: { required: true } }, email: { type: "string", name: "Email", email: true, validation: { required: true, unique: true } }, avatar: { type: "string", name: "Avatar", storage: { storagePath: "avatars", acceptedFiles: ["image/*"], maxSize: 2 * 1024 * 1024 } }, bio: { type: "string", name: "Bio", admin: { multiline: true } } }, admin: { icon: "person" }});Catégories
Section intitulée « Catégories »import { defineCollection } from "@rebasepro/cms-types";export const categoriesCollection = defineCollection({ slug: "categories", name: "Categories", singularName: "Category", table: "categories", properties: { name: { type: "string", name: "Name", validation: { required: true } }, slug: { type: "string", name: "Slug", validation: { required: true, unique: true } }, color: { type: "string", name: "Color", enum: [ { id: "blue", label: "Blue", color: "blue" }, { id: "green", label: "Green", color: "green" }, { id: "red", label: "Red", color: "pink" }, { id: "orange", label: "Orange", color: "orange" } ] } }, admin: { icon: "label" }});Articles
Section intitulée « Articles »import { defineCollection } from "@rebasepro/cms-types";// The row shape, so callbacks below see typed `values` instead of `unknown`.type Article = { title: string; slug: string; status: string; publishedAt?: Date | null;};
export const articlesCollection = defineCollection({ slug: "articles", name: "Articles", singularName: "Article", table: "articles", history: true, properties: { title: { type: "string", name: "Title", validation: { required: true } }, slug: { type: "string", name: "URL Slug", validation: { required: true, unique: true } }, author: { type: "relation", name: "Author", relation: { kind: "belongsTo", target: () => authorsCollection, localKey: "author_id" } }, categories: { type: "relation", name: "Categories", relation: { kind: "manyToMany", target: () => categoriesCollection, through: { table: "article_categories", sourceColumn: "article_id", targetColumn: "category_id" } } }, status: { type: "string", name: "Status", enum: [ { id: "draft", label: "Draft", color: "gray" }, { id: "review", label: "In Review", color: "orange" }, { id: "published", label: "Published", color: "green" } ], defaultValue: "draft" }, cover_image: { type: "string", name: "Cover Image", storage: { storagePath: "articles/covers", acceptedFiles: ["image/*"] } }, content: { type: "string", name: "Content", admin: { markdown: true } }, excerpt: { type: "string", name: "Excerpt", admin: { multiline: true }, validation: { max: 300 } }, publishedAt: { type: "date", name: "Published At" }, createdAt: { type: "date", name: "Created At", autoValue: "on_create", admin: { readOnly: true } } }, callbacks: { beforeSave: async ({ values, status }) => { // Auto-generate slug if (values.title && !values.slug) { values.slug = values.title .toLowerCase() .replace(/[^a-z0-9]+/g, "-"); } // Set publishedAt when publishing if (values.status === "published" && !values.publishedAt) { // `publishedAt` is a `date` property, so its value is a Date. values.publishedAt = new Date(); } return values; } }, securityRules: [ { operation: "select", using: "{status} = 'published'" }, { operation: "select", ownerField: "authorId" }, { operations: ["insert", "update"], ownerField: "authorId" }, { operation: "delete", roles: ["admin"] } ], admin: { icon: "article", defaultViewMode: "table" }});Configuration
Section intitulée « Configuration »- Enregistrez les trois fichiers dans
config/collections/. Ajoutez-les au tableaucollectionsdansconfig/collections/index.tspour choisir où ils apparaissent dans la navigation — chaque fichier du répertoire est chargé dans tous les cas. - Avec
rebase deven cours d’exécution, c’est tout : enregistrer régénère le schéma, redémarre le backend, et le démarrage crée les tables, la table de jonction et leurs politiques de sécurité au niveau des lignes. En dehors derebase dev, contre votre propre PostgreSQL, exécutezrebase schema generatepuisrebase db push.
Vous disposez désormais d’un CMS de blog entièrement fonctionnel avec :
- Gestion des auteurs avec téléversement d’avatars
- Étiquetage par catégories via des relations many-to-many
- Édition de contenu Markdown
- Flux de travail Brouillon → En révision → Publié
- Slugs d’URL générés automatiquement
- Règles RLS limitant les auteurs à leurs propres articles
- Piste d’audit complète grâce à l’historique des entités
Requêtes depuis le SDK
Section intitulée « Requêtes depuis le SDK »Utilisez le SDK typé pour récupérer les articles avec leurs relations :
// The row shape you expect back — without it every field arrives as `unknown`.type Article = { title: string; status: string; publishedAt?: string | null; authorId: string; author?: { name: string }; categories?: { name: string }[];};
// Fetch published articles with author and categories includedconst { data: articles } = await client.data .collection<Article>("articles") .where("status", "==", "published") .include("author", "categories") .orderBy("publishedAt", "desc") .limit(10) .find();
// Rows come back flat — there is no `values` wrapper on the SDK.for (const article of articles) { console.log(article.title); console.log(article.author?.name); // Hydrated relation console.log(article.authorId); // Scalar FK console.log(article.categories); // Array of related entities}Voir aussi
Section intitulée « Voir aussi »- Définir des collections — l’API de collection utilisée dans cette recette
- Relations — les liaisons d’auteurs et de catégories, en détail
- Règles de sécurité (RLS) — publier sans exposer les brouillons