feat: implement server-hosted static site enhancement with real-time content updates

- Add SiteManager for registering and managing static sites with file-based enhancement
- Implement EnhanceInPlace method for in-place file modification using database content
- Integrate automatic file enhancement triggers in UpdateContent API handler
- Add comprehensive site configuration support in insertr.yaml with auto-enhancement
- Extend serve command to automatically register and manage configured sites
- Add backup system for original files before enhancement
- Support multi-site hosting with individual auto-enhancement settings
- Update documentation for server-hosted enhancement workflow

This enables real-time content deployment where database content changes
immediately update static files without requiring rebuilds or redeployment.
The database remains the single source of truth while maintaining static
file performance benefits.
This commit is contained in:
2025-09-10 23:05:09 +02:00
parent 21ce92bf61
commit 8d92c6477b
9 changed files with 576 additions and 9 deletions

View File

@@ -214,3 +214,109 @@ func (e *Enhancer) writeHTML(doc *html.Node, outputPath string) error {
// Write HTML
return html.Render(file, doc)
}
// EnhanceInPlace performs in-place enhancement of static site files
func (e *Enhancer) EnhanceInPlace(sitePath string, siteID string) error {
// Update the injector with the correct siteID
e.injector.siteID = siteID
// Use existing parser logic to discover elements
result, err := e.parser.ParseDirectory(sitePath)
if err != nil {
return fmt.Errorf("parsing directory: %w", err)
}
if len(result.Elements) == 0 {
fmt.Printf("📄 No insertr elements found in %s\n", sitePath)
return nil
}
// Group elements by file for efficient processing
fileElements := make(map[string][]parser.Element)
for _, elem := range result.Elements {
fileElements[elem.FilePath] = append(fileElements[elem.FilePath], elem)
}
// Process each file in-place
enhancedCount := 0
for filePath, elements := range fileElements {
if err := e.enhanceFileInPlace(filePath, elements); err != nil {
fmt.Printf("⚠️ Failed to enhance %s: %v\n", filepath.Base(filePath), err)
} else {
enhancedCount++
}
}
fmt.Printf("✅ Enhanced %d files with %d elements in site %s\n",
enhancedCount, len(result.Elements), siteID)
return nil
}
// enhanceFileInPlace modifies an HTML file in-place with database content
func (e *Enhancer) enhanceFileInPlace(filePath string, elements []parser.Element) error {
// Read original file
htmlContent, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
// Parse HTML
doc, err := html.Parse(strings.NewReader(string(htmlContent)))
if err != nil {
return fmt.Errorf("parsing HTML: %w", err)
}
// Convert parser elements to injector format with content IDs
elementIDs := make([]ElementWithID, 0, len(elements))
for _, elem := range elements {
// Find the corresponding node in the parsed document
node := e.findNodeInDocument(doc, elem)
if node != nil {
elementIDs = append(elementIDs, ElementWithID{
Element: &Element{
Node: node,
Type: string(elem.Type),
Tag: elem.Tag,
},
ContentID: elem.ContentID,
})
}
}
// Use existing bulk injection logic for efficiency
if len(elementIDs) > 0 {
if err := e.injector.InjectBulkContent(elementIDs); err != nil {
return fmt.Errorf("injecting content: %w", err)
}
}
// Write enhanced HTML back to the same file (in-place update)
return e.writeHTML(doc, filePath)
}
// findNodeInDocument finds a specific node in the HTML document tree
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")
}
// 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
}