Files
citpl_cms_react_express_pos…/docs/superpowers/specs/2026-07-09-dynamic-cms-design.md
T
sandhiya-hepl 4b8af23f5f first commit
2026-07-14 15:20:26 +05:30

11 KiB

Dynamic CMS with Admin Panel — Design Spec

Date: 2026-07-09
Status: Approved (architecture)
Goal: Convert the static CITPL marketing site into a dynamic site with instant content updates via a password-protected admin panel hosted on a VPS.


Requirements

Requirement Decision
Content updates Instant — no rebuild required
Data model Latest data only (no versioning/drafts)
Hosting Self-hosted VPS
Admin users Single admin login
Editable content Text, links, and image/video uploads
Public site Existing React SPA, same visual design

Architecture

┌──────────────────────────────────────────────────────────┐
│                     VPS (Ubuntu + Nginx)                  │
│                                                           │
│  citpl.com/              citpl.com/admin                  │
│       │                         │                         │
│       ▼                         ▼                         │
│  React SPA (dist/)         Admin SPA (dist/admin/)        │
│       │                         │                         │
│       └────────────┬────────────┘                         │
│                    ▼                                      │
│           Express API  :3001                            │
│    GET  /api/content          (public)                  │
│    POST /api/auth/login       (public)                  │
│    PUT  /api/content/:section (auth required)           │
│    POST /api/upload           (auth required)           │
│                    │                                      │
│         ┌──────────┴──────────┐                           │
│         ▼                     ▼                           │
│   SQLite (content.db)    /var/www/citpl/uploads/          │
│   - admin user           images, videos, logos            │
│   - content sections                                      │
└──────────────────────────────────────────────────────────┘

Tech Stack

Layer Technology
Public frontend React 19 + Vite 8 (existing)
Admin frontend React 19 + Vite 8 (new entry point)
API Express.js 4
Database better-sqlite3 (SQLite)
Auth bcrypt + jsonwebtoken (JWT, 24h expiry)
File uploads multer (max 50MB, images + video)
Process manager PM2
Reverse proxy Nginx

Content Schema

All site content is stored as JSON documents in a single content table, keyed by section ID. On first boot, the API seeds the database from current hardcoded values.

Sections

type SiteContent = {
  site: {
    title: string
    logo: string          // URL path e.g. /uploads/logo.svg
    copyright: string
  }

  navigation: Array<{
    label: string
    href: string
    type: 'hash' | 'modal'
  }>

  hero: {
    video: string
    badge: string
    headline: string
    subheadline: string
  }

  metrics: Array<{
    value: string
    label: string
  }>

  about: {
    heading: string
    badgeImage: string
    badgeAlt: string
    backgroundImage: string
  }

  partners: {
    heading: string
    subheading: string
    body: string
    logos: Array<{ name: string; image: string }>
  }

  certifications: {
    heading: string
    items: Array<{ image: string; alt: string }>
  }

  services: {
    eyebrow: string
    title: string
    intro: string
    items: Array<{
      id: string
      number: string
      title: string
      boldStatement: string
      description: string
      capabilities: string[]
      cta: string
      glowColor: string
      borderColor: string
    }>
  }

  whyUs: {
    eyebrow: string
    title: string
    cards: Array<{ title: string; description: string }>
    images: Array<{ src: string; alt: string }>
  }

  gallery: {
    eyebrow: string
    title: string
    subtitle: string
    items: Array<{
      id: number
      title: string
      category: string
      excerpt: string
      image: string
      readTime: string
    }>
  }

  products: {
    eyebrow: string
    title: string
    subtitle: string
    items: Array<{
      name: string
      description: string
      image: string
      stats: Array<{ value: string; label: string }>
    }>
  }

  insights: {
    title: string
    subtitle: string
    items: Array<{
      category: string
      title: string
      date: string
      image: string
      excerpt: string
    }>
  }

  footer: {
    cta: {
      headline: string
      body: string
      image: string
      primaryButton: string
      secondaryButton: string
    }
    quickLinks: Array<{ label: string; href: string }>
    productLinks: Array<{ label: string; href: string }>
    social: Array<{ platform: string; url: string }>
    legal: Array<{ label: string; href: string }>
  }
}

API Design

Public Endpoints

Method Path Description
GET /api/content Returns full SiteContent object
GET /api/health Health check for PM2/Nginx

Auth Endpoints

Method Path Body Response
POST /api/auth/login { username, password } { token, expiresAt }
GET /api/auth/me Bearer token { username }

Protected Endpoints (require Authorization: Bearer <token>)

Method Path Body Description
PUT /api/content/:section Section JSON Update one section (e.g. hero, services)
PUT /api/content Full SiteContent Bulk update all sections
POST /api/upload multipart/form-data field file Upload image/video, returns { url }
DELETE /api/upload/:filename Remove uploaded file

Error Responses

{ "error": "Human-readable message", "code": "UNAUTHORIZED" }

HTTP codes: 400 (validation), 401 (auth), 404 (not found), 413 (file too large), 500 (server).


Admin Panel

URL: /admin
Login: Single username/password set via environment variable on first deploy.

Pages

Page Edits
Dashboard Quick links to all sections, last-updated timestamp
Hero & Metrics Headline, video upload, badge text, stat values
About & Partners Headings, partner logos (upload + reorder)
Certifications Upload/replace badge images
Services CRUD for all 6 service cards
Why Us 4 value cards + 3 image uploads
Gallery CRUD gallery items with image upload
Products 3 product cards with stats and screenshots
Insights CRUD blog cards with image upload
Footer & Links CTA text, social URLs, quick links
Settings Change admin password

Admin UX

  • Sidebar navigation matching section list above
  • Inline form fields with save button per section
  • Image fields show current preview + upload/replace button
  • Toast notifications on save success/failure
  • Redirect to /admin/login if token expired

Frontend Changes (Public Site)

  1. Content hookuseContent() fetches /api/content on mount, shows loading skeleton, caches in React context
  2. Refactor App.jsx — Replace hardcoded strings/arrays with content.hero, content.services, etc.
  3. Refactor FeaturedGallery.jsx — Accept content.gallery as prop instead of inline data
  4. Image URLs — All image paths become absolute URLs from API (/uploads/... or external)
  5. Fallback — If API unreachable, show cached content or graceful error banner (not blank page)

New Files

server/
  index.js          # Express app entry
  db.js             # SQLite setup + seed
  auth.js           # JWT middleware
  routes/
    content.js
    auth.js
    upload.js
  seed/
    default-content.json   # Current site content extracted from App.jsx

src/
  context/ContentContext.jsx
  hooks/useContent.js
  admin/
    main.jsx
    App.jsx
    pages/
      Login.jsx
      Dashboard.jsx
      HeroEditor.jsx
      ServicesEditor.jsx
      ... (one per section)
    components/
      ImageUpload.jsx
      SectionForm.jsx
      AdminLayout.jsx

Security

Concern Mitigation
Brute-force login Rate limit /api/auth/login (5 attempts/min per IP)
JWT theft HttpOnly not possible cross-origin; use short 24h expiry + secure flag in production
File uploads Whitelist MIME types: image/*, video/mp4; max 50MB; sanitize filenames
SQL injection Parameterized queries only (better-sqlite3 prepared statements)
Admin password bcrypt hash (cost 12); initial password via ADMIN_PASSWORD env var
CORS Restrict to production domain only

Deployment (VPS)

Environment Variables

PORT=3001
JWT_SECRET=<random-64-char-string>
ADMIN_USERNAME=admin
ADMIN_PASSWORD=<set-on-first-deploy>
UPLOAD_DIR=/var/www/citpl/uploads
DB_PATH=/var/www/citpl/data/content.db
NODE_ENV=production
CORS_ORIGIN=https://citpl.com

Nginx Config (summary)

server {
  listen 80;
  server_name citpl.com;

  # Public site
  location / {
    root /var/www/citpl/dist;
    try_files $uri $uri/ /index.html;
  }

  # Admin panel
  location /admin {
    alias /var/www/citpl/dist-admin;
    try_files $uri $uri/ /admin/index.html;
  }

  # API
  location /api {
    proxy_pass http://127.0.0.1:3001;
  }

  # Uploaded media
  location /uploads {
    alias /var/www/citpl/uploads;
    expires 30d;
  }
}

PM2

pm2 start server/index.js --name citpl-api
pm2 save

Deploy Script

npm run build          # public site → dist/
npm run build:admin    # admin panel → dist-admin/
rsync dist/ dist-admin/ server/ → VPS
pm2 restart citpl-api

Out of Scope (v1)

  • Multiple admin users / roles
  • Content versioning or drafts
  • Careers / Strategy Call form backend (keep EmailJS as-is)
  • Blog detail pages (insights remain cards only)
  • SSL setup (assumed handled separately via Certbot)
  • CI/CD pipeline (manual deploy script provided)

Success Criteria

  1. Admin can log in at /admin with username/password
  2. Admin can edit any section and save — changes appear on public site within seconds (page refresh)
  3. Admin can upload/replace images and hero video
  4. Public site loads all content from API with no hardcoded copy remaining
  5. API + site run stably on a 1GB RAM VPS via PM2