Feature Spec: Dev-Mode WYSIWYG Content Editor

By Claude Sonnet 4.5*πŸ€– AI-generatedΒ·Β  January 27, 2026
docs
🏷️ Tags:feature-specdev-toolscontenteditor

A development-only inline content editor for markdown files that enables quick content updates without manually editing files in an IDE.

Feature Spec: Dev-Mode WYSIWYG Content Editor

Overview

A development-only inline content editor that allows direct editing of markdown files through the browser interface. This feature enables quick content updates without manually editing files in an IDE, significantly improving content workflow efficiency.


Problem Statement

Current Workflow Pain Points

With 30+ content pages, the current editing workflow is inefficient:

  1. Open IDE
  2. Navigate to correct file in /src/pages/
  3. Make content changes
  4. Save file
  5. Wait for hot reload
  6. Review changes
  7. Commit to git

For simple edits (typo fixes, content updates, metadata changes), this is 5+ steps and 2-3 minutes per edit.

At scale (30+ pages, frequent updates), this becomes a major bottleneck.

Success Criteria

After implementation:


Architecture

System Components

.txt
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Browser UI                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Edit Button (dev only)                      β”‚  β”‚
β”‚  β”‚  Content Editable Area                       β”‚  β”‚
β”‚  β”‚  Metadata Form                               β”‚  β”‚
β”‚  β”‚  Save/Cancel Controls                        β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β”‚ POST /api/dev/save-content
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Next.js API Route                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  1. Verify dev environment                   β”‚  β”‚
β”‚  β”‚  2. Validate filepath                        β”‚  β”‚
β”‚  β”‚  3. Parse/update frontmatter                 β”‚  β”‚
β”‚  β”‚  4. Write to filesystem                      β”‚  β”‚
β”‚  β”‚  5. Return success/error                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β”‚ fs.writeFileSync()
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Filesystem                              β”‚
β”‚  /src/pages/posts/my-post.md                        β”‚
β”‚  /src/pages/docs/my-doc.md                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Technology Stack


Detailed Implementation Plan

Phase 1: Edit Mode Toggle

Goal: Add a floating edit button that toggles edit mode on/off.

UI Components:

.tsx
// EditModeButton.tsx
interface EditModeButtonProps {
  isEditMode: boolean;
  onToggle: () => void;
}

// Renders:
// - Floating button in top-right corner
// - Only visible when process.env.NODE_ENV === 'development'
// - Shows "Edit" when not in edit mode
// - Shows "Cancel" when in edit mode

Styling:

State Management:

.tsx
const [isEditMode, setIsEditMode] = useState(false);
const [hasChanges, setHasChanges] = useState(false);

// Warn on exit if unsaved changes
useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (hasChanges) {
      e.preventDefault();
      e.returnValue = '';
    }
  };
  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasChanges]);

Implementation Steps:

  1. Create EditModeButton component
  2. Add to layout/page wrapper (check NODE_ENV)
  3. Wire up toggle state
  4. Style appropriately

Phase 2: Content Editing

Goal: Make page content editable when in edit mode.

Approach Options:

Option A: contentEditable (Simple)

Option B: TipTap (Recommended)

Option C: Slate

Recommended: Start with contentEditable, upgrade to TipTap if needed.

Implementation:

.tsx
// ContentEditor.tsx
interface ContentEditorProps {
  initialContent: string;
  isEditMode: boolean;
  onChange: (content: string) => void;
}

const ContentEditor: React.FC<ContentEditorProps> = ({
  initialContent,
  isEditMode,
  onChange,
}) => {
  const contentRef = useRef<HTMLDivElement>(null);

  const handleInput = () => {
    if (contentRef.current) {
      const newContent = contentRef.current.innerText;
      onChange(newContent);
    }
  };

  return (
    <div
      ref={contentRef}
      contentEditable={isEditMode}
      onInput={handleInput}
      suppressContentEditableWarning
      style={{
        outline: isEditMode ? '2px solid orange' : 'none',
        padding: isEditMode ? '10px' : '0',
      }}
      dangerouslySetInnerHTML={{ __html: initialContent }}
    />
  );
};

Visual Indicators:

Time Estimate: 1-2 hours


Phase 3: Save API Endpoint

Goal: Create API route to save edited content back to markdown files.

Endpoint: POST /api/dev/save-content

Request Body:

.ts
interface SaveContentRequest {
  filepath: string;          // e.g., "/src/pages/posts/my-post.md"
  content: string;           // Updated markdown content
  metadata?: {               // Optional frontmatter updates
    title?: string;
    date?: string;
    tags?: string[];
    [key: string]: any;
  };
}

Response:

.ts
interface SaveContentResponse {
  success: boolean;
  message: string;
  filepath?: string;
  error?: string;
}

Implementation:

.ts
// /app/api/dev/save-content/route.ts
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

export async function POST(request: NextRequest) {
  // 1. Verify dev environment
  if (process.env.NODE_ENV !== 'development') {
    return NextResponse.json(
      { success: false, error: 'This endpoint is only available in development' },
      { status: 403 }
    );
  }

  try {
    const { filepath, content, metadata } = await request.json();

    // 2. Validate filepath
    if (!filepath || typeof filepath !== 'string') {
      return NextResponse.json(
        { success: false, error: 'Invalid filepath' },
        { status: 400 }
      );
    }

    // 3. Ensure filepath is within allowed directories
    const allowedDirs = ['/src/pages/posts', '/src/pages/docs'];
    const isAllowed = allowedDirs.some(dir => filepath.startsWith(dir));
    
    if (!isAllowed) {
      return NextResponse.json(
        { success: false, error: 'Filepath not in allowed directories' },
        { status: 403 }
      );
    }

    // 4. Resolve absolute path
    const absolutePath = path.join(process.cwd(), filepath);

    // 5. Read existing file to preserve frontmatter
    const existingContent = fs.readFileSync(absolutePath, 'utf-8');
    const { data: existingMetadata } = matter(existingContent);

    // 6. Merge metadata
    const updatedMetadata = { ...existingMetadata, ...metadata };

    // 7. Rebuild file with frontmatter
    const newFileContent = matter.stringify(content, updatedMetadata);

    // 8. Write to filesystem
    fs.writeFileSync(absolutePath, newFileContent, 'utf-8');

    return NextResponse.json({
      success: true,
      message: 'Content saved successfully',
      filepath: filepath,
    });

  } catch (error) {
    console.error('Error saving content:', error);
    return NextResponse.json(
      { success: false, error: error.message },
      { status: 500 }
    );
  }
}

Security Considerations:

Error Handling:

Time Estimate: 1-2 hours


Phase 4: Metadata Editing

Goal: Allow editing of frontmatter fields (title, date, tags, etc.)

UI Component:

.tsx
// MetadataEditor.tsx
interface MetadataEditorProps {
  metadata: Record<string, any>;
  onChange: (metadata: Record<string, any>) => void;
  isEditMode: boolean;
}

const MetadataEditor: React.FC<MetadataEditorProps> = ({
  metadata,
  onChange,
  isEditMode,
}) => {
  const [localMetadata, setLocalMetadata] = useState(metadata);

  const handleFieldChange = (key: string, value: any) => {
    const updated = { ...localMetadata, [key]: value };
    setLocalMetadata(updated);
    onChange(updated);
  };

  if (!isEditMode) {
    return (
      <div className="metadata-display">
        <h1>{metadata.title}</h1>
        <p>{metadata.date}</p>
        <div>{metadata.tags?.join(', ')}</div>
      </div>
    );
  }

  return (
    <div className="metadata-editor">
      <label>
        Title:
        <input
          type="text"
          value={localMetadata.title || ''}
          onChange={(e) => handleFieldChange('title', e.target.value)}
        />
      </label>

      <label>
        Date:
        <input
          type="date"
          value={localMetadata.date || ''}
          onChange={(e) => handleFieldChange('date', e.target.value)}
        />
      </label>

      <label>
        Tags (comma-separated):
        <input
          type="text"
          value={localMetadata.tags?.join(', ') || ''}
          onChange={(e) => 
            handleFieldChange('tags', e.target.value.split(',').map(t => t.trim()))
          }
        />
      </label>

      {/* Add more fields as needed */}
    </div>
  );
};

Features:

Time Estimate: 1-2 hours


Phase 5: Save Flow Integration

Goal: Wire everything together into a smooth save workflow.

User Flow:

  1. User clicks "Edit" button
  2. Content and metadata become editable
  3. User makes changes
  4. "Save" and "Cancel" buttons appear
  5. User clicks "Save"
  6. API request sent
  7. Success/error message displayed
  8. Page content updates (or reverts on error)
  9. Edit mode disabled

State Machine:

.ts
type EditorState = 'viewing' | 'editing' | 'saving' | 'error';

const [editorState, setEditorState] = useState<EditorState>('viewing');
const [editedContent, setEditedContent] = useState('');
const [editedMetadata, setEditedMetadata] = useState({});
const [saveError, setSaveError] = useState<string | null>(null);

const handleSave = async () => {
  setEditorState('saving');
  setSaveError(null);

  try {
    const response = await fetch('/api/dev/save-content', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        filepath: currentFilePath,
        content: editedContent,
        metadata: editedMetadata,
      }),
    });

    const result = await response.json();

    if (result.success) {
      setEditorState('viewing');
      // Optionally refresh page or update content in place
      window.location.reload(); // Simple approach
    } else {
      setEditorState('error');
      setSaveError(result.error || 'Save failed');
    }
  } catch (error) {
    setEditorState('error');
    setSaveError(error.message);
  }
};

const handleCancel = () => {
  if (hasChanges) {
    const confirmed = window.confirm('Discard unsaved changes?');
    if (!confirmed) return;
  }
  setEditorState('viewing');
  setEditedContent(originalContent);
  setEditedMetadata(originalMetadata);
};

UI States:

Viewing State:

Editing State:

Saving State:

Error State:

Time Estimate: 1-2 hours


File Detection & Path Resolution

Challenge: How does the editor know which file it's editing?

Solution: Inject filepath into page props during build.

.ts
// In your page generation logic
export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map(post => ({
    slug: post.slug,
    // Add filepath to params
    _filepath: post.filepath, // e.g., "/src/pages/posts/my-post.md"
  }));
}

// In your page component
export default function PostPage({ params }) {
  const filepath = params._filepath;
  
  return (
    <DevEditor filepath={filepath}>
      {/* Page content */}
    </DevEditor>
  );
}

Alternative: Use pattern matching

.ts
// If current URL is /posts/my-post
// Assume filepath is /src/pages/posts/my-post.md
const filepath = `/src/pages${pathname}.md`;

Edge Cases & Error Handling

File System Errors

Case: File doesn't exist

Case: File is read-only

Case: Invalid markdown syntax

Concurrent Editing

Case: File modified externally while editing

Large Files

Case: File > 1MB

Special Characters

Case: Frontmatter contains special YAML chars


Future Enhancements

Phase 6: TSX File Editing (Future)

Challenge: TSX files require code editor, not contentEditable

Approach:

Complexity: High (3-5 hours minimum)

Phase 7: Image Upload

Goal: Drag-and-drop images directly into content

Implementation:

Phase 8: Version History

Goal: Track and restore previous versions

Implementation:

Phase 9: Multi-File Edit

Goal: Edit multiple pages at once

Implementation:


Testing Plan

Manual Testing Checklist

Automated Testing (Future)

.ts
describe('DevEditor', () => {
  it('should only render in development', () => {});
  it('should toggle edit mode', () => {});
  it('should capture content changes', () => {});
  it('should save to filesystem', () => {});
  it('should preserve frontmatter', () => {});
});

Performance Considerations

Debouncing Saves

For auto-save feature (future):

.ts
const debouncedSave = useMemo(
  () => debounce(handleSave, 2000),
  [handleSave]
);

Optimistic Updates

Update UI immediately, rollback on error:

.ts
const handleSave = async () => {
  const previousContent = content;
  
  // Update UI immediately
  setContent(editedContent);
  
  try {
    await saveContent();
  } catch (error) {
    // Rollback on error
    setContent(previousContent);
    showError(error);
  }
};

Security Notes

Critical: This feature is dev-only and should NEVER run in production.

Safeguards:

  1. Environment check: process.env.NODE_ENV !== 'development'
  2. Filepath validation: Only allow specific directories
  3. No arbitrary file system access
  4. Logged operations for audit trail

Production Build:


Implementation Checklist

Phase 1: Edit Mode Toggle

Phase 2: Content Editing

Phase 3: Save API

Phase 4: Metadata Editing

Phase 5: Integration


Timeline

Week 1:


Questions to Answer


Notes

This is a critical feature for scaling content production. The current workflow doesn't scale past ~30 pages. This feature is not about building a CMS for production use - it's about improving the development workflow for a solo developer managing their own content.

Philosophy: Keep it simple. Dev-only. Filesystem-based. No database. No auth. Just make editing faster.

Once this is working, TSX file editing and more advanced features can be considered. But v1 needs to solve the immediate pain: editing markdown files quickly.