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:
@@ -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')) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user