fix: systematic element matching bug in enhancement pipeline

- Problem: Element ID collisions between similar elements (logo h1 vs hero h1)
  causing content to be injected into wrong elements
- Root cause: Enhancer used naive tag+class matching instead of parser's
  sophisticated semantic analysis for element identification

Systematic solution:
- Enhanced parser architecture with exported utilities (GetClasses, ContainsClass)
- Added FindElementInDocument() with content-based semantic matching
- Replaced naive findAndInjectNodes() with parser-based element matching
- Removed code duplication between parser and enhancer packages

Backend improvements:
- Moved ID generation to backend for single source of truth
- Added ElementContext struct for frontend-backend communication
- Updated API handlers to support context-based content ID generation

Frontend improvements:
- Enhanced getElementMetadata() to extract semantic context
- Updated save flow to handle both enhanced and non-enhanced elements
- Improved API client to use backend-generated content IDs

Result:
- Unique content IDs: navbar-logo-200530 vs hero-title-a1de7b
- Precise element matching using content validation
- Single source of truth for DOM utilities in parser package
- Eliminated 40+ lines of duplicate code while fixing core bug
This commit is contained in:
2025-09-11 14:14:57 +02:00
parent f73e21ce6e
commit ef1d1083ce
12 changed files with 575 additions and 314 deletions

163
cmd/restore.go Normal file
View File

@@ -0,0 +1,163 @@
package cmd
import (
"fmt"
"log"
"sort"
"github.com/insertr/insertr/internal/content"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
timestamp string
latest bool
clean bool
)
var restoreCmd = &cobra.Command{
Use: "restore [site-id]",
Short: "Restore a site from backup",
Long: `Restore a registered site from a timestamped backup.
Examples:
insertr restore demo # List available backups
insertr restore demo --clean # Restore from oldest backup (cleanest)
insertr restore demo --latest # Restore from newest backup
insertr restore demo --timestamp 20250910-224704 # Restore from specific backup`,
Args: cobra.ExactArgs(1),
Run: runRestore,
}
func init() {
restoreCmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "specific backup timestamp to restore from")
restoreCmd.Flags().BoolVar(&latest, "latest", false, "restore from most recent backup")
restoreCmd.Flags().BoolVar(&clean, "clean", false, "restore from oldest backup (cleanest state)")
// Bind flags to viper
viper.BindPFlag("restore.timestamp", restoreCmd.Flags().Lookup("timestamp"))
viper.BindPFlag("restore.latest", restoreCmd.Flags().Lookup("latest"))
viper.BindPFlag("restore.clean", restoreCmd.Flags().Lookup("clean"))
}
func runRestore(cmd *cobra.Command, args []string) {
siteID := args[0]
// Initialize content client (we don't actually need it for restore, but SiteManager expects it)
contentClient := content.NewMockClient()
// Initialize site manager
siteManager := content.NewSiteManager(contentClient, "./insertr-backups", false)
// Load sites from configuration to register them
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
if configs, ok := siteConfigs.([]interface{}); ok {
var sites []*content.SiteConfig
for _, configInterface := range configs {
if configMap, ok := configInterface.(map[string]interface{}); ok {
site := &content.SiteConfig{}
if id, ok := configMap["site_id"].(string); ok {
site.SiteID = id
}
if path, ok := configMap["path"].(string); ok {
site.Path = path
}
if domain, ok := configMap["domain"].(string); ok {
site.Domain = domain
}
if autoEnhance, ok := configMap["auto_enhance"].(bool); ok {
site.AutoEnhance = autoEnhance
}
if backupOriginals, ok := configMap["backup_originals"].(bool); ok {
site.BackupOriginals = backupOriginals
}
sites = append(sites, site)
}
}
if err := siteManager.RegisterSites(sites); err != nil {
log.Fatalf("Failed to register sites: %v", err)
}
}
}
// List available backups
backups, err := siteManager.ListBackups(siteID)
if err != nil {
log.Fatalf("Failed to list backups: %v", err)
}
if len(backups) == 0 {
fmt.Printf("❌ No backups found for site '%s'\n", siteID)
fmt.Printf("💡 Backups are created automatically during enhancement when backup_originals is enabled\n")
return
}
// Sort backups chronologically
sort.Strings(backups)
// Handle different restore modes
var targetTimestamp string
if timestamp != "" {
// Specific timestamp provided
targetTimestamp = timestamp
found := false
for _, backup := range backups {
if backup == targetTimestamp {
found = true
break
}
}
if !found {
fmt.Printf("❌ Backup timestamp '%s' not found for site '%s'\n", targetTimestamp, siteID)
fmt.Printf("📋 Available backups:\n")
for i, backup := range backups {
if i == 0 {
fmt.Printf(" %s (oldest/cleanest)\n", backup)
} else if i == len(backups)-1 {
fmt.Printf(" %s (newest)\n", backup)
} else {
fmt.Printf(" %s\n", backup)
}
}
return
}
} else if clean {
// Restore from oldest backup (cleanest)
targetTimestamp = backups[0]
fmt.Printf("🧹 Restoring from oldest backup (cleanest state): %s\n", targetTimestamp)
} else if latest {
// Restore from newest backup
targetTimestamp = backups[len(backups)-1]
fmt.Printf("🔄 Restoring from newest backup: %s\n", targetTimestamp)
} else {
// No specific option - list available backups
fmt.Printf("📋 Available backups for site '%s':\n", siteID)
for i, backup := range backups {
if i == 0 {
fmt.Printf(" %s (oldest/cleanest) ← use --clean\n", backup)
} else if i == len(backups)-1 {
fmt.Printf(" %s (newest) ← use --latest\n", backup)
} else {
fmt.Printf(" %s\n", backup)
}
}
fmt.Printf("\nUsage:\n")
fmt.Printf(" insertr restore %s --clean # restore from oldest backup\n", siteID)
fmt.Printf(" insertr restore %s --latest # restore from newest backup\n", siteID)
fmt.Printf(" insertr restore %s --timestamp %s # restore from specific backup\n", siteID, backups[0])
return
}
// Perform restore
fmt.Printf("🔄 Restoring site '%s' from backup %s...\n", siteID, targetTimestamp)
if err := siteManager.RestoreFromBackup(siteID, targetTimestamp); err != nil {
log.Fatalf("❌ Restore failed: %v", err)
}
fmt.Printf("✅ Successfully restored site '%s' from backup %s\n", siteID, targetTimestamp)
fmt.Printf("💡 Site files have been restored to their state from %s\n", targetTimestamp)
}

View File

@@ -1,138 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - Acme Consulting Services</title>
<link rel="stylesheet" href="assets/style.css">
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>About - Acme Consulting Services</title>
<link rel="stylesheet" href="assets/style.css"/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr">Acme Consulting</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr" >Acme Consulting</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</div>
</nav>
</div>
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr">About Acme Consulting</h1>
<p class="lead insertr">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
</section>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr" >About Acme Consulting</h1>
<p class="lead insertr" >We&#39;re a team of experienced consultants dedicated to helping small businesses thrive in today&#39;s competitive marketplace.</p>
</div>
</section>
<!-- Story Section -->
<section class="services">
<div class="container">
<h2 class="insertr">Our Story</h2>
<div class="insertr-group">
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<!-- Story Section -->
<section class="services">
<div class="container">
<h2 class="insertr" >Our Story</h2>
<div class="insertr-group">
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn&#39;t serving the needs of growing businesses. We set out to change that.</p>
<p>Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
</div>
</div>
</section>
<p>Today, we&#39;ve helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
</div>
</div>
</section>
<!-- Team Section -->
<section class="cta">
<div class="container">
<h2 class="insertr">Our Team</h2>
<p class="insertr">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<!-- Team Section -->
<section class="cta">
<div class="container">
<h2 class="insertr" >Our Team</h2>
<p class="insertr" >We&#39;re a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<div class="services-grid" style="margin-top: 3rem;">
<div class="service-card">
<div class="insertr">
<h3>Sarah Chen</h3>
<p><strong>Founder & CEO</strong></p>
<p>Former <strong>McKinsey consultant</strong> with 15 years of experience in strategy and operations. MBA from Stanford.</p>
</div>
</div>
<div class="service-card">
<div class="insertr">
<h3>Michael Rodriguez</h3>
<p><strong>Head of Operations</strong></p>
<p>20 years in manufacturing and supply chain optimization. Expert in <strong>lean methodologies</strong> and process improvement.</p>
</div>
</div>
<div class="service-card">
<div class="insertr">
<h3>Emma Thompson</h3>
<p><strong>Digital Strategy Lead</strong></p>
<p>Former tech startup founder turned consultant. Specializes in <em>digital transformation</em> and technology adoption.</p>
</div>
</div>
</div>
</div>
</section>
<div class="services-grid" style="margin-top: 3rem;">
<div class="service-card">
<div class="insertr">
<h3>Sarah Chen</h3>
<p><strong>Founder &amp; CEO</strong></p>
<p>Former <strong>McKinsey consultant</strong> with 15 years of experience in strategy and operations. MBA from Stanford.</p>
</div>
</div>
<div class="service-card">
<div class="insertr">
<h3>Michael Rodriguez</h3>
<p><strong>Head of Operations</strong></p>
<p>20 years in manufacturing and supply chain optimization. Expert in <strong>lean methodologies</strong> and process improvement.</p>
</div>
</div>
<div class="service-card">
<div class="insertr">
<h3>Emma Thompson</h3>
<p><strong>Digital Strategy Lead</strong></p>
<p>Former tech startup founder turned consultant. Specializes in <em>digital transformation</em> and technology adoption.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Values Section -->
<section class="testimonial">
<div class="container">
<h2 class="insertr" style="margin-bottom: 2rem;">Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3 class="insertr">Client-First</h3>
<p class="insertr">Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3 class="insertr">Practical Solutions</h3>
<p class="insertr">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3 class="insertr">Long-term Partnership</h3>
<p class="insertr">We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
</div>
</div>
</section>
<!-- Values Section -->
<section class="testimonial">
<div class="container">
<h2 class="insertr" style="margin-bottom: 2rem;" >Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3 class="insertr" >Client-First</h3>
<p class="insertr" >Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3 class="insertr" >Practical Solutions</h3>
<p class="insertr" >We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3 class="insertr" >Long-term Partnership</h3>
<p class="insertr" >We&#39;re not just consultants; we&#39;re partners in your business success for the long haul.</p>
</div>
</div>
</div>
</section>
<!-- Test Section for Insertr Features -->
<section class="testimonial">
<div class="container">
<h2 class="insertr">Feature Tests</h2>
<!-- Test Section for Insertr Features -->
<section class="testimonial">
<div class="container">
<h2 class="insertr" >Feature Tests</h2>
<!-- Test 1: .insertr container expansion (should make each p individually editable) -->
<div style="margin-bottom: 2rem;">
<h3>Test 1: Container Expansion (.insertr)</h3>
<div class="insertr" style="border: 2px dashed #ccc; padding: 1rem;">
<p>This paragraph should be individually editable with a textarea.</p>
<p>This second paragraph should also be individually editable.</p>
<p>Each paragraph should get its own modal when clicked.</p>
</div>
</div>
<!-- Test 1: .insertr container expansion (should make each p individually editable) -->
<div style="margin-bottom: 2rem;">
<h3>Test 1: Container Expansion (.insertr)</h3>
<div class="insertr" style="border: 2px dashed #ccc; padding: 1rem;">
<p>This paragraph should be individually editable with a textarea.</p>
<p>This second paragraph should also be individually editable.</p>
<p>Each paragraph should get its own modal when clicked.</p>
</div>
</div>
<!-- Test 2: .insertr-group collective editing (should edit all together) -->
<div>
<h3>Test 2: Group Editing (.insertr-group)</h3>
<div class="insertr-group" style="border: 2px solid #007cba; padding: 1rem;">
<p>This paragraph is part of a <strong>group</strong>.</p>
<p>Clicking anywhere should open one markdown editor with <em>rich formatting</em>.</p>
<p>All content should be <strong>editable together</strong> as markdown with proper <em>HTML conversion</em>.</p>
</div>
</div>
</div>
</section>
<!-- Test 2: .insertr-group collective editing (should edit all together) -->
<div>
<h3>Test 2: Group Editing (.insertr-group)</h3>
<div class="insertr-group" style="border: 2px solid #007cba; padding: 1rem;">
<p>This paragraph is part of a <strong>group</strong>.</p>
<p>Clicking anywhere should open one markdown editor with <em>rich formatting</em>.</p>
<p>All content should be <strong>editable together</strong> as markdown with proper <em>HTML conversion</em>.</p>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
</div>
</footer>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
</div>
</footer>
<!-- Insertr JavaScript Library -->
<script type="text/javascript" src="insertr.js"></script>
<!-- Insertr JavaScript Library -->
<script type="text/javascript" src="insertr.js"></script>
</body>
</html>
</body></html>

View File

@@ -1,87 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acme Consulting Services - Live Reload Test</title>
<link rel="stylesheet" href="assets/style.css">
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Acme Consulting Services - Live Reload Test</title>
<link rel="stylesheet" href="assets/style.css"/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr">Acme Consulting</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr" >Acme Consulting!!!</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</div>
</nav>
</div>
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr">Transform Your Business with Expert Consulting</h1>
<p class="lead insertr">We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
<a href="contact.html" class="btn-primary insertr">Get Started Today</a>
</div>
</section>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr" >!Transform Your Business with Expert Consulting!</h1>
<p class="lead insertr" >We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success. Superb</p>
<a href="contact.html" class="btn-primary insertr" >Get Started Today?</a>
</div>
</section>
<!-- Services Section -->
<section class="services">
<div class="container">
<h2 class="insertr">Our Services</h2>
<p class="section-subtitle insertr">Comprehensive solutions tailored to your business needs</p>
<!-- Services Section -->
<section class="services">
<div class="container">
<h2 class="insertr" >Our Services</h2>
<p class="section-subtitle insertr" >Comprehensive solutions tailored to your business needs</p>
<div class="services-grid">
<div class="service-card">
<h3 class="insertr">Strategic Planning</h3>
<p class="insertr">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
</div>
<div class="service-card">
<h3 class="insertr">Operations Optimization</h3>
<p class="insertr">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
</div>
<div class="service-card">
<h3 class="insertr">Digital Transformation</h3>
<p class="insertr">Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
</div>
</div>
</div>
</section>
<div class="services-grid">
<div class="service-card">
<h3 class="insertr" >Strategic Planning</h3>
<p class="insertr" >Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
</div>
<div class="service-card">
<h3 class="insertr" >Operations Optimization</h3>
<p class="insertr" >Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
</div>
<div class="service-card">
<h3 class="insertr" >Digital Transformation</h3>
<p class="insertr" >Modernize your technology stack and digital presence to compete effectively in today&#39;s marketplace.</p>
</div>
</div>
</div>
</section>
<!-- Testimonial Section -->
<section class="testimonial">
<div class="container">
<blockquote>
<p class="insertr">"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
<cite class="insertr">Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote>
</div>
</section>
<!-- Testimonial Section -->
<section class="testimonial">
<div class="container">
<blockquote>
<p class="insertr" >&#34;Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations.&#34;</p>
<cite class="insertr" >Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote>
</div>
</section>
<!-- Call to Action -->
<section class="cta">
<div class="container">
<h2 class="insertr">Ready to Transform Your Business?</h2>
<p class="insertr">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
<a href="contact.html" class="btn-primary insertr">Schedule Consultation</a>
</div>
</section>
<!-- Call to Action -->
<section class="cta">
<div class="container">
<h2 class="insertr" >Ready to Transform Your Business?</h2>
<p class="insertr" >Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
<a href="contact.html" class="btn-primary insertr" >Schedule Consultation</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
</div>
</footer>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
</div>
</footer>
<!-- Insertr JavaScript Library -->
<script type="text/javascript" src="insertr.js"></script>
<!-- Insertr JavaScript Library -->
<script type="text/javascript" src="insertr.js"></script>
</body>
</html>
</body></html>

View File

@@ -17,6 +17,8 @@ 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"
)
// ContentHandler handles all content-related HTTP requests
@@ -232,6 +234,16 @@ 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)
}
// Extract user from request using authentication service
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
if authErr != nil {
@@ -246,7 +258,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
switch h.database.GetDBType() {
case "sqlite3":
content, err = h.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
ID: req.ID,
ID: contentID,
SiteID: siteID,
Value: req.Value,
Type: req.Type,
@@ -254,7 +266,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
})
case "postgresql":
content, err = h.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
ID: req.ID,
ID: contentID,
SiteID: siteID,
Value: req.Value,
Type: req.Type,
@@ -728,3 +740,41 @@ 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
idGenerator := parser.NewIDGenerator()
return idGenerator.Generate(virtualNode)
}

View File

@@ -31,13 +31,23 @@ type ContentVersionsResponse struct {
Versions []ContentVersion `json:"versions"`
}
// Element context for backend ID generation
type ElementContext struct {
Tag string `json:"tag"`
Classes []string `json:"classes"`
OriginalContent string `json:"original_content"`
ParentContext string `json:"parent_context"`
Purpose string `json:"purpose"`
}
// Request models
type CreateContentRequest struct {
ID string `json:"id"`
SiteID string `json:"site_id,omitempty"`
Value string `json:"value"`
Type string `json:"type"`
CreatedBy string `json:"created_by,omitempty"`
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"`
}
type UpdateContentRequest struct {

View File

@@ -98,52 +98,32 @@ func (e *Enhancer) injectElementContent(doc *html.Node, elem parser.Element) err
return nil
}
// findAndInjectNodes recursively finds nodes and injects content
func (e *Enhancer) findAndInjectNodes(node *html.Node, elem parser.Element, contentItem *ContentItem) {
if node.Type == html.ElementNode {
// Check if this node matches our element criteria
classes := getClasses(node)
if containsClass(classes, "insertr") && node.Data == elem.Tag {
// This might be our target node - inject content
e.injector.addContentAttributes(node, elem.ContentID, string(elem.Type))
if contentItem != nil {
switch elem.Type {
case parser.ContentText:
e.injector.injectTextContent(node, contentItem.Value)
case parser.ContentMarkdown:
e.injector.injectMarkdownContent(node, contentItem.Value)
case parser.ContentLink:
e.injector.injectLinkContent(node, contentItem.Value)
}
}
}
// findAndInjectNodes finds the specific node for this element and injects content
func (e *Enhancer) findAndInjectNodes(rootNode *html.Node, elem parser.Element, contentItem *ContentItem) {
// Use parser-based element matching to find the correct specific node
targetNode := e.findNodeInDocument(rootNode, elem)
if targetNode == nil {
// Element not found - this is normal for elements without content in database
return
}
// Recursively process children
for child := node.FirstChild; child != nil; child = child.NextSibling {
e.findAndInjectNodes(child, elem, contentItem)
// Inject content attributes for the correctly matched node
e.injector.addContentAttributes(targetNode, elem.ContentID, string(elem.Type))
// Inject content if available
if contentItem != nil {
switch elem.Type {
case parser.ContentText:
e.injector.injectTextContent(targetNode, contentItem.Value)
case parser.ContentMarkdown:
e.injector.injectMarkdownContent(targetNode, contentItem.Value)
case parser.ContentLink:
e.injector.injectLinkContent(targetNode, contentItem.Value)
}
}
}
// Helper functions from parser package
func getClasses(node *html.Node) []string {
for _, attr := range node.Attr {
if attr.Key == "class" {
return strings.Fields(attr.Val)
}
}
return []string{}
}
func containsClass(classes []string, target string) bool {
for _, class := range classes {
if class == target {
return true
}
}
return false
}
// Helper functions are now provided by the parser package
// EnhanceDirectory processes all HTML files in a directory
func (e *Enhancer) EnhanceDirectory(inputDir, outputDir string) error {
@@ -295,28 +275,10 @@ func (e *Enhancer) enhanceFileInPlace(filePath string, elements []parser.Element
return e.writeHTML(doc, filePath)
}
// findNodeInDocument finds a specific node in the HTML document tree
// findNodeInDocument finds a specific node in the HTML document tree using parser utilities
func (e *Enhancer) findNodeInDocument(doc *html.Node, elem parser.Element) *html.Node {
// This is a simplified approach - in a production system we might need
// more sophisticated node matching based on attributes, position, etc.
return e.findNodeByTagAndClass(doc, elem.Tag, "insertr")
// Use parser's sophisticated matching logic
return parser.FindElementInDocument(doc, elem)
}
// findNodeByTagAndClass recursively searches for a node with specific tag and class
func (e *Enhancer) findNodeByTagAndClass(node *html.Node, targetTag, targetClass string) *html.Node {
if node.Type == html.ElementNode && node.Data == targetTag {
classes := getClasses(node)
if containsClass(classes, targetClass) {
return node
}
}
// Search children
for child := node.FirstChild; child != nil; child = child.NextSibling {
if result := e.findNodeByTagAndClass(child, targetTag, targetClass); result != nil {
return result
}
}
return nil
}
// All element matching functions are now provided by the parser package

View File

@@ -28,7 +28,9 @@ func (g *IDGenerator) Generate(node *html.Node) string {
contentHash := g.getContentHash(node)
baseID := g.createBaseID(context, purpose, contentHash)
return g.ensureUnique(baseID)
finalID := g.ensureUnique(baseID)
return finalID
}
// getSemanticContext determines the semantic context from parent elements
@@ -36,11 +38,11 @@ func (g *IDGenerator) getSemanticContext(node *html.Node) string {
// Walk up the tree to find semantic containers
parent := node.Parent
for parent != nil && parent.Type == html.ElementNode {
classes := getClasses(parent)
classes := GetClasses(parent)
// Check for common semantic section classes
for _, class := range []string{"hero", "services", "nav", "navbar", "footer", "about", "contact", "testimonial"} {
if containsClass(classes, class) {
if ContainsClass(classes, class) {
return class
}
}
@@ -68,7 +70,7 @@ func (g *IDGenerator) getSemanticContext(node *html.Node) string {
// getPurpose determines the purpose/role of the element
func (g *IDGenerator) getPurpose(node *html.Node) string {
tag := strings.ToLower(node.Data)
classes := getClasses(node)
classes := GetClasses(node)
// Check for specific CSS classes that indicate purpose
for _, class := range classes {

View File

@@ -90,15 +90,15 @@ func (p *Parser) parseFile(filePath string) ([]Element, []string, error) {
// findInsertrElements recursively finds all elements with "insertr" class
func (p *Parser) findInsertrElements(node *html.Node, filePath string, elements *[]Element, warnings *[]string) {
if node.Type == html.ElementNode {
classes := getClasses(node)
classes := GetClasses(node)
// Check if element has "insertr" class
if containsClass(classes, "insertr") {
if ContainsClass(classes, "insertr") {
if isContainer(node) {
// Container element - expand to viable children
viableChildren := findViableChildren(node)
for _, child := range viableChildren {
childClasses := getClasses(child)
childClasses := GetClasses(child)
element, warning := p.createElement(child, filePath, childClasses)
*elements = append(*elements, element)
if warning != "" {
@@ -181,13 +181,13 @@ func (p *Parser) resolveContentID(node *html.Node) (string, bool) {
// detectContentType determines the content type based on element and classes
func (p *Parser) detectContentType(node *html.Node, classes []string) ContentType {
// Check for explicit type classes first
if containsClass(classes, "insertr-markdown") {
if ContainsClass(classes, "insertr-markdown") {
return ContentMarkdown
}
if containsClass(classes, "insertr-link") {
if ContainsClass(classes, "insertr-link") {
return ContentLink
}
if containsClass(classes, "insertr-text") {
if ContainsClass(classes, "insertr-text") {
return ContentText
}

View File

@@ -6,8 +6,8 @@ import (
"golang.org/x/net/html"
)
// getClasses extracts CSS classes from an HTML node
func getClasses(node *html.Node) []string {
// GetClasses extracts CSS classes from an HTML node
func GetClasses(node *html.Node) []string {
classAttr := getAttribute(node, "class")
if classAttr == "" {
return []string{}
@@ -17,8 +17,8 @@ func getClasses(node *html.Node) []string {
return classes
}
// containsClass checks if a class list contains a specific class
func containsClass(classes []string, target string) bool {
// ContainsClass checks if a class list contains a specific class
func ContainsClass(classes []string, target string) bool {
for _, class := range classes {
if class == target {
return true
@@ -157,3 +157,39 @@ func isSelfClosing(node *html.Node) bool {
return selfClosingTags[node.Data]
}
// FindElementInDocument finds a parser element in HTML document tree using semantic matching
func FindElementInDocument(doc *html.Node, element Element) *html.Node {
return findElementWithContext(doc, element)
}
// findElementWithContext uses the parser's semantic understanding to find the correct element
func findElementWithContext(node *html.Node, target Element) *html.Node {
if node.Type == html.ElementNode && node.Data == target.Tag {
classes := GetClasses(node)
if ContainsClass(classes, "insertr") {
// Content-based validation for precise matching
textContent := extractTextContent(node)
nodeContent := strings.TrimSpace(textContent)
targetContent := strings.TrimSpace(target.Content)
if nodeContent == targetContent {
return node
}
}
}
// Recursively search children
for child := node.FirstChild; child != nil; child = child.NextSibling {
if result := findElementWithContext(child, target); result != nil {
return result
}
}
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)
}

View File

@@ -58,27 +58,39 @@ export class ApiClient {
}
}
async createContent(contentId, content, type) {
async createContent(contentId, content, type, elementContext = null) {
try {
const payload = {
value: content,
type: type
};
if (contentId) {
// Enhanced site - provide existing ID
payload.id = contentId;
} else if (elementContext) {
// Non-enhanced site - provide context for backend ID generation
payload.element_context = elementContext;
} else {
throw new Error('Either contentId or elementContext must be provided');
}
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify({
id: contentId,
value: content,
type: type
})
body: JSON.stringify(payload)
});
if (response.ok) {
console.log(`✅ Content created: ${contentId} (${type})`);
return true;
const result = await response.json();
console.log(`✅ Content created: ${result.id} (${result.type})`);
return result;
} else {
console.warn(`⚠️ Create failed (${response.status}): ${contentId}`);
return false;
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`);
return null;
}
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {

View File

@@ -102,31 +102,34 @@ export class InsertrEditor {
contentValue = formData.text || formData;
}
// Try to update existing content first
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
if (!updateSuccess) {
// If update fails, try to create new content
if (meta.hasExistingId) {
// Enhanced site - update existing content
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
if (!updateSuccess) {
console.error('❌ Failed to update content:', meta.contentId);
} else {
console.log(`✅ Content updated:`, meta.contentId, contentValue);
}
} else {
// Non-enhanced site - create with backend ID generation
const contentType = this.determineContentType(meta.element);
const createSuccess = await this.apiClient.createContent(meta.contentId, contentValue, contentType);
const result = await this.apiClient.createContent(null, contentValue, contentType, meta.elementContext);
if (!createSuccess) {
console.error('❌ Failed to save content to server:', meta.contentId);
// Still update the UI optimistically
if (result) {
// Store the backend-generated ID in the element
meta.element.setAttribute('data-content-id', result.id);
meta.element.setAttribute('data-content-type', result.type);
console.log(`✅ Content created with backend ID: ${result.id}`, contentValue);
} else {
console.error('❌ Failed to save content to server');
}
}
// Close form
this.formRenderer.closeForm();
console.log(`✅ Content saved:`, meta.contentId, contentValue);
} catch (error) {
console.error('❌ Error saving content:', error);
this.formRenderer.closeForm();
}
}

View File

@@ -104,10 +104,36 @@ export class InsertrCore {
// Get element metadata
getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id');
if (existingId) {
// Enhanced site - use existing ID
return {
contentId: existingId,
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element,
hasExistingId: true
};
} else {
// Non-enhanced site - prepare context for backend ID generation
return {
contentId: null, // Backend will generate
contentType: this.detectContentType(element),
element: element,
elementContext: this.extractElementContext(element),
hasExistingId: false
};
}
}
// Extract element context for backend ID generation
extractElementContext(element) {
return {
contentId: element.getAttribute('data-content-id') || this.generateDeterministicId(element),
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element
tag: element.tagName.toLowerCase(),
classes: Array.from(element.classList),
original_content: element.textContent.trim(),
parent_context: this.getSemanticContext(element),
purpose: this.getPurpose(element)
};
}