[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Headless CMS with Live Preview

Build a Headless CMS with Live Preview

Build a content management system where marketers see changes instantly in a live preview — with version history, approval workflows, and scheduled publishing that eliminated the "deploy to see your changes" bottleneck.

#web-framework#edge#cloudflare#bun#deno
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
>

nextjs

v1.0.0

Assists with building production-grade React applications using Next.js. Use when working with the App Router, Server Components, Server Actions, Middleware, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, react framework, ssr, isr.

93/100 quality
1.16× impact
SAFE
View skill
$

The Problem

A marketing team manages 500+ pages, blog posts, and landing pages. Every content change requires an engineer to deploy: marketers write in a Google Doc, email it to engineering, an engineer copies it to the codebase, pushes to staging for review, then deploys. Turnaround: 2-3 days for a typo fix. The marketing team publishes 15 pieces of content per week but can only get 5 deployed because engineering is the bottleneck. During product launches, the CMO begs engineers to "just push one more thing" at midnight.

Step 1: Content Model and API

typescript
// src/cms/content-model.ts
import { z } from 'zod';
import { Pool } from 'pg';

const db = new Pool({ connectionString: process.env.DATABASE_URL });

export const ContentEntry = z.object({
  id: z.string().uuid(),
  contentType: z.string(),          // 'page', 'blog_post', 'landing_page'
  slug: z.string(),
  locale: z.string().default('en'),
  status: z.enum(['draft', 'review', 'approved', 'published', 'archived']),
  version: z.number().int(),
  fields: z.record(z.string(), z.unknown()), // flexible field data
  metadata: z.object({
    title: z.string(),
    description: z.string().optional(),
    ogImage: z.string().url().optional(),
    publishedAt: z.string().datetime().optional(),
    scheduledAt: z.string().datetime().optional(),
    author: z.string(),
  }),
  createdBy: z.string(),
  updatedBy: z.string(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

// Content type definitions (schema for each content type)
export const contentTypes: Record<string, z.ZodObject<any>> = {
  blog_post: z.object({
    title: z.string().min(1).max(200),
    subtitle: z.string().max(300).optional(),
    body: z.string(),               // Rich text / MDX
    heroImage: z.string().url().optional(),
    category: z.string(),
    tags: z.array(z.string()),
    readTimeMinutes: z.number().int().optional(),
  }),
  landing_page: z.object({
    headline: z.string().min(1).max(100),
    subheadline: z.string().max(200),
    heroImage: z.string().url(),
    ctaText: z.string().max(50),
    ctaUrl: z.string().url(),
    sections: z.array(z.object({
      type: z.enum(['text', 'image', 'testimonial', 'features', 'pricing', 'cta']),
      content: z.record(z.string(), z.unknown()),
    })),
  }),
};

// Save with version history
export async function saveContent(
  id: string,
  fields: Record<string, unknown>,
  metadata: any,
  userId: string
): Promise<{ version: number }> {
  // Get current version
  const { rows } = await db.query(
    'SELECT version FROM content WHERE id = $1 ORDER BY version DESC LIMIT 1',
    [id]
  );
  const newVersion = (rows[0]?.version ?? 0) + 1;

  // Save new version (old versions kept for history)
  await db.query(`
    INSERT INTO content_versions (content_id, version, fields, metadata, created_by, created_at)
    VALUES ($1, $2, $3, $4, $5, NOW())
  `, [id, newVersion, JSON.stringify(fields), JSON.stringify(metadata), userId]);

  // Update current
  await db.query(`
    UPDATE content SET fields = $1, metadata = $2, version = $3, updated_by = $4, updated_at = NOW()
    WHERE id = $5
  `, [JSON.stringify(fields), JSON.stringify(metadata), newVersion, userId, id]);

  return { version: newVersion };
}

// Version diff for review
export async function getVersionDiff(contentId: string, v1: number, v2: number): Promise<{
  fieldChanges: Array<{ field: string; before: unknown; after: unknown }>;
}> {
  const { rows } = await db.query(
    `SELECT version, fields FROM content_versions WHERE content_id = $1 AND version IN ($2, $3)`,
    [contentId, v1, v2]
  );

  const old = rows.find(r => r.version === v1)?.fields ?? {};
  const current = rows.find(r => r.version === v2)?.fields ?? {};

  const changes: any[] = [];
  const allKeys = new Set([...Object.keys(old), ...Object.keys(current)]);
  for (const key of allKeys) {
    if (JSON.stringify(old[key]) !== JSON.stringify(current[key])) {
      changes.push({ field: key, before: old[key], after: current[key] });
    }
  }

  return { fieldChanges: changes };
}

Step 2: Live Preview API

typescript
// src/cms/preview.ts
import { Hono } from 'hono';
import { Pool } from 'pg';
import { Redis } from 'ioredis';

const app = new Hono();
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL!);

// Live preview: returns draft content for preview iframe
app.get('/v1/preview/:slug', async (c) => {
  const slug = c.req.param('slug');
  const previewToken = c.req.query('token');

  // Validate preview token (short-lived, per-session)
  const userId = await redis.get(`preview:token:${previewToken}`);
  if (!userId) return c.json({ error: 'Invalid preview token' }, 401);

  // Return draft content (not published)
  const { rows } = await db.query(`
    SELECT * FROM content WHERE slug = $1
    ORDER BY version DESC LIMIT 1
  `, [slug]);

  if (!rows[0]) return c.json({ error: 'Content not found' }, 404);

  return c.json({
    ...rows[0],
    fields: rows[0].fields,
    _preview: true,
    _version: rows[0].version,
  });
});

// Real-time preview updates via Server-Sent Events
app.get('/v1/preview/:slug/stream', async (c) => {
  const slug = c.req.param('slug');

  return new Response(
    new ReadableStream({
      start(controller) {
        const sub = redis.duplicate();
        sub.subscribe(`content:update:${slug}`);
        sub.on('message', (channel, message) => {
          controller.enqueue(`data: ${message}\n\n`);
        });
      },
    }),
    { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' } }
  );
});

// Notify preview when content changes
export async function notifyPreview(slug: string, fields: any): Promise<void> {
  await redis.publish(`content:update:${slug}`, JSON.stringify(fields));
}

export default app;

Step 3: Publishing Workflow

typescript
// src/cms/workflow.ts
import { Pool } from 'pg';

const db = new Pool({ connectionString: process.env.DATABASE_URL });

export async function submitForReview(contentId: string, userId: string): Promise<void> {
  await db.query(`UPDATE content SET status = 'review', updated_by = $1 WHERE id = $2`, [userId, contentId]);
}

export async function approve(contentId: string, approverId: string): Promise<void> {
  await db.query(`UPDATE content SET status = 'approved', updated_by = $1 WHERE id = $2`, [approverId, contentId]);
}

export async function publish(contentId: string, userId: string): Promise<void> {
  await db.query(`
    UPDATE content SET status = 'published', metadata = metadata || '{"publishedAt": "${new Date().toISOString()}"}'::jsonb, updated_by = $1
    WHERE id = $2
  `, [userId, contentId]);

  // Purge CDN cache
  await fetch(process.env.CDN_PURGE_URL!, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.CDN_TOKEN}` },
    body: JSON.stringify({ contentId }),
  }).catch(() => {});
}

// Scheduled publishing
export async function publishScheduled(): Promise<number> {
  const { rowCount } = await db.query(`
    UPDATE content SET status = 'published'
    WHERE status = 'approved'
      AND (metadata->>'scheduledAt')::timestamptz <= NOW()
  `);
  return rowCount ?? 0;
}

Results

  • Content turnaround: 15 minutes (was 2-3 days)
  • Engineering bottleneck: eliminated — marketers publish independently
  • Content output: 15 pieces/week actually published (was 5 due to bottleneck)
  • Live preview: marketers see exactly what users will see, no surprises
  • Midnight deploys: zero — scheduled publishing handles launches
  • Version history: every change tracked, easy rollback
  • Workflow: draft → review → approve → publish, with clear accountability