Custom CMS Development for Website Content Management
Custom CMS is built when no existing platform fits business logic — or fits but requires so much customization that building your own is simpler. Typical cases: unique data model, non-standard approval workflows, integration with internal corporate systems, performance requirements at tens of thousands of requests per minute.
CMS Components
Minimum modules for functional CMS:
- Content Types — data structure definitions: fields, types, relations
- Content Editor — interface for creating and editing records
- Media Manager — upload, storage, processing images and files
- User & Roles — authentication, authorization, permissions
- Publishing Workflow — drafts, approval, publication scheduler
- API — REST or GraphQL for frontend and mobile
- Audit Log — who changed what and when
Tech Stack
Admin panel typically uses React or Vue. Backend — Laravel, Node.js, Go, or Django.
Example stack:
Backend API: Laravel 11 + PostgreSQL
Admin SPA: React 18 + TypeScript + TanStack Query
Media CDN: S3-compatible storage + imgproxy
Auth: JWT + refresh tokens
Data Models: Flexibility vs Strictness
Two approaches:
Structured — separate table per content type:
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(255) UNIQUE,
title JSONB NOT NULL, -- {ru: "...", en: "..."}
body JSONB NOT NULL,
author_id BIGINT REFERENCES users(id),
status VARCHAR(32) DEFAULT 'draft',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ
);
Pros: indexes, type safety, JOINs. Cons: schema changes need migrations.
EAV/JSONB — fields in JSON:
CREATE TABLE content_items (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(64), -- 'article', 'product'
data JSONB NOT NULL, -- all fields inside
status VARCHAR(32),
created_at TIMESTAMPTZ
);
Pros: new fields without migrations. Cons: harder to query.
Hybrid approach — common table with key fields (slug, status, author_id) + JSONB — gives best of both worlds.
Content Editor
Choice affects whole UX:
- Lexical (Meta) — flexible, extensible
- TipTap (ProseMirror-based) — good extension ecosystem
- Slate.js — maximum flexibility
- TinyMCE / CKEditor — familiar Word-like interface
Access Rights
RBAC model based on permissions:
$permissions = [
'editor' => ['create:article', 'edit:own:article', 'publish:article'],
'moderator' => ['edit:any:article', 'delete:article'],
'admin' => ['*'],
];
Content Versioning
CREATE TABLE content_revisions (
id BIGSERIAL PRIMARY KEY,
content_id BIGINT REFERENCES content_items(id) ON DELETE CASCADE,
data JSONB NOT NULL,
author_id BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ
);
Last N revisions stored for each record.
Media Manager
Upload via presigned S3 URL:
Client → POST /api/media/presign → presigned URL
Client → PUT presigned_url (S3) → upload
Client → POST /api/media/confirm → success notice
Server → imgproxy or CloudFront → resize on demand
Timeline
Basic CMS: one content type, editor, media, roles, REST API — 3–4 weeks. Full multilingual CMS with multiple types, versioning, workflow, GraphQL, CDN — 2–3 months.







