Aller au contenu

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
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"
}
});
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"
}
});
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"
}
});
  1. Enregistrez les trois fichiers dans config/collections/. Ajoutez-les au tableau collections dans config/collections/index.ts pour choisir où ils apparaissent dans la navigation — chaque fichier du répertoire est chargé dans tous les cas.
  2. Avec rebase dev en 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 de rebase dev, contre votre propre PostgreSQL, exécutez rebase schema generate puis rebase 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

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 included
const { 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
}