Compare commits

...

10 Commits

Author SHA1 Message Date
16ad759880 Fix template deduplication by separating structure comparison from content storage
- Replace content-aware extractCleanTemplate with structure-only extractStructureSignature for template comparison
- Add extractTemplateForStorage to preserve actual content for meaningful template display
- Update generateTemplateSignature to use purely structural comparison ignoring text content
- Remove redundant extractClassSignature function (functionality moved to extractStructureSignature)
- Resolves issue where identical DOM structures created multiple templates due to content differences
- Knowledge cards and other collections now correctly deduplicate to single templates while preserving content for previews
2025-11-01 23:09:46 +01:00
163cbf7eea Implement live collection preview system with contextual template selection
Replace isolated template previews with live collection reconstruction:
- Frontend now reconstructs collection container with all template variants
- Users click directly on rendered templates in proper CSS context
- Perfect preservation of grid/flex layouts and responsive behavior
- Simplified API: preview endpoint returns container_html + templates for frontend reconstruction
- Enhanced UX: WYSIWYG template selection shows exactly what will be added
- Removed redundant templates endpoint in favor of unified preview approach

Backend changes:
- Add GET /api/collections/{id}/preview endpoint
- Remove GET /api/collections/{id}/templates endpoint
- Return container HTML + templates for frontend reconstruction

Frontend changes:
- Replace isolated template modal with live collection preview
- Add generateLivePreview() method for container reconstruction
- Update CollectionManager to use preview API
- Add interactive CSS styling for template selection

This provides true contextual template selection where CSS inheritance,
grid layouts, and responsive design work perfectly in preview mode.
2025-10-31 22:41:12 +01:00
81ec8edf36 Removed individual demo configs. 2025-10-31 21:05:55 +01:00
900f91bc25 Improve collection management: fix template selection UI and item positioning
This commit addresses multiple collection management issues to improve user experience:

## Template Selection Modal Improvements
- Replace inline styles with CSS classes for reliable visual feedback
- Fix default template selection conflicts that showed multiple templates as selected
- Add styled template previews that show actual CSS styling differences
- Improve modal responsiveness and visual hierarchy

## Collection Item Creation Fixes
- Fix empty collection items with no content/height that were unclickable
- Preserve template placeholder content during item creation instead of clearing it
- Implement proper positioning system using GetMaxPosition to place new items at collection end
- Add position calculation logic to prevent new items from jumping to beginning

## Backend Positioning System
- Add GetMaxPosition method to all repository implementations (SQLite, PostgreSQL, HTTPClient)
- Update CreateCollectionItemFromTemplate to calculate correct position (maxPos + 1)
- Maintain reconstruction ordering by position ASC for consistent item placement

## Frontend Template Selection
- CSS class-based selection states replace problematic inline style manipulation
- Template previews now render actual HTML with real page styling
- Improved hover states and selection visual feedback
- Fixed auto-selection interference with user interaction

These changes ensure collection items appear in expected order and template selection
provides clear visual feedback with actual styling previews.
2025-10-30 22:06:44 +01:00
00255cb105 Implement class-based template differentiation and fix collection item creation
- Add class-based template comparison to differentiate styling variants
- Implement template deduplication based on structure + class signatures
- Add GetCollectionTemplate method to repository interface and implementations
- Fix collection item creation by replacing unimplemented CreateCollectionItemAtomic
- Add template selection modal with auto-default selection in frontend
- Generate meaningful template names from distinctive CSS classes
- Fix unique constraint violations with timestamp-based collection item IDs
- Add collection templates API endpoint for frontend template fetching
- Update simple demo with featured/compact/dark testimonial variants for testing
2025-10-27 21:02:59 +01:00
0bad96d866 Fix collection content injection regression introduced during engine refactoring
Restore missing content hydration logic in reconstructCollectionItems method that was accidentally removed during the engine file split (b46f643). Collection items were appearing empty instead of displaying original developer content. This fix restores the database-first behavior where content is properly extracted, stored, and injected back into .insertr elements within collection items.
2025-10-26 21:45:03 +01:00
448b66a974 Fix critical enhancement hanging bug caused by nil context in content injection
Replace nil context with context.Background() in content.go to prevent database operations from hanging indefinitely. Clean up outdated documentation files and add current project structure analysis.
2025-10-26 21:26:48 +01:00
b46f643df7 Refactor engine into focused files to improve maintainability
Split monolithic engine.go (776 lines) into specialized files:
- engine.go: Core orchestration (142 lines, 82% reduction)
- collection.go: Collection processing and management (445 lines)
- content.go: Content injection and extraction (152 lines)
- discovery.go: Element discovery and DOM traversal (85 lines)

Benefits:
- Single responsibility principle applied to each file
- Better code organization and navigation
- Improved testability of individual components
- Easier team development and code reviews
- Maintained full API compatibility with no breaking changes
2025-10-26 19:15:55 +01:00
a52d9bb600 Consolidate DOM manipulation utilities to eliminate code duplication
- Move addClass and setAttribute from ContentEngine/Injector to utils.go
- Remove duplicate hasInsertrClass implementation
- Add RemoveClass and HasClass utilities for completeness
- Eliminates 74+ lines of exact duplication across files
2025-10-26 18:07:12 +01:00
c34a1a033e Manual code review by an actual human. 2025-10-24 20:53:49 +02:00
28 changed files with 2992 additions and 1935 deletions

View File

@@ -1,20 +0,0 @@
# Before v0.1
- [x] .insertr-gate
- [x] .insertr
- [ ] .insertr-content / .insertr-article
- [x] .insertr-add
- [ ] .insertr history and version control. Users can see previous version and see who changed what.
- [ ] Authentication
- [ ] Set up Authentik app
- [ ] Dev dashboard
- [ ] Overview of your sites
- [ ] Manage editor access
- [ ] User dashboard?
- [ ] Production checklist
- [ ] Library served from CDN
- [ ] Clean up app configuration
- [ ] Complete documentation.
# Sometime
- [ ] Product/library website

View File

@@ -1,347 +0,0 @@
# Feature Document: Draft/Publish System for Insertr CMS
## Overview
This document outlines the design and implementation plan for adding draft/publish functionality to Insertr CMS. Currently, all content changes are immediately reflected when enhancement is triggered manually. This feature will introduce a proper editorial workflow with draft states and controlled publishing.
## Problem Statement
### Current State
- All content stored in database is immediately available for enhancement
- Manual "Enhance" button triggers immediate file updates with latest content
- No separation between working drafts and production-ready content
- Not suitable for production environments where editorial approval is needed
### User Pain Points
- Editors cannot safely make changes without affecting live site
- No preview workflow for reviewing changes before going live
- All-or-nothing manual enhancement process
- No rollback mechanism for published content
## Industry Research: How Other CMS Handle Drafts
### WordPress
**Approach**: Single table (`wp_posts`) with state field (`post_status`)
**States**: draft, pending, publish, future, private, trash, auto-draft
**Storage**: All content in one table, differentiated by status field
**Pros**: Simple schema, easy queries, unified storage
**Cons**: No separation between draft and live data, potential performance issues
### Drupal
**Approach**: Content moderation module with workflow states
**States**: Configurable (draft, needs_review, published, archived, etc.)
**Storage**: Moderation state entities linked to content revisions
**Pros**: Flexible workflows, proper revision tracking, role-based transitions
**Cons**: Complex architecture, steep learning curve
### Contentful (Headless)
**Approach**: Separate published/draft versions with sync API
**States**: draft, published, changed, archived
**Storage**: Maintains both draft and published versions simultaneously
**Pros**: Performance optimized, global CDN delivery, precise change tracking
**Cons**: Complex API, higher storage overhead, sync complexity
### Ghost
**Approach**: Single table with status field plus scheduled publishing
**States**: draft, published, scheduled, sent
**Storage**: Uses `status` field + `published_at` timestamp
**Pros**: Simple but effective, good scheduling support
**Cons**: Limited editorial workflow, no approval processes
### Strapi
**Approach**: Draft & Publish feature with timestamp-based differentiation
**States**: draft, published
**Storage**: Single table with `published_at` field (null = draft)
**Pros**: Clean API separation, optional feature, good performance
**Cons**: Limited workflow states, manual schema management
## Requirements
### Functional Requirements
- **FR1**: Editors can save draft content without affecting published site
- **FR2**: Editors can preview changes before publishing
- **FR3**: Authorized users can publish draft content to live site
- **FR4**: System supports rollback to previous published versions
- **FR5**: Clear visual indication of draft vs published state
- **FR6**: Auto-save functionality to prevent content loss
### Non-Functional Requirements
- **NFR1**: Backward compatibility with existing content
- **NFR2**: Minimal performance impact on content editing
- **NFR3**: Support for concurrent editing workflows
- **NFR4**: Audit trail for all publishing actions
## Recommended Solution: State-Based Approach
Based on industry research and our existing architecture, we recommend following the **WordPress/Ghost pattern** with a state field approach. This provides the best balance of simplicity, performance, and functionality.
### Schema Changes
**Core Change**: Add state tracking to existing `content_versions` table:
```sql
-- Add state column to existing content_versions table
ALTER TABLE content_versions ADD COLUMN state TEXT DEFAULT 'history' NOT NULL
CHECK (state IN ('history', 'draft', 'live'));
-- Create index for efficient state-based queries
CREATE INDEX idx_content_versions_state ON content_versions(content_id, site_id, state);
-- Ensure only one draft and one live version per content item
CREATE UNIQUE INDEX idx_content_versions_unique_draft
ON content_versions(content_id, site_id) WHERE state = 'draft';
CREATE UNIQUE INDEX idx_content_versions_unique_live
ON content_versions(content_id, site_id) WHERE state = 'live';
```
**Migration Strategy**:
1. All existing `content_versions` entries become `state='history'`
2. Current `content` table entries migrate to `content_versions` with `state='live'`
3. Drop `content` table after migration (everything now in `content_versions`)
### Content States
| State | Description | Query Pattern |
|-------|-------------|---------------|
| `history` | Previous versions, for rollback | `WHERE state = 'history' ORDER BY created_at DESC` |
| `draft` | Current working version, not published | `WHERE state = 'draft'` |
| `live` | Currently published version | `WHERE state = 'live'` |
### Workflow Logic
**Auto-save Process**:
1. User edits content → Auto-save creates/updates `state='draft'` version
2. Only one draft version exists per content item (upsert pattern)
3. Previous draft becomes `state='history'`
**Publishing Process**:
1. User clicks "Publish" → Current draft version updated to `state='live'`
2. Previous live version becomes `state='history'`
3. Enhancement triggered with all `state='live'` content
**Rollback Process**:
1. User selects historical version → Copy to new `state='live'` version
2. Previous live version becomes `state='history'`
3. Enhancement triggered
### API Design
**New Endpoints**:
```
GET /api/content/{id}?state=draft|live|history # Get content in specific state
POST /api/content/{id}/save-draft # Save as draft (auto-save)
POST /api/content/{id}/publish # Publish draft to live
POST /api/content/{id}/rollback/{version_id} # Rollback to specific version
GET /api/content/{id}/diff # Compare draft vs live
POST /api/enhancement/preview # Preview site with draft content
GET /api/status/changes # List all unpublished changes
POST /api/content/bulk-publish # Publish multiple items
```
**Enhanced Endpoints**:
```
PUT /api/content/{id} # Now saves as draft by default
POST /api/enhancement # Only processes 'live' content
```
### Repository Layer Changes
**Core Queries**:
```go
// Get current live content for enhancement
func (r *Repository) GetLiveContent(siteID, contentID string) (*Content, error) {
return r.queryContent(siteID, contentID, "live")
}
// Get current draft for editing
func (r *Repository) GetDraftContent(siteID, contentID string) (*Content, error) {
return r.queryContent(siteID, contentID, "draft")
}
// Save as draft (upsert pattern)
func (r *Repository) SaveDraft(content *Content) error {
// Mark existing draft as history
r.updateState(content.ID, content.SiteID, "draft", "history")
// Insert new draft
return r.insertContentVersion(content, "draft")
}
// Publish draft to live
func (r *Repository) PublishDraft(siteID, contentID, publishedBy string) error {
// Mark existing live as history
r.updateState(contentID, siteID, "live", "history")
// Update draft to live
return r.updateState(contentID, siteID, "draft", "live")
}
```
## Strengths of This Approach
### 1. **Simplicity**
- Single table with state field (WordPress/Ghost pattern)
- Minimal schema changes to existing system
- Easy to understand and maintain
### 2. **Performance**
- Efficient state-based queries with proper indexing
- No complex joins between draft/live tables
- Leverages existing version history system
### 3. **Backward Compatibility**
- Existing content migrates cleanly to 'live' state
- Current APIs work with minimal changes
- Gradual rollout possible
### 4. **Storage Efficiency**
- No duplicate content storage (unlike Contentful approach)
- Reuses existing version infrastructure
- History naturally maintained
### 5. **Query Simplicity**
```sql
-- Get all draft content for a site
SELECT * FROM content_versions WHERE site_id = ? AND state = 'draft';
-- Get all live content for enhancement
SELECT * FROM content_versions WHERE site_id = ? AND state = 'live';
-- Check if content has unpublished changes
SELECT COUNT(*) FROM content_versions
WHERE content_id = ? AND site_id = ? AND state = 'draft';
```
## Weaknesses and Potential Roadblocks
### 1. **State Management Complexity**
**Risk**: Ensuring state transitions are atomic and consistent
**Mitigation**:
- Use database transactions for state changes
- Implement state validation triggers
- Add comprehensive error handling
### 2. **Concurrent Editing Conflicts**
**Risk**: Multiple editors creating conflicting draft versions
**Mitigation**:
- Unique constraints prevent multiple drafts
- Last-writer-wins with conflict detection
- Consider optimistic locking for future enhancement
### 3. **Auto-save Performance**
**Risk**: Frequent auto-save creating too many history versions
**Mitigation**:
- Implement debounced auto-save (30-second intervals)
- Consider version consolidation for excessive history
- Monitor database growth patterns
### 4. **Migration Risk**
**Risk**: Data loss or corruption during content table migration
**Mitigation**:
- Comprehensive backup before migration
- Gradual migration with validation steps
- Rollback plan if migration fails
### 5. **Limited Workflow States**
**Risk**: Only 3 states may be insufficient for complex editorial workflows
**Mitigation**:
- Start simple, extend states later if needed
- Most CMS start with basic draft/live model
- Consider "scheduled" state for future enhancement
## UI/UX Changes
### Control Panel Updates
- Replace "🔄 Enhance" with "💾 Save Draft" / "🚀 Publish"
- Add state indicators: 🟡 Draft Pending, 🟢 Published, 🔴 Error
- Add "👁️ Preview Changes" button for draft enhancement
- Show "📊 Publishing Status" with count of unpublished changes
### New UI Components
- Diff viewer showing draft vs published changes
- Publishing confirmation dialog with change summary
- Bulk publishing interface for multiple content items
- Version history with rollback capability
## Implementation Plan
### Phase 1: Database Foundation (Week 1)
- [ ] Add `state` column to `content_versions` table
- [ ] Create state-based indexes and constraints
- [ ] Write migration script for existing content
- [ ] Test migration on demo sites
### Phase 2: Repository Layer (Week 2)
- [ ] Update repository interfaces for state-based queries
- [ ] Implement draft save/publish/rollback operations
- [ ] Add state transition validation
- [ ] Update existing content operations
### Phase 3: API Integration (Week 3)
- [ ] Implement new draft/publish endpoints
- [ ] Update existing endpoints for state handling
- [ ] Add preview enhancement functionality
- [ ] Implement bulk publishing API
### Phase 4: UI Implementation (Week 4)
- [ ] Update control panel with new buttons and states
- [ ] Implement auto-save functionality
- [ ] Add diff viewer and publishing dialogs
- [ ] Create publishing status dashboard
### Phase 5: Testing & Polish (Week 5)
- [ ] Comprehensive testing across demo sites
- [ ] Performance optimization and monitoring
- [ ] Error handling and edge cases
- [ ] Documentation and migration guides
## Testing Strategy
### Migration Testing
- Test content migration with various demo site configurations
- Validate data integrity before/after migration
- Test rollback procedures if migration fails
### Workflow Testing
- Draft save/publish cycles with various content types
- Concurrent editing scenarios
- Auto-save reliability under different conditions
- Enhancement preview vs live comparison
### Performance Testing
- State-based query performance with large datasets
- Auto-save frequency impact on database
- Enhancement speed with draft vs live content
## Success Metrics
### Functional Success
- ✅ Zero data loss during migration
- ✅ All demo sites work without modification post-migration
- ✅ Draft/publish workflow completes in <5 seconds
- ✅ Auto-save prevents content loss in all scenarios
### User Experience Success
- ✅ Clear visual distinction between draft and published states
- ✅ Intuitive publishing workflow requiring minimal training
- ✅ Preview functionality accurately reflects published output
### Technical Success
- ✅ State-based queries perform within 100ms
- ✅ Database size increase <10% due to state optimization
- ✅ 100% test coverage for new draft/publish functionality
## Future Enhancements
### Near-term (Next 6 months)
- **Scheduled Publishing**: Add `scheduled` state with `publish_at` timestamp
- **Bulk Operations**: Enhanced multi-content publishing interface
- **Content Conflicts**: Optimistic locking for concurrent editing
### Long-term (6+ months)
- **Approval Workflows**: Multi-step editorial approval process
- **Content Branching**: Multiple draft versions per content item
- **Real-time Collaboration**: Live editing with conflict resolution
---
*This approach follows industry best practices from WordPress and Ghost while leveraging Insertr's existing version infrastructure for maximum simplicity and reliability.*

View File

@@ -1,397 +0,0 @@
# .insertr-content Feature Specification
**Status**: Design Phase
**Version**: 0.1.0
**Last Updated**: October 16, 2025
## Executive Summary
The `.insertr-content` feature extends Insertr's CMS capabilities to handle long-form, complex content like blog posts and articles. While `.insertr` handles individual elements and `.insertr-add` manages collections, `.insertr-content` provides a rich text editing experience for larger content blocks that require sophisticated formatting, structure, and content management workflows.
## Current State Analysis
### Existing Insertr Architecture
- **StyleAwareEditor**: Rich text editing with automatic style detection
- **HTML Preservation Engine**: Perfect fidelity editing maintaining all attributes
- **Style Detection Engine**: Converts nested elements to formatting options
- **Collection Management**: `.insertr-add` for dynamic content collections
- **Static Site Enhancement**: Build-time content injection and file processing
### Gaps for Blog/Article Content
- No unified interface for long-form content editing
- Limited content structure management (headings, sections, media)
- No publishing workflow (drafts, scheduling, SEO)
- Missing blog-specific features (excerpts, metadata, relationships)
## Feature Requirements
### Core Functionality
#### 1. Rich Content Editor
```html
<!-- Developer Implementation -->
<article class="insertr-content blog-post">
<h1>Existing Title</h1>
<p>Content with <strong class="brand-highlight">custom styling</strong></p>
<blockquote class="testimonial">Styled quotes</blockquote>
</article>
```
**Capabilities:**
- Inline editing mode with contextual toolbar
- Style detection and preservation of developer CSS classes
- Block-based content management (headings, paragraphs, lists, quotes)
- Media insertion and management
- Markdown shortcuts for power users
#### 2. Content Structure Management
- Automatic heading hierarchy detection and validation
- Drag & drop block reordering
- Content outline/table of contents generation
- Block templates for common patterns
- Live content structure preview
#### 3. Enhanced Writing Experience
- Distraction-free full-screen mode
- Auto-save with conflict resolution
- Word count and reading time estimation
- Typography optimization for readability
- Smart formatting (quotes, dashes, spacing)
### Static Site Integration
#### 1. Enhanced File Processing
```bash
# Enhance existing pages
insertr enhance blog-post.html --output dist/blog-post.html
# Generate content-driven pages
insertr enhance templates/ --output dist/ --generate-content
```
#### 2. Template-Based Generation
```yaml
# insertr.yaml
content_types:
blog_posts:
template: "templates/blog-post.html"
output_pattern: "blog/{slug}.html"
fields:
title: text
content: insertr-content
excerpt: text
published_date: date
author: text
```
#### 3. Content Storage Strategy
```go
type BlogPost struct {
ID string `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Content string `json:"content"` // Rich HTML from .insertr-content
Excerpt string `json:"excerpt"`
Author string `json:"author"`
PublishedAt time.Time `json:"published_at"`
Status string `json:"status"` // draft, published, archived
Template string `json:"template"`
Metadata JSON `json:"metadata"` // SEO, social media, etc.
}
```
### Blog Management Features
#### 1. Publishing Workflow
- Draft → Review → Published status management
- Content scheduling for future publication
- Version history with rollback capabilities
- Content approval workflow for teams
#### 2. SEO and Metadata
- Meta title and description management
- Open Graph and Twitter Card optimization
- Structured data (JSON-LD) generation
- Reading time and content analysis
- Heading structure validation
#### 3. Content Organization
- Categories and tags management
- Related content suggestions
- Content series/collections
- Author management and attribution
## Technical Architecture
### Frontend Components
#### 1. ContentEditor Class
```javascript
class ContentEditor extends StyleAwareEditor {
constructor(element, options) {
super(element, {
mode: 'content',
showOutline: true,
enableMarkdownShortcuts: true,
autoSave: true,
...options
});
}
// Content-specific methods
insertBlock(type) { /* Insert heading, quote, code block */ }
reorderBlocks() { /* Drag & drop reordering */ }
generateOutline() { /* TOC generation */ }
validateStructure() { /* SEO and accessibility checks */ }
}
```
#### 2. Enhanced UI Components
- **Block Selector**: Visual insertion of headings, quotes, media
- **Outline Panel**: Collapsible content structure navigator
- **Style Panel**: Context-aware formatting options
- **Media Browser**: Integrated asset management
- **Metadata Editor**: SEO and social media optimization
#### 3. Content Structure Engine
```javascript
class ContentStructureEngine extends StyleDetectionEngine {
analyzeContentBlocks(element) {
// Detect semantic structure (headings, sections, articles)
// Generate content outline and navigation
// Identify reusable content patterns
}
validateContentStructure(structure) {
// SEO heading hierarchy validation
// Accessibility compliance checks
// Content readability analysis
}
}
```
### Backend Enhancements
#### 1. Content Generation Pipeline
```go
func (e *Enhancer) EnhanceWithContentGeneration(inputDir, outputDir string) error {
// 1. Enhance existing HTML files (current behavior)
err := e.EnhanceDirectory(inputDir, outputDir)
if err != nil {
return err
}
// 2. Generate content-driven pages
return e.generateContentPages(outputDir)
}
```
#### 2. Template Processing
- Mustache/Handlebars template engine integration
- Content type-specific template routing
- Dynamic page generation from database content
- Static asset optimization and copying
#### 3. Content API Extensions
- Blog post CRUD operations
- Content publishing workflow endpoints
- SEO metadata management
- Media upload and optimization
- Version control and history tracking
## Implementation Phases
### Phase 1: Core Rich Text Editor (4-6 weeks)
**Deliverables:**
- Basic `.insertr-content` class recognition
- Inline editing with floating toolbar
- Style detection and preservation
- Auto-save functionality
- Block-based content management
**Success Criteria:**
- Edit long-form content in-place
- Preserve all existing CSS styling
- Support basic rich text formatting
- Maintain HTML structure integrity
### Phase 2: Content Structure & Management (4-6 weeks)
**Deliverables:**
- Content outline generation
- Drag & drop block reordering
- Media insertion and management
- Heading hierarchy validation
- SEO metadata editing
**Success Criteria:**
- Navigate content via outline
- Reorder content blocks visually
- Insert and manage images/media
- Validate content structure
- Edit meta descriptions and titles
### Phase 3: Static Site Integration (6-8 weeks)
**Deliverables:**
- Template-based page generation
- Content type configuration
- Publishing workflow
- Build process integration
- Documentation and examples
**Success Criteria:**
- Generate blog pages from templates
- Publish/unpublish content
- Integrate with existing build tools
- Complete documentation
- Demo implementations
### Phase 4: Advanced Features (6-8 weeks)
**Deliverables:**
- Collaborative editing
- Advanced SEO tools
- Content relationships
- Performance optimizations
- Third-party integrations
**Success Criteria:**
- Multiple editors simultaneously
- Comprehensive SEO analysis
- Related content suggestions
- Sub-second editor load times
- Hugo/Jekyll/Gatsby examples
## Potential Challenges & Mitigation Strategies
### Technical Challenges
#### 1. **Complex Content Structure Preservation**
**Challenge**: Maintaining perfect HTML fidelity while providing rich editing
**Mitigation**:
- Extend existing HTMLPreservationEngine
- Comprehensive test suite for edge cases
- Gradual rollout with fallback mechanisms
#### 2. **Performance with Large Content**
**Challenge**: Editor performance degrades with very long articles
**Mitigation**:
- Virtual scrolling for large documents
- Lazy loading of editor features
- Incremental parsing and rendering
- Memory management optimizations
#### 3. **Style Detection Complexity**
**Challenge**: Complex CSS styling may not map well to editing interfaces
**Mitigation**:
- Configurable style mapping rules
- Developer override mechanisms
- Graceful degradation to basic formatting
- Comprehensive style detection testing
### User Experience Challenges
#### 4. **Editor Complexity vs Simplicity**
**Challenge**: Power users need advanced features, casual users need simplicity
**Mitigation**:
- Progressive disclosure of features
- Configurable interface complexity
- Role-based feature availability
- Contextual help and onboarding
#### 5. **Content Migration**
**Challenge**: Moving existing blog content into Insertr system
**Mitigation**:
- Import tools for common formats (Markdown, HTML, WordPress)
- Bulk migration utilities
- Content validation and cleanup tools
- Migration documentation and tutorials
### Integration Challenges
#### 6. **Static Site Generator Compatibility**
**Challenge**: Different SSGs have different content conventions
**Mitigation**:
- Plugin architecture for SSG-specific adaptations
- Standard export formats (Markdown, JSON, HTML)
- Configuration templates for popular SSGs
- Community-driven integration examples
#### 7. **Build Process Integration**
**Challenge**: Fitting into existing development workflows
**Mitigation**:
- CLI-first approach matching existing tools
- CI/CD pipeline integration guides
- Watch mode for development
- Incremental build optimizations
### Content Management Challenges
#### 8. **Version Control and Conflicts**
**Challenge**: Managing content changes across multiple editors and builds
**Mitigation**:
- Operational transformation for real-time collaboration
- Clear conflict resolution interfaces
- Audit trail for all content changes
- Backup and recovery mechanisms
#### 9. **SEO and Performance Balance**
**Challenge**: Rich editing features may impact site performance
**Mitigation**:
- Minimal runtime overhead for visitors
- Conditional loading of editing features
- Static generation maintains performance
- Performance monitoring and optimization
## Success Metrics
### User Adoption
- Number of sites using `.insertr-content`
- Content creation frequency
- User retention and engagement
- Community feedback and contributions
### Technical Performance
- Editor load time (target: <2 seconds)
- Content save latency (target: <500ms)
- Static site build impact (target: <10% increase)
- Memory usage optimization
### Content Quality
- Content structure validation pass rate
- SEO score improvements
- Accessibility compliance metrics
- User-reported content issues
## Open Questions & Decisions Needed
### Design Decisions
1. **Block vs. Inline Editing**: Should we prioritize block-based editing (like Gutenberg) or seamless inline editing?
2. **Markdown Support**: How much markdown compatibility should we maintain vs. pure HTML?
3. **Template Engine**: Which template engine should we standardize on for content generation?
### Technical Decisions
1. **Database Schema**: How should we structure content types and metadata in the database?
2. **API Design**: Should we extend existing APIs or create new content-specific endpoints?
3. **Caching Strategy**: How do we handle content caching across the editing and static generation pipeline?
### Integration Decisions
1. **SSG Priority**: Which static site generators should we prioritize for integration?
2. **Media Handling**: Should we build media management or integrate with existing solutions?
3. **Deployment**: How do we handle automated deployments when content is published?
## Next Steps
1. **Technical Spike** (1 week): Prototype core editing interface with existing StyleAwareEditor
2. **Design Review** (1 week): Validate UI/UX approach with user research
3. **Architecture Review** (1 week): Finalize technical architecture and database schema
4. **Phase 1 Kickoff**: Begin implementation of core rich text editor
## References
- [CLASSES.md](./CLASSES.md) - Current class system documentation
- [StyleAwareEditor](./lib/src/ui/style-aware-editor.js) - Existing editor implementation
- [HTMLPreservationEngine](./lib/src/utils/html-preservation.js) - Current preservation approach
- [Content Enhancement Pipeline](./internal/content/enhancer.go) - Static site processing
---
*This document is a living specification that will evolve as we learn more about user needs and technical constraints. All stakeholders should contribute to its refinement.*

494
RELEASE_PLAN.md Normal file
View File

@@ -0,0 +1,494 @@
# Insertr Release Plan & Feature Roadmap
**Last Updated**: October 26, 2025
**Status**: Consolidated from all project documentation
**Purpose**: Unified release planning to replace haphazard development approach
---
## Current State Assessment
### ✅ **Implemented & Working**
**Core Architecture**
- ✅ Unified Go binary (serve + enhance commands)
- ✅ Multi-database support (SQLite dev, PostgreSQL prod)
- ✅ HTML-first content processing engine
- ✅ Container expansion with syntactic sugar (`class="insertr"` on containers)
- ✅ Style detection and preservation system
- ✅ Version control with complete edit history
- ✅ Build-time content enhancement
**Authentication System**
- ✅ Mock authentication for development
- ✅ Authentik OIDC integration for production
- ✅ JWT-based session management
- ✅ Secure cookie handling
**Frontend Library**
- ✅ Zero-dependency JavaScript library (222KB built)
- ✅ Style-aware editor with automatic CSS detection
- ✅ HTML preservation engine
- ✅ API client with authentication integration
- ✅ Control panel UI
- ✅ Form-based editing interfaces
**Content Management**
- ✅ Full REST API for content operations
- ✅ Content versioning and rollback
- ✅ Multi-site content management
- ✅ Real-time content injection during development
**Class System**
-`.insertr` - Basic element editing
-`.insertr-gate` - Authentication triggers
-`.insertr-add` - Dynamic content collections
- ✅ Container expansion intelligence
**Developer Experience**
- ✅ Hot reload development workflow
- ✅ Multiple demo sites for testing
- ✅ Just/npm script integration
- ✅ Comprehensive configuration system
### 🟡 **Partially Implemented**
**Content Types**
- 🟡 `.insertr-content` - Planned but not fully implemented
- 🟡 Rich text editing - Basic implementation, needs enhancement
- 🟡 Media management - No implementation
- 🟡 SEO metadata - No implementation
**Publishing Workflow**
- 🟡 Draft/publish system - Fully designed in DRAFT_PUBLISH_FEATURE.md but not implemented
- 🟡 Content approval workflows - Not implemented
- 🟡 Scheduled publishing - Not implemented
**Error Handling & UX**
- 🟡 Error states and feedback - Basic implementation
- 🟡 Offline handling - Not implemented
- 🟡 Loading indicators - Basic implementation
- 🟡 Auto-save - Not implemented
### ❌ **Missing for v1.0**
**Critical Gaps**
- ❌ Comprehensive testing (no test files found)
- ❌ Production deployment guides
- ❌ Performance benchmarking
- ❌ Error logging and monitoring
- ❌ CDN hosting setup for library assets
**User Experience Gaps**
- ❌ Media upload and management
- ❌ SEO optimization tools
- ❌ Content search and filtering
- ❌ Bulk content operations
**Enterprise Features**
- ❌ Role-based permissions
- ❌ Multi-user collaboration
- ❌ Audit trails
- ❌ Performance monitoring
---
## Version 1.0 Release Plan
### **Target Release Date**: January 31, 2026 (3 months)
### **v1.0 Success Criteria**
> **Primary Goal**: Production-ready CMS that delivers on "The Tailwind of CMS" promise with zero-config HTML-first editing.
**Must-Have Features**:
1.**Zero Configuration**: Add `class="insertr"` to any element and get editing
2.**Perfect HTML Preservation**: Maintain all CSS styling and attributes
3.**Production Authentication**: Secure OIDC integration for real deployments
4.**Version Control**: Complete edit history with rollback capabilities
5. ⚠️ **Reliable Publishing**: Draft/publish workflow for production content management
6. ⚠️ **Essential Testing**: Comprehensive test coverage for reliability
7. ⚠️ **Production Deployment**: Clear guides for real-world deployments
**Quality Gates**:
- 🎯 **90%+ Test Coverage** across core functionality
- 🎯 **Production Deployment** examples for 3+ hosting platforms
- 🎯 **Performance Benchmarks** meeting static site standards
- 🎯 **Security Audit** completed for authentication and content handling
- 🎯 **Documentation Complete** for all core features
---
## v1.0 Feature Implementation Plan
### **Phase 1: Foundation & Quality** (4-6 weeks)
**Priority: CRITICAL** - Must complete before adding new features
#### **1.1 Comprehensive Testing Framework**
```bash
# Target test structure
tests/
├── unit/
│ ├── frontend/ # JavaScript unit tests
│ │ ├── core/ # Business logic tests
│ │ ├── ui/ # Component tests
│ │ └── utils/ # Utility function tests
│ └── backend/ # Go unit tests
│ ├── api/ # HTTP handler tests
│ ├── auth/ # Authentication tests
│ ├── db/ # Database tests
│ └── engine/ # Content processing tests
├── integration/
│ ├── api_test.go # Full API integration tests
│ ├── auth_test.go # Authentication flow tests
│ └── enhancement_test.go # Build pipeline tests
└── e2e/
├── editing_workflow/ # End-to-end editing tests
├── publishing_flow/ # Draft/publish workflow tests
└── authentication/ # Auth integration tests
```
**Implementation Tasks**:
- [ ] **JavaScript Testing**: Jest + Testing Library setup
- [ ] **Go Testing**: Unit tests for all packages with testify
- [ ] **API Integration Tests**: Full HTTP API test suite
- [ ] **E2E Testing**: Playwright tests for editing workflows
- [ ] **Performance Tests**: Benchmark suite for enhancement pipeline
- [ ] **CI/CD Integration**: GitHub Actions with test automation
**Success Criteria**:
- ✅ 90%+ code coverage across frontend and backend
- ✅ All core user workflows covered by E2E tests
- ✅ Performance benchmarks established and monitored
- ✅ CI/CD pipeline blocks releases on test failures
#### **1.2 Error Handling & Monitoring**
```go
// Target error handling structure
type InsertrError struct {
Code string `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
UserMsg string `json:"user_message"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id"`
}
type Logger interface {
Info(msg string, fields ...any)
Warn(msg string, fields ...any)
Error(msg string, err error, fields ...any)
}
```
**Implementation Tasks**:
- [ ] **Structured Logging**: JSON logging with levels and context
- [ ] **Error Types**: Standardized error codes and user messages
- [ ] **Request Tracing**: Request ID tracking through system
- [ ] **Health Checks**: Comprehensive health monitoring endpoints
- [ ] **Metrics Collection**: Prometheus-compatible metrics
- [ ] **Frontend Error Handling**: User-friendly error states and recovery
#### **1.3 Performance Optimization**
**Implementation Tasks**:
- [ ] **Frontend Bundle Optimization**: Code splitting and lazy loading
- [ ] **Database Query Optimization**: Indexes and query performance
- [ ] **Caching Layer**: Multi-tier caching for content and assets
- [ ] **Content Enhancement Performance**: Optimize HTML processing pipeline
- [ ] **Memory Management**: Proper cleanup and resource management
### **Phase 2: Draft/Publish System** (3-4 weeks)
**Priority: HIGH** - Essential for production content management
Based on comprehensive design in `DRAFT_PUBLISH_FEATURE.md`:
#### **2.1 Database Schema Implementation**
```sql
-- Add state tracking to content_versions table
ALTER TABLE content_versions ADD COLUMN state TEXT DEFAULT 'history' NOT NULL
CHECK (state IN ('history', 'draft', 'live'));
-- Create indexes for efficient state-based queries
CREATE INDEX idx_content_versions_state ON content_versions(content_id, site_id, state);
CREATE UNIQUE INDEX idx_content_versions_unique_draft
ON content_versions(content_id, site_id) WHERE state = 'draft';
CREATE UNIQUE INDEX idx_content_versions_unique_live
ON content_versions(content_id, site_id) WHERE state = 'live';
```
#### **2.2 API Implementation**
**New Endpoints**:
- `GET /api/content/{id}?state=draft|live|history`
- `POST /api/content/{id}/save-draft`
- `POST /api/content/{id}/publish`
- `POST /api/content/{id}/rollback/{version_id}`
- `GET /api/content/{id}/diff`
- `POST /api/enhancement/preview`
- `GET /api/status/changes`
#### **2.3 Frontend Implementation**
**UI Components**:
- [ ] **Draft/Publish Controls**: Replace "Enhance" with "Save Draft"/"Publish"
- [ ] **State Indicators**: Visual indicators for draft vs published content
- [ ] **Publishing Dashboard**: Overview of unpublished changes
- [ ] **Diff Viewer**: Compare draft vs published content
- [ ] **Auto-save**: LocalStorage drafts with conflict resolution
### **Phase 3: Essential Features** (4-5 weeks)
**Priority: HIGH** - Core features expected in modern CMS
#### **3.1 Media Management System**
```go
// Media handling architecture
type MediaAsset struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
URL string `json:"url"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
}
```
**Implementation Tasks**:
- [ ] **File Upload API**: Multipart upload with validation
- [ ] **Image Optimization**: Automatic resizing and format conversion
- [ ] **CDN Integration**: Asset hosting and delivery optimization
- [ ] **Media Browser**: Frontend file management interface
- [ ] **Image Editor Integration**: Basic crop/resize functionality
#### **3.2 SEO & Metadata Management**
```javascript
// SEO interface structure
class SEOManager {
generateMetaTags(content) {
// Auto-generate meta descriptions
// Open Graph optimization
// Twitter Card generation
}
analyzeContent(html) {
// Heading structure validation
// Readability scoring
// SEO recommendations
}
}
```
**Implementation Tasks**:
- [ ] **Meta Field Management**: Title, description, keywords
- [ ] **Open Graph Tags**: Social media optimization
- [ ] **Structured Data**: JSON-LD schema generation
- [ ] **Content Analysis**: SEO recommendations and scoring
- [ ] **Sitemap Generation**: Automatic XML sitemap creation
#### **3.3 Enhanced Content Types**
**Complete `.insertr-content` Implementation**:
- [ ] **Rich Text Editor**: Enhanced editing with formatting toolbar
- [ ] **Block Management**: Drag-and-drop content blocks
- [ ] **Style Detection**: Advanced CSS style preservation
- [ ] **Content Structure**: Heading hierarchy validation
- [ ] **Markdown Support**: Optional markdown shortcuts
### **Phase 4: Production Readiness** (2-3 weeks)
**Priority: CRITICAL** - Must complete for v1.0 release
#### **4.1 Deployment & Documentation**
**Implementation Tasks**:
- [ ] **Production Deployment Guides**: Netlify, Vercel, CloudFlare Pages
- [ ] **CDN Setup**: Library hosting and version management
- [ ] **Docker Images**: Containerized deployment options
- [ ] **Database Migrations**: Schema versioning and update scripts
- [ ] **Security Documentation**: Authentication setup and best practices
#### **4.2 Developer Experience**
**Implementation Tasks**:
- [ ] **CLI Enhancements**: Project scaffolding and migration tools
- [ ] **Integration Examples**: Hugo, Jekyll, Next.js, Gatsby
- [ ] **VS Code Extension**: Syntax highlighting and tooling
- [ ] **Development Tools**: Debug mode and diagnostic utilities
- [ ] **Performance Profiling**: Development optimization tools
#### **4.3 Quality Assurance**
**Implementation Tasks**:
- [ ] **Security Audit**: Authentication and content handling review
- [ ] **Performance Benchmarking**: Compare against competing solutions
- [ ] **Accessibility Audit**: WCAG compliance for editor interfaces
- [ ] **Browser Compatibility**: Cross-browser testing and support
- [ ] **Load Testing**: Multi-site performance under load
---
## v1.0 Feature Checklist
### **Core Functionality**
-`.insertr` class editing with style preservation
-`.insertr-gate` authentication integration
-`.insertr-add` dynamic content collections
- ⚠️ `.insertr-content` rich text editing (needs enhancement)
- ⚠️ Version control with rollback (needs UI polish)
- ❌ Draft/publish workflow
- ❌ Media upload and management
- ❌ SEO metadata management
### **Authentication & Security**
- ✅ Mock authentication for development
- ✅ Authentik OIDC for production
- ✅ JWT session management
- ❌ Role-based permissions (v2.0)
- ❌ Security audit completion
### **API & Backend**
- ✅ Full REST API for content operations
- ✅ Multi-database support (SQLite/PostgreSQL)
- ✅ Content versioning system
- ❌ Draft/publish endpoints
- ❌ Media upload endpoints
- ❌ Performance monitoring
### **Frontend & UI**
- ✅ Zero-dependency JavaScript library
- ✅ Style-aware editor
- ✅ Control panel interface
- ❌ Draft/publish UI controls
- ❌ Media browser interface
- ❌ Error states and loading indicators
- ❌ Auto-save functionality
### **Developer Experience**
- ✅ Hot reload development workflow
- ✅ Multi-site demo environment
- ✅ Configuration management
- ❌ Comprehensive testing framework
- ❌ Production deployment guides
- ❌ CLI enhancement tools
- ❌ Integration examples
### **Quality & Performance**
- ❌ 90%+ test coverage
- ❌ Performance benchmarks
- ❌ Error handling and monitoring
- ❌ CDN integration for assets
- ❌ Browser compatibility testing
---
## Post-v1.0 Roadmap
### **Version 1.1** (Q2 2026) - Enhanced UX
**Focus**: User experience improvements and missing convenience features
**Key Features**:
- **Real-time Collaboration**: Multi-user editing with conflict resolution
- **Advanced Media Management**: Image editing, gallery management
- **Content Templates**: Reusable content blocks and page templates
- **Enhanced SEO Tools**: Advanced analytics and optimization
- **Mobile Editing**: Responsive editor interfaces
### **Version 1.5** (Q3 2026) - Enterprise Features
**Focus**: Enterprise adoption and advanced workflows
**Key Features**:
- **Role-based Permissions**: Granular access control
- **Approval Workflows**: Multi-step content approval processes
- **Audit Trails**: Comprehensive activity logging
- **API Analytics**: Usage monitoring and optimization
- **White-label Solutions**: Agency and reseller capabilities
### **Version 2.0** (Q4 2026) - Platform Evolution
**Focus**: Platform expansion and ecosystem development
**Key Features**:
- **Plugin Architecture**: Third-party extensions and integrations
- **Visual Page Builder**: Drag-and-drop page construction
- **AI Content Assistance**: Smart suggestions and optimization
- **E-commerce Integration**: Product management and shopping carts
- **Advanced Analytics**: Content performance and user engagement
### **Version 2.5** (Q1 2027) - Next-Generation CMS
**Focus**: Innovation and market leadership
**Key Features**:
- **Edge Computing**: Content personalization at edge locations
- **Advanced AI**: Content generation and automated optimization
- **Cross-platform Publishing**: Multi-channel content distribution
- **Advanced Performance**: Sub-second global content delivery
- **Developer Ecosystem**: Marketplace and community platform
---
## Implementation Strategy
### **Development Approach**
1. **Quality First**: Comprehensive testing before new features
2. **User-Centric**: Focus on real-world use cases and pain points
3. **Performance Obsessed**: Maintain zero runtime overhead advantage
4. **Documentation Driven**: Complete docs for every feature
5. **Community Building**: Open development with transparent roadmap
### **Release Schedule**
- **Monthly Releases**: Regular feature additions and improvements
- **Security Patches**: Immediate response to security issues
- **LTS Versions**: Long-term support for major releases
- **Beta Releases**: Early access for testing and feedback
### **Success Metrics**
- **Adoption**: 1000+ production deployments by end of 2026
- **Performance**: Sub-2-second editor load times maintained
- **Community**: 100+ contributors and 5000+ GitHub stars
- **Enterprise**: 50+ enterprise customers with dedicated support
- **Ecosystem**: 20+ community plugins and integrations
---
## Risk Mitigation
### **Technical Risks**
1. **Performance Degradation**: Continuous benchmarking and optimization
2. **Security Vulnerabilities**: Regular audits and penetration testing
3. **Browser Compatibility**: Automated cross-browser testing
4. **Scalability Issues**: Load testing and performance monitoring
### **Market Risks**
1. **Competition**: Focus on unique value proposition and innovation
2. **Adoption Barriers**: Comprehensive documentation and examples
3. **Enterprise Requirements**: Flexible architecture for custom needs
4. **Technology Evolution**: Modular design for easy adaptation
### **Operational Risks**
1. **Team Scaling**: Clear development processes and documentation
2. **Community Management**: Dedicated community engagement resources
3. **Support Load**: Self-service documentation and automation
4. **Infrastructure Costs**: Efficient resource usage and optimization
---
## Conclusion
This release plan consolidates all existing documentation into a concrete, actionable roadmap for v1.0. The focus is on completing the production-ready foundation with essential features before expanding into advanced capabilities.
**Key Principles**:
- **Quality over quantity**: Better to ship fewer features that work perfectly
- **User-focused development**: Real-world use cases drive feature priorities
- **Performance first**: Maintain the core advantage of zero runtime overhead
- **Documentation complete**: Every feature fully documented and tested
The goal is to transition from the current haphazard development approach to a structured, milestone-driven process that delivers a genuinely production-ready CMS that fulfills the "Tailwind of CMS" vision.
---
**Next Steps**:
1. Review and approve this consolidated plan
2. Begin Phase 1 implementation (Testing & Quality)
3. Establish weekly progress reviews and milestone tracking
4. Set up project management tools for feature tracking
5. Begin community engagement and early user feedback collection
*This document supersedes all previous roadmap documents and serves as the single source of truth for Insertr development planning.*

833
STRUCTURAL_ANALYSIS.md Normal file
View File

@@ -0,0 +1,833 @@
# Insertr: Comprehensive Structural Analysis & Strategic Roadmap
**Analysis Date**: October 26, 2025
**Project Status**: Full-Stack CMS - Production Ready
**Document Purpose**: Deep architectural analysis, market positioning, and strategic roadmap
## Executive Summary
Insertr represents a paradigm-shifting approach to content management that bridges the gap between traditional CMS complexity and modern developer workflows. With its "Tailwind CSS for CMS" philosophy, the project has achieved a functionally complete full-stack system that addresses significant market gaps through zero-configuration HTML-first editing.
### Key Findings
**🟢 Strengths**
- **Unique Architecture**: Build-time enhancement + runtime editing provides best-of-both-worlds performance
- **Zero Configuration**: Genuinely delivers on "just add a class" promise with no schema definition required
- **HTML Preservation**: Perfect fidelity editing maintains all CSS styling and attributes - no competing solution matches this
- **Framework Agnostic**: Works with any static site generator without architectural changes
- **Production Ready**: Full authentication, version control, and database persistence implemented
**🟡 Opportunities**
- **Market Positioning**: Significant white space between over-engineered enterprise solutions and under-powered simple tools
- **Developer Experience**: Superior DX compared to existing solutions - fastest time-to-value in the market
- **Performance Leadership**: Zero runtime overhead for regular visitors while providing rich editing for authenticated users
**🔴 Challenges**
- **Discovery Gap**: Limited market awareness of unique value proposition
- **Feature Completeness**: Missing some expected modern CMS features (media management, SEO tools, collaborative editing)
- **Enterprise Features**: Needs advanced user management and workflow capabilities for larger organizations
## 1. Architectural Analysis
### Current Architecture Strengths
**Unified Binary Approach**
- Single Go binary handles both build-time enhancement and runtime API server
- Eliminates deployment complexity common in microservice CMS architectures
- Simplifies local development workflow significantly
- Embedded frontend assets reduce dependency management
**HTML-First Content Processing**
- Perfect preservation of developer-defined CSS styling and element attributes
- Style detection engine automatically converts nested elements to formatting options
- No lossy markdown conversion - maintains complete HTML fidelity
- Element-based behavior system provides intuitive editing interfaces
**Database Architecture**
- Multi-database support (SQLite development, PostgreSQL production) with identical APIs
- Content versioning system with complete edit history and rollback capabilities
- User attribution tracking for enterprise audit requirements
- Generated code via SQLC ensures type safety and performance
**Authentication System**
- Mock authentication for development with zero configuration
- Production-ready Authentik OIDC integration with PKCE security
- JWT-based session management with secure cookie handling
- Flexible provider architecture for future authentication methods
### Architectural Innovations
**Container Expansion Intelligence**
```html
<!-- Developer writes syntactic sugar -->
<section class="hero insertr">
<h1>Hero Title</h1>
<p>Hero description</p>
<button>Call to Action</button>
</section>
<!-- System transforms to granular editing -->
<section class="hero">
<h1 class="insertr" data-content-id="hero-title-abc123">Hero Title</h1>
<p class="insertr" data-content-id="hero-desc-def456">Hero description</p>
<button class="insertr" data-content-id="hero-cta-ghi789">Call to Action</button>
</section>
```
**Style Detection & Preservation**
- Analyzes existing markup to preserve developer-defined nested styles as formatting options
- One-layer deep analysis prevents infinite complexity while maintaining design fidelity
- Shared functionality between `.insertr` and `.insertr-content` ensures consistent behavior
- Automatic generation of editing interfaces based on detected styling patterns
**Performance-First Loading**
```javascript
// Regular visitors: zero overhead
<h1 class="insertr">Welcome to Our Site</h1>
// Authenticated editors: rich editing with injected content
<h1 class="insertr" data-content-id="hero-title-abc123"
data-editor-loaded="true">Latest Content From Database</h1>
```
### Technical Architecture Score: **A- (Excellent)**
**Strengths:**
- Innovative HTML-first approach solves real developer pain points
- Clean separation of concerns between build-time and runtime functionality
- Excellent database design with proper versioning and multi-database support
- Security-conscious authentication with enterprise-grade OIDC support
**Areas for Improvement:**
- Frontend bundle optimization for large pages with many editable elements
- Real-time collaboration features for concurrent editing scenarios
- Enhanced error handling and recovery mechanisms
- Performance monitoring and optimization tooling
## 2. Market Position & Competitive Analysis
### Market Landscape Assessment
**Tier 1 Enterprise Leaders**
- **Contentful**: $489+/month, complex API setup, extensive features
- **Sanity**: Real-time collaboration, content lake architecture, developer-focused
- **Strapi**: 70k GitHub stars, fully customizable, requires setup and configuration
**Tier 2 Innovative Challengers**
- **Payload CMS**: TypeScript-first, Next.js native, still requires schema definition
- **TinaCMS**: Git-based visual editing, markdown-focused, loses HTML fidelity
- **Directus**: Database-first approach, requires existing database schema
**Tier 3 Specialized Solutions**
- **Ghost**: Publishing-focused, membership features, limited customization
- **Keystone**: GraphQL-native, React admin UI, complex setup
### Insertr's Unique Market Position
**"The Tailwind of CMS"** - Positioned between complexity and simplicity:
```
Enterprise CMS | Insertr | Simple Tools
(Over-engineered) | (Just Right) | (Under-powered)
| |
Contentful | | Ghost
Strapi | 🎯 | WordPress.com
Sanity | Perfect | Wix/Squarespace
| Spot |
```
**Competitive Advantages:**
1. **Zero Configuration**: Only solution that delivers genuine "drop-in" capability
2. **HTML Preservation**: No competitor maintains perfect design fidelity
3. **Performance**: Static site performance with dynamic editing capabilities
4. **Framework Agnostic**: Works with any static site generator without modification
**Market Gaps Addressed:**
- **Existing Static Sites**: Add CMS without architectural rebuild
- **Designer-Developer Teams**: Preserve design fidelity while enabling editing
- **Jamstack Adopters**: Simple editing without complex API integration
- **Small-Medium Business**: Immediate value without over-engineering
### Market Opportunity Score: **A (Exceptional)**
**Evidence:**
- Underserved market segment of existing static sites (millions of websites)
- Growing Jamstack adoption (Hugo: 84k stars, growing 20%+ annually)
- Developer pain points with existing solutions well-documented
- Significant pricing gap between simple and enterprise solutions
## 3. Codebase Structure Analysis
### Frontend Architecture (JavaScript Library)
**Current Structure:**
```
lib/src/
├── core/ # Business logic
│ ├── insertr.js # Core discovery and initialization
│ ├── editor.js # Content editing management
│ ├── auth.js # Authentication handling
│ └── api-client.js # Backend communication
├── ui/ # Presentation layer
│ ├── control-panel.js # Unified editing interface
│ ├── style-aware-editor.js # Rich text editing with style detection
│ ├── collection-manager.js # Dynamic content collections
│ └── form-renderer.js # Form-based editing interfaces
├── utils/ # Shared utilities
│ ├── html-preservation.js # HTML fidelity maintenance
│ └── style-detection.js # CSS style analysis engine
└── styles/
└── insertr.css # Editor styling (copied to dist)
```
**Architecture Quality: B+ (Good with room for improvement)**
**Strengths:**
- Clean separation between business logic and presentation
- Modular component architecture with clear responsibilities
- Zero external dependencies reduces bundle size and complexity
- ES6+ modules with modern JavaScript practices
**Areas for Improvement:**
- Bundle optimization for large pages with many editable elements
- State management could benefit from more formal pattern (Redux-like)
- Error boundaries and recovery mechanisms need enhancement
- Performance monitoring and analytics integration missing
### Backend Architecture (Go Binary)
**Current Structure:**
```
internal/
├── api/ # HTTP API layer
│ ├── handlers.go # HTTP request handlers
│ ├── middleware.go # Authentication, CORS, logging
│ └── models.go # Request/response models
├── auth/ # Authentication system
│ ├── auth.go # Provider interface and implementations
│ └── context.go # Request context management
├── config/ # Configuration management
│ ├── config.go # Configuration structs
│ ├── loader.go # YAML/env loading with precedence
│ └── validation.go # Configuration validation
├── db/ # Database layer
│ ├── database.go # Repository interface
│ ├── sqlite_repository.go # SQLite implementation
│ ├── postgresql_repository.go # PostgreSQL implementation
│ └── queries/ # SQL queries for SQLC generation
├── engine/ # Content processing
│ ├── engine.go # Main content processing orchestration
│ ├── content.go # Content type detection and handling
│ ├── injector.go # HTML content injection
│ ├── file.go # File system operations
│ └── utils.go # HTML parsing and manipulation utilities
└── sites/ # Multi-site management
└── manager.go # Site hosting and enhancement coordination
```
**Architecture Quality: A- (Excellent)**
**Strengths:**
- Clean hexagonal architecture with clear boundaries
- Excellent abstraction layers (repository pattern, provider interfaces)
- Type-safe database operations via SQLC code generation
- Comprehensive configuration management with environment precedence
- Security-conscious design with proper authentication handling
**Areas for Improvement:**
- Caching layer for frequently accessed content
- Metrics and observability infrastructure
- Enhanced error handling with structured logging
- Background job processing for long-running operations
### Code Quality Assessment
**Positive Indicators:**
- Consistent Go idioms and error handling patterns
- Comprehensive configuration management
- Proper separation of concerns
- Type safety throughout the codebase
- Security-conscious authentication implementation
**Technical Debt:**
- Limited test coverage (no test files present in analysis)
- Missing performance benchmarks
- Lack of metrics and monitoring instrumentation
- Error handling could be more structured
## 4. Structural Issues & Recommendations
### Critical Issues
**1. Testing Infrastructure (HIGH PRIORITY)**
```
Current State: No visible test coverage
Risk: Regression bugs, difficult refactoring, reduced confidence
Recommendation: Implement comprehensive testing strategy
- Unit tests for core business logic
- Integration tests for API endpoints
- End-to-end tests for editing workflows
- Performance benchmarks for enhancement pipeline
```
**2. Observability & Monitoring (HIGH PRIORITY)**
```
Current State: Limited logging and no metrics
Risk: Difficult debugging, no performance insights
Recommendation: Implement observability stack
- Structured logging with levels and context
- Metrics collection (Prometheus-compatible)
- Distributed tracing for request flows
- Performance monitoring and alerting
```
**3. Error Handling Standardization (MEDIUM PRIORITY)**
```
Current State: Inconsistent error handling patterns
Risk: Poor user experience, difficult debugging
Recommendation: Standardize error handling
- Consistent error types and codes
- User-friendly error messages
- Structured error logging
- Client-side error recovery patterns
```
### Performance Optimizations
**Frontend Bundle Optimization**
```javascript
// Current: Single bundle for all features
// Recommended: Code splitting and lazy loading
// Core functionality loaded immediately
import { InsertrCore } from './core/insertr.js';
// Editor loaded only when needed
const loadEditor = () => import('./core/editor.js');
// Style detection loaded on demand
const loadStyleDetection = () => import('./utils/style-detection.js');
```
**Backend Caching Strategy**
```go
// Recommended: Multi-layer caching
type CacheLayer struct {
InMemory cache.Cache // Hot content cache
Redis redis.Client // Distributed cache
FileSystem fs.Cache // Static file cache
}
// Cache content by site and ID with TTL
// Cache enhancement results with content versioning
// Cache style detection results per template
```
**Database Query Optimization**
```sql
-- Current: Individual content queries
-- Recommended: Batch operations and indexing
-- Bulk content retrieval for enhancement
SELECT * FROM content_versions
WHERE site_id = ? AND state = 'live'
ORDER BY content_id;
-- Optimized indexes for common query patterns
CREATE INDEX idx_content_site_state ON content_versions(site_id, state);
CREATE INDEX idx_versions_history ON content_versions(content_id, created_at);
```
### Scalability Considerations
**Multi-Site Performance**
- Current implementation handles multiple sites effectively
- Recommend: Site-specific content caching
- Recommend: Concurrent enhancement processing
- Recommend: Site isolation and resource limits
**Database Scaling**
- Current: Single database handles all sites
- Recommend: Connection pooling optimization
- Recommend: Read replica support for high-traffic sites
- Consider: Site-specific database sharding for enterprise
**CDN Integration**
- Current: Local file serving during development
- Recommend: CDN upload integration for enhanced files
- Recommend: Asset versioning and cache busting
- Recommend: Edge-side caching strategies
## 5. Feature Gap Analysis
### Missing Modern CMS Features
**Media Management System**
```
Current State: No integrated media handling
User Expectation: Drag-and-drop upload, optimization, CDN integration
Priority: HIGH - Essential for content creators
Implementation: Dedicated media API with image optimization
```
**SEO Optimization Tools**
```
Current State: No SEO metadata management
User Expectation: Meta tags, structured data, content analysis
Priority: HIGH - Critical for marketing sites
Implementation: SEO field management and optimization suggestions
```
**Collaborative Editing**
```
Current State: Single-user editing sessions
User Expectation: Real-time collaboration, conflict resolution
Priority: MEDIUM - Important for teams
Implementation: WebSocket-based real-time editing with operational transform
```
**Advanced User Management**
```
Current State: Simple authentication with mock/OIDC
User Expectation: Role-based permissions, team management
Priority: MEDIUM - Required for enterprise adoption
Implementation: Role-based access control with granular permissions
```
**Content Workflow & Publishing**
```
Current State: Immediate content updates
User Expectation: Draft/publish states, approval workflows
Priority: HIGH - Essential for production sites
Implementation: State-based content management (partially planned)
```
### Future Features Analysis
**Near-term Opportunities (3-6 months)**
**1. Visual Page Builder**
```html
<!-- Current: Static HTML editing -->
<div class="insertr">Editable content</div>
<!-- Future: Component-based page building -->
<div class="insertr-builder">
<component type="hero" editable="title,subtitle,cta" />
<component type="features" editable="items" repeatable="true" />
<component type="testimonials" editable="quotes" />
</div>
```
**2. E-commerce Integration**
```yaml
# Proposed: E-commerce content types
content_types:
product:
fields:
name: text
description: insertr-content
price: number
images: media[]
variants: collection
```
**3. Multi-language Support**
```go
// Proposed: Localization layer
type LocalizedContent struct {
ContentID string `json:"content_id"`
Language string `json:"language"`
Value string `json:"value"`
Status string `json:"status"` // translated, needs_review, auto_translated
}
```
**Medium-term Features (6-12 months)**
**1. AI Content Assistance**
- Smart content suggestions based on context
- Automated SEO optimization recommendations
- Translation assistance with quality scoring
- Content performance analytics and improvements
**2. Advanced Analytics**
- Content performance tracking
- User engagement metrics
- A/B testing for content variations
- Conversion tracking and optimization
**3. Enterprise Workflow Engine**
- Multi-step approval processes
- Content review and editorial workflows
- Automated publishing schedules
- Compliance and audit trails
**Long-term Vision (12+ months)**
**1. Platform Ecosystem**
- Third-party plugin architecture
- Marketplace for Insertr extensions
- Integration with popular tools (analytics, marketing, e-commerce)
- Developer SDK for custom extensions
**2. Advanced Performance Features**
- Edge-side content personalization
- Dynamic content optimization
- AI-powered performance recommendations
- Automated accessibility improvements
## 6. Strategic Recommendations
### Immediate Actions (Next 30 Days)
**1. Establish Testing Foundation**
```bash
# Implement comprehensive testing strategy
go test ./... # Unit tests
make test-integration # API integration tests
npm run test:e2e # End-to-end editing workflows
```
**2. Performance Benchmarking**
```bash
# Establish performance baselines
make benchmark-enhancement # Enhancement pipeline performance
make benchmark-api # API response times
make benchmark-frontend # Editor loading performance
```
**3. Documentation Audit**
```markdown
# Complete documentation review
- Developer onboarding guides
- API reference documentation
- Integration examples for popular frameworks
- Troubleshooting and FAQ sections
```
### Near-term Development (3-6 months)
**1. Feature Completion for v1.0**
**Draft/Publish System Implementation**
- Complete the draft/publish feature outlined in DRAFT_PUBLISH_FEATURE.md
- State-based content management with proper workflows
- Preview functionality for draft content
- Bulk publishing capabilities
**Media Management System**
```go
// Proposed media handling
type MediaAsset struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
URL string `json:"url"`
Metadata JSON `json:"metadata"` // alt text, dimensions, etc.
}
```
**SEO & Metadata Management**
```javascript
// Proposed SEO interface
class SEOManager {
generateMetaTags(content) {
// Auto-generate meta descriptions
// Structured data markup
// Open Graph optimization
}
analyzeContent(html) {
// Heading structure validation
// Readability scoring
// SEO recommendations
}
}
```
**2. Developer Experience Enhancements**
**CLI Tool Improvements**
```bash
# Enhanced CLI capabilities
insertr init # Project scaffolding
insertr content:export # Content backup and migration
insertr content:import # Content import from other CMS
insertr analyze # Site analysis and recommendations
```
**Framework Integration Examples**
```bash
# Official integration guides and examples
examples/
├── hugo-integration/ # Hugo static site example
├── next-js-integration/ # Next.js integration
├── gatsby-integration/ # Gatsby integration
└── eleventy-integration/ # 11ty integration
```
**IDE Support & Tooling**
```json
// VSCode extension for Insertr
{
"name": "insertr-vscode",
"features": [
"Syntax highlighting for .insertr classes",
"Auto-completion for content types",
"Preview integration",
"Content management panel"
]
}
```
### Medium-term Strategy (6-12 months)
**1. Market Expansion**
**Enterprise Feature Development**
- Advanced user management and role-based access control
- Multi-site management with centralized administration
- Compliance features (GDPR, SOC 2, accessibility)
- Advanced workflow and approval processes
**Agency Partnership Program**
- White-label solutions for web development agencies
- Reseller programs with technical support
- Co-marketing opportunities
- Custom integration development
**Template Marketplace**
- Insertr-enhanced themes for popular static site generators
- Component libraries with built-in editing capabilities
- Best practice examples and starter templates
- Community-contributed content types
**2. Technical Platform Evolution**
**Performance & Scalability**
```go
// Proposed scaling architecture
type InsertrCluster struct {
LoadBalancer *LoadBalancer
APINodes []*APINode // Horizontally scalable API servers
DatabasePool *DatabasePool // Connection pooling and read replicas
CacheLayer *CacheLayer // Multi-tier caching
CDN *CDNIntegration // Edge content delivery
}
```
**Real-time Collaboration**
```javascript
// Proposed collaboration engine
class CollaborationEngine {
constructor() {
this.websocket = new WebSocket('/api/collaborate');
this.operationalTransform = new OTEngine();
this.conflictResolver = new ConflictResolver();
}
handleContentChange(change) {
// Operational transform for concurrent editing
// Real-time synchronization
// Conflict detection and resolution
}
}
```
**API Evolution**
```graphql
# Proposed GraphQL API for advanced queries
type Content {
id: ID!
siteId: String!
value: String!
type: ContentType!
versions: [ContentVersion!]!
author: User!
publishedAt: DateTime
seo: SEOMetadata
}
type Query {
content(siteId: String!, filters: ContentFilters): [Content!]!
site(id: String!): Site!
analytics(siteId: String!, range: DateRange!): Analytics!
}
```
### Long-term Vision (12+ months)
**1. Platform Ecosystem Development**
**Plugin Architecture**
```go
// Proposed plugin system
type Plugin interface {
Name() string
Version() string
Initialize(config Config) error
HandleContent(content *Content) (*Content, error)
RegisterRoutes(router *Router)
Shutdown() error
}
type PluginManager struct {
plugins map[string]Plugin
config *Config
}
```
**Marketplace & Extensions**
- Third-party content type plugins
- Integration plugins for popular services
- Custom field type development
- Community-driven feature development
**2. AI & Automation Integration**
**Content Intelligence**
```go
// Proposed AI integration
type ContentAI struct {
Translation *TranslationService
SEOOptimizer *SEOService
Accessibility *A11yService
Performance *PerformanceService
}
func (ai *ContentAI) AnalyzeContent(content string) *ContentSuggestions {
// AI-powered content improvements
// SEO optimization suggestions
// Accessibility recommendations
// Performance optimizations
}
```
**Automated Workflows**
- Content scheduling and automated publishing
- SEO optimization suggestions
- Accessibility compliance checking
- Performance optimization recommendations
## 7. Competitive Strategy
### Differentiation Reinforcement
**Technical Differentiation**
1. **HTML-First Advantage**: Continue to emphasize perfect design fidelity - no competitor matches this
2. **Zero Configuration**: Maintain the "just add a class" simplicity that sets Insertr apart
3. **Performance Leadership**: Keep zero runtime overhead for regular visitors as core value prop
4. **Framework Freedom**: Remain truly framework agnostic while competitors lock into specific technologies
**Developer Experience Leadership**
1. **Fastest Time-to-Value**: From zero to editing in under 5 minutes
2. **Familiar Workflow**: Works with existing HTML/CSS knowledge
3. **Minimal Learning Curve**: No schema definition or complex setup required
4. **Powerful Defaults**: Intelligent behavior with minimal configuration
### Market Positioning Strategy
**"The Tailwind of CMS"**
- Position as the utility-first approach to content management
- Emphasize developer productivity and immediate value
- Highlight simplicity without sacrificing power
- Build community around zero-configuration philosophy
**Target Segments**
1. **Primary**: Web developers and agencies building static sites
2. **Secondary**: Design teams who need to preserve design fidelity
3. **Tertiary**: Small-medium businesses wanting simple content management
**Competitive Messaging**
```markdown
vs. Contentful: "Enterprise complexity for simple sites?"
vs. Strapi: "Why configure when you can just add a class?"
vs. Ghost: "Design freedom without platform constraints"
vs. TinaCMS: "Perfect design fidelity, not markdown approximation"
```
### Go-to-Market Strategy
**Phase 1: Developer Community (Months 1-6)**
- Open source development with transparent roadmap
- Integration examples for popular static site generators
- Conference talks and developer community engagement
- Technical blog content and tutorials
**Phase 2: Agency Partnerships (Months 6-12)**
- White-label solutions for web development agencies
- Partner program with technical support
- Case studies and success stories
- Co-marketing opportunities
**Phase 3: Enterprise Expansion (Months 12-24)**
- Enterprise features and compliance capabilities
- Sales team development and enterprise support
- Strategic partnerships with hosting providers
- Enterprise customer success program
## 8. Conclusion & Next Steps
### Project Health Assessment: **A- (Excellent)**
Insertr represents a genuinely innovative approach to content management that addresses real market gaps. The project has achieved remarkable completeness with a full-stack implementation that delivers on its core value proposition of zero-configuration HTML-first editing.
**Key Success Factors:**
1. **Unique Value Proposition**: Solves genuine pain points with no competing solution
2. **Technical Excellence**: Well-architected system with clean abstractions
3. **Market Opportunity**: Significant underserved market segments
4. **Execution Quality**: Production-ready implementation with enterprise features
**Critical Success Dependencies:**
1. **Testing & Quality**: Implement comprehensive testing to ensure reliability
2. **Performance**: Maintain speed advantage through optimization
3. **Feature Completeness**: Add missing modern CMS features (media, SEO, collaboration)
4. **Community Building**: Develop developer community and ecosystem
### Immediate Priorities (Next 30-60 Days)
1. **Establish Testing Foundation**
- Unit tests for core business logic
- Integration tests for API endpoints
- End-to-end tests for editing workflows
- Performance benchmarking suite
2. **Complete Feature Set for v1.0**
- Implement draft/publish system
- Add basic media management
- Enhance SEO metadata capabilities
- Improve error handling and user feedback
3. **Documentation & Developer Experience**
- Comprehensive integration guides
- Video tutorials and examples
- API reference documentation
- Troubleshooting guides
4. **Performance Optimization**
- Frontend bundle optimization
- Backend caching implementation
- Database query optimization
- CDN integration planning
### Strategic Recommendations
**Market Positioning**: Continue to emphasize the unique "HTML-first" and "zero-configuration" value proposition while building out missing features that prevent enterprise adoption.
**Technical Strategy**: Maintain architectural excellence while adding comprehensive testing, monitoring, and performance optimization.
**Product Strategy**: Complete the draft/publish feature, add media management, and implement collaborative editing to match modern CMS expectations.
**Go-to-Market Strategy**: Focus on developer community building through open source development, conference presentations, and high-quality documentation.
Insertr is exceptionally well-positioned to disrupt the CMS market by providing genuine simplicity without sacrificing power. The technical foundation is solid, the market opportunity is significant, and the execution quality is high. With focused effort on testing, feature completion, and community building, Insertr can become the leading solution for HTML-first content management.
---
**Document Prepared By**: Claude Code Analysis
**Analysis Scope**: Complete project structure, market research, and strategic assessment
**Confidence Level**: High (based on comprehensive codebase analysis and market research)
**Recommended Review Cycle**: Quarterly updates as project evolves

225
TODO.md
View File

@@ -1,225 +0,0 @@
# Insertr Development Roadmap
## 🎯 **Current Status** (September 2025)
### **✅ Complete Full-Stack CMS**
- **Style-Aware Editor**: Rich text editing with automatic style detection and formatting toolbar
- **HTML Preservation**: Perfect fidelity editing that maintains all element attributes and styling
- **HTTP API Server**: Full REST API with authentication, version control, and rollback
- **Multi-Database Support**: SQLite (development) + PostgreSQL (production)
- **Authentication System**: Mock (development) + Authentik OIDC (production)
- **Build-Time Enhancement**: Content injection from database to static HTML
- **Development Workflow**: Hot reload, auto-enhanced demo sites, seamless testing
- **Container Transformation**: CLASSES.md syntactic sugar - containers auto-expand to viable children
### **🏗️ Architecture Achievements**
- **Zero Configuration**: Just add `class="insertr"` to any element
- **Framework Agnostic**: Works with any static site generator
- **Performance First**: Regular visitors get pure static HTML with zero CMS overhead
- **HTML-First**: No lossy markdown conversion - perfect attribute preservation
- **Unified System**: Single HTML preservation path for all content types
- **Element-Based Behavior**: Automatic editing interface based on HTML tag semantics
---
## 🚀 **Priority Roadmap**
### **🔴 Phase 1: Editor Integration Polish** (High Priority)
#### **Frontend-Backend Integration**
- [x] **Editor-API Connection**: StyleAware editor saves successfully to HTTP API
- [ ] **Error Handling**: Proper error states, loading indicators, offline handling
- [ ] **Content Validation**: Client-side validation before API calls
- [ ] **Save Feedback**: Professional save/error feedback in editor interface
#### **User Experience Enhancements**
- [ ] **Draft Auto-Save**: LocalStorage drafts during editing with recovery
- [ ] **Optimistic Updates**: Immediate UI feedback, background sync
- [ ] **Conflict Resolution**: Handle concurrent editing scenarios
- [ ] **Editor Performance**: Optimize style detection for large pages
### **🟡 Phase 2: Production Deployment** (Medium Priority)
#### **Production Workflows**
- [ ] **CI/CD Integration**: GitHub Actions templates for static site generators
- [ ] **Deployment Examples**: Netlify, Vercel, CloudFlare Pages integration guides
- [ ] **CDN Configuration**: Library asset hosting and optimization
- [ ] **Database Migrations**: Schema versioning and update strategies
#### **Enterprise Features**
- [ ] **Multi-Site API**: Single server managing multiple site content
- [ ] **User Management**: Role-based access control and permissions
- [ ] **Content Approval**: Editorial workflows and publishing controls
- [ ] **Performance Monitoring**: Analytics and optimization tools
### **✅ Phase 3: Container Expansion Intelligence** (Complete)
#### **Element Classification and Boundaries**
- [x] **HTML Semantics Approach**: Use HTML tag semantics for block vs inline detection
- [x] **Framework Agnostic Processing**: No special framework container detection
- [x] **Boundary Rules**: Only `.insertr` elements are boundaries, traverse all other containers
- [x] **Block/Inline Classification**: Clear rules for when elements get `.insertr` vs formatting
#### **Implementation Status**
- [x] **Backend Container Transformation**: Implemented syntactic sugar transformation in `internal/engine/engine.go`
- [x] **Frontend Container Logic Removal**: Cleaned up `lib/src/core/insertr.js` - frontend finds enhanced elements only
- [x] **Backend Viable Children**: Updated `internal/engine/utils.go` with comprehensive block/inline logic
- [x] **Recursive Traversal**: Deep nesting support with proper boundary respect implemented
- [x] **CLASSES.md Compliance**: Container expansion now follows specification exactly
#### **Complex Element Handling** (Deferred)
- [ ] **Table Editing**: Complex hierarchy needs separate planning for `<table>`, `<tr>`, `<td>` elements
- Tables have nested semantic structure that doesn't fit simple block/inline model
- Need to determine: Should individual cells be editable? Entire table? Row-level?
- Consider: Table headers, captions, complex layouts, accessibility concerns
- [ ] **Form Element Editing**: Interactive form controls need specialized editors
- `<input>` fields: Different types need different editing interfaces (text, email, etc.)
- `<textarea>`: Should get rich text editing or preserve plain text?
- `<select>` options: Need dynamic option management interface
- `<form>` containers: Validation rules, action URLs, method selection
- Consider: Form submission handling, validation, accessibility
- [ ] **Self-Closing Element Management**: Media and input elements
- `<img>`: Alt text, src, responsive image sets, lazy loading
- `<video>/<audio>`: Multiple sources, controls, accessibility features
- `<input>`: Type-specific validation, placeholder text, required fields
### **🟢 Phase 4: Advanced CMS Features** (Low Priority)
#### **Content Management Enhancements**
- [ ] **Media Management**: Image upload, asset management, optimization
- [ ] **Content Templates**: Reusable content blocks and page templates
- [ ] **Search and Filtering**: Content discovery and organization tools
- [ ] **Import/Export**: Bulk content operations and migration tools
#### **Developer Experience**
- [ ] **Plugin System**: Extensible content types and field configurations
- [ ] **Testing Framework**: Automated testing for content workflows
- [ ] **Documentation Site**: Interactive documentation with live examples
- [ ] **Performance Profiling**: Development tools and optimization guides
---
## 🌐 **Future Features** (Planned)
### **Production Static Site Hosting**
**Goal**: Extend current development multi-site server to production static site hosting
**Current State**: Development server hosts enhanced demo sites at `/sites/{site_id}/` for testing convenience.
**Future Enhancement**: Production-ready static site hosting with content management.
#### **Proposed Production Static Site Server**
- **Use Case**: Small to medium sites that want unified hosting + content management
- **Alternative to**: Netlify CMS + hosting, Forestry + Vercel, etc.
- **Benefit**: Single server handles both static hosting AND content API
#### **Architecture**: Static file serving WITHOUT enhancement
- **Static Serving**: Serve pre-enhanced files efficiently (like nginx/Apache)
- **Content API**: Separate `/api/*` endpoints for content management
- **Build Triggers**: Content changes trigger static site rebuilds
- **Multi-Tenant**: Multiple sites with custom domains
#### **Configuration Example**
```yaml
# insertr.yaml (future production mode)
server:
mode: "production"
sites:
- site_id: "mysite"
domain: "mysite.com"
path: "/var/www/mysite" # Pre-enhanced static files
ssl_cert: "/etc/ssl/mysite.pem"
rebuild_command: "hugo && insertr enhance ./public --output /var/www/mysite"
- site_id: "blog"
domain: "blog.example.com"
path: "/var/www/blog"
rebuild_command: "npm run build"
```
#### **Implementation Plan**
- [ ] **Static File Server**: Efficient static file serving (no enhancement)
- [ ] **Domain Routing**: Route custom domains to appropriate site directories
- [ ] **SSL/TLS Support**: Automatic certificate management (Let's Encrypt)
- [ ] **Build Triggers**: Webhook system to trigger site rebuilds after content changes
- [ ] **Performance**: CDN integration, compression, caching headers
- [ ] **Monitoring**: Site uptime, performance metrics, error logging
**Priority**: Low - implement after core content management features are stable
### **Advanced Style Preview System**
**Current State**: Basic style button previews using `getComputedStyle()` to show formatting effects.
#### **Future Style Preview Enhancements**
- [ ] **Enhanced Style Support**: Background colors, borders, typography with safety constraints
- [ ] **Interactive Previews**: Hover effects, animations, responsive previews
- [ ] **Custom Style Creation**: Visual style picker with live preview
- [ ] **Style Inheritance Display**: Show which properties come from which CSS classes
- [ ] **Accessibility Validation**: Ensure previews meet contrast and readability standards
### **Advanced Access Control**
**Current State**: Simple boolean authentication gate for page-level editing access.
**Future Enhancement**: Role-based access control and section-level permissions for enterprise applications.
#### **Potential Extended Gate Classes**
```html
<!-- Current: Simple page-level auth -->
<div class="insertr-gate"></div>
<!-- Future: Role-based section permissions -->
<div class="admin-content insertr-gate-admin">
<div class="insertr-add">Admin-only dynamic content</div>
</div>
<div class="editor-section insertr-gate-editor">
<div class="insertr-content">Editor-level rich content</div>
</div>
```
#### **Enterprise Use Cases**
- **Multi-tenant Applications**: Different organizations editing separate content areas
- **Editorial Workflows**: Writers, editors, and admins with different capabilities
- **Subscription Content**: Different content areas for different subscription tiers
- **Department Permissions**: Marketing vs Engineering vs Sales content areas
**Priority**: Low - implement after core functionality is stable and enterprise customers request advanced permissions.
---
## 📊 **Success Metrics**
### **Phase 1 Complete When**:
- ✅ Editor saves successfully to HTTP API in all demo sites
- ✅ Error handling provides clear feedback for all failure scenarios
- ✅ Draft auto-save prevents content loss during editing
- ✅ Performance is acceptable on large pages with many editable elements
### **Phase 2 Complete When**:
- ✅ Production deployment guides for major platforms (Netlify, Vercel, etc.)
- ✅ Enterprise authentication working with real Authentik instances
- ✅ Multi-site content management for production use cases
- ✅ CDN hosting for insertr.js library with version management
### **Production Ready When**:
- ✅ Real-world sites using Insertr in production successfully
- ✅ Performance benchmarks meet or exceed existing CMS solutions
- ✅ Security audit completed for authentication and content handling
- ✅ Documentation and examples cover all major use cases
---
## 🔧 **Development Principles**
1. **Zero Configuration**: Markup-driven approach, no schema files
2. **HTML-First**: Perfect attribute preservation, no lossy conversions
3. **Performance**: Zero runtime cost for regular site visitors
4. **Framework Agnostic**: Works with any static site generator
5. **Developer Experience**: Minimal cognitive overhead, stays in markup
6. **Progressive Enhancement**: Sites work without JavaScript, editing enhances with JavaScript
**Built with ❤️ for developers who want powerful editing without the complexity**

20
collection-example.html Normal file
View File

@@ -0,0 +1,20 @@
<!-- insertr collections is defined with .insertr-add -->
<div class="insertr-add">
<!-- the collection containers children are collection items. -->
<!-- this is meant mostly for structural elements such as cards, testimonials etc. -->
<!-- it is not meant for text editing (such as multiple paragraphs) and you should instead use .insertr-content -->
<div class="variant-1">
<!-- The cards content can be edited as normal insertr elements -->
<h2 class="insertr">Card 1</h2>
<p class="insertr lead">This is a lead paragraph</p>
<p class="insertr">This is the main paragraph that could be longer and give additional info</p>
</div>
<!-- Since the developer has defined two templates in the markup two templates must be generated. -->
<!-- they might have different structure, or been styled differently. (imagine an alternating color on testimonial cards). the variant-x classnames are only for ilustration and is not a keyword -->
<div class="variant-2">
<h2 class="insertr">Card 2</h2>
<p class="insertr">This is the main paragraph that could be longer and give additional info</p>
<button type="button insertr">Call to action</button>
</div>
</div>

View File

@@ -1,79 +0,0 @@
# The Forager's Journal - Blog Demo Configuration
site_id: "mushroom-blog"
# Discovery settings for this demo
discovery:
enabled: true
aggressive: false
containers: true
individual: true
# Content types for future blog functionality
content_types:
blog_posts:
template: "templates/blog-post.html"
output_pattern: "posts/{slug}.html"
fields:
title: text
subtitle: text
content: insertr-content
excerpt: text
published_date: date
author: text
category: text
tags: list
featured_image: image
featured_image_alt: text
pages:
template: "templates/page.html"
output_pattern: "{slug}.html"
fields:
title: text
content: insertr-content
meta_description: text
# Example content structure for blog posts
sample_content:
chanterelles_guide:
type: blog_posts
title: "Golden Treasures: A Guide to Foraging Chanterelles"
subtitle: "Discover the secrets to finding these golden beauties"
category: "Field Guide"
published_date: "2024-03-15"
author: "Elena Rodriguez"
tags: ["Chanterelles", "Foraging", "Identification", "Sustainable Harvesting"]
featured_image: "https://images.unsplash.com/photo-1518264344460-b2b8c61dbeda"
featured_image_alt: "Golden chanterelle mushrooms growing in forest moss"
spring_safety:
type: blog_posts
title: "Spring Safety: Avoiding Dangerous Look-Alikes"
subtitle: "Essential knowledge for safe spring foraging"
category: "Safety"
published_date: "2024-03-08"
author: "Elena Rodriguez"
tags: ["Safety", "Identification", "Spring Foraging", "Look-alikes"]
mushroom_risotto:
type: blog_posts
title: "From Forest to Table: Wild Mushroom Risotto"
subtitle: "Transform your harvest into an elegant dinner"
category: "Recipes"
published_date: "2024-02-28"
author: "Elena Rodriguez"
tags: ["Recipes", "Cooking", "Chanterelles", "Wild Mushrooms"]
# SEO and metadata settings
seo:
site_title: "The Forager's Journal"
site_description: "Expert guides to mushroom foraging, identification, and sustainable harvesting practices from an experienced mycophile."
author: "Elena Rodriguez"
language: "en"
# Social media settings
social:
twitter_handle: "@foragers_journal"
facebook_page: "TheForagersJournal"
instagram: "@foragers.journal"

View File

@@ -1,27 +0,0 @@
# Insertr Configuration for Default Demo Site
# Main configuration for the default demo site
# Global settings
dev_mode: true # Development mode for demos
# Database configuration
database:
path: "./insertr.db" # Shared database with main config
# Demo-specific configuration
demo:
site_id: "default" # Unique site ID for default demo
inject_demo_gate: true # Auto-inject demo gate if no gates exist
mock_auth: true # Use mock authentication for demos
api_endpoint: "http://localhost:8080/api/content"
demo_port: 3000 # Port for live-server
# CLI enhancement configuration
cli:
site_id: "default" # Site ID for this demo
output: "./demos/default_enhanced" # Output directory for enhanced files
inject_demo_gate: true # Inject demo gate in development mode
# Authentication configuration (for demo)
auth:
provider: "mock" # Mock auth for demos

View File

@@ -142,6 +142,29 @@
.testimonial-item cite:before { .testimonial-item cite:before {
content: "— "; content: "— ";
} }
/* Styling variants for template differentiation testing */
.testimonial-item.featured {
border-left: 4px solid #3b82f6;
background: #eff6ff;
}
.testimonial-item.compact {
padding: 1rem;
}
.testimonial-item.dark {
background: #1f2937;
color: white;
}
.testimonial-item.dark blockquote {
color: #e5e7eb;
}
.testimonial-item.dark cite {
color: #9ca3af;
}
</style> </style>
</head> </head>
<body> <body>
@@ -225,11 +248,11 @@
<blockquote class="insertr">Not all that is gold does glitter</blockquote> <blockquote class="insertr">Not all that is gold does glitter</blockquote>
<cite class="insertr">Tolkien</cite> <cite class="insertr">Tolkien</cite>
</div> </div>
<div class="testimonial-item"> <div class="testimonial-item featured">
<blockquote class="insertr">The journey of a thousand miles begins with one step</blockquote> <blockquote class="insertr">The journey of a thousand miles begins with one step</blockquote>
<cite class="insertr">Lao Tzu</cite> <cite class="insertr">Lao Tzu</cite>
</div> </div>
<div class="testimonial-item"> <div class="testimonial-item compact dark">
<blockquote class="insertr">Innovation distinguishes between a leader and a follower</blockquote> <blockquote class="insertr">Innovation distinguishes between a leader and a follower</blockquote>
<cite class="insertr">Steve Jobs</cite> <cite class="insertr">Steve Jobs</cite>
</div> </div>

View File

@@ -1,27 +0,0 @@
# Insertr Configuration for Simple Demo Site
# Specific configuration for the simple test site demo
# Global settings
dev_mode: true # Development mode for demos
# Database configuration
database:
path: "./insertr.db" # Shared database with main config
# Demo-specific configuration
demo:
site_id: "simple" # Unique site ID for simple demo
inject_demo_gate: true # Auto-inject demo gate if no gates exist
mock_auth: true # Use mock authentication for demos
api_endpoint: "http://localhost:8080/api/content"
demo_port: 3000 # Port for live-server
# CLI enhancement configuration
cli:
site_id: "simple" # Site ID for this demo
output: "./demos/simple_enhanced" # Output directory for enhanced files
inject_demo_gate: true # Inject demo gate in development mode
# Authentication configuration (for demo)
auth:
provider: "mock" # Mock auth for demos

View File

@@ -405,6 +405,40 @@ func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Reque
json.NewEncoder(w).Encode(items) json.NewEncoder(w).Encode(items)
} }
// GetCollectionPreview handles GET /api/collections/{id}/preview
func (h *ContentHandler) GetCollectionPreview(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
// Get collection container
collection, err := h.repository.GetCollection(context.Background(), siteID, collectionID)
if err != nil {
http.Error(w, fmt.Sprintf("Collection not found: %v", err), http.StatusNotFound)
return
}
// Get all templates for this collection
templates, err := h.repository.GetCollectionTemplates(context.Background(), siteID, collectionID)
if err != nil {
http.Error(w, fmt.Sprintf("Templates not found: %v", err), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"collection_id": collectionID,
"container_html": collection.ContainerHTML,
"templates": templates,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// CreateCollectionItem handles POST /api/collections/{id}/items // CreateCollectionItem handles POST /api/collections/{id}/items
func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) { func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) {
userInfo, authErr := h.authService.ExtractUserFromRequest(r) userInfo, authErr := h.authService.ExtractUserFromRequest(r)
@@ -437,15 +471,29 @@ func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Req
} }
if req.TemplateID == 0 { if req.TemplateID == 0 {
req.TemplateID = 1 // Default to first template http.Error(w, "template_id is required", http.StatusBadRequest)
return
} }
// Use atomic collection item creation from repository // Get the specific template by ID
createdItem, err := h.repository.CreateCollectionItemAtomic( selectedTemplate, err := h.repository.GetCollectionTemplate(context.Background(), req.TemplateID)
context.Background(), if err != nil {
http.Error(w, fmt.Sprintf("Template %d not found: %v", req.TemplateID, err), http.StatusBadRequest)
return
}
// Verify template belongs to the requested collection and site
if selectedTemplate.CollectionID != req.CollectionID || selectedTemplate.SiteID != req.SiteID {
http.Error(w, fmt.Sprintf("Template %d not found in collection %s", req.TemplateID, req.CollectionID), http.StatusBadRequest)
return
}
// Use engine's unified collection item creation
createdItem, err := h.engine.CreateCollectionItemFromTemplate(
req.SiteID, req.SiteID,
req.CollectionID, req.CollectionID,
req.TemplateID, req.TemplateID,
selectedTemplate.HTMLTemplate,
userInfo.ID, userInfo.ID,
) )
if err != nil { if err != nil {
@@ -487,10 +535,10 @@ func (h *ContentHandler) RegisterRoutes(r chi.Router) {
// COLLECTION MANAGEMENT - Groups of related content // COLLECTION MANAGEMENT - Groups of related content
// ============================================================================= // =============================================================================
r.Route("/collections", func(r chi.Router) { r.Route("/collections", func(r chi.Router) {
// Public routes r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X
r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X
r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X
r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X r.Get("/{id}/preview", h.GetCollectionPreview) // GET /api/collections/{id}/preview?site_id=X
// Protected routes // Protected routes
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {

View File

@@ -192,6 +192,10 @@ func (c *HTTPClient) GetCollectionTemplates(ctx context.Context, siteID, collect
return nil, fmt.Errorf("collection operations not implemented in HTTPClient") return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
} }
func (c *HTTPClient) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
}
func (c *HTTPClient) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { func (c *HTTPClient) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient") return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
} }
@@ -200,6 +204,10 @@ func (c *HTTPClient) CreateCollectionItemAtomic(ctx context.Context, siteID, col
return nil, fmt.Errorf("collection operations not implemented in HTTPClient") return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
} }
func (c *HTTPClient) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) {
return 0, fmt.Errorf("collection operations not implemented in HTTPClient")
}
func (c *HTTPClient) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { func (c *HTTPClient) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
return nil, fmt.Errorf("content update operations not implemented in HTTPClient") return nil, fmt.Errorf("content update operations not implemented in HTTPClient")
} }

View File

@@ -214,6 +214,24 @@ func (r *PostgreSQLRepository) GetCollectionTemplates(ctx context.Context, siteI
return result, nil return result, nil
} }
// GetCollectionTemplate retrieves a single template by ID
func (r *PostgreSQLRepository) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
template, err := r.queries.GetCollectionTemplate(ctx, int32(templateID))
if err != nil {
return nil, err
}
result := &CollectionTemplateItem{
TemplateID: int(template.TemplateID),
CollectionID: template.CollectionID,
SiteID: template.SiteID,
Name: template.Name,
HTMLTemplate: template.HtmlTemplate,
IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN
}
return result, nil
}
// CreateCollectionItem creates a new collection item // CreateCollectionItem creates a new collection item
func (r *PostgreSQLRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { func (r *PostgreSQLRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
item, err := r.queries.CreateCollectionItem(ctx, postgresql.CreateCollectionItemParams{ item, err := r.queries.CreateCollectionItem(ctx, postgresql.CreateCollectionItemParams{
@@ -264,6 +282,23 @@ func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, s
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL") return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL")
} }
// GetMaxPosition returns the maximum position for items in a collection
func (r *PostgreSQLRepository) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) {
result, err := r.queries.GetMaxPosition(ctx, postgresql.GetMaxPositionParams{
CollectionID: collectionID,
SiteID: siteID,
})
if err != nil {
return 0, err
}
// Convert interface{} to int (PostgreSQL returns int64)
if maxPos, ok := result.(int64); ok {
return int(maxPos), nil
}
return 0, nil
}
// UpdateContent updates an existing content item // UpdateContent updates an existing content item
func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{ content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{

View File

@@ -18,9 +18,11 @@ type ContentRepository interface {
CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error)
GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error)
GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error)
GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error)
CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error)
CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error)
CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error)
GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error)
ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error
// Transaction support // Transaction support

View File

@@ -219,6 +219,24 @@ func (r *SQLiteRepository) GetCollectionTemplates(ctx context.Context, siteID, c
return result, nil return result, nil
} }
// GetCollectionTemplate retrieves a single template by ID
func (r *SQLiteRepository) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
template, err := r.queries.GetCollectionTemplate(ctx, int64(templateID))
if err != nil {
return nil, err
}
result := &CollectionTemplateItem{
TemplateID: int(template.TemplateID),
CollectionID: template.CollectionID,
SiteID: template.SiteID,
Name: template.Name,
HTMLTemplate: template.HtmlTemplate,
IsDefault: template.IsDefault != 0, // SQLite uses INTEGER for boolean
}
return result, nil
}
// CreateCollectionItem creates a new collection item // CreateCollectionItem creates a new collection item
func (r *SQLiteRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { func (r *SQLiteRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
item, err := r.queries.CreateCollectionItem(ctx, sqlite.CreateCollectionItemParams{ item, err := r.queries.CreateCollectionItem(ctx, sqlite.CreateCollectionItemParams{
@@ -269,6 +287,23 @@ func (r *SQLiteRepository) CreateCollectionItemAtomic(ctx context.Context, siteI
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for SQLite") return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for SQLite")
} }
// GetMaxPosition returns the maximum position for items in a collection
func (r *SQLiteRepository) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) {
result, err := r.queries.GetMaxPosition(ctx, sqlite.GetMaxPositionParams{
CollectionID: collectionID,
SiteID: siteID,
})
if err != nil {
return 0, err
}
// Convert interface{} to int (SQLite returns int64)
if maxPos, ok := result.(int64); ok {
return int(maxPos), nil
}
return 0, nil
}
// UpdateContent updates an existing content item // UpdateContent updates an existing content item
func (r *SQLiteRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { func (r *SQLiteRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
content, err := r.queries.UpdateContent(ctx, sqlite.UpdateContentParams{ content, err := r.queries.UpdateContent(ctx, sqlite.UpdateContentParams{

View File

@@ -0,0 +1,535 @@
package engine
import (
"context"
"fmt"
"strings"
"time"
"github.com/insertr/insertr/internal/db"
"golang.org/x/net/html"
)
// CollectionElement represents an insertr-add collection element found in HTML
type CollectionElement struct {
Node *html.Node
}
// hasInsertrAddClass checks if node has class="insertr-add" (collection)
func (e *ContentEngine) hasInsertrAddClass(node *html.Node) bool {
classes := GetClasses(node)
return ContainsClass(classes, "insertr-add")
}
// processCollection handles collection detection, persistence and reconstruction
func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionID, siteID string) error {
// 1. Check if collection exists in database
existingCollection, err := e.client.GetCollection(context.Background(), siteID, collectionID)
collectionExists := (err == nil && existingCollection != nil)
if !collectionExists {
// 2. New collection: extract container HTML and create collection record
containerHTML := e.extractOriginalTemplate(collectionNode)
_, err := e.client.CreateCollection(context.Background(), siteID, collectionID, containerHTML, "system")
if err != nil {
return fmt.Errorf("failed to create collection %s: %w", collectionID, err)
}
// 3. Extract templates and store initial items from existing children
err = e.extractAndStoreTemplatesAndItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to extract templates and items for collection %s: %w", collectionID, err)
}
fmt.Printf("✅ Created new collection: %s with templates and initial items\n", collectionID)
} else {
// 4. Existing collection: Always reconstruct from database (database is source of truth)
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct collection %s: %w", collectionID, err)
}
// Optional: Show item count for feedback
existingItems, _ := e.client.GetCollectionItems(context.Background(), siteID, collectionID)
fmt.Printf("✅ Reconstructed collection: %s from database (%d items)\n", collectionID, len(existingItems))
}
return nil
}
// extractAndStoreTemplatesAndItems extracts templates and stores initial items from existing collection children
func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.Node, collectionID, siteID string) error {
var templateIDs []int
templateCount := 0
// Walk through direct children of the collection
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
templateCount++
}
}
// If no templates found, create a default template
if templateCount == 0 {
_, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, "default", "<div>New item</div>", true)
if err != nil {
return fmt.Errorf("failed to create default template: %w", err)
}
fmt.Printf("✅ Created default template for collection %s\n", collectionID)
return nil
}
// Create templates for each unique child structure and styling (deduplicated)
seenTemplates := make(map[string]int) // templateSignature -> templateID
templateIndex := 0
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
templateHTML := e.extractTemplateForStorage(child)
templateSignature := e.generateTemplateSignature(child)
// Check if we've already seen this exact template structure + styling
if existingTemplateID, exists := seenTemplates[templateSignature]; exists {
// Reuse existing template
templateIDs = append(templateIDs, existingTemplateID)
fmt.Printf("✅ Reusing existing template for identical structure+styling in collection %s\n", collectionID)
} else {
// Create new template for unique structure+styling combination
templateName := e.generateTemplateNameFromSignature(child, templateIndex+1)
isDefault := templateIndex == 0
template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault)
if err != nil {
return fmt.Errorf("failed to create template %s: %w", templateName, err)
}
// Store the mapping and append to results
seenTemplates[templateSignature] = template.TemplateID
templateIDs = append(templateIDs, template.TemplateID)
templateIndex++
fmt.Printf("✅ Created new template '%s' for collection %s\n", templateName, collectionID)
}
}
}
// Store original children as initial collection items (database-first approach)
err := e.storeChildrenAsCollectionItems(collectionNode, collectionID, siteID, templateIDs)
if err != nil {
return fmt.Errorf("failed to store initial collection items: %w", err)
}
// Clear HTML children and reconstruct from database (ensures consistency)
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct initial collection items: %w", err)
}
return nil
}
// reconstructCollectionItems rebuilds collection items from database and adds them to DOM
func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, collectionID, siteID string) error {
// Get all items for this collection from database
items, err := e.client.GetCollectionItems(context.Background(), siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection items: %w", err)
}
// Get templates for this collection
templates, err := e.client.GetCollectionTemplates(context.Background(), siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection templates: %w", err)
}
// Build template lookup for efficiency
templateLookup := make(map[int]*db.CollectionTemplateItem)
for _, template := range templates {
templateLookup[template.TemplateID] = &template
}
// Clear existing children from the collection node
for child := collectionNode.FirstChild; child != nil; {
next := child.NextSibling
collectionNode.RemoveChild(child)
child = next
}
// Reconstruct items in order from database
for _, item := range items {
_, exists := templateLookup[item.TemplateID]
if !exists {
fmt.Printf("⚠️ Template %d not found for item %s, skipping\n", item.TemplateID, item.ItemID)
continue
}
// Parse the stored structural template HTML
structuralDoc, err := html.Parse(strings.NewReader(item.HTMLContent))
if err != nil {
fmt.Printf("⚠️ Failed to parse structural template for item %s: %v\n", item.ItemID, err)
continue
}
// Find the body and extract its children (stored as complete structure)
var structuralChild *html.Node
e.walkNodes(structuralDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
// Get the first element child of body
for child := n.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
structuralChild = child
break
}
}
}
})
if structuralChild != nil {
// Remove from its current parent before adding to collection
if structuralChild.Parent != nil {
structuralChild.Parent.RemoveChild(structuralChild)
}
// RESTORED: Inject content into .insertr elements within collection items
// Walk through structural elements and hydrate with content from content table
e.walkNodes(structuralChild, func(n *html.Node) {
if n.Type == html.ElementNode && HasClass(n, "insertr") {
// Get content ID from data attribute
contentID := GetAttribute(n, "data-content-id")
if contentID != "" {
// Get actual content from database and inject it
contentItem, err := e.client.GetContent(context.Background(), siteID, contentID)
if err == nil && contentItem != nil {
// Use injector to hydrate content (unified .insertr approach)
e.injector.siteID = siteID
e.injector.injectHTMLContent(n, contentItem.HTMLContent)
}
}
}
})
// Inject data-item-id attribute for collection item identification
if structuralChild.Type == html.ElementNode {
SetAttribute(structuralChild, "data-item-id", item.ItemID)
}
collectionNode.AppendChild(structuralChild)
}
}
fmt.Printf("✅ Reconstructed %d items for collection %s\n", len(items), collectionID)
return nil
}
// processChildElementsAsContent processes .insertr elements within a collection child and stores them as individual content
func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, siteID, itemID string) ([]ContentEntry, error) {
var contentEntries []ContentEntry
// Walk through the child element and find .insertr elements
e.walkNodes(childElement, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
// Generate content ID for this .insertr element, including item ID for uniqueness
contentID := e.idGenerator.Generate(n, fmt.Sprintf("%s-content", itemID))
// Extract the content
htmlContent := e.extractHTMLContent(n)
template := e.extractTemplateForStorage(n)
// Store content entry
contentEntries = append(contentEntries, ContentEntry{
ID: contentID,
SiteID: siteID,
HTMLContent: htmlContent,
Template: template,
})
// Set the data-content-id attribute
SetAttribute(n, "data-content-id", contentID)
// Keep content for initial display - don't clear it
// The content is already stored in the database and will be available for editing
// Preserving content ensures elements have height and are clickable
}
})
return contentEntries, nil
}
// generateStructuralTemplateFromChild creates a structural template with placeholders for content
func (e *ContentEngine) generateStructuralTemplateFromChild(childElement *html.Node, contentEntries []ContentEntry) (string, error) {
// Clone the child to avoid modifying the original
clonedChild := e.cloneNode(childElement)
// Walk through and replace .insertr content with data-content-id attributes
entryIndex := 0
e.walkNodes(clonedChild, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
if entryIndex < len(contentEntries) {
// Set the data-content-id attribute
SetAttribute(n, "data-content-id", contentEntries[entryIndex].ID)
// Keep content for structural template - ensures elements have height and are clickable
// Content is stored separately in database for editing
entryIndex++
}
}
})
// Generate HTML for the structural template
var buf strings.Builder
if err := html.Render(&buf, clonedChild); err != nil {
return "", fmt.Errorf("failed to render structural template: %w", err)
}
return buf.String(), nil
}
// createVirtualElementFromTemplate creates a virtual DOM element from template HTML
func (e *ContentEngine) createVirtualElementFromTemplate(templateHTML string) (*html.Node, error) {
// Parse template HTML into a virtual DOM
templateDoc, err := html.Parse(strings.NewReader(templateHTML))
if err != nil {
return nil, fmt.Errorf("failed to parse template HTML: %w", err)
}
// Find the first element in the body
var templateElement *html.Node
e.walkNodes(templateDoc, func(n *html.Node) {
if templateElement == nil && n.Type == html.ElementNode && n.Data != "html" && n.Data != "head" && n.Data != "body" {
templateElement = n
}
})
if templateElement == nil {
return nil, fmt.Errorf("no valid element found in template HTML")
}
return templateElement, nil
}
// CreateCollectionItemFromTemplate creates a collection item using the unified engine approach
func (e *ContentEngine) CreateCollectionItemFromTemplate(
siteID, collectionID string,
templateID int,
templateHTML string,
lastEditedBy string,
) (*db.CollectionItemWithTemplate, error) {
// Create virtual element from template for ID generation
virtualElement, err := e.createVirtualElementFromTemplate(templateHTML)
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Generate unique item ID using unified generator with collection context + timestamp for uniqueness
baseID := e.idGenerator.Generate(virtualElement, "collection-item")
itemID := fmt.Sprintf("%s-%d", baseID, time.Now().UnixNano()%1000000) // Add 6-digit unique suffix
// Process any .insertr elements within the template and store as content
contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID)
if err != nil {
return nil, fmt.Errorf("failed to process child elements: %w", err)
}
// Store individual content entries in content table
for _, entry := range contentEntries {
_, err := e.client.CreateContent(context.Background(), entry.SiteID, entry.ID, entry.HTMLContent, entry.Template, lastEditedBy)
if err != nil {
return nil, fmt.Errorf("failed to create content entry %s: %w", entry.ID, err)
}
}
// Generate structural template for the collection item
structuralTemplate, err := e.generateStructuralTemplateFromChild(virtualElement, contentEntries)
if err != nil {
return nil, fmt.Errorf("failed to generate structural template: %w", err)
}
// Get next position to place new item at the end of collection
maxPosition, err := e.client.GetMaxPosition(context.Background(), siteID, collectionID)
if err != nil {
return nil, fmt.Errorf("failed to get max position for collection %s: %w", collectionID, err)
}
nextPosition := maxPosition + 1
fmt.Printf("🔢 Max position for collection %s: %d, assigning new item position: %d\n", collectionID, maxPosition, nextPosition)
// Create collection item with structural template at end position
collectionItem, err := e.client.CreateCollectionItem(context.Background(),
siteID, collectionID, itemID, templateID, structuralTemplate, nextPosition, lastEditedBy,
)
if err != nil {
return nil, fmt.Errorf("failed to create collection item: %w", err)
}
return collectionItem, nil
}
// storeChildrenAsCollectionItems stores HTML children as collection items in database
func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node, collectionID, siteID string, templateIDs []int) error {
var childElements []*html.Node
// Walk through direct children of the collection
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
childElements = append(childElements, child)
}
}
if len(childElements) == 0 {
fmt.Printf(" No children found to store as collection items for %s\n", collectionID)
return nil
}
// Store each child as a collection item
for i, childElement := range childElements {
// Use corresponding template ID, or default to first template
templateID := templateIDs[0] // Default to first template
if i < len(templateIDs) {
templateID = templateIDs[i]
}
// Generate item ID using unified generator with collection context
itemID := e.idGenerator.Generate(childElement, "collection-item")
// Process any .insertr elements within this child and store as content
contentEntries, err := e.processChildElementsAsContent(childElement, siteID, itemID)
if err != nil {
return fmt.Errorf("failed to process child elements: %w", err)
}
// Store individual content entries in content table
for _, entry := range contentEntries {
_, err := e.client.CreateContent(context.Background(), entry.SiteID, entry.ID, entry.HTMLContent, entry.Template, "system")
if err != nil {
return fmt.Errorf("failed to create content entry %s: %w", entry.ID, err)
}
}
// Generate structural template for this collection item
structuralTemplate, err := e.generateStructuralTemplateFromChild(childElement, contentEntries)
if err != nil {
return fmt.Errorf("failed to generate structural template: %w", err)
}
// Store structural template in collection_items (content lives in content table)
_, err = e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, i+1, "system")
if err != nil {
return fmt.Errorf("failed to create collection item %s: %w", itemID, err)
}
fmt.Printf("✅ Stored initial collection item: %s (template %d) with %d content entries\n", itemID, templateID, len(contentEntries))
}
return nil
}
// collectionProcessor handles collection-specific processing logic
type collectionProcessor struct {
engine *ContentEngine
}
// newCollectionProcessor creates a collection processor
func (e *ContentEngine) newCollectionProcessor() *collectionProcessor {
return &collectionProcessor{engine: e}
}
// process handles the full collection processing workflow
func (cp *collectionProcessor) process(collectionNode *html.Node, collectionID, siteID string) error {
return cp.engine.processCollection(collectionNode, collectionID, siteID)
}
// extractAndStoreTemplatesAndItems delegates to engine method
func (cp *collectionProcessor) extractAndStoreTemplatesAndItems(collectionNode *html.Node, collectionID, siteID string) error {
return cp.engine.extractAndStoreTemplatesAndItems(collectionNode, collectionID, siteID)
}
// reconstructItems delegates to engine method
func (cp *collectionProcessor) reconstructItems(collectionNode *html.Node, collectionID, siteID string) error {
return cp.engine.reconstructCollectionItems(collectionNode, collectionID, siteID)
}
// cloneNode creates a deep copy of an HTML node
func (e *ContentEngine) cloneNode(node *html.Node) *html.Node {
cloned := &html.Node{
Type: node.Type,
Data: node.Data,
DataAtom: node.DataAtom,
Namespace: node.Namespace,
}
// Clone attributes
for _, attr := range node.Attr {
cloned.Attr = append(cloned.Attr, html.Attribute{
Namespace: attr.Namespace,
Key: attr.Key,
Val: attr.Val,
})
}
// Clone children recursively
for child := node.FirstChild; child != nil; child = child.NextSibling {
clonedChild := e.cloneNode(child)
cloned.AppendChild(clonedChild)
}
return cloned
}
// generateTemplateSignature creates a unique signature for template comparison
// This is purely structure + class based, completely ignoring content
func (e *ContentEngine) generateTemplateSignature(element *html.Node) string {
// Get content-agnostic structure signature
return e.extractStructureSignature(element)
}
// generateTemplateNameFromSignature creates human-readable template names
func (e *ContentEngine) generateTemplateNameFromSignature(element *html.Node, fallbackIndex int) string {
// Extract root element classes for naming
rootClasses := GetClasses(element)
if len(rootClasses) > 0 {
// Find distinctive classes (exclude common structural and base classes)
var distinctiveClasses []string
commonClasses := map[string]bool{
"insertr": true, "insertr-add": true,
// Common base classes that don't indicate variants
"testimonial-item": true, "card": true, "item": true, "post": true,
"container": true, "wrapper": true, "content": true,
}
for _, class := range rootClasses {
if !commonClasses[class] {
distinctiveClasses = append(distinctiveClasses, class)
}
}
if len(distinctiveClasses) > 0 {
// Use distinctive classes for naming
name := strings.Join(distinctiveClasses, "_")
// Capitalize and clean up
name = strings.ReplaceAll(name, "-", "_")
if len(name) > 20 {
name = name[:20]
}
return strings.Title(strings.ToLower(name))
} else if len(rootClasses) > 1 {
// If only common classes, use the last non-insertr class
for i := len(rootClasses) - 1; i >= 0; i-- {
if rootClasses[i] != "insertr" && rootClasses[i] != "insertr-add" {
name := strings.ReplaceAll(rootClasses[i], "-", "_")
return strings.Title(strings.ToLower(name))
}
}
}
}
// Fallback to numbered template
return fmt.Sprintf("template_%d", fallbackIndex)
}
// min returns the smaller of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}

160
internal/engine/content.go Normal file
View File

@@ -0,0 +1,160 @@
package engine
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"golang.org/x/net/html"
)
// addContentAttributes adds data-content-id attribute only
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) {
// Add data-content-id attribute
SetAttribute(node, "data-content-id", contentID)
}
// injectContent injects content from database into elements
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
for i := range elements {
elem := &elements[i]
// Get content from database by ID - FIXED: Use context.Background() instead of nil
contentItem, err := e.client.GetContent(context.Background(), siteID, elem.ID)
if err != nil {
// Content not found - skip silently (enhancement mode should not fail on missing content)
continue
}
if contentItem != nil {
// Inject the content into the element
elem.Content = contentItem.HTMLContent
// Update injector siteID for this operation
// HACK: I do not like this. Injector refactor?
e.injector.siteID = siteID
e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent)
}
}
return nil
}
// extractHTMLContent extracts the inner HTML content from a node
func (e *ContentEngine) extractHTMLContent(node *html.Node) string {
var content strings.Builder
// Render all child nodes in order to preserve HTML structure
for child := node.FirstChild; child != nil; child = child.NextSibling {
if err := html.Render(&content, child); err == nil {
// All nodes (text and element) rendered in correct order
}
}
return strings.TrimSpace(content.String())
}
// extractOriginalTemplate extracts the outer HTML of the element (including the element itself)
func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
var buf strings.Builder
if err := html.Render(&buf, node); err != nil {
return ""
}
return buf.String()
}
// extractStructureSignature creates a content-agnostic signature for template comparison
// This only considers DOM structure and class attributes, completely ignoring text content
func (e *ContentEngine) extractStructureSignature(node *html.Node) string {
var signature strings.Builder
e.walkNodes(node, func(n *html.Node) {
if n.Type == html.ElementNode {
// Get classes for this element
classes := GetClasses(n)
if len(classes) > 0 {
// Sort classes for consistent comparison
sortedClasses := make([]string, len(classes))
copy(sortedClasses, classes)
sort.Strings(sortedClasses)
// Add to signature: element[class1,class2,...]
signature.WriteString(fmt.Sprintf("%s[%s];", n.Data, strings.Join(sortedClasses, ",")))
} else {
// Element with no classes
signature.WriteString(fmt.Sprintf("%s[];", n.Data))
}
}
// Completely ignore text nodes and their content
})
return signature.String()
}
// extractTemplateForStorage extracts template HTML while preserving content but removing data-content-id attributes
func (e *ContentEngine) extractTemplateForStorage(node *html.Node) string {
// Clone the node to avoid modifying the original
clonedNode := e.cloneNode(node)
// Remove all data-content-id attributes but preserve all content
e.walkNodes(clonedNode, func(n *html.Node) {
if n.Type == html.ElementNode {
// Remove data-content-id attribute
e.removeAttribute(n, "data-content-id")
}
})
var buf strings.Builder
if err := html.Render(&buf, clonedNode); err != nil {
return ""
}
return buf.String()
}
// removeAttribute removes an attribute from an HTML node
func (e *ContentEngine) removeAttribute(n *html.Node, key string) {
for i, attr := range n.Attr {
if attr.Key == key {
n.Attr = slices.Delete(n.Attr, i, i+1)
break
}
}
}
// hasClass checks if a node has a specific class
func (e *ContentEngine) hasClass(n *html.Node, className string) bool {
for _, attr := range n.Attr {
if attr.Key == "class" {
classes := strings.Fields(attr.Val)
if slices.Contains(classes, className) {
return true
}
}
}
return false
}
// getPlaceholderForElement returns appropriate placeholder text for different element types
func (e *ContentEngine) getPlaceholderForElement(elementType string) string {
placeholders := map[string]string{
"h1": "Heading 1",
"h2": "Heading 2",
"h3": "Heading 3",
"h4": "Heading 4",
"h5": "Heading 5",
"h6": "Heading 6",
"p": "Paragraph text",
"span": "Text",
"div": "Content block",
"button": "Button",
"a": "Link text",
"li": "List item",
"blockquote": "Quote text",
}
if placeholder, exists := placeholders[elementType]; exists {
return placeholder
}
return "Enter content..."
}

View File

@@ -0,0 +1,96 @@
package engine
import (
"golang.org/x/net/html"
)
// InsertrElement represents an insertr element found in HTML
type InsertrElement struct {
Node *html.Node
}
// findEditableElements finds all editable elements (.insertr and .insertr-add)
func (e *ContentEngine) findEditableElements(doc *html.Node) ([]InsertrElement, []CollectionElement) {
// Phase 1: Pure discovery
insertrElements, collectionElements, containers := e.discoverElements(doc)
// Phase 2: Container expansion (separate concern)
expandedElements := e.expandContainers(containers)
insertrElements = append(insertrElements, expandedElements...)
return insertrElements, collectionElements
}
// discoverElements performs pure element discovery without transformation
func (e *ContentEngine) discoverElements(doc *html.Node) ([]InsertrElement, []CollectionElement, []*html.Node) {
var insertrElements []InsertrElement
var collectionElements []CollectionElement
var containersToTransform []*html.Node
// Walk the document and categorize elements
e.walkNodes(doc, func(n *html.Node) {
if n.Type == html.ElementNode {
if e.hasInsertrAddClass(n) {
// Collection element
if hasInsertrClass(n) {
// Handle .insertr.insertr-add combination:
// Remove .insertr from container, add .insertr to viable children
RemoveClass(n, "insertr")
viableChildren := FindViableChildren(n)
for _, child := range viableChildren {
if !hasInsertrClass(child) {
AddClass(child, "insertr")
}
}
}
// Add as collection (collections take precedence)
collectionElements = append(collectionElements, CollectionElement{
Node: n,
})
} else if hasInsertrClass(n) {
// Regular insertr element (only if not a collection)
if isContainer(n) {
// Container element - mark for transformation
containersToTransform = append(containersToTransform, n)
} else {
// Regular element - add directly
insertrElements = append(insertrElements, InsertrElement{
Node: n,
})
}
}
}
})
return insertrElements, collectionElements, containersToTransform
}
// expandContainers transforms container elements by removing .insertr from containers
// and adding .insertr to their viable children
func (e *ContentEngine) expandContainers(containers []*html.Node) []InsertrElement {
var expandedElements []InsertrElement
for _, container := range containers {
// Remove .insertr class from container
RemoveClass(container, "insertr")
// Find viable children and add .insertr class to them
viableChildren := FindViableChildren(container)
for _, child := range viableChildren {
AddClass(child, "insertr")
expandedElements = append(expandedElements, InsertrElement{
Node: child,
})
}
}
return expandedElements
}
// walkNodes walks through all nodes in the document
func (e *ContentEngine) walkNodes(n *html.Node, fn func(*html.Node)) {
fn(n)
for c := n.FirstChild; c != nil; c = c.NextSibling {
e.walkNodes(c, fn)
}
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db"
"golang.org/x/net/html" "golang.org/x/net/html"
"slices"
) )
// AuthProvider represents authentication provider information // AuthProvider represents authentication provider information
@@ -106,7 +105,7 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
collectionID := e.idGenerator.Generate(collectionElem.Node, input.FilePath) collectionID := e.idGenerator.Generate(collectionElem.Node, input.FilePath)
// Add data-collection-id attribute to the collection container // Add data-collection-id attribute to the collection container
e.setAttribute(collectionElem.Node, "data-collection-id", collectionID) SetAttribute(collectionElem.Node, "data-collection-id", collectionID)
// Process collection during enhancement or content injection // Process collection during enhancement or content injection
if input.Mode == Enhancement || input.Mode == ContentInjection { if input.Mode == Enhancement || input.Mode == ContentInjection {
@@ -141,694 +140,3 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
GeneratedIDs: generatedIDs, GeneratedIDs: generatedIDs,
}, nil }, nil
} }
// InsertrElement represents an insertr element found in HTML
type InsertrElement struct {
Node *html.Node
}
// CollectionElement represents an insertr-add collection element found in HTML
type CollectionElement struct {
Node *html.Node
}
// findEditableElements finds all editable elements (.insertr and .insertr-add)
func (e *ContentEngine) findEditableElements(doc *html.Node) ([]InsertrElement, []CollectionElement) {
var insertrElements []InsertrElement
var collectionElements []CollectionElement
var containersToTransform []*html.Node
// First pass: find all .insertr and .insertr-add elements
e.walkNodes(doc, func(n *html.Node) {
if n.Type == html.ElementNode {
if e.hasInsertrClass(n) {
if isContainer(n) {
// Container element - mark for transformation
containersToTransform = append(containersToTransform, n)
} else {
// Regular element - add directly
insertrElements = append(insertrElements, InsertrElement{
Node: n,
})
}
}
if e.hasInsertrAddClass(n) {
// Collection element - add directly (no container transformation for collections)
collectionElements = append(collectionElements, CollectionElement{
Node: n,
})
}
}
})
// Second pass: transform .insertr containers (remove .insertr from container, add to children)
for _, container := range containersToTransform {
// Remove .insertr class from container
e.removeClass(container, "insertr")
// Find viable children and add .insertr class to them
viableChildren := FindViableChildren(container)
for _, child := range viableChildren {
e.addClass(child, "insertr")
insertrElements = append(insertrElements, InsertrElement{
Node: child,
})
}
}
return insertrElements, collectionElements
}
// walkNodes walks through all nodes in the document
func (e *ContentEngine) walkNodes(n *html.Node, fn func(*html.Node)) {
fn(n)
for c := n.FirstChild; c != nil; c = c.NextSibling {
e.walkNodes(c, fn)
}
}
// hasInsertrClass checks if node has class="insertr"
func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
classes := GetClasses(node)
return slices.Contains(classes, "insertr")
}
// hasInsertrAddClass checks if node has class="insertr-add" (collection)
func (e *ContentEngine) hasInsertrAddClass(node *html.Node) bool {
classes := GetClasses(node)
return slices.Contains(classes, "insertr-add")
}
// addContentAttributes adds data-content-id attribute only
// HTML-first approach: no content-type attribute needed
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) {
// Add data-content-id attribute
e.setAttribute(node, "data-content-id", contentID)
}
// setAttribute sets an attribute on an HTML node
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
// Remove existing attribute if it exists
for i, attr := range node.Attr {
if attr.Key == key {
node.Attr[i].Val = value
return
}
}
// Add new attribute
node.Attr = append(node.Attr, html.Attribute{
Key: key,
Val: value,
})
}
// addClass safely adds a class to an HTML node
func (e *ContentEngine) addClass(node *html.Node, className string) {
var classAttr *html.Attribute
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classAttr = &attr
classIndex = idx
break
}
}
var classes []string
if classAttr != nil {
classes = strings.Fields(classAttr.Val)
}
// Check if class already exists
for _, class := range classes {
if class == className {
return // Class already exists
}
}
// Add new class
classes = append(classes, className)
newClassValue := strings.Join(classes, " ")
if classIndex >= 0 {
// Update existing class attribute
node.Attr[classIndex].Val = newClassValue
} else {
// Add new class attribute
node.Attr = append(node.Attr, html.Attribute{
Key: "class",
Val: newClassValue,
})
}
}
// removeClass safely removes a class from an HTML node
func (e *ContentEngine) removeClass(node *html.Node, className string) {
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classIndex = idx
break
}
}
if classIndex == -1 {
return // No class attribute found
}
// Parse existing classes
classes := strings.Fields(node.Attr[classIndex].Val)
// Filter out the target class
var newClasses []string
for _, class := range classes {
if class != className {
newClasses = append(newClasses, class)
}
}
// Update or remove class attribute
if len(newClasses) == 0 {
// Remove class attribute entirely if no classes remain
node.Attr = append(node.Attr[:classIndex], node.Attr[classIndex+1:]...)
} else {
// Update class attribute with remaining classes
node.Attr[classIndex].Val = strings.Join(newClasses, " ")
}
}
// injectContent injects content from database into elements
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
for i := range elements {
elem := &elements[i]
// Try to get content from database
contentItem, err := e.client.GetContent(context.Background(), siteID, elem.ID)
if err != nil {
// Content not found is not an error - element just won't have injected content
continue
}
if contentItem != nil {
// Inject the content into the element
elem.Content = contentItem.HTMLContent
// Update injector siteID for this operation
e.injector.siteID = siteID
e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent)
}
}
return nil
}
// extractHTMLContent extracts the inner HTML content from a node
func (e *ContentEngine) extractHTMLContent(node *html.Node) string {
var content strings.Builder
// Render all child nodes in order to preserve HTML structure
for child := node.FirstChild; child != nil; child = child.NextSibling {
if err := html.Render(&content, child); err == nil {
// All nodes (text and element) rendered in correct order
}
}
return strings.TrimSpace(content.String())
}
// extractOriginalTemplate extracts the outer HTML of the element (including the element itself)
func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
var buf strings.Builder
if err := html.Render(&buf, node); err != nil {
return ""
}
return buf.String()
}
// extractCleanTemplate extracts a clean template without data-content-id attributes and with placeholder content
func (e *ContentEngine) extractCleanTemplate(node *html.Node) string {
// Clone the node to avoid modifying the original
clonedNode := e.cloneNode(node)
// Remove all data-content-id attributes and replace content with placeholders
e.walkNodes(clonedNode, func(n *html.Node) {
if n.Type == html.ElementNode {
// Remove data-content-id attribute
e.removeAttribute(n, "data-content-id")
// If this is an .insertr element, replace content with placeholder
if e.hasClass(n, "insertr") {
placeholderText := e.getPlaceholderForElement(n.Data)
// Clear existing children and add placeholder text
for child := n.FirstChild; child != nil; {
next := child.NextSibling
n.RemoveChild(child)
child = next
}
n.AppendChild(&html.Node{
Type: html.TextNode,
Data: placeholderText,
})
}
}
})
var buf strings.Builder
if err := html.Render(&buf, clonedNode); err != nil {
return ""
}
return buf.String()
}
// removeAttribute removes an attribute from an HTML node
func (e *ContentEngine) removeAttribute(n *html.Node, key string) {
for i, attr := range n.Attr {
if attr.Key == key {
n.Attr = append(n.Attr[:i], n.Attr[i+1:]...)
break
}
}
}
// hasClass checks if an HTML node has a specific class
func (e *ContentEngine) hasClass(n *html.Node, className string) bool {
for _, attr := range n.Attr {
if attr.Key == "class" {
classes := strings.Fields(attr.Val)
for _, class := range classes {
if class == className {
return true
}
}
}
}
return false
}
// getPlaceholderForElement returns appropriate placeholder text for an element type
func (e *ContentEngine) getPlaceholderForElement(elementType string) string {
placeholders := map[string]string{
"blockquote": "Enter your quote here...",
"cite": "Enter author name...",
"h1": "Enter heading...",
"h2": "Enter heading...",
"h3": "Enter heading...",
"p": "Enter text...",
"span": "Enter text...",
"div": "Enter content...",
"a": "Enter link text...",
}
if placeholder, exists := placeholders[elementType]; exists {
return placeholder
}
return "Enter content..."
}
// processCollection handles collection detection, persistence and reconstruction
func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionID, siteID string) error {
// 1. Check if collection exists in database
existingCollection, err := e.client.GetCollection(context.Background(), siteID, collectionID)
collectionExists := (err == nil && existingCollection != nil)
if !collectionExists {
// 2. New collection: extract container HTML and create collection record
containerHTML := e.extractOriginalTemplate(collectionNode)
_, err := e.client.CreateCollection(context.Background(), siteID, collectionID, containerHTML, "system")
if err != nil {
return fmt.Errorf("failed to create collection %s: %w", collectionID, err)
}
// 3. Extract templates and store initial items from existing children
err = e.extractAndStoreTemplatesAndItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to extract templates and items for collection %s: %w", collectionID, err)
}
fmt.Printf("✅ Created new collection: %s with templates and initial items\n", collectionID)
} else {
// 4. Existing collection: Always reconstruct from database (database is source of truth)
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct collection %s: %w", collectionID, err)
}
// Get final item count for logging
existingItems, _ := e.client.GetCollectionItems(context.Background(), siteID, collectionID)
fmt.Printf("✅ Reconstructed collection: %s from database (%d items)\n", collectionID, len(existingItems))
}
return nil
}
// extractAndStoreTemplatesAndItems extracts templates and stores initial items from existing collection children
func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.Node, collectionID, siteID string) error {
// Find existing children elements to use as templates
var templateElements []*html.Node
// Walk through direct children of the collection
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
templateElements = append(templateElements, child)
}
}
if len(templateElements) == 0 {
// No existing children - create a default empty template
_, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, "default", "<div>New item</div>", true)
if err != nil {
return fmt.Errorf("failed to create default template: %w", err)
}
fmt.Printf("✅ Created default template for collection %s\n", collectionID)
return nil
}
// Extract templates from existing children and store them
var templateIDs []int
for i, templateElement := range templateElements {
templateHTML := e.extractCleanTemplate(templateElement)
templateName := fmt.Sprintf("template-%d", i+1)
isDefault := (i == 0) // First template is default
template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault)
if err != nil {
return fmt.Errorf("failed to create template %s: %w", templateName, err)
}
templateIDs = append(templateIDs, template.TemplateID)
fmt.Printf("✅ Created template '%s' for collection %s\n", templateName, collectionID)
}
// Store original children as initial collection items (database-first approach)
err := e.storeChildrenAsCollectionItems(collectionNode, collectionID, siteID, templateIDs)
if err != nil {
return fmt.Errorf("failed to store initial collection items: %w", err)
}
// Reconstruct items from database to ensure proper data-item-id injection
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct initial collection items: %w", err)
}
return nil
}
// reconstructCollectionItems rebuilds collection items from database and adds them to DOM
func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, collectionID, siteID string) error {
// Get all items for this collection from database
items, err := e.client.GetCollectionItems(context.Background(), siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection items: %w", err)
}
// Get templates for this collection
templates, err := e.client.GetCollectionTemplates(context.Background(), siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection templates: %w", err)
}
// Create a template map for quick lookup
templateMap := make(map[int]string)
for _, template := range templates {
templateMap[template.TemplateID] = template.HTMLTemplate
}
// Clear existing children (they will be replaced with database items)
for child := collectionNode.FirstChild; child != nil; {
next := child.NextSibling
collectionNode.RemoveChild(child)
child = next
}
// Add items from database in position order using unified .insertr approach
for _, item := range items {
// Parse the stored structural HTML with content IDs (no template needed for reconstruction)
structuralDoc, err := html.Parse(strings.NewReader(item.HTMLContent))
if err != nil {
fmt.Printf("⚠️ Failed to parse stored HTML for %s: %v\n", item.ItemID, err)
continue
}
var structuralBody *html.Node
e.walkNodes(structuralDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
structuralBody = n
}
})
if structuralBody != nil {
// Process each .insertr element using Injector pattern (unified approach)
injector := NewInjector(e.client, siteID, nil)
// Walk through structural elements and hydrate with content from content table
e.walkNodes(structuralBody, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
// Get content ID from data attribute
contentID := GetAttribute(n, "data-content-id")
if contentID != "" {
// Use Injector to hydrate content (unified .insertr approach)
element := &Element{Node: n, Type: "html"}
err := injector.InjectContent(element, contentID)
if err != nil {
fmt.Printf("⚠️ Failed to inject content for %s: %v\n", contentID, err)
}
}
}
})
// Add hydrated structural elements directly to collection (stored HTML has complete structure)
for structuralChild := structuralBody.FirstChild; structuralChild != nil; {
next := structuralChild.NextSibling
structuralBody.RemoveChild(structuralChild)
// Inject data-item-id attribute for collection item identification
if structuralChild.Type == html.ElementNode {
e.setAttribute(structuralChild, "data-item-id", item.ItemID)
}
collectionNode.AppendChild(structuralChild)
structuralChild = next
}
}
}
fmt.Printf("✅ Reconstructed %d items for collection %s\n", len(items), collectionID)
return nil
}
// processChildElementsAsContent processes .insertr elements within a collection child and stores them as individual content
func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, siteID, itemID string) ([]ContentEntry, error) {
var contentEntries []ContentEntry
elementIndex := 0
// Walk through child element to find .insertr elements
e.walkNodes(childElement, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
// Use core IDGenerator for unified ID generation (like individual .insertr elements)
contentID := e.idGenerator.Generate(n, "collection-item")
// Extract actual content from the element
actualContent := ExtractTextContent(n)
// Store as individual content entry (unified .insertr approach)
_, err := e.client.CreateContent(context.Background(), siteID, contentID, actualContent, "", "system")
if err != nil {
fmt.Printf("⚠️ Failed to create content %s: %v\n", contentID, err)
return
}
// Add to content entries for structural template generation
contentEntries = append(contentEntries, ContentEntry{
ID: contentID,
SiteID: siteID,
HTMLContent: actualContent,
Template: e.extractOriginalTemplate(n),
})
elementIndex++
}
})
return contentEntries, nil
}
// generateStructuralTemplateFromChild creates structure-only HTML from child element with content IDs
func (e *ContentEngine) generateStructuralTemplateFromChild(childElement *html.Node, contentEntries []ContentEntry) (string, error) {
// Clone the child element to avoid modifying original
clonedChild := e.cloneNode(childElement)
entryIndex := 0
// Walk through cloned element and replace content with content IDs
e.walkNodes(clonedChild, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
if entryIndex < len(contentEntries) {
// Set the data-content-id attribute
e.setAttribute(n, "data-content-id", contentEntries[entryIndex].ID)
// Clear content - this will be hydrated during reconstruction
for child := n.FirstChild; child != nil; {
next := child.NextSibling
n.RemoveChild(child)
child = next
}
entryIndex++
}
}
})
// Render the complete structural template including container
var sb strings.Builder
html.Render(&sb, clonedChild)
return sb.String(), nil
}
// createVirtualElementFromTemplate creates a virtual DOM element from template HTML for API usage
// This allows API path to use the same structure extraction as enhancement path
func (e *ContentEngine) createVirtualElementFromTemplate(templateHTML string) (*html.Node, error) {
// Parse the template HTML
templateDoc, err := html.Parse(strings.NewReader(templateHTML))
if err != nil {
return nil, fmt.Errorf("failed to parse template HTML: %w", err)
}
// Find the body element and extract the template structure
var templateBody *html.Node
e.walkNodes(templateDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
templateBody = n
}
})
if templateBody != nil && templateBody.FirstChild != nil {
// Return the first child of body (the actual template element)
return templateBody.FirstChild, nil
}
return nil, fmt.Errorf("template does not contain valid structure")
}
// CreateCollectionItemFromTemplate creates a collection item using the unified engine approach
// This replaces TemplateProcessor with engine-native functionality
func (e *ContentEngine) CreateCollectionItemFromTemplate(
siteID, collectionID string,
templateID int,
templateHTML string,
lastEditedBy string,
) (*db.CollectionItemWithTemplate, error) {
// Create virtual element from template (like enhancement path)
virtualElement, err := e.createVirtualElementFromTemplate(templateHTML)
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Generate unique item ID using unified generator with collection context
itemID := e.idGenerator.Generate(virtualElement, "collection-item")
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Process .insertr elements and create content entries (unified approach)
contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID)
if err != nil {
return nil, fmt.Errorf("failed to process content entries: %w", err)
}
// Generate structural template using unified engine method
structuralTemplate, err := e.generateStructuralTemplateFromChild(virtualElement, contentEntries)
if err != nil {
return nil, fmt.Errorf("failed to generate structural template: %w", err)
}
// Create collection item with structural template
collectionItem, err := e.client.CreateCollectionItem(context.Background(),
siteID, collectionID, itemID, templateID, structuralTemplate, 0, lastEditedBy,
)
if err != nil {
return nil, fmt.Errorf("failed to create collection item: %w", err)
}
return collectionItem, nil
}
// cloneNode creates a deep copy of an HTML node
func (e *ContentEngine) cloneNode(node *html.Node) *html.Node {
cloned := &html.Node{
Type: node.Type,
Data: node.Data,
DataAtom: node.DataAtom,
Namespace: node.Namespace,
}
// Clone attributes
for _, attr := range node.Attr {
cloned.Attr = append(cloned.Attr, html.Attribute{
Namespace: attr.Namespace,
Key: attr.Key,
Val: attr.Val,
})
}
// Clone children recursively
for child := node.FirstChild; child != nil; child = child.NextSibling {
clonedChild := e.cloneNode(child)
cloned.AppendChild(clonedChild)
}
return cloned
}
// storeChildrenAsCollectionItems stores HTML children as collection items in database
func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node, collectionID, siteID string, templateIDs []int) error {
// Find existing children elements to store as items
var childElements []*html.Node
// Walk through direct children of the collection
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
childElements = append(childElements, child)
}
}
if len(childElements) == 0 {
fmt.Printf(" No children found to store as collection items for %s\n", collectionID)
return nil
}
// Store each child using unified .insertr approach (content table + structural template)
for i, childElement := range childElements {
// Generate item ID using unified generator with collection context
itemID := e.idGenerator.Generate(childElement, "collection-item")
// Process .insertr elements within this child (unified approach)
contentEntries, err := e.processChildElementsAsContent(childElement, siteID, itemID)
if err != nil {
return fmt.Errorf("failed to process content for item %s: %w", itemID, err)
}
// Generate structural template with content IDs (no actual content)
structuralTemplate, err := e.generateStructuralTemplateFromChild(childElement, contentEntries)
if err != nil {
return fmt.Errorf("failed to generate structural template for item %s: %w", itemID, err)
}
// Use appropriate template ID (cycle through available templates)
templateID := templateIDs[i%len(templateIDs)]
// Store structural template in collection_items (content lives in content table)
_, err = e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, i+1, "system")
if err != nil {
return fmt.Errorf("failed to create collection item %s: %w", itemID, err)
}
fmt.Printf("✅ Stored initial collection item: %s (template %d) with %d content entries\n", itemID, templateID, len(contentEntries))
}
return nil
}

View File

@@ -62,7 +62,7 @@ func (e *ContentEngine) ProcessDirectory(inputDir, outputDir, siteID string, mod
}) })
} }
// writeHTMLDocument writes an HTML document to a file // writeHTMLDocument writes a HTML document to a file
func writeHTMLDocument(outputPath string, doc *html.Node) error { func writeHTMLDocument(outputPath string, doc *html.Node) error {
// Create output directory // Create output directory
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {

View File

@@ -9,7 +9,6 @@ import (
"github.com/insertr/insertr/internal/config" "github.com/insertr/insertr/internal/config"
"github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db"
"golang.org/x/net/html" "golang.org/x/net/html"
"slices"
) )
// Injector handles content injection into HTML elements // Injector handles content injection into HTML elements
@@ -168,8 +167,8 @@ func (i *Injector) findElementByTag(node *html.Node, tag string) *html.Node {
// AddContentAttributes adds necessary data attributes and insertr class for editor functionality // AddContentAttributes adds necessary data attributes and insertr class for editor functionality
func (i *Injector) AddContentAttributes(node *html.Node, contentID string, contentType string) { func (i *Injector) AddContentAttributes(node *html.Node, contentID string, contentType string) {
i.setAttribute(node, "data-content-id", contentID) SetAttribute(node, "data-content-id", contentID)
i.addClass(node, "insertr") AddClass(node, "insertr")
} }
// InjectEditorAssets adds editor JavaScript to HTML document and injects demo gate if needed // InjectEditorAssets adds editor JavaScript to HTML document and injects demo gate if needed
@@ -200,63 +199,6 @@ func (i *Injector) findHeadElement(node *html.Node) *html.Node {
return nil return nil
} }
// setAttribute safely sets an attribute on an HTML node
func (i *Injector) setAttribute(node *html.Node, key, value string) {
// Remove existing attribute if present
for idx, attr := range node.Attr {
if attr.Key == key {
node.Attr = slices.Delete(node.Attr, idx, idx+1)
break
}
}
// Add new attribute
node.Attr = append(node.Attr, html.Attribute{
Key: key,
Val: value,
})
}
// addClass safely adds a class to an HTML node
func (i *Injector) addClass(node *html.Node, className string) {
var classAttr *html.Attribute
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classAttr = &attr
classIndex = idx
break
}
}
var classes []string
if classAttr != nil {
classes = strings.Fields(classAttr.Val)
}
// Check if class already exists
if slices.Contains(classes, className) {
return // Class already exists
}
// Add new class
classes = append(classes, className)
newClassValue := strings.Join(classes, " ")
if classIndex >= 0 {
// Update existing class attribute
node.Attr[classIndex].Val = newClassValue
} else {
// Add new class attribute
node.Attr = append(node.Attr, html.Attribute{
Key: "class",
Val: newClassValue,
})
}
}
// Element represents a parsed HTML element with metadata // Element represents a parsed HTML element with metadata
type Element struct { type Element struct {
Node *html.Node Node *html.Node

View File

@@ -7,9 +7,8 @@ import (
"slices" "slices"
) )
// GetClasses extracts CSS classes from an HTML node
func GetClasses(node *html.Node) []string { func GetClasses(node *html.Node) []string {
classAttr := getAttribute(node, "class") classAttr := GetAttribute(node, "class")
if classAttr == "" { if classAttr == "" {
return []string{} return []string{}
} }
@@ -23,8 +22,8 @@ func ContainsClass(classes []string, target string) bool {
return slices.Contains(classes, target) return slices.Contains(classes, target)
} }
// getAttribute gets an attribute value from an HTML node // GetAttribute gets an attribute value from an HTML node
func getAttribute(node *html.Node, key string) string { func GetAttribute(node *html.Node, key string) string {
for _, attr := range node.Attr { for _, attr := range node.Attr {
if attr.Key == key { if attr.Key == key {
return attr.Val return attr.Val
@@ -33,6 +32,112 @@ func getAttribute(node *html.Node, key string) string {
return "" return ""
} }
// SetAttribute sets an attribute on an HTML node
func SetAttribute(node *html.Node, key, value string) {
// Check for existing attribute and update in place
for i, attr := range node.Attr {
if attr.Key == key {
node.Attr[i].Val = value
return
}
}
// Add new attribute if not found
node.Attr = append(node.Attr, html.Attribute{
Key: key,
Val: value,
})
}
// AddClass safely adds a class to an HTML node
func AddClass(node *html.Node, className string) {
var classAttr *html.Attribute
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classAttr = &attr
classIndex = idx
break
}
}
var classes []string
if classAttr != nil {
classes = strings.Fields(classAttr.Val)
}
// Check if class already exists
if slices.Contains(classes, className) {
return
}
// Add new class
classes = append(classes, className)
newClassValue := strings.Join(classes, " ")
if classIndex >= 0 {
// Update existing class attribute
node.Attr[classIndex].Val = newClassValue
} else {
// Add new class attribute
node.Attr = append(node.Attr, html.Attribute{
Key: "class",
Val: newClassValue,
})
}
}
// RemoveClass safely removes a class from an HTML node
func RemoveClass(node *html.Node, className string) {
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classIndex = idx
break
}
}
if classIndex == -1 {
return // No class attribute found
}
// Parse existing classes
classes := strings.Fields(node.Attr[classIndex].Val)
// Filter out the target class
var newClasses []string
for _, class := range classes {
if class != className {
newClasses = append(newClasses, class)
}
}
// Update or remove class attribute
if len(newClasses) == 0 {
// Remove class attribute entirely if no classes remain
node.Attr = slices.Delete(node.Attr, classIndex, classIndex+1)
} else {
// Update class attribute with remaining classes
node.Attr[classIndex].Val = strings.Join(newClasses, " ")
}
}
// HasClass checks if a node has a specific class
func HasClass(node *html.Node, className string) bool {
for _, attr := range node.Attr {
if attr.Key == "class" {
classes := strings.Fields(attr.Val)
if slices.Contains(classes, className) {
return true
}
}
}
return false
}
// Inline formatting elements that are safe for editing // Inline formatting elements that are safe for editing
var inlineFormattingTags = map[string]bool{ var inlineFormattingTags = map[string]bool{
"strong": true, "strong": true,
@@ -76,9 +181,9 @@ var blockingElements = map[string]bool{
"dl": true, "dl": true,
} }
// hasEditableContent checks if a node contains content that can be safely edited // HasEditableContent checks if a node contains content that can be safely edited
// This includes text and safe inline formatting elements // This includes text and safe inline formatting elements
func hasEditableContent(node *html.Node) bool { func HasEditableContent(node *html.Node) bool {
if node.Type != html.ElementNode { if node.Type != html.ElementNode {
return false return false
} }
@@ -129,16 +234,15 @@ func isContainer(node *html.Node) bool {
"main": true, "main": true,
"aside": true, "aside": true,
"nav": true, "nav": true,
"ul": true, // Phase 3: Lists are containers "ul": true,
"ol": true, "ol": true,
} }
return containerTags[node.Data] return containerTags[node.Data]
} }
// findViableChildren finds all descendant elements that should get .insertr class // FindViableChildren finds all descendant elements that should get .insertr class
// Phase 3: Recursive traversal with block/inline classification and boundary respect func FindViableChildren(node *html.Node) []*html.Node {
func findViableChildren(node *html.Node) []*html.Node {
var viable []*html.Node var viable []*html.Node
traverseForViableElements(node, &viable) traverseForViableElements(node, &viable)
return viable return viable
@@ -266,12 +370,7 @@ func isDeferredElement(node *html.Node) bool {
// hasInsertrClass checks if node has class="insertr" // hasInsertrClass checks if node has class="insertr"
func hasInsertrClass(node *html.Node) bool { func hasInsertrClass(node *html.Node) bool {
classes := GetClasses(node) classes := GetClasses(node)
for _, class := range classes { return slices.Contains(classes, "insertr")
if class == "insertr" {
return true
}
}
return false
} }
// isSelfClosing checks if an element is typically self-closing // isSelfClosing checks if an element is typically self-closing
@@ -331,23 +430,6 @@ func findElementWithContent(node *html.Node, targetTag, targetContent string) *h
return nil return nil
} }
// GetAttribute gets an attribute value from an HTML node (exported version)
func GetAttribute(node *html.Node, key string) string {
return getAttribute(node, key)
}
// HasEditableContent checks if a node has editable content (exported version)
func HasEditableContent(node *html.Node) bool {
return hasEditableContent(node)
}
// FindViableChildren finds viable children for editing (exported version)
func FindViableChildren(node *html.Node) []*html.Node {
return findViableChildren(node)
}
// Text extraction utility functions
// ExtractTextContent extracts all text content from an HTML node recursively // ExtractTextContent extracts all text content from an HTML node recursively
func ExtractTextContent(node *html.Node) string { func ExtractTextContent(node *html.Node) string {
var text strings.Builder var text strings.Builder
@@ -355,7 +437,6 @@ func ExtractTextContent(node *html.Node) string {
return strings.TrimSpace(text.String()) return strings.TrimSpace(text.String())
} }
// extractTextRecursiveUnified is the internal unified implementation
func extractTextRecursiveUnified(node *html.Node, text *strings.Builder) { func extractTextRecursiveUnified(node *html.Node, text *strings.Builder) {
if node.Type == html.TextNode { if node.Type == html.TextNode {
text.WriteString(node.Data) text.WriteString(node.Data)

View File

@@ -37,8 +37,7 @@ dev: build-lib build
echo "🌐 All sites available at:" echo "🌐 All sites available at:"
echo " Default: http://localhost:8080/sites/default/" echo " Default: http://localhost:8080/sites/default/"
echo " Simple: http://localhost:8080/sites/simple/" echo " Simple: http://localhost:8080/sites/simple/"
echo " Dan Eden: http://localhost:8080/sites/dan-eden-portfolio/" echo " Simple: http://localhost:8080/sites/blog/"
echo " Devigo (NO): http://localhost:8080/sites/devigo-web/"
echo "" echo ""
echo "📝 Full-stack ready - edit content with real-time persistence!" echo "📝 Full-stack ready - edit content with real-time persistence!"
echo "🔄 Press Ctrl+C to shutdown" echo "🔄 Press Ctrl+C to shutdown"

View File

@@ -242,6 +242,29 @@ export class ApiClient {
} }
} }
/**
* Get collection preview data (container + templates for frontend reconstruction)
* @param {string} collectionId - Collection ID
* @returns {Promise<Object>} Object with collection_id, container_html, and templates
*/
async getCollectionPreview(collectionId) {
try {
const collectionsUrl = this.getCollectionsUrl();
const response = await fetch(`${collectionsUrl}/${collectionId}/preview?site_id=${this.siteId}`);
if (response.ok) {
const result = await response.json();
return result;
} else {
console.warn(`⚠️ Failed to fetch collection preview (${response.status}): ${collectionId}`);
return null;
}
} catch (error) {
console.error('Failed to fetch collection preview:', collectionId, error);
return null;
}
}
/** /**
* Reorder collection items in bulk * Reorder collection items in bulk
* @param {string} collectionId - Collection ID * @param {string} collectionId - Collection ID

View File

@@ -22,9 +22,6 @@ window.Insertr = {
// Initialize the library // Initialize the library
init(options = {}) { init(options = {}) {
console.log('🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)');
// Load CSS first
this.loadStyles(options); this.loadStyles(options);
// Initialize core business logic modules // Initialize core business logic modules

View File

@@ -814,6 +814,176 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
border-radius: var(--insertr-border-radius); border-radius: var(--insertr-border-radius);
} }
/* =================================================================
TEMPLATE SELECTION MODAL STYLES
================================================================= */
/* Template selection modal container */
.insertr-template-selector {
background: white;
border-radius: 8px;
padding: 24px;
max-width: 600px;
width: 95%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
font-family: var(--insertr-font-family);
margin: 20px;
}
.insertr-template-selector h3 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: var(--insertr-text-primary);
}
/* Template options container */
.insertr-template-options {
margin-bottom: 20px;
}
/* Individual template option */
.insertr-template-option {
border: 2px solid var(--insertr-border-color);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--insertr-bg-primary);
}
/* Default template styling - subtle indicator only */
.insertr-template-option.insertr-template-default {
border-left: 3px solid var(--insertr-primary);
background: #f8fafc;
}
/* Selected template styling - prominent selection indicator */
.insertr-template-option.insertr-template-selected {
border-color: var(--insertr-primary) !important;
background: #eff6ff !important;
box-shadow: 0 0 0 2px var(--insertr-primary) !important;
}
/* Default template when selected - override default styling */
.insertr-template-option.insertr-template-default.insertr-template-selected {
border-left: 2px solid var(--insertr-primary) !important;
border-color: var(--insertr-primary) !important;
background: #eff6ff !important;
box-shadow: 0 0 0 2px var(--insertr-primary) !important;
}
/* Hover state for template options (not selected) */
.insertr-template-option:hover:not(.insertr-template-selected) {
border-color: var(--insertr-text-secondary);
background: var(--insertr-bg-secondary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Default template hover (when not selected) */
.insertr-template-option.insertr-template-default:hover:not(.insertr-template-selected) {
background: #f1f5f9;
border-left-color: var(--insertr-primary-hover);
}
/* Selected template hover - enhance the selected state */
.insertr-template-option.insertr-template-selected:hover {
background: #dbeafe !important;
border-color: var(--insertr-primary-hover) !important;
box-shadow: 0 0 0 2px var(--insertr-primary-hover) !important;
}
/* Template name and info */
.insertr-template-name {
font-weight: 500;
margin-bottom: 4px;
color: var(--insertr-text-primary);
}
/* Template preview container */
.insertr-template-preview-container {
background: var(--insertr-bg-secondary);
border: 1px solid var(--insertr-border-color);
border-radius: 4px;
padding: 12px;
overflow: hidden;
max-height: 120px;
position: relative;
}
/* Styled template preview - preserves original styling */
.insertr-template-preview-render {
pointer-events: none;
overflow: hidden;
max-height: 100px;
}
/* Style placeholder content in previews */
.insertr-preview-content {
display: inline-block !important;
}
/* Fallback preview for errors */
.insertr-template-preview-fallback {
font-size: 13px;
color: var(--insertr-text-secondary);
font-family: var(--insertr-font-family);
font-style: italic;
padding: 8px;
}
/* Modal actions */
.insertr-template-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--insertr-border-color);
}
.insertr-template-btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-family: var(--insertr-font-family);
font-size: var(--insertr-font-size-base);
font-weight: 500;
transition: all 0.2s ease;
}
.insertr-template-btn-cancel {
border: 1px solid var(--insertr-border-color);
background: var(--insertr-bg-primary);
color: var(--insertr-text-primary);
}
.insertr-template-btn-cancel:hover {
background: var(--insertr-bg-secondary);
border-color: var(--insertr-text-secondary);
}
.insertr-template-btn-select {
background: var(--insertr-primary);
color: white;
border: none;
opacity: 0.5;
cursor: not-allowed;
}
.insertr-template-btn-select:not(:disabled) {
opacity: 1;
cursor: pointer;
}
.insertr-template-btn-select:not(:disabled):hover {
background: var(--insertr-primary-hover);
}
/* Add button positioned in top right of container */ /* Add button positioned in top right of container */
.insertr-add-btn { .insertr-add-btn {
position: absolute; position: absolute;
@@ -928,3 +1098,147 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
font-size: 14px; font-size: 14px;
} }
} }
/* =================================================================
LIVE COLLECTION PREVIEW MODAL
================================================================= */
.insertr-collection-preview-modal {
background: var(--insertr-bg-primary);
color: var(--insertr-text-primary);
border-radius: var(--insertr-border-radius);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
max-width: 90vw;
max-height: 90vh;
width: auto;
overflow: hidden;
position: relative;
z-index: var(--insertr-z-modal);
display: flex;
flex-direction: column;
}
.insertr-preview-header {
padding: var(--insertr-spacing-lg);
border-bottom: 1px solid var(--insertr-border-color);
text-align: center;
}
.insertr-preview-header h3 {
margin: 0 0 var(--insertr-spacing-xs) 0;
font-size: 1.2rem;
color: var(--insertr-text-primary);
}
.insertr-preview-header p {
margin: 0;
color: var(--insertr-text-secondary);
font-size: 0.9rem;
}
.insertr-preview-container {
padding: var(--insertr-spacing-lg);
overflow-y: auto;
flex: 1;
}
.insertr-preview-actions {
padding: var(--insertr-spacing-md) var(--insertr-spacing-lg);
border-top: 1px solid var(--insertr-border-color);
display: flex;
justify-content: center;
gap: var(--insertr-spacing-md);
}
/* Preview item selection styling */
.insertr-preview-item {
cursor: pointer;
transition: all 0.2s ease;
position: relative;
border-radius: var(--insertr-border-radius);
overflow: hidden;
}
.insertr-preview-item:hover {
transform: scale(1.02);
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.25);
z-index: 1;
}
.insertr-preview-item::after {
content: 'Click to select';
position: absolute;
top: var(--insertr-spacing-xs);
right: var(--insertr-spacing-xs);
background: rgba(59, 130, 246, 0.95);
color: white;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
border-radius: var(--insertr-border-radius);
font-size: 0.75rem;
font-weight: 500;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
white-space: nowrap;
}
.insertr-preview-item:hover::after {
opacity: 1;
}
/* Enhanced hover effect for better visual feedback */
.insertr-preview-item:hover {
outline: 2px solid var(--insertr-primary-color);
outline-offset: 2px;
}
/* Button styling for preview modal */
.insertr-template-btn {
background: var(--insertr-bg-secondary);
color: var(--insertr-text-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: var(--insertr-spacing-sm) var(--insertr-spacing-md);
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px;
}
.insertr-template-btn:hover {
background: var(--insertr-bg-hover);
border-color: var(--insertr-primary-color);
}
.insertr-template-btn-cancel {
background: var(--insertr-bg-secondary);
color: var(--insertr-text-primary);
}
.insertr-template-btn-cancel:hover {
background: var(--insertr-danger-color);
color: white;
border-color: var(--insertr-danger-color);
}
/* Mobile responsiveness for preview modal */
@media (max-width: 768px) {
.insertr-collection-preview-modal {
max-width: 95vw;
max-height: 95vh;
margin: var(--insertr-spacing-sm);
}
.insertr-preview-container {
padding: var(--insertr-spacing-md);
}
.insertr-preview-item:hover {
transform: scale(1.01);
}
.insertr-preview-item::after {
font-size: 0.7rem;
padding: 2px 6px;
}
}

View File

@@ -28,6 +28,7 @@ export class CollectionManager {
this.template = null; this.template = null;
this.items = []; this.items = [];
this.isActive = false; this.isActive = false;
this.cachedPreview = null; // Cache for collection preview data
// UI elements // UI elements
this.addButton = null; this.addButton = null;
@@ -411,17 +412,31 @@ export class CollectionManager {
} }
try { try {
// 1. Create collection item in database first (backend-first approach) // 1. Get collection preview data
const templateId = 1; // Use first template by default const previewData = await this.getCollectionPreview();
const collectionItem = await this.apiClient.createCollectionItem(this.collectionId, templateId); if (!previewData || !previewData.templates || previewData.templates.length === 0) {
console.error('❌ No templates available for collection:', this.collectionId);
alert('No templates available for this collection. Please refresh the page.');
return;
}
// 2. Select template (auto-select if only one, otherwise show live preview)
const selectedTemplate = await this.selectTemplate(previewData);
if (!selectedTemplate) {
console.log('Template selection cancelled by user');
return;
}
// 3. Create collection item in database first (backend-first approach)
const collectionItem = await this.apiClient.createCollectionItem(this.collectionId, selectedTemplate.template_id);
// 2. Create DOM element from the returned collection item data // 4. Create DOM element from the returned collection item data
const newItem = this.createItemFromCollectionData(collectionItem); const newItem = this.createItemFromCollectionData(collectionItem);
// 3. Add to DOM // 5. Add to DOM
this.container.insertBefore(newItem, this.addButton); this.container.insertBefore(newItem, this.addButton);
// 4. Update items array with backend data // 6. Update items array with backend data
const newItemData = { const newItemData = {
element: newItem, element: newItem,
index: this.items.length, index: this.items.length,
@@ -430,16 +445,16 @@ export class CollectionManager {
}; };
this.items.push(newItemData); this.items.push(newItemData);
// 5. Add controls to new item // 7. Add controls to new item
this.addItemControls(newItem, this.items.length - 1); this.addItemControls(newItem, this.items.length - 1);
// 6. Re-initialize any .insertr elements in the new item // 8. Re-initialize any .insertr elements in the new item
this.initializeInsertrElements(newItem); this.initializeInsertrElements(newItem);
// 7. Update all item controls (indices may have changed) // 9. Update all item controls (indices may have changed)
this.updateAllItemControls(); this.updateAllItemControls();
// 8. Trigger site enhancement to update static files // 10. Trigger site enhancement to update static files
await this.apiClient.enhanceSite(); await this.apiClient.enhanceSite();
console.log('✅ New item added successfully:', collectionItem.item_id); console.log('✅ New item added successfully:', collectionItem.item_id);
@@ -448,6 +463,164 @@ export class CollectionManager {
alert('Failed to add new item. Please try again.'); alert('Failed to add new item. Please try again.');
} }
} }
/**
* Get collection preview data (container + templates)
* @returns {Promise<Object>} Object with collection_id, container_html, and templates
*/
async getCollectionPreview() {
try {
if (!this.cachedPreview) {
console.log('🔍 Fetching preview for collection:', this.collectionId);
this.cachedPreview = await this.apiClient.getCollectionPreview(this.collectionId);
console.log('📋 Preview fetched:', this.cachedPreview);
}
return this.cachedPreview;
} catch (error) {
console.error('❌ Failed to fetch preview for collection:', this.collectionId, error);
return null;
}
}
/**
* Select a template for creating new items
* @param {Object} previewData - Preview data with container_html and templates
* @returns {Promise<Object|null>} Selected template or null if cancelled
*/
async selectTemplate(previewData) {
const templates = previewData.templates;
// Auto-select if only one template
if (templates.length === 1) {
console.log('🎯 Auto-selecting single template:', templates[0].name);
return templates[0];
}
// Present live collection preview for multiple templates
console.log('🎨 Multiple templates available, showing live preview');
return this.showLiveCollectionPreview(previewData);
}
/**
* Show live collection preview for template selection
* @param {Object} previewData - Preview data with container_html and templates
* @returns {Promise<Object|null>} Selected template or null if cancelled
*/
async showLiveCollectionPreview(previewData) {
return new Promise((resolve) => {
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'insertr-modal-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'insertr-collection-preview-modal';
// Generate live preview by reconstructing collection with all templates
const previewHTML = this.generateLivePreview(previewData.container_html, previewData.templates);
modal.innerHTML = `
<div class="insertr-preview-header">
<h3>Choose Template</h3>
<p>Click on the item you want to add</p>
</div>
<div class="insertr-preview-container">
${previewHTML}
</div>
<div class="insertr-preview-actions">
<button class="insertr-template-btn insertr-template-btn-cancel">Cancel</button>
</div>
`;
// Handle template selection by clicking on preview items
modal.addEventListener('click', (e) => {
const previewItem = e.target.closest('.insertr-preview-item');
if (previewItem) {
const templateId = parseInt(previewItem.dataset.templateId);
const selectedTemplate = previewData.templates.find(t => t.template_id === templateId);
if (selectedTemplate) {
console.log('🎯 Template selected from preview:', selectedTemplate.name);
document.body.removeChild(overlay);
resolve(selectedTemplate);
}
}
});
// Cancel button handler
modal.querySelector('.insertr-template-btn-cancel').addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(null);
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
resolve(null);
}
});
overlay.appendChild(modal);
document.body.appendChild(overlay);
});
}
/**
* Create safe template preview text (no HTML truncation)
* @param {string} html - HTML string
* @param {number} maxLength - Maximum character length
* @returns {string} Safe preview text
*/
createTemplatePreview(html, maxLength = 60) {
try {
// Create a temporary DOM element to safely extract text
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Extract just the text content
const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Truncate the text (not HTML)
if (textContent.length <= maxLength) {
return textContent;
}
return textContent.substring(0, maxLength).trim() + '...';
} catch (error) {
// Fallback to safer extraction
console.warn('Template preview extraction failed:', error);
return html.replace(/<[^>]*>/g, '').substring(0, maxLength) + '...';
}
}
/**
* Create styled template preview that shows actual template styling
* @param {string} html - HTML template string
* @returns {string} HTML preview with actual styles
*/
createStyledTemplatePreview(html) {
try {
// Clean the HTML and replace .insertr elements with placeholder content
let previewHtml = html
.replace(/class="insertr"/g, 'class="insertr-preview-content"')
.replace(/class="([^"]*\s+)?insertr(\s+[^"]*)?"/g, 'class="$1insertr-preview-content$2"')
.replace(/>([^<]{0,50})</g, (match, content) => {
// Replace long content with placeholder text
if (content.trim().length > 30) {
return '>Sample content...<';
}
return match;
});
// Wrap in a preview container with scaling
return `<div class="insertr-template-preview-render">${previewHtml}</div>`;
} catch (error) {
console.warn('Styled template preview failed:', error);
// Fallback to text preview
return `<div class="insertr-template-preview-fallback">${this.createTemplatePreview(html, 50)}</div>`;
}
}
/** /**
* Create a DOM element from collection item data returned by backend * Create a DOM element from collection item data returned by backend
@@ -770,4 +943,57 @@ export class CollectionManager {
console.log('🧹 CollectionManager destroyed'); console.log('🧹 CollectionManager destroyed');
} }
/**
* Generate live collection preview by reconstructing container with all template variants
* @param {string} containerHTML - Collection container HTML
* @param {Array} templates - Array of template objects
* @returns {string} HTML string with reconstructed collection
*/
generateLivePreview(containerHTML, templates) {
try {
// Parse the container HTML
const tempContainer = document.createElement('div');
tempContainer.innerHTML = containerHTML;
const collectionContainer = tempContainer.querySelector('.insertr-add');
if (!collectionContainer) {
console.error('❌ No .insertr-add container found in collection HTML');
return '<div>Preview generation failed</div>';
}
// Clear existing children
collectionContainer.innerHTML = '';
// Add one instance of each template with preview classes
templates.forEach(template => {
// Parse template HTML
const templateContainer = document.createElement('div');
templateContainer.innerHTML = template.html_template;
const templateElement = templateContainer.firstElementChild;
if (templateElement) {
// Clone the template element
const previewElement = templateElement.cloneNode(true);
// Add preview classes and data attributes for selection
previewElement.classList.add('insertr-preview-item');
previewElement.setAttribute('data-template-id', template.template_id);
previewElement.setAttribute('data-template-name', template.name);
// Add the preview element to the collection container
collectionContainer.appendChild(previewElement);
console.log(`✅ Added template ${template.template_id} (${template.name}) to preview`);
}
});
// Return the complete collection HTML
return tempContainer.innerHTML;
} catch (error) {
console.error('❌ Failed to generate live preview:', error);
return '<div>Preview generation failed</div>';
}
}
} }