authorly-editor
v0.1.8
Published
A rich text editor for authors, blogs, and documentation with clean, publish-ready output.
Maintainers
Readme
Why Authorly?
| Feature | Authorly | Other Editors | |---------|----------|---------------| | Output | Pure semantic HTML | JSON AST / Custom format | | Dependencies | React + Lucide icons | Heavy frameworks | | Bundle size | ~30kb gzipped | 100kb+ | | Learning curve | Minutes | Hours/Days | | Database storage | Just HTML string | Complex serialization |
<!-- What you get: Clean, portable HTML ready to publish -->
<h1>My Article</h1>
<p>A paragraph with <strong>bold</strong> and <em>italic</em> text.</p>
<ul>
<li>Simple</li>
<li>Clean</li>
<li>Works everywhere</li>
</ul>Installation
npm install authorly-editoryarn add authorly-editorpnpm add authorly-editorQuick Start
import { ContentBlocksEditor } from 'authorly-editor';
function App() {
const [content, setContent] = useState('<p>Hello World!</p>');
return (
<ContentBlocksEditor
initialContent={content}
onChange={setContent}
/>
);
}That's it. No configuration needed.
Components
1. ContentBlocksEditor
The main editor component for creating and editing content.
import { ContentBlocksEditor } from 'authorly-editor';
<ContentBlocksEditor
initialContent="<p>Start writing...</p>"
onChange={(html) => console.log(html)}
onSave={(html) => saveToDatabase(html)}
darkMode={false}
showToolbar={true}
placeholder="Type '/' for commands..."
/>2. ContentBlocksRenderer
Display saved HTML content with beautiful styling. No editor overhead.
import { ContentBlocksRenderer } from 'authorly-editor';
<ContentBlocksRenderer
html={savedContent}
darkMode={false}
enableCodeCopy={true}
/>3. TableOfContents
Auto-generate navigation from your content headings.
import { TableOfContents, ContentBlocksRenderer } from 'authorly-editor';
<div style={{ display: 'flex' }}>
<aside style={{ width: 200 }}>
<TableOfContents html={content} title="Contents" />
</aside>
<main>
<ContentBlocksRenderer html={content} enableHeadingIds={true} />
</main>
</div>Block Types
| Block | Description | HTML Output |
|-------|-------------|-------------|
| Paragraph | Basic text | <p> |
| Heading 1-6 | Section headings | <h1> - <h6> |
| Bullet List | Unordered list | <ul><li> |
| Numbered List | Ordered list | <ol><li> |
| Checklist | Todo items | <ul><li><input type="checkbox"> |
| Quote | Blockquote | <blockquote> |
| Code | Code block | <pre><code> |
| Image | Image with caption | <figure><img><figcaption> |
| Video | YouTube/Vimeo/MP4 | <figure><iframe> |
| Table | Data table | <table> |
| Divider | Horizontal rule | <hr> |
| Callout | Info/Warning/Error | <aside> |
| Accordion | Collapsible section | <details><summary> |
Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Ctrl/Cmd + B | Bold |
| Ctrl/Cmd + I | Italic |
| Ctrl/Cmd + U | Underline |
| Ctrl/Cmd + S | Save (triggers onSave) |
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Y | Redo |
| Ctrl/Cmd + 1/2/3 | Heading 1/2/3 |
| / | Open block menu |
| Enter | New block / New list item |
| Backspace | Delete empty block / Merge |
| Tab | Indent list / Navigate table |
| ↑ / ↓ | Navigate between blocks |
API Reference
ContentBlocksEditor Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| initialContent | string | '' | Initial HTML content |
| onChange | (html: string) => void | - | Called on content change |
| onSave | (html: string) => void | - | Called on Ctrl+S |
| onFocus | () => void | - | Called when editor gains focus |
| onBlur | () => void | - | Called when editor loses focus |
| onReady | (editor: EditorInstance) => void | - | Called when editor is ready |
| darkMode | boolean | false | Enable dark theme |
| showToolbar | boolean | true | Show formatting toolbar |
| toolbarPosition | 'top' \| 'bottom' | 'top' | Toolbar position |
| placeholder | string | 'Type "/" for commands...' | Placeholder text |
| readOnly | boolean | false | Disable editing |
| autoFocus | boolean | false | Focus on mount |
| spellCheck | boolean | true | Enable spell check |
| className | string | '' | Custom class name |
| style | CSSProperties | - | Custom styles |
EditorRef Methods
Access editor methods using a ref:
import { useRef } from 'react';
import { ContentBlocksEditor, EditorRef } from 'authorly-editor';
function MyEditor() {
const editorRef = useRef<EditorRef>(null);
return (
<>
<ContentBlocksEditor ref={editorRef} />
<button onClick={() => console.log(editorRef.current?.getHTML())}>
Get HTML
</button>
</>
);
}| Method | Description |
|--------|-------------|
| getHTML() | Returns the current HTML content |
| setHTML(html: string) | Sets the editor content |
| getText() | Returns plain text content |
| focus() | Focuses the editor |
| blur() | Blurs the editor |
| insertBlock(type, data?) | Inserts a new block |
| getEditor() | Returns the full editor instance |
ContentBlocksRenderer Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| html | string | '' | HTML content to render |
| darkMode | boolean | false | Enable dark theme |
| enableCodeCopy | boolean | true | Add copy button to code blocks |
| enableHeadingIds | boolean | true | Add IDs to headings |
| enableChecklistStyles | boolean | true | Strikethrough checked items |
| className | string | '' | Custom class name |
| style | CSSProperties | - | Custom styles |
TableOfContents Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| html | string | '' | HTML to extract headings from |
| darkMode | boolean | false | Enable dark theme |
| title | string | 'Table of Contents' | Title text |
| minLevel | number | 1 | Min heading level (1-6) |
| maxLevel | number | 6 | Max heading level (1-6) |
| onNavigate | (id, item) => void | - | Custom navigation handler |
| smoothScroll | boolean | true | Smooth scroll to heading |
| collapsible | boolean | false | Make TOC collapsible |
Examples
Blog Editor with Preview
import { useState, useRef } from 'react';
import {
ContentBlocksEditor,
ContentBlocksRenderer,
EditorRef
} from 'authorly-editor';
function BlogEditor() {
const editorRef = useRef<EditorRef>(null);
const [content, setContent] = useState('<p>Write your post...</p>');
const [showPreview, setShowPreview] = useState(false);
const handleSave = async (html: string) => {
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ content: html }),
});
};
return (
<div>
<button onClick={() => setShowPreview(!showPreview)}>
{showPreview ? 'Edit' : 'Preview'}
</button>
{showPreview ? (
<ContentBlocksRenderer html={content} />
) : (
<ContentBlocksEditor
ref={editorRef}
initialContent={content}
onChange={setContent}
onSave={handleSave}
/>
)}
</div>
);
}Documentation Page with TOC
import {
ContentBlocksRenderer,
TableOfContents
} from 'authorly-editor';
function DocsPage({ content }) {
return (
<div style={{ display: 'grid', gridTemplateColumns: '250px 1fr', gap: '2rem' }}>
<aside style={{ position: 'sticky', top: '1rem', height: 'fit-content' }}>
<TableOfContents
html={content}
title="On this page"
maxLevel={3}
/>
</aside>
<main>
<ContentBlocksRenderer
html={content}
enableHeadingIds={true}
enableCodeCopy={true}
/>
</main>
</div>
);
}Dark Mode Support
import { useState } from 'react';
import { ContentBlocksEditor } from 'authorly-editor';
function ThemedEditor() {
const [darkMode, setDarkMode] = useState(false);
return (
<div style={{
background: darkMode ? '#0f172a' : '#ffffff',
minHeight: '100vh',
padding: '2rem'
}}>
<button onClick={() => setDarkMode(!darkMode)}>
Toggle Theme
</button>
<ContentBlocksEditor darkMode={darkMode} />
</div>
);
}Customization
CSS Variables
Override these CSS variables to customize the editor appearance:
.cb-editor {
/* Colors */
--cb-primary: #3b82f6;
--cb-primary-hover: #2563eb;
--cb-bg: #ffffff;
--cb-bg-secondary: #f9fafb;
--cb-bg-tertiary: #f3f4f6;
--cb-text: #111827;
--cb-text-secondary: #6b7280;
--cb-border: #e5e7eb;
--cb-border-focus: #3b82f6;
/* Spacing */
--cb-spacing-xs: 0.25rem;
--cb-spacing-sm: 0.5rem;
--cb-spacing-md: 1rem;
--cb-spacing-lg: 1.5rem;
/* Border radius */
--cb-radius-sm: 0.25rem;
--cb-radius-md: 0.375rem;
--cb-radius-lg: 0.5rem;
/* Typography */
--cb-font-family: system-ui, -apple-system, sans-serif;
--cb-font-mono: 'SF Mono', Monaco, Consolas, monospace;
}Custom Blocks
Register your own block types:
import { blockRegistry, BlockDefinition } from 'authorly-editor';
const myCustomBlock: BlockDefinition = {
name: 'custom',
tag: 'div',
editable: true,
allowedChildren: ['text', 'inline'],
label: 'Custom Block',
icon: 'box',
create: (data) => {
const el = document.createElement('div');
el.className = 'my-custom-block';
el.contentEditable = 'true';
el.innerHTML = data?.content || '';
return el;
},
getData: (el) => ({ content: el.innerHTML }),
update: (el, data) => { el.innerHTML = data.content; },
};
blockRegistry.register(myCustomBlock);Browser Support
| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ |
TypeScript
Full TypeScript support with exported types:
import type {
EditorRef,
EditorInstance,
BlockType,
BlockData,
ContentBlocksEditorProps,
ContentBlocksRendererProps,
TableOfContentsProps,
TocItem,
} from 'authorly-editor';FAQ
The editor outputs plain HTML strings. Save it directly:
const handleSave = async (html: string) => {
await db.posts.create({ content: html });
};
<ContentBlocksEditor onSave={handleSave} />Use the ContentBlocksRenderer component:
const post = await db.posts.findOne(id);
<ContentBlocksRenderer html={post.content} />Currently, Authorly is React-only. The output HTML can be used anywhere, but the editor component requires React 17+.
Not built-in. For real-time collaboration, you'd need to integrate with a service like Yjs or Liveblocks on top of this editor.
The editor supports pasting images (as base64) and entering URLs. For server uploads, handle it in your app:
const handleImageUpload = async (file: File) => {
const url = await uploadToS3(file);
editorRef.current?.insertBlock('image', { src: url });
};Contributing
Contributions are welcome! Please read our contributing guidelines first.
# Clone the repo
git clone https://github.com/your-username/authorly.git
# Install dependencies
npm install
# Start dev server
npm run dev
# Run tests
npm test
# Build
npm run buildLicense
MIT © Aaditya Hasabnis
