Entities Overview
Entities Overview
Section titled “Entities Overview”Entities are the fundamental building blocks of content in Stelo CMS. They provide a structured, type-safe way to manage all types of content for your client websites.
Core Entity Types
Section titled “Core Entity Types”Stelo CMS organizes content into four main entity types:
🧑💼 Users
Section titled “🧑💼 Users”Administrative users who manage content in the CMS.
- Admins: Full access to all content and settings
- Editors: Content creation and editing permissions
- Viewers: Read-only access for stakeholders
📄 Pages
Section titled “📄 Pages”Static content pages with fixed URLs and purpose.
- About Us: Company information and history
- Contact: Contact forms and information
- Privacy Policy: Legal pages and policies
- Landing Pages: Marketing and campaign pages
📚 Collections
Section titled “📚 Collections”Dynamic content types that can have multiple entries.
- Services: Business offerings and descriptions
- Blog Posts: Articles and news content
- Team Members: Staff profiles and bios
- Testimonials: Client reviews and feedback
- Products: E-commerce catalog items
🌐 Globals
Section titled “🌐 Globals”Site-wide settings and reusable content.
- Header: Navigation menus and branding
- Footer: Links, contact info, and social media
- SEO Settings: Default meta tags and analytics
- Contact Information: Phone, email, address
Entity Architecture
Section titled “Entity Architecture”Database Schema Pattern
Section titled “Database Schema Pattern”All entities follow a consistent schema pattern:
model EntityName { id String @id @default(cuid()) slug String @unique title Json // Localized content content Json // Localized content metadata Json? // SEO and additional data published Boolean @default(false) publishedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
// Relationships authorId String? author User? @relation(fields: [authorId], references: [id])
// Indexes for performance @@index([published]) @@index([slug]) @@index([createdAt])}Localization Structure
Section titled “Localization Structure”Every content field supports multiple locales using JSON columns:
// Example: Multi-language titletitle: { en: "Welcome to Our Website", fr: "Bienvenue sur notre site", es: "Bienvenido a nuestro sitio"}
// Example: Rich content with imagescontent: { en: { body: "<p>Welcome to our company...</p>", excerpt: "Brief description...", featuredImage: "https://storage.com/image-en.jpg" }, fr: { body: "<p>Bienvenue dans notre entreprise...</p>", excerpt: "Brève description...", featuredImage: "https://storage.com/image-fr.jpg" }}Metadata Structure
Section titled “Metadata Structure”SEO and additional metadata follow a standardized format:
metadata: { seo: { title: { en: "Custom SEO Title", fr: "Titre SEO personnalisé" }, description: { en: "Meta description for search engines", fr: "Description méta pour les moteurs de recherche" }, keywords: ["keyword1", "keyword2"], ogImage: "https://storage.com/og-image.jpg" }, custom: { // Entity-specific custom fields featured: true, category: "technology", priority: 1 }}Content Workflow
Section titled “Content Workflow”1. Draft → Review → Published
Section titled “1. Draft → Review → Published”graph LR A[Draft] --> B[Review] B --> C[Published] B --> A C --> B C --> D[Archived]Draft State
Section titled “Draft State”- Content is being created or edited
- Not visible on the frontend
- Can be saved and resumed later
Review State
Section titled “Review State”- Ready for editorial review
- Can be previewed with special URLs
- Awaiting approval or revision
Published State
Section titled “Published State”- Live on the frontend website
- Available via public APIs
- Cached for performance
Archived State
Section titled “Archived State”- No longer active but preserved
- Not visible on frontend
- Accessible in CMS for reference
2. Version Control
Section titled “2. Version Control”Every entity maintains a revision history:
model EntityRevision { id String @id @default(cuid()) entityId String version Int data Json // Full entity data snapshot changedBy String changeLog String? // Summary of changes createdAt DateTime @default(now())
@@unique([entityId, version])}API Structure
Section titled “API Structure”tRPC Router Organization
Section titled “tRPC Router Organization”Each entity type has its own tRPC router:
export const appRouter = router({ // Entity routers users: usersRouter, pages: pagesRouter, collections: collectionsRouter, globals: globalsRouter,
// Utility routers media: mediaRouter, search: searchRouter, analytics: analyticsRouter,});Common Operations
Section titled “Common Operations”All entity routers provide standard CRUD operations:
// Example: Pages routerexport const pagesRouter = router({ // Read operations getAll: publicProcedure.query(async () => { ... }), getById: publicProcedure.input(z.string()).query(async ({ input }) => { ... }), getBySlug: publicProcedure.input(z.string()).query(async ({ input }) => { ... }), getPublished: publicProcedure.query(async () => { ... }),
// Write operations (protected) create: protectedProcedure.input(createPageSchema).mutation(async ({ input }) => { ... }), update: protectedProcedure.input(updatePageSchema).mutation(async ({ input }) => { ... }), delete: protectedProcedure.input(z.string()).mutation(async ({ input }) => { ... }),
// Status operations publish: protectedProcedure.input(z.string()).mutation(async ({ input }) => { ... }), unpublish: protectedProcedure.input(z.string()).mutation(async ({ input }) => { ... }),
// Bulk operations bulkUpdate: adminProcedure.input(bulkUpdateSchema).mutation(async ({ input }) => { ... }), bulkDelete: adminProcedure.input(z.array(z.string())).mutation(async ({ input }) => { ... }),});Validation & Type Safety
Section titled “Validation & Type Safety”Zod Schemas
Section titled “Zod Schemas”Each entity uses Zod schemas for validation:
// Base schema for all entitiesexport const baseEntitySchema = z.object({ id: z.string().cuid().optional(), slug: z.string().min(1).max(100), title: localizedStringSchema, content: localizedRichContentSchema, metadata: entityMetadataSchema.optional(), published: z.boolean().default(false), publishedAt: z.date().optional(),});
// Localized string schemaexport const localizedStringSchema = z.record( z.enum(['en', 'fr', 'es']), // Supported locales z.string().min(1));
// Rich content schema with images and formattingexport const localizedRichContentSchema = z.record( z.enum(['en', 'fr', 'es']), z.object({ body: z.string(), excerpt: z.string().optional(), featuredImage: z.string().url().optional(), gallery: z.array(z.string().url()).optional(), }));TypeScript Types
Section titled “TypeScript Types”Generated TypeScript types ensure type safety:
// Generated from Prisma schemaimport type { Page, Collection, Global, User } from '@prisma/client';
// Custom types for API responsesexport type LocalizedPage = Page & { title: Record<Locale, string>; content: Record<Locale, RichContent>;};
export type PageWithAuthor = Page & { author: Pick<User, 'id' | 'name' | 'email'>;};
export type PublishedPage = Pick<Page, 'id' | 'slug' | 'title' | 'content' | 'publishedAt'>;Permission System
Section titled “Permission System”Role-Based Access Control
Section titled “Role-Based Access Control”Each entity operation respects user roles:
// Permission matrixconst permissions = { users: { read: ['admin', 'editor', 'viewer'], create: ['admin'], update: ['admin'], delete: ['admin'] }, pages: { read: ['admin', 'editor', 'viewer'], create: ['admin', 'editor'], update: ['admin', 'editor'], delete: ['admin'] }, collections: { read: ['admin', 'editor', 'viewer'], create: ['admin', 'editor'], update: ['admin', 'editor'], delete: ['admin', 'editor'] }, globals: { read: ['admin', 'editor', 'viewer'], create: ['admin'], update: ['admin', 'editor'], delete: ['admin'] }};Content Ownership
Section titled “Content Ownership”Editors can only modify content they created:
// Check ownership or admin roleasync function canModifyEntity(userId: string, entityId: string, userRole: string) { if (userRole === 'admin') return true;
const entity = await prisma.page.findUnique({ where: { id: entityId }, select: { authorId: true } });
return entity?.authorId === userId;}Performance Optimization
Section titled “Performance Optimization”Database Indexing
Section titled “Database Indexing”Strategic indexes improve query performance:
-- Common query patternsCREATE INDEX idx_pages_published ON pages(published);CREATE INDEX idx_pages_slug ON pages(slug);CREATE INDEX idx_pages_created_at ON pages(created_at);CREATE INDEX idx_pages_author_published ON pages(author_id, published);
-- Full-text searchCREATE INDEX idx_pages_search ON pages USING gin(to_tsvector('english', title || ' ' || content));Caching Strategy
Section titled “Caching Strategy”API responses are cached based on entity type:
// Cache configurationconst cacheConfig = { pages: { ttl: 3600, // 1 hour for static pages tags: ['pages'] }, collections: { ttl: 1800, // 30 minutes for dynamic content tags: ['collections'] }, globals: { ttl: 86400, // 24 hours for site settings tags: ['globals'] }};Next Steps
Section titled “Next Steps”Explore each entity type in detail:
- Users - User management and roles
- Pages - Static page creation and management
- Collections - Dynamic content types
- Globals - Site-wide settings and content
Each section provides specific implementation details, best practices, and code examples for working with that entity type.