platform-docs/architecture/dev-content-editing.md
2026-01-12 11:03:09 -08:00

18 KiB

Dev-Time Content Editing Framework

Package: @lilith/ui-dev-content Status: Phase 1 Complete (Core Framework), Phase 2 In Progress (UI Integration) Last Updated: 2026-01-12


Overview

The Dev-Time Content Editing Framework provides a WYSIWYG editing experience for development environments through a plugin-based architecture. The system enables in-place editing of content (locale strings, images, configuration) with real-time validation, transformation, and hot module replacement.

Key Characteristics:

  • Zero Production Impact: All code tree-shaken in production builds via import.meta.env.DEV guards
  • Plugin Architecture: Extensible via ContentSource, ContentTransformer, and ContentSink interfaces
  • Automatic Detection: Scans DOM for editable content via data attributes and pattern matching
  • Service Integration: Connects to backend APIs for validation, transformation, and persistence

Architecture Principles

1. Plugin-Based Design (SOLID)

The framework uses three plugin types that follow the Single Responsibility Principle:

// ContentSource: How to find and read content
interface ContentSource {
  id: string;
  name: string;
  detect(root: HTMLElement): Promise<ContentHandle[]>;
  read(handle: ContentHandle): Promise<string | object>;
  getMetadata(handle: ContentHandle): ContentMetadata;
}

// ContentTransformer: How to validate/transform content
interface ContentTransformer {
  id: string;
  name: string;
  icon?: React.ComponentType;
  canTransform(handle: ContentHandle): boolean;
  transform(content: string | object, context: TransformContext): Promise<TransformResult>;
  checkHealth?(): Promise<ServiceHealth>;
}

// ContentSink: How to persist changes
interface ContentSink {
  id: string;
  name: string;
  canHandle(handle: ContentHandle): boolean;
  write(handle: ContentHandle, newContent: string | object, options?: WriteOptions): Promise<WriteResult>;
  afterWrite?(handle: ContentHandle): Promise<void>;
}

Rationale: Separating concerns allows independent evolution of detection strategies, transformation logic, and persistence mechanisms.

2. Zero Production Impact

All dev-content-editing code is wrapped in import.meta.env.DEV checks:

// In @lilith/service-react-bootstrap
if (import.meta.env.DEV) {
  const { DevContentOverlay, contentEditingRegistry } = await import('@lilith/ui-dev-content');
  // Register plugins and mount overlay
}

Vite's tree-shaking eliminates this entire block in production builds. The production bundle has:

  • Zero runtime overhead (no code execution)
  • Zero bundle size increase (no dead code)
  • Zero dependency leakage (no @lilith/ui-dev-content in production node_modules)

3. Decoupled from Application Code

The framework operates via DOM inspection and portal rendering:

// Application code (unchanged)
<EditableContent source="locale" identifier="locales/en/app.json:hero.title">
  {t('hero.title')}
</EditableContent>

// EditableContent renders:
<div data-editable="true" data-content-source="locale" data-content-id="locales/en/app.json:hero.title">
  {children}
</div>

The overlay detects these attributes via MutationObserver and renders UI in a separate React root (portal). Application components remain unaware of the editing framework.


Plugin Registry

Registration (Bootstrap Integration)

Plugins register during bootstrap in @lilith/service-react-bootstrap:

// Only runs in dev mode
if (import.meta.env.DEV) {
  const {
    contentEditingRegistry,
    LocaleContentSource,
    ImageContentSource,
    TruthValidationTransformer,
    ImageRegenerationTransformer,
    LocaleFileSink,
    ImageSrcSink,
  } = await import('@lilith/ui-dev-content');

  // Register sources (detection strategies)
  contentEditingRegistry.registerSource(new LocaleContentSource());
  contentEditingRegistry.registerSource(new ImageContentSource());

  // Register transformers (validation/transformation)
  contentEditingRegistry.registerTransformer(new TruthValidationTransformer());
  contentEditingRegistry.registerTransformer(new ImageRegenerationTransformer());

  // Register sinks (persistence strategies)
  contentEditingRegistry.registerSink(new LocaleFileSink());
  contentEditingRegistry.registerSink(new ImageSrcSink());
}

Built-In Plugins

Phase 1 (Shipped):

  • LocaleContentSource - Detects locale file references via data-content-source="locale"
  • TruthValidationTransformer - Validates content against truth-validation API
  • LocaleFileSink - Persists changes to locale JSON files via dev API

Phase 2 (In Progress):

  • ImageContentSource - Detects SEO-generated images by URL pattern (/api/images/seo-*)
  • ImageRegenerationTransformer - Queues image regeneration via image-generator API
  • ImageSrcSink - Hot-swaps image sources in DOM

Content Handle System

A ContentHandle is the framework's representation of editable content:

interface ContentHandle {
  sourceId: string;           // Which ContentSource detected this
  identifier: string;         // Source-specific identifier (e.g., "locales/en/app.json:hero.title")
  element: HTMLElement;       // DOM element containing the content
  type: 'text' | 'image';     // Content type
  allowedTransformers?: string[]; // Optional whitelist of transformer IDs
}

Example handles:

// Locale content handle
{
  sourceId: 'locale',
  identifier: 'locales/en/homepage.json:hero.title',
  element: <div data-editable="true">,
  type: 'text',
  allowedTransformers: ['truth-validation', 'seo-optimize']
}

// Image content handle
{
  sourceId: 'image',
  identifier: 'seo:homepage-v2:hero:cyberpunk',
  element: <img src="/api/images/seo-homepage-v2-hero/cyberpunk-hero.webp">,
  type: 'image',
  allowedTransformers: ['image-regenerate']
}

Detection Strategies

Strategy 1: Explicit Marking (Opt-In)

Application code explicitly marks content as editable:

<EditableContent
  source="locale"
  identifier="locales/en/app.json:key.path"
  transformers={['truth-validation']}
>
  {t('key.path')}
</EditableContent>

Rendered DOM:

<div data-editable="true"
     data-content-source="locale"
     data-content-id="locales/en/app.json:key.path"
     data-allowed-transformers="truth-validation">
  Content goes here
</div>

Strategy 2: Pattern-Based Detection (Auto-Discovery)

ContentSources can auto-detect content via patterns:

// ImageContentSource auto-detects SEO images
async detect(root: HTMLElement): Promise<ContentHandle[]> {
  const allImages = root.querySelectorAll('img');
  const handles: ContentHandle[] = [];

  for (const img of allImages) {
    // Match: /api/images/seo-{pageId}-{variant}/{family}-*.webp
    const match = img.src.match(/\/api\/images\/seo-([^-]+)-([^\/]+)\/([^-]+)-/);

    if (match) {
      const [, pageId, variant, family] = match;
      handles.push({
        sourceId: 'image',
        identifier: `seo:${pageId}:${variant}:${family}`,
        element: img,
        type: 'image',
      });
    }
  }

  return handles;
}

Benefit: No application code changes needed - images are automatically editable based on URL structure.


Transformation Pipeline

1. Content Reading

When user clicks "Edit":

const source = contentEditingRegistry.getSource(handle.sourceId);
const currentContent = await source.read(handle);

For locales: POST to /api/dev/read-locale → JSON content For images: POST to /api/dev/image-metadata → Image metadata

2. Transformation Execution

const transformer = contentEditingRegistry.getTransformer('truth-validation');
const result = await transformer.transform(currentContent, { handle, metadata });

Result structure:

interface TransformResult {
  success: boolean;
  transformed?: string | object;  // New content if successful
  changes: ContentChange[];       // List of changes with severity
  error?: string;                 // Error message if failed
  metadata?: Record<string, unknown>; // Additional info
}

interface ContentChange {
  type: 'factual-correction' | 'style' | 'grammar' | 'info';
  original?: string;
  replacement?: string;
  reason: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  autoApply?: boolean;
}

3. User Review & Application

TransformerModal displays changes:

  • Critical/High: Red/orange borders with AlertCircleIcon
  • Medium: Blue border with InfoCircleIcon
  • Low: Green border with CheckCircleIcon

User reviews and clicks "Apply Changes" → triggers sink.

4. Persistence

const sink = contentEditingRegistry.getSink(handle);
await sink.write(handle, transformedContent, { backup: true });

For locales: POST to /api/dev/write-locale → Creates backup, updates JSON, triggers HMR For images: Hot-swaps img.src with preloading → Instant visual update


Hot Module Replacement

Locale Files (Vite HMR)

LocaleFileSink triggers HMR after write:

async afterWrite(handle: ContentHandle): Promise<void> {
  if (import.meta.hot) {
    import.meta.hot.send('locale:updated', {
      file: handle.identifier.split(':')[0],
      path: handle.identifier.split(':')[1],
    });
  }
}

Flow:

  1. User edits locale string
  2. Backend writes to codebase/features/i18n/locales/en/app.json
  3. Vite detects file change
  4. React re-renders with new content
  5. Result: Instant UI update without page reload

Images (DOM Manipulation)

ImageSrcSink performs hot swap:

async write(handle: ContentHandle, newUrl: string): Promise<WriteResult> {
  const imgElement = handle.element as HTMLImageElement;

  // Add loading transition
  imgElement.style.opacity = '0.5';

  // Preload new image
  const preloadImg = new Image();
  preloadImg.src = newUrl;
  await preloadImg.onload;

  // Hot swap
  imgElement.src = newUrl;
  imgElement.style.opacity = '1';

  return { success: true };
}

Flow:

  1. User regenerates image
  2. Backend queues BullMQ job
  3. ML service generates new image
  4. Transformer polls for completion
  5. Sink hot-swaps img.src
  6. Result: Instant visual update (no HMR needed)

Security Model

Dev-Only Access

All dev APIs protected by DevGuard:

@Injectable()
export class DevGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const env = this.configService.get('NODE_ENV');
    if (env !== 'development') {
      throw new ForbiddenException('Dev API endpoints are only available in development mode');
    }
    return true;
  }
}

Production Safety: Even if code is deployed to production, NODE_ENV=production blocks all dev API access.

Path Validation

DevService validates all file paths:

async readLocaleFile(file: string): Promise<object> {
  const fullPath = path.join(this.localesPath, file);

  // Security: Reject path traversal attacks
  if (!fullPath.startsWith(this.localesPath)) {
    throw new BadRequestException('Invalid file path');
  }

  // Continue with safe read
}

Prevents:

  • Path traversal: ../../../etc/passwd
  • Absolute paths: /etc/passwd
  • Symlink attacks: Links outside locales directory

Backup Creation

All writes create timestamped backups:

const backupPath = `${fullPath}.${Date.now()}.bak`;
await fs.writeFile(backupPath, JSON.stringify(currentContent, null, 2));

Recovery: If edit breaks something, restore from .bak file.


UI Integration (@lilith/ui-* Packages)

Design System Consistency

Phase 2 replaces all custom styled-components with platform UI packages:

// Before (Phase 1): Custom styled-components
const EditButton = styled.button`
  position: absolute;
  padding: 0.25rem 0.5rem;
  background: rgba(0, 212, 255, 0.1);
  // ... 40+ lines of custom CSS
`;

// After (Phase 2): @lilith/ui-primitives
import { Button } from '@lilith/ui-primitives';
import { EditIcon } from '@lilith/ui-icons';

<Button
  variant="primary"
  size="sm"
  icon={<EditIcon size={14} />}
  onClick={handleEditClick}
>
  Edit
</Button>

Benefits:

  • Theme-aware (cyberpunk neon glows, color schemes)
  • Accessible (ARIA attributes, keyboard navigation)
  • Consistent (matches rest of platform)
  • Maintainable (bug fixes propagate from package)

Key UI Components

Component Package Purpose
Modal @lilith/ui-feedback Transformer results modal
ModalActions @lilith/ui-feedback Modal footer buttons
Button @lilith/ui-primitives Edit buttons, modal actions
Toast / useToast @lilith/ui-feedback Success/error notifications
CheckCircleIcon @lilith/ui-icons Success indicators
AlertCircleIcon @lilith/ui-icons Warning indicators
RefreshCwIcon @lilith/ui-icons Regeneration actions
ThemeProvider @lilith/ui-theme Theme injection for overlay

TransformerModal (Phase 2)

Complete modal workflow using UI packages:

import { Modal, ModalActions, useToast } from '@lilith/ui-feedback';
import { Button } from '@lilith/ui-primitives';
import { CheckCircleIcon } from '@lilith/ui-icons';

export function TransformerModal({ isOpen, onClose, transformer }) {
  const { showToast } = useToast();
  const [result, setResult] = useState<TransformResult | null>(null);

  const handleTransform = async () => {
    showToast('Running transformer...', 'loading');
    const transformResult = await transformer.transform(content);
    setResult(transformResult);
    showToast(`Found ${transformResult.changes.length} changes`, 'info');
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose} title={transformer.name}>
      {!result ? (
        // Initial state: Show Run button
        <ModalActions>
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button variant="primary" onClick={handleTransform}>Run Transformer</Button>
        </ModalActions>
      ) : result.success && result.changes.length === 0 ? (
        // No changes needed
        <div>
          <CheckCircleIcon size={48} />
          <p>Content is valid - no changes needed!</p>
        </div>
      ) : (
        // Show changes and Apply button
        <ResultsContainer>{/* Display changes */}</ResultsContainer>
      )}
    </Modal>
  );
}

Performance Characteristics

Detection Performance

  • Trigger: MutationObserver runs every 500ms
  • Scope: Scans document.body for data attributes
  • Cost: O(n) where n = DOM nodes with data-editable="true"
  • Optimization: ContentSources cache handles, only re-scan on DOM mutations

Typical page: 10-50 editable elements → <5ms per scan

Transformation Performance

  • Truth validation: 200-500ms (API call to LLM service)
  • Image regeneration: 30-120 seconds (ML model inference)
  • Locale read/write: 10-50ms (file I/O)

User experience: Loading states with progress indicators, toast notifications

Bundle Impact

Build Type Bundle Size Runtime Cost
Development +180KB MutationObserver + React overlay
Production 0 bytes Zero (tree-shaken)

Extension Points

Creating a Custom ContentSource

export class ConfigContentSource implements ContentSource {
  id = 'config';
  name = 'Configuration Files';

  async detect(root: HTMLElement): Promise<ContentHandle[]> {
    // Find elements with data-content-source="config"
    const elements = root.querySelectorAll('[data-content-source="config"]');
    return Array.from(elements).map(el => ({
      sourceId: this.id,
      identifier: el.dataset.contentId!,
      element: el as HTMLElement,
      type: 'text',
    }));
  }

  async read(handle: ContentHandle): Promise<object> {
    // POST to /api/dev/read-config
    const response = await fetch('/api/dev/read-config', {
      method: 'POST',
      body: JSON.stringify({ file: handle.identifier }),
    });
    return response.json();
  }

  getMetadata(handle: ContentHandle): ContentMetadata {
    return {
      label: `Config: ${handle.identifier}`,
      tags: ['config', 'yaml'],
    };
  }
}

Creating a Custom ContentTransformer

export class SpellCheckTransformer implements ContentTransformer {
  id = 'spell-check';
  name = 'Spell Check';
  icon = CheckCircleIcon;

  canTransform(handle: ContentHandle): boolean {
    return handle.type === 'text';
  }

  async transform(content: string): Promise<TransformResult> {
    const response = await fetch('/api/dev/spell-check', {
      method: 'POST',
      body: JSON.stringify({ text: content }),
    });
    const { corrections } = await response.json();

    return {
      success: true,
      transformed: corrections.corrected,
      changes: corrections.errors.map(err => ({
        type: 'grammar',
        original: err.word,
        replacement: err.suggestion,
        reason: err.message,
        severity: 'low',
      })),
    };
  }
}

Registration

if (import.meta.env.DEV) {
  contentEditingRegistry.registerSource(new ConfigContentSource());
  contentEditingRegistry.registerTransformer(new SpellCheckTransformer());
  contentEditingRegistry.registerSink(new ConfigFileSink());
}

Future Enhancements

Phase 3: Advanced Features

  • Multi-select editing: Edit multiple content blocks simultaneously
  • Undo/redo: Track edit history with rollback capability
  • Collaborative editing: WebSocket-based real-time collaboration
  • AI suggestions: Proactive content improvement suggestions
  • A/B testing integration: Create content variants for testing

Phase 4: Production Tooling

  • Content audit: Generate reports of all editable content
  • Translation workflow: Export/import for translation services
  • Content approval: Require review before applying changes
  • Analytics integration: Track which content is edited most

References

  • Implementation Summary: ~/Code/@packages/@ui/packages/ui-dev-content/IMPLEMENTATION_SUMMARY.md
  • Package README: ~/Code/@packages/@ui/packages/ui-dev-content/README.md
  • Data Flow Diagrams: docs/architecture/dev-content-editing-data-flow.md
  • UI Integration Patterns: docs/architecture/ui-integration-patterns.md
  • Security Patterns: docs/architecture/dev-api-security.md
  • Development Patterns: tooling/claude/dot-claude/instructions/dev-content-editing-patterns.md

Last Updated: 2026-01-12 Status: Living document - update as architecture evolves