feat: implement unified content engine to eliminate ID generation inconsistencies

- Create internal/engine module as single source of truth for content processing
- Consolidate 4 separate ID generation systems into one unified engine
- Update API handlers to use engine for consistent server-side ID generation
- Remove frontend client-side ID generation, delegate to server engine
- Ensure identical HTML markup + file path produces identical content IDs
- Resolve content persistence failures caused by ID fragmentation between manual editing and enhancement processes
This commit is contained in:
2025-09-16 15:04:27 +02:00
parent c1bc28d107
commit 84c90f428d
12 changed files with 1426 additions and 267 deletions

View File

@@ -19,8 +19,7 @@ import (
"github.com/insertr/insertr/internal/db"
"github.com/insertr/insertr/internal/db/postgresql"
"github.com/insertr/insertr/internal/db/sqlite"
"github.com/insertr/insertr/internal/parser"
"golang.org/x/net/html"
"github.com/insertr/insertr/internal/engine"
)
// ContentHandler handles all content-related HTTP requests
@@ -28,14 +27,19 @@ type ContentHandler struct {
database *db.Database
authService *auth.AuthService
siteManager *content.SiteManager
engine *engine.ContentEngine
}
// NewContentHandler creates a new content handler
func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler {
// Create database client for engine
dbClient := engine.NewDatabaseClient(database)
return &ContentHandler{
database: database,
authService: authService,
siteManager: nil, // Will be set via SetSiteManager
engine: engine.NewContentEngine(dbClient),
}
}
@@ -236,16 +240,31 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
siteID = "default" // final fallback
}
// Determine content ID - use provided ID or generate from element context
contentID := req.ID
if contentID == "" {
if req.ElementContext == nil {
http.Error(w, "Either ID or element_context required", http.StatusBadRequest)
return
}
contentID = h.generateContentID(req.ElementContext)
// Generate content ID using the unified engine
if req.HTMLMarkup == "" {
http.Error(w, "html_markup is required", http.StatusBadRequest)
return
}
result, engineErr := h.engine.ProcessContent(engine.ContentInput{
HTML: []byte(req.HTMLMarkup),
FilePath: req.FilePath,
SiteID: siteID,
Mode: engine.IDGeneration,
})
if engineErr != nil {
http.Error(w, fmt.Sprintf("ID generation failed: %v", engineErr), http.StatusInternalServerError)
return
}
if len(result.Elements) == 0 {
http.Error(w, "No insertr elements found in HTML markup", http.StatusBadRequest)
return
}
// Use the ID generated by the engine for the first element
contentID := result.Elements[0].ID
// Extract user from request using authentication service
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
if authErr != nil {
@@ -681,44 +700,7 @@ func (h *ContentHandler) versionMatches(version interface{}, contentID, siteID s
return false
}
// generateContentID creates a content ID from element context using the parser
func (h *ContentHandler) generateContentID(ctx *ElementContext) string {
// Create virtual node for existing parser ID generation
virtualNode := &html.Node{
Type: html.ElementNode,
Data: ctx.Tag,
Attr: []html.Attribute{
{Key: "class", Val: strings.Join(ctx.Classes, " ")},
},
}
// Add parent context as a virtual parent node if provided
if ctx.ParentContext != "" && ctx.ParentContext != "content" {
parentNode := &html.Node{
Type: html.ElementNode,
Data: "section",
Attr: []html.Attribute{
{Key: "class", Val: ctx.ParentContext},
},
}
parentNode.AppendChild(virtualNode)
virtualNode.Parent = parentNode
}
// Add text content for hash generation
if ctx.OriginalContent != "" {
textNode := &html.Node{
Type: html.TextNode,
Data: ctx.OriginalContent,
}
virtualNode.AppendChild(textNode)
}
// Use existing parser ID generator
// For API-generated IDs, use a placeholder filePath since we don't have file context
idGenerator := parser.NewIDGenerator()
return idGenerator.Generate(virtualNode, "api-generated")
}
// generateContentID function removed - using unified ContentEngine instead
// ServeInsertrJS handles GET /insertr.js - serves the insertr JavaScript library
func (h *ContentHandler) ServeInsertrJS(w http.ResponseWriter, r *http.Request) {

View File

@@ -42,12 +42,12 @@ type ElementContext struct {
// Request models
type CreateContentRequest struct {
ID string `json:"id,omitempty"` // For enhanced sites
ElementContext *ElementContext `json:"element_context,omitempty"` // For non-enhanced sites
SiteID string `json:"site_id,omitempty"`
Value string `json:"value"`
Type string `json:"type"`
CreatedBy string `json:"created_by,omitempty"`
HTMLMarkup string `json:"html_markup"` // HTML markup of the element
FilePath string `json:"file_path"` // File path for consistent ID generation
Value string `json:"value"` // Content value
Type string `json:"type"` // Content type
SiteID string `json:"site_id,omitempty"` // Site identifier
CreatedBy string `json:"created_by,omitempty"` // User who created the content
}
type RollbackContentRequest struct {