Ricetta: Blog CMS
Panoramica
Sezione intitolata “Panoramica”Crea un backend per blog con:
- Articoli con contenuti in markdown e immagini di copertina
- Autori con profili
- Categorie con una relazione molti-a-molti
Collezioni
Sezione intitolata “Collezioni”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" }});Categorie
Sezione intitolata “Categorie”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" }});Articoli
Sezione intitolata “Articoli”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" }});Configurazione
Sezione intitolata “Configurazione”- Salva i tre file in
config/collections/. Aggiungili all’arraycollectionsinconfig/collections/index.tsper scegliere dove compaiono nella navigazione — ogni file nella cartella viene comunque caricato. - Con
rebase devin esecuzione è tutto: salvare rigenera lo schema, riavvia il backend, e l’avvio crea le tabelle, la tabella di giunzione e le relative policy di row-level security. Fuori darebase dev, contro il tuo stesso PostgreSQL, eseguirebase schema generatee poirebase db push.
Ora disponi di un CMS per blog completamente funzionante con:
- Gestione degli autori con caricamento degli avatar
- Assegnazione dei tag tramite categorie con relazioni molti-a-molti
- Modifica dei contenuti in markdown
- Flusso di lavoro Bozza → Revisione → Pubblicato
- Slug URL generati automaticamente
- Criteri RLS che limitano gli autori ai propri articoli
- Audit trail completo tramite la cronologia delle entità
Esecuzione di query dall’SDK
Sezione intitolata “Esecuzione di query dall’SDK”Usa l’SDK tipizzato per recuperare gli articoli con le relative relazioni:
// 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}Risorse correlate
Sezione intitolata “Risorse correlate”- Definizione delle collezioni — l’API delle collezioni utilizzata in questa guida
- Relazioni — i collegamenti completi tra autori e tag
- Regole di sicurezza (RLS) — pubblicare senza esporre le bozze