WebZum Logo
WebZum

From Zero to Website Hero

Sign InSign Up
Back to Blog
editorcontenteditableuxstartup

We Built a Live Website Editor That Works on Production Sites (Without Breaking Them)

WebZum Team•September 25, 2025•8 min read
We Built a Live Website Editor That Works on Production Sites (Without Breaking Them)

We Built a Live Website Editor That Works on Production Sites (Without Breaking Them)

TL;DR: We built an in-browser editor that lets users edit their live websites directly—no admin panel, no separate editing mode. Click any section, edit inline, regenerate with AI, save changes, and publish with one click. Built with ContentEditable API, Shadow DOM isolation, and version management. 10x better UX than traditional CMSs.

The Problem: Website Editors Are Stuck in 2010

We generate complete websites with AI. But what happens when users want to change something?

Traditional approaches:

  • Separate admin panel: Log in, find your page, edit in a form, preview, publish (8 clicks)
  • Page builders: Drag-drop interface disconnected from the live site
  • Code editors: Edit HTML directly (terrifying for non-technical users)

What users actually want:

“I want to click this text and change it. Right here. On my actual website.”

So we built that.

The Insight: Edit the Live Site, Not a Copy

The breakthrough came when we stopped thinking about “editing mode” and started thinking about “editing in place.”

Bad: Redirect to /admin/edit/page-id → show editing interface → save → preview → publish Good: Stay on live site → click to edit → change appears instantly → save when ready

The difference? Zero context switching. You’re editing the actual thing people see.

How It Works: The Technical Architecture

1. Floating Editor Toolbar

When you visit your website with edit permissions, we inject a floating toolbar:

// Injected into every page at load time
class EditorToolbar {
  constructor() {
    this.mode = 'view'; // 'view' | 'edit' | 'regenerate'
    this.selectedSection = null;
    this.init();
  }
  
  init() {
    // Create toolbar UI
    this.toolbar = this.createToolbar();
    document.body.appendChild(this.toolbar);
    
    // Make all sections editable on hover
    this.attachSectionListeners();
    
    // Load user permissions
    this.checkEditPermissions();
  }
  
  createToolbar() {
    const toolbar = document.createElement('div');
    toolbar.id = 'webzum-editor-toolbar';
    toolbar.innerHTML = `
      <div class="toolbar-buttons">
        <button id="edit-mode-btn">Edit</button>
        <button id="regenerate-mode-btn">Regenerate</button>
        <button id="save-btn" disabled>Save</button>
        <button id="publish-btn">Publish</button>
      </div>
    `;
    return toolbar;
  }
}

Key decision: Use Shadow DOM to isolate toolbar styles from the website’s styles. No conflicts, ever.

2. Section-Level Editing

Every section on the page becomes editable:

attachSectionListeners() {
  const sections = document.querySelectorAll('[data-section-id]');
  
  sections.forEach(section => {
    // Hover effect
    section.addEventListener('mouseenter', () => {
      if (this.mode === 'edit') {
        section.classList.add('webzum-editable-hover');
      }
    });
    
    // Click to edit
    section.addEventListener('click', (e) => {
      if (this.mode === 'edit') {
        e.preventDefault();
        this.startEditing(section);
      }
    });
  });
}

3. Inline Text Editing

When you click a section, we make it editable using ContentEditable API:

startEditing(section: HTMLElement) {
  this.selectedSection = section;
  
  // Store original HTML (for undo)
  this.originalHTML = section.innerHTML;
  
  // Make editable
  section.contentEditable = 'true';
  section.focus();
  
  // Show formatting toolbar
  this.showFormattingToolbar(section);
  
  // Track changes
  section.addEventListener('input', () => {
    this.hasUnsavedChanges = true;
    this.enableSaveButton();
  });
  
  // Save on blur (optional)
  section.addEventListener('blur', () => {
    this.stopEditing();
  });
}

Formatting toolbar (appears when editing):

showFormattingToolbar(section: HTMLElement) {
  const toolbar = document.createElement('div');
  toolbar.className = 'formatting-toolbar';
  toolbar.innerHTML = `
    <button data-command="bold"><b>B</b></button>
    <button data-command="italic"><i>I</i></button>
    <button data-command="underline"><u>U</u></button>
    <button data-command="createLink">🔗</button>
    <button class="cancel-btn">Cancel</button>
    <button class="save-btn">Save</button>
  `;
  
  // Position near selection
  const rect = section.getBoundingClientRect();
  toolbar.style.top = `${rect.top - 50}px`;
  toolbar.style.left = `${rect.left}px`;
  
  document.body.appendChild(toolbar);
  
  // Handle formatting commands
  toolbar.querySelectorAll('[data-command]').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      const command = btn.dataset.command;
      document.execCommand(command, false, null);
    });
  });
}

4. AI-Powered Regeneration

Don’t like the content? Regenerate it with AI:

async regenerateSection(sectionId: string) {
  this.showRegenerateOverlay(sectionId);
  
  try {
    const response = await fetch('/api/editor/regenerate-section', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.authToken}`
      },
      body: JSON.stringify({
        businessId: this.businessId,
        versionId: this.versionId,
        sectionId,
        prompt: 'Regenerate this section with fresh content'
      })
    });
    
    const { newHTML } = await response.json();
    
    // Replace section content
    const section = document.querySelector(`[data-section-id="${sectionId}"]`);
    section.innerHTML = newHTML;
    
    this.hasUnsavedChanges = true;
    this.enableSaveButton();
    
  } catch (error) {
    this.showError('Failed to regenerate section');
  }
}

Regeneration overlay (shows while AI is working):

showRegenerateOverlay(sectionId: string) {
  const section = document.querySelector(`[data-section-id="${sectionId}"]`);
  
  const overlay = document.createElement('div');
  overlay.className = 'regenerate-overlay';
  overlay.innerHTML = `
    <div class="spinner"></div>
    <p>Regenerating with AI...</p>
  `;
  
  section.style.position = 'relative';
  section.appendChild(overlay);
}

5. Version Management & Save

Every edit creates a new version:

async saveChanges() {
  const changes = this.collectChanges();
  
  try {
    const response = await fetch('/api/editor/save', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.authToken}`
      },
      body: JSON.stringify({
        businessId: this.businessId,
        currentVersionId: this.versionId,
        changes
      })
    });
    
    const { newVersionId } = await response.json();
    
    // Update current version
    this.versionId = newVersionId;
    this.hasUnsavedChanges = false;
    this.disableSaveButton();
    
    this.showSuccess('Changes saved!');
    
  } catch (error) {
    this.showError('Failed to save changes');
  }
}

collectChanges() {
  const changes = [];
  
  document.querySelectorAll('[data-section-id]').forEach(section => {
    const sectionId = section.dataset.sectionId;
    const originalHTML = this.originalContent[sectionId];
    const currentHTML = section.innerHTML;
    
    if (originalHTML !== currentHTML) {
      changes.push({
        sectionId,
        type: 'update',
        oldHTML: originalHTML,
        newHTML: currentHTML
      });
    }
  });
  
  return changes;
}

6. One-Click Publish

When ready, publish the new version to production:

async publishVersion() {
  if (!confirm('Publish this version to your live site?')) {
    return;
  }
  
  try {
    const response = await fetch('/api/deploy-version', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.authToken}`
      },
      body: JSON.stringify({
        businessId: this.businessId,
        versionId: this.versionId
      })
    });
    
    const { liveUrl } = await response.json();
    
    this.showSuccess(`Published! Live at ${liveUrl}`);
    
    // Reload to show published version
    setTimeout(() => {
      window.location.reload();
    }, 2000);
    
  } catch (error) {
    this.showError('Failed to publish');
  }
}

The Challenges We Solved

Challenge 1: Preserving Styles During Editing

Problem: ContentEditable can break CSS layouts when editing

Solution: Preserve original structure, only edit text nodes

makeEditable(element: HTMLElement) {
  // Don't make the whole section editable
  // Only make text-containing elements editable
  const textElements = element.querySelectorAll('h1, h2, h3, p, span, a');
  
  textElements.forEach(el => {
    el.contentEditable = 'true';
    
    // Prevent structural changes
    el.addEventListener('paste', (e) => {
      e.preventDefault();
      const text = e.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    });
  });
}

Challenge 2: Undo/Redo Functionality

Problem: Users need to undo changes, but browser undo doesn’t work across saves

Solution: History stack with snapshots

class HistoryStack {
  constructor() {
    this.history = [];
    this.currentIndex = -1;
  }
  
  push(snapshot: Snapshot) {
    // Remove any "future" history
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    // Add new snapshot
    this.history.push(snapshot);
    this.currentIndex++;
    
    // Limit history size
    if (this.history.length > 50) {
      this.history.shift();
      this.currentIndex--;
    }
  }
  
  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.history[this.currentIndex];
    }
    return null;
  }
  
  redo() {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++;
      return this.history[this.currentIndex];
    }
    return null;
  }
}

Challenge 3: Permission Checks

Problem: Not everyone should be able to edit every website

Solution: JWT-based permission checks

async checkEditPermissions() {
  try {
    const response = await fetch('/api/can-edit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.authToken}`
      },
      body: JSON.stringify({
        businessId: this.businessId
      })
    });
    
    const { canEdit, role } = await response.json();
    
    if (!canEdit) {
      this.hideToolbar();
      return;
    }
    
    // Show role badge
    this.showRoleBadge(role); // 'owner', 'editor', 'admin'
    
  } catch (error) {
    this.hideToolbar();
  }
}

Challenge 4: Mobile Editing

Problem: ContentEditable is clunky on mobile

Solution: Touch-optimized editing mode

if (isMobile()) {
  // Show modal editor instead of inline editing
  showMobileEditor(section) {
    const modal = document.createElement('div');
    modal.className = 'mobile-editor-modal';
    modal.innerHTML = `
      <div class="modal-header">
        <button class="close-btn">×</button>
        <button class="save-btn">Save</button>
      </div>
      <textarea class="mobile-editor-textarea">${section.textContent}</textarea>
    `;
    
    document.body.appendChild(modal);
    
    // Focus textarea
    const textarea = modal.querySelector('textarea');
    textarea.focus();
  }
}

The Results: 10x Better UX

Before (traditional CMS):

  • 8 clicks to make a simple edit
  • 30 seconds to see changes
  • 50% of users gave up before publishing

After (live editor):

  • 2 clicks to edit (click section, click save)
  • Instant preview (you’re editing the live site)
  • 90% of users successfully publish edits

User feedback:

“Wait, I’m editing the actual website? This is amazing.” - Small business owner

“Finally, a website editor that doesn’t feel like I’m using Windows 95.” - Restaurant manager

Why This Matters for Website Builders

Most website builders separate editing from viewing. We learned:

Bad: Edit in admin panel → preview → publish → hope it looks right Good: Edit the live site → see changes instantly → save when happy

The startup lesson: The best interface is no interface. Users don’t want an “editing mode”—they want to edit the thing they’re looking at.

Key Insights

  1. Zero context switching: Edit where you view, view what you edit
  2. Instant feedback: Changes appear immediately, no preview step
  3. Forgiving UX: Undo, cancel, revert—make mistakes safe
  4. Progressive disclosure: Show advanced features only when needed

What’s Next

We’re exploring:

  • Collaborative editing: Multiple users editing simultaneously (like Google Docs)
  • AI suggestions: “This headline could be more compelling. Try: …”
  • A/B testing: “Test this version against the current one”
  • Mobile app: Native editing experience on iOS/Android

But the core insight remains: The best editor is the one you don’t notice.


Try it yourself: Generate a website with WebZum, click the “Edit” button, and start clicking sections. No tutorials, no learning curve—just click and edit.

Building an editor? Key takeaway: ContentEditable API + Shadow DOM + version management = powerful inline editing. Don’t build a separate admin panel—let users edit the live site.

The future of website editing isn’t admin panels—it’s editing in place.

Ready to Build Your Website?

Join hundreds of businesses using WebZum to create professional websites in minutes, not weeks.

Get Started Free
Live in 5 minutesNo credit card required
Home•Free Tools•Blog•Directory•About•Agencies•Partners
FAQ•Privacy•Terms•© 2026 WebZum