Add hybrid editing system with markdown collections

Implement markdown collection support alongside existing element-level editing. Users can now choose between individual element editing (headlines, buttons) and rich markdown editing (stories, bios). Includes responsive overlay forms, smart field detection, and comprehensive documentation updates.

Features:
- Markdown collections with data-field-type="markdown"
- Responsive overlay forms that adapt to element width
- Element-level editing for granular control
- Updated documentation and integration guide
- Fixed marked.js renderer configuration issues
- Enhanced about.html to demonstrate both approaches
This commit is contained in:
2025-09-01 13:20:29 +02:00
parent a13546aac2
commit 39e60e0b3f
5 changed files with 306 additions and 70 deletions

111
README.md
View File

@@ -1,22 +1,40 @@
# Insertr
> **Edit-in-place CMS for client websites** - Simple integration with class-based content editing
> **Element-Level Edit-in-place CMS** - Hybrid approach with individual elements and markdown collections
Insertr allows developers to make any website content editable by simply adding a CSS class. Clients can then log in and edit content directly on their website without needing to learn a complex admin interface.
Insertr allows developers to make any website content editable by simply adding a CSS class. Clients get appropriate editing interfaces based on content type - individual fields for headlines/buttons, rich markdown editors for flowing content.
## ✨ The Vision
## ✨ Two Editing Approaches
### **Element-Level Editing** (Granular Control)
```html
<!-- Developer adds this once -->
<script src="https://insertr.example.com/insertr.js"></script>
<h1 class="insertr" data-content-id="headline">Main Title</h1>
<p class="insertr" data-content-id="subtitle">Short description</p>
<a class="btn-primary insertr" data-content-id="cta">Get Started</a>
```
<!-- Then marks content as editable -->
<div class="insertr" data-content-id="hero">
<h1>Your Editable Content</h1>
Perfect for:
- Headlines, titles, taglines
- Call-to-action buttons (text + URL)
- Contact information
- Short descriptions
### **Markdown Collections** (Rich Content)
```html
<div class="insertr" data-content-id="story" data-field-type="markdown">
<p>Our company was founded in 2020...</p>
<p>We recognized that **small businesses** needed access to...</p>
<p>Today, we've helped over [200 clients](portfolio) achieve their goals.</p>
</div>
```
**That's it!** Your clients can now edit this content inline.
Perfect for:
- Story sections, articles
- Team member bios
- Product descriptions
- Multi-paragraph content with formatting
**That's it!** Your clients get the right editing experience for each content type.
## 🎯 Three User Types
@@ -27,23 +45,28 @@ Insertr allows developers to make any website content editable by simply adding
### 2. **The Client** (Content Manager)
- Logs in to see the same website with subtle edit buttons
- Clicks edit buttons to modify content inline
- Can edit both simple text and rich markdown content
- Changes are saved immediately
- Gets appropriate editing interfaces:
- **Text inputs** for headlines and short content
- **Link editors** for buttons (text + URL fields)
- **Markdown editors** for rich content with formatting
- Changes are saved immediately with responsive overlay forms
### 3. **The Developer** (You)
- Simple integration: just add `class="insertr"`
- **Element-level**: Add `class="insertr"` to any individual element
- **Markdown collections**: Add `data-field-type="markdown"` for rich content
- No complex setup or framework dependencies
- Works with any existing website
## 🚀 Current Status
**✅ Frontend Prototype Complete**
- Working edit-in-place functionality
- Mock authentication system
- Local persistence
- Multi-page support
- Responsive design
- **Hybrid editing system**: Element-level + markdown collections
- **Smart field detection**: Automatic input types based on HTML tags
- **Responsive overlay forms**: Width adapts to element size
- **Markdown support**: Powered by marked.js for rich content
- **Mock authentication system** with role switching
- **Local persistence** with automatic saving
- **Multi-page support** with consistent experience
**🔄 In Development**
- Go backend with REST API
@@ -67,6 +90,44 @@ Insertr allows developers to make any website content editable by simply adding
3. **Open http://localhost:3000** and test the demo!
## 📚 Integration Guide
### Basic Setup
```html
<!-- Include marked.js for markdown support -->
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
<!-- Include Insertr -->
<script src="insertr/insertr.js"></script>
```
### Element-Level Editing
```html
<!-- Headlines get text input -->
<h1 class="insertr" data-content-id="main-title">Your Title</h1>
<!-- Paragraphs get textarea -->
<p class="insertr" data-content-id="description">Your description</p>
<!-- Buttons get text + URL fields -->
<a href="/contact" class="btn-primary insertr" data-content-id="cta">Contact Us</a>
```
### Markdown Collections
```html
<!-- Rich content gets markdown editor -->
<div class="insertr" data-content-id="story" data-field-type="markdown">
<p>Your story begins here...</p>
<p>Use **bold**, *italic*, and [links](url) naturally.</p>
<p>Line breaks create new paragraphs automatically.</p>
</div>
```
### Field Types
- `<h1-h6>.insertr` → Text input with appropriate length limits
- `<p>.insertr` → Textarea (larger for `.lead` paragraphs)
- `<a>.insertr` → Link editor with text and URL fields
- `<div data-field-type="markdown">` → Large markdown editor with formatting support
## 📖 Documentation
- **[Development Guide](DEVELOPMENT.md)** - Setup, workflow, and contribution guide
@@ -76,7 +137,8 @@ Insertr allows developers to make any website content editable by simply adding
## 🏗️ Architecture
### Current (Phase 1)
- **Frontend**: Vanilla JavaScript + Alpine.js
- **Frontend**: Vanilla JavaScript with marked.js for markdown
- **Editing**: Hybrid element-level + markdown collection system
- **Storage**: localStorage (for prototype)
- **Authentication**: Mock system
- **Deployment**: Static files
@@ -96,12 +158,13 @@ Insertr allows developers to make any website content editable by simply adding
## 🎬 Demo Features
**Try the prototype to see:**
- ✏️ **Inline Editing** - Click edit buttons to modify content
- 📝 **Rich Content** - Markdown editing for formatted text
- ✏️ **Element-Level Editing** - Individual text inputs for headlines, textareas for paragraphs, link editors for buttons
- 📝 **Markdown Collections** - Large editor for rich content with **bold**, *italic*, [links], line breaks → paragraphs
- 🎯 **Smart Field Detection** - Automatic input types based on HTML tags and classes
- 📏 **Responsive Overlays** - Edit forms resize to match element width
- 👤 **Authentication** - Login simulation with role switching
- 💾 **Persistence** - Content saves automatically
- 📱 **Responsive** - Works on desktop and mobile
- 🔄 **Multi-page** - Consistent experience across pages
- 💾 **Persistence** - Content saves automatically with localStorage
- 🔄 **Multi-page** - Consistent experience across pages (index.html + about.html)
## 🤝 Contributing

View File

@@ -11,9 +11,7 @@
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<div class="insertr" data-content-id="nav-logo">
<h1 class="logo">Acme Consulting</h1>
</div>
<h1 class="logo insertr" data-content-id="nav-logo">Acme Consulting</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
@@ -30,23 +28,21 @@
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="insertr" data-content-id="about-hero" data-content-type="rich">
<h1>About Acme Consulting</h1>
<p class="lead">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
<h1 class="insertr" data-content-id="about-title">About Acme Consulting</h1>
<p class="lead insertr" data-content-id="about-subtitle">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
</section>
<!-- Story Section -->
<section class="services">
<div class="container">
<div class="insertr" data-content-id="our-story" data-content-type="rich">
<h2>Our Story</h2>
<h2 class="insertr" data-content-id="story-title">Our Story</h2>
<div class="insertr" data-content-id="story-content" data-field-type="markdown">
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
<p>Today, we've helped over 200 businesses streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
<p>Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
</div>
</div>
</section>
@@ -54,31 +50,29 @@
<!-- Team Section -->
<section class="cta">
<div class="container">
<div class="insertr" data-content-id="team-section">
<h2>Our Team</h2>
<p>We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
</div>
<h2 class="insertr" data-content-id="team-title">Our Team</h2>
<p class="insertr" data-content-id="team-subtitle">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<div class="services-grid" style="margin-top: 3rem;">
<div class="service-card">
<div class="insertr" data-content-id="team-member-1" data-content-type="rich">
<div class="insertr" data-content-id="member1-info" data-field-type="markdown">
<h3>Sarah Chen</h3>
<p><strong>Founder & CEO</strong></p>
<p>Former McKinsey consultant with 15 years of experience in strategy and operations. MBA from Stanford.</p>
<p>Former **McKinsey consultant** with 15 years of experience in strategy and operations. MBA from Stanford.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="team-member-2" data-content-type="rich">
<div class="insertr" data-content-id="member2-info" data-field-type="markdown">
<h3>Michael Rodriguez</h3>
<p><strong>Head of Operations</strong></p>
<p>20 years in manufacturing and supply chain optimization. Expert in lean methodologies and process improvement.</p>
<p>20 years in manufacturing and supply chain optimization. Expert in **lean methodologies** and process improvement.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="team-member-3" data-content-type="rich">
<div class="insertr" data-content-id="member3-info" data-field-type="markdown">
<h3>Emma Thompson</h3>
<p><strong>Digital Strategy Lead</strong></p>
<p>Former tech startup founder turned consultant. Specializes in digital transformation and technology adoption.</p>
<p>Former tech startup founder turned consultant. Specializes in *digital transformation* and technology adoption.</p>
</div>
</div>
</div>
@@ -88,21 +82,19 @@
<!-- Values Section -->
<section class="testimonial">
<div class="container">
<div class="insertr" data-content-id="values-section" data-content-type="rich">
<h2 style="margin-bottom: 2rem;">Our Values</h2>
<h2 class="insertr" data-content-id="values-title" style="margin-bottom: 2rem;">Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3>Client-First</h3>
<p>Every recommendation we make is designed with your specific business context and goals in mind.</p>
<h3 class="insertr" data-content-id="value1-title">Client-First</h3>
<p class="insertr" data-content-id="value1-desc">Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3>Practical Solutions</h3>
<p>We believe in strategies that you can actually implement with your current resources and capabilities.</p>
<h3 class="insertr" data-content-id="value2-title">Practical Solutions</h3>
<p class="insertr" data-content-id="value2-desc">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3>Long-term Partnership</h3>
<p>We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
<h3 class="insertr" data-content-id="value3-title">Long-term Partnership</h3>
<p class="insertr" data-content-id="value3-desc">We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
</div>
</div>
@@ -111,16 +103,13 @@
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="insertr" data-content-id="footer-info">
<p>&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p>📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
<p class="insertr" data-content-id="footer-copyright">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" data-content-id="footer-contact">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
</footer>
<!-- Insertr JavaScript Library -->
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="insertr/insertr.js"></script>
</body>
</html>

View File

@@ -85,6 +85,7 @@
</footer>
<!-- Insertr JavaScript Library -->
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
<script src="insertr/insertr.js"></script>
</body>
</html>

View File

@@ -45,7 +45,6 @@
.insertr-edit-overlay {
position: absolute;
z-index: 1000;
min-width: 300px;
}
/* Edit form container */
@@ -55,7 +54,8 @@
border-radius: 8px;
padding: 1rem;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
min-width: 280px;
width: 100%;
box-sizing: border-box;
}
/* Form header */
@@ -111,6 +111,14 @@
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.insertr-markdown-editor {
min-height: 200px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.9rem;
line-height: 1.5;
background-color: #f8fafc;
}
/* Form actions */
.insertr-form-actions {
display: flex;

View File

@@ -42,6 +42,36 @@ class Insertr {
if (this.options.autoInit) {
this.init();
}
// Initialize markdown support
this.initializeMarkdown();
}
initializeMarkdown() {
// Check if marked is available
if (typeof marked === 'undefined' && typeof window.marked === 'undefined') {
console.warn('Marked.js not loaded! Markdown collections will not work.');
return;
}
// Get the marked object
this.marked = window.marked || marked;
this.markedParser = this.marked.marked || this.marked.parse || this.marked;
if (typeof this.markedParser !== 'function') {
console.warn('Cannot find marked parse function');
return;
}
console.log('✅ Marked.js loaded successfully');
// Configure marked for basic use - keep it simple for now
if (this.marked.use) {
this.marked.use({
breaks: true,
gfm: true
});
}
}
async init() {
@@ -99,6 +129,17 @@ class Insertr {
}
generateFieldConfig(element) {
// Check for markdown collection type first
const fieldType = element.getAttribute('data-field-type');
if (fieldType === 'markdown') {
return {
type: 'markdown',
label: 'Content (Markdown)',
rows: 8,
placeholder: 'Enter content in Markdown format...\n\nUse **bold**, *italic*, [links](url), and double line breaks for new paragraphs.'
};
}
const tagName = element.tagName;
let config = { ...this.fieldTypeMap[tagName] } || { type: 'text', label: 'Content', placeholder: 'Enter content...' };
@@ -186,7 +227,16 @@ class Insertr {
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (config.type === 'link' && config.includeUrl) {
if (config.type === 'markdown') {
// Markdown collection editing
formHTML += `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
rows="${config.rows || 8}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
</div>
`;
} else if (config.type === 'link' && config.includeUrl) {
// Special handling for links - text and URL fields
const element = this.editableElements.get(contentId);
const currentUrl = element.href || '';
@@ -283,7 +333,7 @@ class Insertr {
overlay.style.top = `${rect.bottom + scrollTop + 10}px`;
overlay.style.left = `${rect.left + scrollLeft}px`;
overlay.style.zIndex = '1000';
overlay.style.minWidth = `${Math.min(300, rect.width)}px`;
overlay.style.width = `${Math.max(300, rect.width)}px`;
}
async saveElementContent(contentId, form) {
@@ -294,7 +344,11 @@ class Insertr {
let newContent = {};
// Extract form data based on field type
if (config.type === 'link' && config.includeUrl) {
if (config.type === 'markdown') {
// For markdown, store just the string directly
const input = form.querySelector('textarea[name="content"]');
newContent = input.value;
} else if (config.type === 'link' && config.includeUrl) {
newContent.text = form.querySelector('input[name="text"]').value;
newContent.url = form.querySelector('input[name="url"]').value;
} else {
@@ -336,7 +390,10 @@ class Insertr {
applyContentToElement(element, content) {
const config = element._insertrConfig;
if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
if (config.type === 'markdown') {
// Handle markdown collection - content is a string
this.applyMarkdownContent(element, content);
} else if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
// Update link text and URL
element.textContent = content.text || element.textContent;
if (content.url) {
@@ -348,6 +405,46 @@ class Insertr {
}
}
applyMarkdownContent(element, markdownText) {
if (!this.markedParser) {
console.error('Marked parser not available');
element.textContent = markdownText;
return;
}
// Ensure we have a string
if (typeof markdownText !== 'string') {
console.error('Expected markdown string, got:', typeof markdownText, markdownText);
element.textContent = 'Content type error: ' + typeof markdownText;
return;
}
try {
// Convert markdown to HTML
const html = this.markedParser(markdownText);
if (typeof html !== 'string') {
console.error('Marked parser returned non-string:', typeof html, html);
element.textContent = 'Markdown parsing error';
return;
}
// Store original edit button
const editBtn = element.querySelector('.insertr-edit-btn');
// Update element content
element.innerHTML = html;
// Re-add edit button
if (editBtn) {
element.appendChild(editBtn);
}
} catch (error) {
console.error('Error parsing markdown:', error);
element.textContent = 'Markdown parsing error: ' + error.message;
}
}
cancelEditing(contentId) {
const element = this.editableElements.get(contentId);
if (!element) return;
@@ -371,6 +468,31 @@ class Insertr {
}
extractContentFromElement(element) {
const config = element._insertrConfig;
if (config.type === 'markdown') {
// For markdown collections, return the stored markdown or extract from cache
const contentId = element.getAttribute('data-content-id');
const cached = this.state.contentCache.get(contentId);
if (cached) {
// Handle both old format (object) and new format (string)
if (typeof cached === 'string') {
return cached;
} else if (cached.text && typeof cached.text === 'string') {
return cached.text;
}
}
// Fallback: extract basic text content (this happens on first edit)
const clone = element.cloneNode(true);
const editBtn = clone.querySelector('.insertr-edit-btn');
if (editBtn) editBtn.remove();
// Convert basic HTML structure to markdown
return this.basicHtmlToMarkdown(clone.innerHTML);
}
// Clone element to avoid modifying original
const clone = element.cloneNode(true);
@@ -384,6 +506,36 @@ class Insertr {
return clone.textContent.trim();
}
basicHtmlToMarkdown(html) {
// Simple conversion for initial content only
let markdown = html;
// Basic paragraph conversion - capture content and trim each paragraph
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, (match, content) => {
return content.trim() + '\n\n';
});
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
// Remove HTML tags
markdown = markdown.replace(/<[^>]*>/g, '');
// Clean up entities
markdown = markdown.replace(/&nbsp;/g, ' ');
markdown = markdown.replace(/&amp;/g, '&');
markdown = markdown.replace(/&lt;/g, '<');
markdown = markdown.replace(/&gt;/g, '>');
// Aggressive whitespace cleanup
markdown = markdown
.split('\n') // Split into lines
.map(line => line.trim()) // Trim each line
.join('\n') // Rejoin
.replace(/\n\n\n+/g, '\n\n') // Multiple blank lines to double
.trim(); // Final trim
return markdown;
}
// Authentication and UI methods (unchanged from previous version)
setupAuthenticationControls() {
const authToggle = document.getElementById('auth-toggle');
@@ -489,6 +641,29 @@ class Insertr {
return div.innerHTML;
}
// Helper method to detect if a paragraph should have 'lead' class
isLeadParagraph(text) {
return text.length > 100 && (
text.toLowerCase().includes('we help') ||
text.toLowerCase().includes('our team') ||
text.toLowerCase().includes('experience') ||
text.toLowerCase().includes('business') ||
text.toLowerCase().includes('services')
);
}
// Helper method to detect button-like text
looksLikeButton(text) {
const buttonKeywords = [
'get started', 'start', 'begin', 'click here', 'learn more',
'contact', 'call', 'schedule', 'book', 'download', 'sign up',
'register', 'join', 'subscribe', 'buy', 'order', 'purchase'
];
const lowerText = text.toLowerCase();
return buttonKeywords.some(keyword => lowerText.includes(keyword));
}
// Storage methods
loadContentFromStorage() {
try {