feat: Complete HTML-first architecture implementation with API integration
- Replace value field with html_content for direct HTML storage - Add original_template field for style detection preservation - Remove all markdown processing from injector (delete markdown.go) - Fix critical content extraction/injection bugs in engine - Add missing UpdateContent PUT handler for content persistence - Fix API client field names and add updateContent() method - Resolve content type validation (only allow text/link types) - Add UUID-based ID generation to prevent collisions - Complete first-pass processing workflow for unprocessed elements - Verify end-to-end: Enhancement → Database → API → Editor → Persistence All 37 files updated for HTML-first content management system. Phase 3a implementation complete and production ready.
This commit is contained in:
@@ -164,3 +164,10 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateContent creates a new content item via HTTP API
|
||||
func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) {
|
||||
// For now, HTTPClient CreateContent is not implemented for enhancer use
|
||||
// This would typically be used in API-driven enhancement scenarios
|
||||
return nil, fmt.Errorf("CreateContent not implemented for HTTPClient - use DatabaseClient for enhancement")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ import (
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// Helper function to convert sql.NullString to string
|
||||
func getStringFromNullString(ns sql.NullString) string {
|
||||
if ns.Valid {
|
||||
return ns.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DatabaseClient implements ContentClient for direct database access
|
||||
type DatabaseClient struct {
|
||||
db *db.Database
|
||||
@@ -132,20 +140,22 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten
|
||||
case "sqlite3":
|
||||
c := content.(sqlite.Content)
|
||||
return engine.ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HTMLContent: c.HtmlContent,
|
||||
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
||||
Type: c.Type,
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||
}
|
||||
case "postgresql":
|
||||
c := content.(postgresql.Content)
|
||||
return engine.ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HTMLContent: c.HtmlContent,
|
||||
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
||||
Type: c.Type,
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
return engine.ContentItem{} // Should never happen
|
||||
@@ -171,3 +181,61 @@ func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []eng
|
||||
}
|
||||
return []engine.ContentItem{} // Should never happen
|
||||
}
|
||||
|
||||
// CreateContent creates a new content item
|
||||
func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) {
|
||||
switch c.db.GetDBType() {
|
||||
case "sqlite3":
|
||||
content, err := c.db.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
HtmlContent: htmlContent,
|
||||
OriginalTemplate: toNullString(originalTemplate),
|
||||
Type: contentType,
|
||||
LastEditedBy: lastEditedBy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &engine.ContentItem{
|
||||
ID: content.ID,
|
||||
SiteID: content.SiteID,
|
||||
HTMLContent: content.HtmlContent,
|
||||
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||
Type: content.Type,
|
||||
LastEditedBy: content.LastEditedBy,
|
||||
}, nil
|
||||
|
||||
case "postgresql":
|
||||
content, err := c.db.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
HtmlContent: htmlContent,
|
||||
OriginalTemplate: toNullString(originalTemplate),
|
||||
Type: contentType,
|
||||
LastEditedBy: lastEditedBy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &engine.ContentItem{
|
||||
ID: content.ID,
|
||||
SiteID: content.SiteID,
|
||||
HTMLContent: content.HtmlContent,
|
||||
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||
Type: content.Type,
|
||||
LastEditedBy: content.LastEditedBy,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", c.db.GetDBType())
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert string to sql.NullString
|
||||
func toNullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
return sql.NullString{String: s, Valid: true}
|
||||
}
|
||||
|
||||
@@ -17,82 +17,82 @@ func NewMockClient() *MockClient {
|
||||
data := map[string]engine.ContentItem{
|
||||
// Navigation (index.html has collision suffix)
|
||||
"navbar-logo-2b10ad": {
|
||||
ID: "navbar-logo-2b10ad",
|
||||
SiteID: "demo",
|
||||
Value: "Acme Consulting Solutions",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "navbar-logo-2b10ad",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "Acme Consulting Solutions",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
"navbar-logo-2b10ad-a44bad": {
|
||||
ID: "navbar-logo-2b10ad-a44bad",
|
||||
SiteID: "demo",
|
||||
Value: "Acme Business Advisors",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "navbar-logo-2b10ad-a44bad",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "Acme Business Advisors",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
|
||||
// Hero Section - index.html (updated with actual IDs)
|
||||
"hero-title-7cfeea": {
|
||||
ID: "hero-title-7cfeea",
|
||||
SiteID: "demo",
|
||||
Value: "Transform Your Business with Strategic Expertise",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "hero-title-7cfeea",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "Transform Your Business with Strategic Expertise",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
"hero-lead-e47475": {
|
||||
ID: "hero-lead-e47475",
|
||||
SiteID: "demo",
|
||||
Value: "We help **ambitious businesses** grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.",
|
||||
Type: "markdown",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "hero-lead-e47475",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "We help <strong>ambitious businesses</strong> grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
"hero-link-76c620": {
|
||||
ID: "hero-link-76c620",
|
||||
SiteID: "demo",
|
||||
Value: "Schedule Free Consultation",
|
||||
Type: "link",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "hero-link-76c620",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "Schedule Free Consultation",
|
||||
Type: "link",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
|
||||
// Hero Section - about.html
|
||||
"hero-title-c70343": {
|
||||
ID: "hero-title-c70343",
|
||||
SiteID: "demo",
|
||||
Value: "About Our Consulting Expertise",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "hero-title-c70343",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "About Our Consulting Expertise",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
"hero-lead-673026": {
|
||||
ID: "hero-lead-673026",
|
||||
SiteID: "demo",
|
||||
Value: "We're a team of **experienced consultants** dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.",
|
||||
Type: "markdown",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "hero-lead-673026",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "We're a team of <strong>experienced consultants</strong> dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
|
||||
// Services Section
|
||||
"services-subtitle-c8927c": {
|
||||
ID: "services-subtitle-c8927c",
|
||||
SiteID: "demo",
|
||||
Value: "Our Story",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "services-subtitle-c8927c",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "Our Story",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
"services-text-0d96da": {
|
||||
ID: "services-text-0d96da",
|
||||
SiteID: "demo",
|
||||
Value: "**Founded in 2020**, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.",
|
||||
Type: "markdown",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "services-text-0d96da",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "<strong>Founded in 2020</strong>, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
|
||||
// Default fallback for any missing content
|
||||
"default": {
|
||||
ID: "default",
|
||||
SiteID: "demo",
|
||||
Value: "[Enhanced Content]",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
ID: "default",
|
||||
SiteID: "demo",
|
||||
HTMLContent: "[Enhanced Content]",
|
||||
Type: "text",
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -138,3 +138,22 @@ func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateContent creates a new mock content item
|
||||
func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) {
|
||||
// For mock client, just create and store the item
|
||||
item := engine.ContentItem{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
HTMLContent: htmlContent,
|
||||
OriginalTemplate: originalTemplate,
|
||||
Type: contentType,
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
LastEditedBy: lastEditedBy,
|
||||
}
|
||||
|
||||
// Store in mock data
|
||||
m.data[contentID] = item
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user