Overview
The CMS/Website Builder example demonstrates how to build a visual content management system with Yoopta Editor. It features a drag-and-drop interface, sidebar settings panel, and live preview mode.
Live Demo Try the interactive CMS builder demo
This is an early preview. The current demo shows basic CMS functionality with a handful of content blocks. The full vision includes AI agents, plugin marketplace, and visual theme builder.
Features
Current Implementation
Visual Editor Drag-and-drop block editor with real-time preview
Sidebar Settings Configure page settings, SEO, and block properties
Content Blocks Rich set of content blocks for building pages
Live Preview See changes in real-time as you edit
Roadmap Features
An AI agent that builds and edits pages for you — like Lovable, but with inline editing and a rich plugin ecosystem. Describe what you want and watch it assemble layouts, write copy, and style blocks in real time.
Publish your own plugins or install community-built ones. A shared ecosystem where anyone can extend the CMS with custom blocks, integrations, and themes.
Design themes with a live preview — tweak fonts, colors, spacing, and layout. Export and share themes or apply community ones in a click.
Growing library of content blocks — pricing tables, testimonials, hero sections, FAQ accordions, charts, forms, and more — all drag-and-drop ready.
Implementation Approach
Layout Structure
A typical CMS builder uses a split-panel layout:
import { useState } from 'react' ;
import YooptaEditor , { createYooptaEditor } from '@yoopta/editor' ;
function CMSBuilder () {
const [ sidebarOpen , setSidebarOpen ] = useState ( true );
const [ previewMode , setPreviewMode ] = useState ( false );
return (
< div className = "flex h-screen" >
{ /* Sidebar */ }
{ sidebarOpen && (
< aside className = "w-80 border-r bg-neutral-50" >
< PageSettings />
< BlockSettings />
</ aside >
) }
{ /* Main Editor */ }
< main className = "flex-1 overflow-auto" >
< EditorToolbar
onToggleSidebar = { () => setSidebarOpen ( ! sidebarOpen ) }
onTogglePreview = { () => setPreviewMode ( ! previewMode ) }
/>
< div className = "max-w-5xl mx-auto p-8" >
< YooptaEditor
editor = { editor }
readOnly = { previewMode }
>
{ /* UI components */ }
</ YooptaEditor >
</ div >
</ main >
</ div >
);
}
Page Settings Component
import { Input } from '@/components/ui/input' ;
import { Label } from '@/components/ui/label' ;
import { Textarea } from '@/components/ui/textarea' ;
function PageSettings () {
const [ settings , setSettings ] = useState ({
title: '' ,
slug: '' ,
description: '' ,
keywords: '' ,
});
return (
< div className = "p-4 space-y-4" >
< h3 className = "font-semibold" > Page Settings </ h3 >
< div className = "space-y-2" >
< Label > Page Title </ Label >
< Input
value = { settings . title }
onChange = { ( e ) => setSettings ({ ... settings , title: e . target . value }) }
/>
</ div >
< div className = "space-y-2" >
< Label > URL Slug </ Label >
< Input
value = { settings . slug }
onChange = { ( e ) => setSettings ({ ... settings , slug: e . target . value }) }
/>
</ div >
< div className = "space-y-2" >
< Label > Meta Description </ Label >
< Textarea
value = { settings . description }
onChange = { ( e ) => setSettings ({ ... settings , description: e . target . value }) }
/>
</ div >
</ div >
);
}
Block Settings
Customize individual block properties:
import { useYooptaEditor , Blocks } from '@yoopta/editor' ;
function BlockSettings () {
const editor = useYooptaEditor ();
const [ selectedBlock , setSelectedBlock ] = useState ( null );
useEffect (() => {
editor . on ( 'path-change' , ({ path }) => {
if ( path . current ) {
const block = Blocks . getBlock ( editor , { id: path . current . blockId });
setSelectedBlock ( block );
}
});
}, [ editor ]);
if ( ! selectedBlock ) {
return < div className = "p-4 text-sm text-neutral-500" > No block selected </ div > ;
}
return (
< div className = "p-4 space-y-4" >
< h3 className = "font-semibold" > Block Settings </ h3 >
{ /* Block-specific settings based on type */ }
{ selectedBlock . type === 'Image' && < ImageBlockSettings block = { selectedBlock } /> }
{ selectedBlock . type === 'Callout' && < CalloutBlockSettings block = { selectedBlock } /> }
{ /* Common settings */ }
< div className = "space-y-2" >
< Label > Block ID </ Label >
< Input value = { selectedBlock . id } readOnly />
</ div >
</ div >
);
}
Preview Mode
Toggle between edit and preview modes:
function EditorToolbar ({ onTogglePreview }) {
const [ isPreview , setIsPreview ] = useState ( false );
const handlePreviewToggle = () => {
setIsPreview ( ! isPreview );
onTogglePreview ();
};
return (
< div className = "flex items-center justify-between p-4 border-b" >
< div className = "flex gap-2" >
< Button onClick = { handlePreviewToggle } >
{ isPreview ? 'Edit' : 'Preview' }
</ Button >
< Button variant = "outline" > Save Draft </ Button >
< Button variant = "default" > Publish </ Button >
</ div >
</ div >
);
}
CMS-Specific Plugins
Configure plugins optimized for landing pages:
import Paragraph from '@yoopta/paragraph' ;
import Headings from '@yoopta/headings' ;
import Image from '@yoopta/image' ;
import Video from '@yoopta/video' ;
import Callout from '@yoopta/callout' ;
import Accordion from '@yoopta/accordion' ;
import Tabs from '@yoopta/tabs' ;
import Steps from '@yoopta/steps' ;
import Divider from '@yoopta/divider' ;
export const CMS_PLUGINS = [
Paragraph ,
Headings . HeadingOne ,
Headings . HeadingTwo ,
Headings . HeadingThree ,
Image . extend ({
options: {
async onUpload ( file ) {
const formData = new FormData ();
formData . append ( 'file' , file );
const response = await fetch ( '/api/upload' , {
method: 'POST' ,
body: formData ,
});
const { url } = await response . json ();
return { src: url };
},
},
}),
Video ,
Callout ,
Accordion ,
Tabs ,
Steps ,
Divider ,
];
Export to HTML
Generate production-ready HTML:
import { useYooptaEditor } from '@yoopta/editor' ;
function ExportButton () {
const editor = useYooptaEditor ();
const handleExport = () => {
const html = editor . getHTML ();
// Add page wrapper and styles
const fullPage = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> ${ pageSettings . title } </title>
<meta name="description" content=" ${ pageSettings . description } ">
<link rel="stylesheet" href="/styles/page.css">
</head>
<body>
<main class="container">
${ html }
</main>
</body>
</html>
` ;
// Download or save to server
const blob = new Blob ([ fullPage ], { type: 'text/html' });
const url = URL . createObjectURL ( blob );
const a = document . createElement ( 'a' );
a . href = url ;
a . download = ` ${ pageSettings . slug } .html` ;
a . click ();
};
return < Button onClick = { handleExport } > Export HTML </ Button > ;
}
Saving and Loading Pages
import { useEffect } from 'react' ;
import { createYooptaEditor , YooptaContentValue } from '@yoopta/editor' ;
function CMSEditor ({ pageId } : { pageId : string }) {
const editor = useMemo (() => createYooptaEditor ({ plugins: CMS_PLUGINS }), []);
// Load page content
useEffect (() => {
async function loadPage () {
const response = await fetch ( `/api/pages/ ${ pageId } ` );
const data = await response . json ();
editor . setEditorValue ( data . content );
}
loadPage ();
}, [ pageId ]);
// Auto-save on change
const handleChange = async ( value : YooptaContentValue ) => {
await fetch ( `/api/pages/ ${ pageId } ` , {
method: 'PUT' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ content: value }),
});
};
return (
< YooptaEditor
editor = { editor }
onChange = { handleChange }
>
{ /* UI components */ }
</ YooptaEditor >
);
}
Source Code
View Full Source Complete CMS implementation on GitHub
Use Cases
Landing Pages Build marketing pages with hero sections, features, and CTAs
Blog CMS Create and manage blog posts with rich formatting
Documentation Sites Build knowledge bases and documentation
Portfolio Sites Showcase projects and work samples
Next Steps
Email Builder Learn how to build an email template editor
Exports Export content to HTML, Markdown, and more