[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Rich Text Editor with Collaboration

Build a Rich Text Editor with Collaboration

Build a Notion-style rich text editor with block-based content, slash commands, inline formatting, image embeds, @mentions, and real-time collaboration using Tiptap and Y.js.

#nextjs#react#full-stack#ssr#server-components
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 92/100·All SAFE
>

typescript

v

Not yet scored
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
>

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
>

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
>

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
$

The Problem

Olga leads product at a 30-person knowledge base company. Their editor is a plain <textarea> with markdown preview. Users want WYSIWYG editing — bold, headers, images, code blocks, tables — without learning markdown. Two people editing the same document overwrite each other. They lose content when the browser crashes mid-edit. They need a modern editor with rich formatting, slash commands for power users, autosave, and real-time collaboration where multiple editors see each other's cursors.

Step 1: Build the Collaborative Editor

typescript
// src/editor/collaborative-editor.tsx — Tiptap + Y.js collaborative rich text editor
"use client";

import { useEditor, EditorContent, BubbleMenu, FloatingMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Link from "@tiptap/extension-link";
import Placeholder from "@tiptap/extension-placeholder";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import Mention from "@tiptap/extension-mention";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
import { useState, useEffect, useCallback, useRef } from "react";

interface EditorProps {
  documentId: string;
  userId: string;
  userName: string;
  userColor: string;
  onSave?: (content: any) => void;
}

// Slash command menu items
const SLASH_COMMANDS = [
  { title: "Heading 1", command: "heading", attrs: { level: 1 }, icon: "H1" },
  { title: "Heading 2", command: "heading", attrs: { level: 2 }, icon: "H2" },
  { title: "Heading 3", command: "heading", attrs: { level: 3 }, icon: "H3" },
  { title: "Bullet List", command: "bulletList", icon: "•" },
  { title: "Numbered List", command: "orderedList", icon: "1." },
  { title: "Task List", command: "taskList", icon: "☑" },
  { title: "Code Block", command: "codeBlock", icon: "</>" },
  { title: "Blockquote", command: "blockquote", icon: "❝" },
  { title: "Table", command: "table", icon: "▦" },
  { title: "Image", command: "image", icon: "🖼" },
  { title: "Divider", command: "horizontalRule", icon: "—" },
];

export function CollaborativeEditor({ documentId, userId, userName, userColor, onSave }: EditorProps) {
  const [showSlashMenu, setShowSlashMenu] = useState(false);
  const [slashFilter, setSlashFilter] = useState("");
  const [saving, setSaving] = useState(false);
  const [lastSaved, setLastSaved] = useState<Date | null>(null);

  // Y.js document for collaboration
  const ydoc = useRef(new Y.Doc());

  // Connect to collaboration server
  const provider = useRef(
    new HocuspocusProvider({
      url: process.env.NEXT_PUBLIC_COLLAB_URL || "ws://localhost:8080",
      name: documentId,
      document: ydoc.current,
      token: userId,
    })
  );

  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        history: false,          // Y.js handles undo/redo
      }),
      Image.configure({
        allowBase64: false,
        HTMLAttributes: { class: "editor-image" },
      }),
      Link.configure({
        openOnClick: false,
        HTMLAttributes: { class: "editor-link" },
      }),
      Placeholder.configure({
        placeholder: 'Type "/" for commands...',
      }),
      TaskList,
      TaskItem.configure({ nested: true }),
      CodeBlockLowlight,
      Table.configure({ resizable: true }),
      TableRow, TableCell, TableHeader,
      Mention.configure({
        HTMLAttributes: { class: "mention" },
        suggestion: {
          items: ({ query }: any) => searchUsers(query),
          render: () => ({
            onStart: (props: any) => { /* show mention popup */ },
            onUpdate: (props: any) => { /* update popup */ },
            onExit: () => { /* hide popup */ },
          }),
        },
      }),
      // Real-time collaboration
      Collaboration.configure({
        document: ydoc.current,
      }),
      CollaborationCursor.configure({
        provider: provider.current,
        user: { name: userName, color: userColor },
      }),
    ],
    onUpdate: ({ editor }) => {
      // Debounced autosave
      debouncedSave(editor.getJSON());
    },
  });

  // Autosave every 3 seconds of inactivity
  const debouncedSave = useCallback(
    debounce(async (content: any) => {
      setSaving(true);
      try {
        await fetch(`/api/documents/${documentId}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ content }),
        });
        setLastSaved(new Date());
      } finally {
        setSaving(false);
      }
    }, 3000),
    [documentId]
  );

  // Handle slash command execution
  const executeSlashCommand = useCallback((command: typeof SLASH_COMMANDS[0]) => {
    if (!editor) return;

    // Delete the "/" trigger
    editor.chain().focus().deleteRange({
      from: editor.state.selection.from - slashFilter.length - 1,
      to: editor.state.selection.from,
    }).run();

    switch (command.command) {
      case "heading":
        editor.chain().focus().toggleHeading(command.attrs as any).run();
        break;
      case "bulletList":
        editor.chain().focus().toggleBulletList().run();
        break;
      case "orderedList":
        editor.chain().focus().toggleOrderedList().run();
        break;
      case "taskList":
        editor.chain().focus().toggleTaskList().run();
        break;
      case "codeBlock":
        editor.chain().focus().toggleCodeBlock().run();
        break;
      case "blockquote":
        editor.chain().focus().toggleBlockquote().run();
        break;
      case "table":
        editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
        break;
      case "image":
        const url = prompt("Image URL:");
        if (url) editor.chain().focus().setImage({ src: url }).run();
        break;
      case "horizontalRule":
        editor.chain().focus().setHorizontalRule().run();
        break;
    }

    setShowSlashMenu(false);
  }, [editor, slashFilter]);

  // Image upload via drag & drop
  const handleDrop = useCallback(async (event: DragEvent) => {
    if (!editor) return;
    const files = event.dataTransfer?.files;
    if (!files?.length) return;

    event.preventDefault();

    for (const file of Array.from(files)) {
      if (!file.type.startsWith("image/")) continue;

      const formData = new FormData();
      formData.append("file", file);

      const res = await fetch("/api/upload", { method: "POST", body: formData });
      const { url } = await res.json();

      editor.chain().focus().setImage({ src: url }).run();
    }
  }, [editor]);

  const filteredCommands = SLASH_COMMANDS.filter((cmd) =>
    cmd.title.toLowerCase().includes(slashFilter.toLowerCase())
  );

  if (!editor) return null;

  return {
    editor,
    showSlashMenu,
    filteredCommands,
    saving,
    lastSaved,
    executeSlashCommand,
    handleDrop,
  };
}

async function searchUsers(query: string): Promise<Array<{ id: string; label: string }>> {
  const res = await fetch(`/api/users/search?q=${encodeURIComponent(query)}&limit=5`);
  const users = await res.json();
  return users.map((u: any) => ({ id: u.id, label: u.name }));
}

function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T {
  let timer: any;
  return ((...args: any[]) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }) as T;
}

Results

  • Markdown barrier eliminated — WYSIWYG editing means non-technical team members create content without learning syntax; adoption across the company tripled
  • Slash commands for power users — type "/" to see all formatting options; experienced users insert tables, code blocks, and images without touching the toolbar
  • No more content loss — autosave every 3 seconds means browser crashes lose at most 3 seconds of work; Y.js preserves document state across reconnections
  • Real-time collaboration — multiple editors see each other's cursors and changes instantly; no more "someone else was editing this document" conflicts
  • @mentions drive engagement — mentioning a teammate in a document sends them a notification; document discussions happen in context instead of Slack