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
This commit is contained in:
2025-10-27 21:02:59 +01:00
parent 0bad96d866
commit 00255cb105
10 changed files with 486 additions and 33 deletions

View File

@@ -3,7 +3,9 @@ package engine
import (
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/insertr/insertr/internal/db"
"golang.org/x/net/html"
@@ -79,21 +81,35 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No
return nil
}
// Create templates for each unique child structure
// 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 {
templateName := fmt.Sprintf("template_%d", templateIndex+1)
templateHTML := e.extractCleanTemplate(child)
isDefault := templateIndex == 0
templateSignature := e.generateTemplateSignature(child)
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)
// 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)
}
templateIDs = append(templateIDs, template.TemplateID)
templateIndex++
fmt.Printf("✅ Created template '%s' for collection %s\n", templateName, collectionID)
}
}
@@ -212,8 +228,8 @@ func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, s
// 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
contentID := e.idGenerator.Generate(n, "collection-item")
// 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)
@@ -312,8 +328,9 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate(
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")
// 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)
@@ -456,3 +473,95 @@ func (e *ContentEngine) cloneNode(node *html.Node) *html.Node {
return cloned
}
// generateTemplateSignature creates a unique signature for template comparison
// This combines structural HTML + class-based styling differences
func (e *ContentEngine) generateTemplateSignature(element *html.Node) string {
// Get the clean template HTML (structure)
structuralHTML := e.extractCleanTemplate(element)
// Extract class-based styling signature
stylingSignature := e.extractClassSignature(element)
// Combine both for a unique signature
return fmt.Sprintf("%s|%s", structuralHTML, stylingSignature)
}
// extractClassSignature recursively extracts and normalizes class attributes
func (e *ContentEngine) extractClassSignature(element *html.Node) string {
var signature strings.Builder
e.walkNodes(element, 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))
}
}
})
return signature.String()
}
// 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
}