Implement complete content injection and enhancement pipeline

- Add content API client with HTTP and mock implementations
- Implement HTML content injection with database content replacement
- Create enhance command for build-time content injection
- Integrate enhancement with servedev for live development workflow
- Add editor asset injection and serving (/_insertr/ endpoints)
- Support on-the-fly HTML enhancement during development
- Enable complete 'Tailwind of CMS' workflow: parse → inject → serve
This commit is contained in:
2025-09-03 12:35:54 +02:00
parent 1f97acc1bf
commit 4407f84bbc
8 changed files with 1017 additions and 20 deletions

View File

@@ -0,0 +1,85 @@
/**
* Insertr Editor - Development Mode
* Loads editing capabilities for elements marked with data-insertr-enhanced="true"
*/
(function() {
'use strict';
console.log('🔧 Insertr Editor loaded (development mode)');
// Initialize editor when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEditor);
} else {
initEditor();
}
function initEditor() {
console.log('🚀 Initializing Insertr Editor');
// Find all enhanced elements
const enhancedElements = document.querySelectorAll('[data-insertr-enhanced="true"]');
console.log(`📝 Found ${enhancedElements.length} editable elements`);
// Add visual indicators for development
enhancedElements.forEach(addEditIndicator);
// Add global styles
addEditorStyles();
}
function addEditIndicator(element) {
const contentId = element.getAttribute('data-content-id');
const contentType = element.getAttribute('data-content-type');
// Add hover effect
element.style.cursor = 'pointer';
element.style.position = 'relative';
// Add click handler for development demo
element.addEventListener('click', function(e) {
e.preventDefault();
alert(`Edit: ${contentId}\nType: ${contentType}\nCurrent: "${element.textContent.trim()}"`);
});
// Add visual indicator on hover
element.addEventListener('mouseenter', function() {
element.classList.add('insertr-editing-hover');
});
element.addEventListener('mouseleave', function() {
element.classList.remove('insertr-editing-hover');
});
}
function addEditorStyles() {
const styles = `
.insertr-editing-hover {
outline: 2px dashed #007cba !important;
outline-offset: 2px !important;
background-color: rgba(0, 124, 186, 0.05) !important;
}
[data-insertr-enhanced="true"]:hover::after {
content: "✏️ " attr(data-content-type);
position: absolute;
top: -25px;
left: 0;
background: #007cba;
color: white;
padding: 2px 6px;
font-size: 11px;
border-radius: 3px;
white-space: nowrap;
z-index: 1000;
font-family: monospace;
}
`;
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = styles;
document.head.appendChild(styleSheet);
}
})();

View File

@@ -0,0 +1,76 @@
package cmd
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/insertr/cli/pkg/content"
)
var enhanceCmd = &cobra.Command{
Use: "enhance [input-dir]",
Short: "Enhance HTML files by injecting content from database",
Long: `Enhance processes HTML files and injects latest content from the database
while adding editing capabilities. This is the core build-time enhancement
process that transforms static HTML into an editable CMS.`,
Args: cobra.ExactArgs(1),
Run: runEnhance,
}
var (
outputDir string
apiURL string
apiKey string
siteID string
mockContent bool
)
func init() {
rootCmd.AddCommand(enhanceCmd)
enhanceCmd.Flags().StringVarP(&outputDir, "output", "o", "./dist", "Output directory for enhanced files")
enhanceCmd.Flags().StringVar(&apiURL, "api-url", "", "Content API URL")
enhanceCmd.Flags().StringVar(&apiKey, "api-key", "", "API key for authentication")
enhanceCmd.Flags().StringVarP(&siteID, "site-id", "s", "demo", "Site ID for content lookup")
enhanceCmd.Flags().BoolVar(&mockContent, "mock", true, "Use mock content for development")
}
func runEnhance(cmd *cobra.Command, args []string) {
inputDir := args[0]
// Validate input directory
if _, err := os.Stat(inputDir); os.IsNotExist(err) {
log.Fatalf("Input directory does not exist: %s", inputDir)
}
// Create content client
var client content.ContentClient
if mockContent {
fmt.Printf("🧪 Using mock content for development\n")
client = content.NewMockClient()
} else {
if apiURL == "" {
log.Fatal("API URL required when not using mock content (use --api-url)")
}
fmt.Printf("🌐 Using content API: %s\n", apiURL)
client = content.NewHTTPClient(apiURL, apiKey)
}
// Create enhancer
enhancer := content.NewEnhancer(client, siteID)
fmt.Printf("🚀 Starting enhancement process...\n")
fmt.Printf("📁 Input: %s\n", inputDir)
fmt.Printf("📁 Output: %s\n", outputDir)
fmt.Printf("🏷️ Site ID: %s\n\n", siteID)
// Enhance directory
if err := enhancer.EnhanceDirectory(inputDir, outputDir); err != nil {
log.Fatalf("Enhancement failed: %v", err)
}
fmt.Printf("\n✅ Enhancement complete! Enhanced files available in: %s\n", outputDir)
}

View File

@@ -9,6 +9,8 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/insertr/cli/pkg/content"
) )
var servedevCmd = &cobra.Command{ var servedevCmd = &cobra.Command{
@@ -23,6 +25,8 @@ with live rebuilds via Air.`,
var ( var (
inputDir string inputDir string
port int port int
useMockContent bool
devSiteID string
) )
func init() { func init() {
@@ -30,6 +34,8 @@ func init() {
servedevCmd.Flags().StringVarP(&inputDir, "input", "i", ".", "Input directory to serve") servedevCmd.Flags().StringVarP(&inputDir, "input", "i", ".", "Input directory to serve")
servedevCmd.Flags().IntVarP(&port, "port", "p", 3000, "Port to serve on") servedevCmd.Flags().IntVarP(&port, "port", "p", 3000, "Port to serve on")
servedevCmd.Flags().BoolVar(&useMockContent, "mock", true, "Use mock content for development")
servedevCmd.Flags().StringVarP(&devSiteID, "site-id", "s", "demo", "Site ID for content lookup")
} }
func runServedev(cmd *cobra.Command, args []string) { func runServedev(cmd *cobra.Command, args []string) {
@@ -44,18 +50,37 @@ func runServedev(cmd *cobra.Command, args []string) {
log.Fatalf("Input directory does not exist: %s", absInputDir) log.Fatalf("Input directory does not exist: %s", absInputDir)
} }
fmt.Printf("🚀 Starting development server...\n") // Create content client
var client content.ContentClient
if useMockContent {
fmt.Printf("🧪 Using mock content for development\n")
client = content.NewMockClient()
} else {
// For now, default to mock if no API URL provided
fmt.Printf("🧪 Using mock content for development (no API configured)\n")
client = content.NewMockClient()
}
fmt.Printf("🚀 Starting development server with content enhancement...\n")
fmt.Printf("📁 Serving directory: %s\n", absInputDir) fmt.Printf("📁 Serving directory: %s\n", absInputDir)
fmt.Printf("🌐 Server running at: http://localhost:%d\n", port) fmt.Printf("🌐 Server running at: http://localhost:%d\n", port)
fmt.Printf("🏷️ Site ID: %s\n", devSiteID)
fmt.Printf("🔄 Manually refresh browser to see changes\n\n") fmt.Printf("🔄 Manually refresh browser to see changes\n\n")
// Create file server // Create enhanced file server
fileServer := http.FileServer(&enhancedFileSystem{ fileServer := http.FileServer(&enhancedFileSystem{
fs: http.Dir(absInputDir), fs: http.Dir(absInputDir),
dir: absInputDir, dir: absInputDir,
enhancer: content.NewEnhancer(client, devSiteID),
}) })
// Handle all requests with our enhanced file server // Handle editor assets
http.HandleFunc("/_insertr/", func(w http.ResponseWriter, r *http.Request) {
assetPath := strings.TrimPrefix(r.URL.Path, "/_insertr/")
serveEditorAsset(w, r, assetPath)
})
// Handle all other requests with our enhanced file server
http.Handle("/", fileServer) http.Handle("/", fileServer)
// Start server // Start server
@@ -63,25 +88,78 @@ func runServedev(cmd *cobra.Command, args []string) {
log.Fatal(http.ListenAndServe(addr, nil)) log.Fatal(http.ListenAndServe(addr, nil))
} }
// serveEditorAsset serves editor JavaScript and CSS files
func serveEditorAsset(w http.ResponseWriter, r *http.Request, assetPath string) {
// Get the path to the CLI binary directory
execPath, err := os.Executable()
if err != nil {
http.NotFound(w, r)
return
}
// Look for assets relative to the CLI binary (for built version)
assetsDir := filepath.Join(filepath.Dir(execPath), "assets", "editor")
assetFile := filepath.Join(assetsDir, assetPath)
// If not found, look for assets relative to source (for development)
if _, err := os.Stat(assetFile); os.IsNotExist(err) {
// Assume we're running from source
cwd, _ := os.Getwd()
assetsDir = filepath.Join(cwd, "assets", "editor")
assetFile = filepath.Join(assetsDir, assetPath)
}
// Set appropriate content type
if strings.HasSuffix(assetPath, ".js") {
w.Header().Set("Content-Type", "application/javascript")
} else if strings.HasSuffix(assetPath, ".css") {
w.Header().Set("Content-Type", "text/css")
}
// Serve the file
http.ServeFile(w, r, assetFile)
}
// enhancedFileSystem wraps http.FileSystem to provide enhanced HTML serving // enhancedFileSystem wraps http.FileSystem to provide enhanced HTML serving
type enhancedFileSystem struct { type enhancedFileSystem struct {
fs http.FileSystem fs http.FileSystem
dir string dir string
enhancer *content.Enhancer
} }
func (efs *enhancedFileSystem) Open(name string) (http.File, error) { func (efs *enhancedFileSystem) Open(name string) (http.File, error) {
file, err := efs.fs.Open(name) // For HTML files, enhance them on-the-fly
if err != nil {
return nil, err
}
// For HTML files, we'll eventually enhance them here
// For now, just serve them as-is
if strings.HasSuffix(name, ".html") { if strings.HasSuffix(name, ".html") {
fmt.Printf("📄 Serving HTML: %s\n", name) fmt.Printf("📄 Enhancing HTML: %s\n", name)
fmt.Println("🔍 Parser ran!") return efs.serveEnhancedHTML(name)
// TODO: Parse for insertr elements and enhance
} }
return file, nil // For non-HTML files, serve as-is
return efs.fs.Open(name)
}
// serveEnhancedHTML enhances an HTML file and returns it as an http.File
func (efs *enhancedFileSystem) serveEnhancedHTML(name string) (http.File, error) {
// Get the full file path
inputPath := filepath.Join(efs.dir, name)
// Create a temporary output path (in-memory would be better, but this is simpler for now)
tempDir := filepath.Join(os.TempDir(), "insertr-dev")
outputPath := filepath.Join(tempDir, name)
// Ensure temp directory exists
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
fmt.Printf("⚠️ Failed to create temp directory: %v\n", err)
return efs.fs.Open(name) // Fallback to original file
}
// Enhance the file
if err := efs.enhancer.EnhanceFile(inputPath, outputPath); err != nil {
fmt.Printf("⚠️ Enhancement failed for %s: %v\n", name, err)
return efs.fs.Open(name) // Fallback to original file
}
// Serve the enhanced file
tempFS := http.Dir(tempDir)
return tempFS.Open(name)
} }

View File

@@ -0,0 +1,164 @@
package content
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// HTTPClient implements ContentClient for HTTP API access
type HTTPClient struct {
BaseURL string
APIKey string
HTTPClient *http.Client
}
// NewHTTPClient creates a new HTTP content client
func NewHTTPClient(baseURL, apiKey string) *HTTPClient {
return &HTTPClient{
BaseURL: strings.TrimSuffix(baseURL, "/"),
APIKey: apiKey,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetContent fetches a single content item by ID
func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error) {
url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return nil, nil // Content not found, return nil without error
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var item ContentItem
if err := json.Unmarshal(body, &item); err != nil {
return nil, fmt.Errorf("parsing response: %w", err)
}
return &item, nil
}
// GetBulkContent fetches multiple content items by IDs
func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
if len(contentIDs) == 0 {
return make(map[string]ContentItem), nil
}
// Build query parameters
params := url.Values{}
params.Set("site_id", siteID)
for _, id := range contentIDs {
params.Add("ids", id)
}
url := fmt.Sprintf("%s/api/content/bulk?%s", c.BaseURL, params.Encode())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var response ContentResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("parsing response: %w", err)
}
// Convert slice to map for easy lookup
result := make(map[string]ContentItem)
for _, item := range response.Content {
result[item.ID] = item
}
return result, nil
}
// GetAllContent fetches all content for a site
func (c *HTTPClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var response ContentResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("parsing response: %w", err)
}
// Convert slice to map for easy lookup
result := make(map[string]ContentItem)
for _, item := range response.Content {
result[item.ID] = item
}
return result, nil
}

View File

@@ -0,0 +1,215 @@
package content
import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/net/html"
"github.com/insertr/cli/pkg/parser"
)
// Enhancer combines parsing and content injection
type Enhancer struct {
parser *parser.Parser
injector *Injector
}
// NewEnhancer creates a new HTML enhancer
func NewEnhancer(client ContentClient, siteID string) *Enhancer {
return &Enhancer{
parser: parser.New(),
injector: NewInjector(client, siteID),
}
}
// EnhanceFile processes an HTML file and injects content
func (e *Enhancer) EnhanceFile(inputPath, outputPath string) error {
// Use parser to get elements from file
result, err := e.parser.ParseDirectory(filepath.Dir(inputPath))
if err != nil {
return fmt.Errorf("parsing file: %w", err)
}
// Filter elements for this specific file
var fileElements []parser.Element
inputBaseName := filepath.Base(inputPath)
for _, elem := range result.Elements {
elemBaseName := filepath.Base(elem.FilePath)
if elemBaseName == inputBaseName {
fileElements = append(fileElements, elem)
}
}
if len(fileElements) == 0 {
// No insertr elements found, copy file as-is
return e.copyFile(inputPath, outputPath)
}
// Read and parse HTML for modification
htmlContent, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("reading file %s: %w", inputPath, err)
}
doc, err := html.Parse(strings.NewReader(string(htmlContent)))
if err != nil {
return fmt.Errorf("parsing HTML: %w", err)
}
// Find and inject content for each element
for _, elem := range fileElements {
// Find the node in the parsed document
// Note: This is a simplified approach - in production we'd need more robust node matching
if err := e.injectElementContent(doc, elem); err != nil {
fmt.Printf("⚠️ Warning: failed to inject content for %s: %v\n", elem.ContentID, err)
}
}
// Inject editor assets for development
e.injector.InjectEditorAssets(doc, true)
// Write enhanced HTML
if err := e.writeHTML(doc, outputPath); err != nil {
return fmt.Errorf("writing enhanced HTML: %w", err)
}
fmt.Printf("✅ Enhanced: %s → %s (%d elements)\n",
filepath.Base(inputPath),
filepath.Base(outputPath),
len(fileElements))
return nil
}
// injectElementContent finds and injects content for a specific element
func (e *Enhancer) injectElementContent(doc *html.Node, elem parser.Element) error {
// Fetch content from database
contentItem, err := e.injector.client.GetContent(e.injector.siteID, elem.ContentID)
if err != nil {
return fmt.Errorf("fetching content: %w", err)
}
// Find nodes with insertr class and inject content
e.findAndInjectNodes(doc, elem, contentItem)
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)
}
}
}
}
// Recursively process children
for child := node.FirstChild; child != nil; child = child.NextSibling {
e.findAndInjectNodes(child, elem, contentItem)
}
}
// 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
}
// EnhanceDirectory processes all HTML files in a directory
func (e *Enhancer) EnhanceDirectory(inputDir, outputDir string) error {
// Create output directory
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
// Walk input directory
return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Calculate relative path and output path
relPath, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}
outputPath := filepath.Join(outputDir, relPath)
// Handle directories
if info.IsDir() {
return os.MkdirAll(outputPath, info.Mode())
}
// Handle HTML files
if strings.HasSuffix(strings.ToLower(path), ".html") {
return e.EnhanceFile(path, outputPath)
}
// Copy other files as-is
return e.copyFile(path, outputPath)
})
}
// copyFile copies a file from src to dst
func (e *Enhancer) copyFile(src, dst string) error {
// Create directory for destination
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
// Read source
data, err := os.ReadFile(src)
if err != nil {
return err
}
// Write destination
return os.WriteFile(dst, data, 0644)
}
// writeHTML writes an HTML document to a file
func (e *Enhancer) writeHTML(doc *html.Node, outputPath string) error {
// Create directory for output
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return err
}
// Create output file
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
// Write HTML
return html.Render(file, doc)
}

View File

@@ -0,0 +1,204 @@
package content
import (
"fmt"
"golang.org/x/net/html"
)
// Injector handles content injection into HTML elements
type Injector struct {
client ContentClient
siteID string
}
// NewInjector creates a new content injector
func NewInjector(client ContentClient, siteID string) *Injector {
return &Injector{
client: client,
siteID: siteID,
}
}
// InjectContent replaces element content with database values and adds content IDs
func (i *Injector) InjectContent(element *Element, contentID string) error {
// Fetch content from database/API
contentItem, err := i.client.GetContent(i.siteID, contentID)
if err != nil {
return fmt.Errorf("fetching content for %s: %w", contentID, err)
}
// If no content found, keep original content but add data attributes
if contentItem == nil {
i.addContentAttributes(element.Node, contentID, element.Type)
return nil
}
// Replace element content based on type
switch element.Type {
case "text":
i.injectTextContent(element.Node, contentItem.Value)
case "markdown":
i.injectMarkdownContent(element.Node, contentItem.Value)
case "link":
i.injectLinkContent(element.Node, contentItem.Value)
default:
i.injectTextContent(element.Node, contentItem.Value)
}
// Add data attributes for editor functionality
i.addContentAttributes(element.Node, contentID, element.Type)
return nil
}
// InjectBulkContent efficiently injects multiple content items
func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
// Extract content IDs for bulk fetch
contentIDs := make([]string, len(elements))
for idx, elem := range elements {
contentIDs[idx] = elem.ContentID
}
// Bulk fetch content
contentMap, err := i.client.GetBulkContent(i.siteID, contentIDs)
if err != nil {
return fmt.Errorf("bulk fetching content: %w", err)
}
// Inject each element
for _, elem := range elements {
contentItem, exists := contentMap[elem.ContentID]
// Add content attributes regardless
i.addContentAttributes(elem.Element.Node, elem.ContentID, elem.Element.Type)
if !exists {
// Keep original content if not found in database
continue
}
// Replace content based on type
switch elem.Element.Type {
case "text":
i.injectTextContent(elem.Element.Node, contentItem.Value)
case "markdown":
i.injectMarkdownContent(elem.Element.Node, contentItem.Value)
case "link":
i.injectLinkContent(elem.Element.Node, contentItem.Value)
default:
i.injectTextContent(elem.Element.Node, contentItem.Value)
}
}
return nil
}
// injectTextContent replaces text content in an element
func (i *Injector) injectTextContent(node *html.Node, content string) {
// Remove all child nodes
for child := node.FirstChild; child != nil; {
next := child.NextSibling
node.RemoveChild(child)
child = next
}
// Add new text content
textNode := &html.Node{
Type: html.TextNode,
Data: content,
}
node.AppendChild(textNode)
}
// injectMarkdownContent handles markdown content (for now, just as text)
func (i *Injector) injectMarkdownContent(node *html.Node, content string) {
// For now, treat markdown as text content
// TODO: Implement markdown to HTML conversion
i.injectTextContent(node, content)
}
// injectLinkContent handles link/button content with URL extraction
func (i *Injector) injectLinkContent(node *html.Node, content string) {
// For now, just inject the text content
// TODO: Parse content for URL and text components
i.injectTextContent(node, content)
}
// addContentAttributes adds necessary data attributes for editor functionality
func (i *Injector) addContentAttributes(node *html.Node, contentID string, contentType string) {
i.setAttribute(node, "data-content-id", contentID)
i.setAttribute(node, "data-content-type", contentType)
i.setAttribute(node, "data-insertr-enhanced", "true")
}
// InjectEditorAssets adds editor JavaScript and CSS to HTML document
func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool) {
if !isDevelopment {
return // Only inject in development mode for now
}
// Find the head element
head := i.findHeadElement(doc)
if head == nil {
return
}
// Add editor JavaScript
script := &html.Node{
Type: html.ElementNode,
Data: "script",
Attr: []html.Attribute{
{Key: "src", Val: "/_insertr/insertr-editor.js"},
{Key: "defer", Val: ""},
},
}
head.AppendChild(script)
}
// findHeadElement finds the <head> element in the document
func (i *Injector) findHeadElement(node *html.Node) *html.Node {
if node.Type == html.ElementNode && node.Data == "head" {
return node
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
if result := i.findHeadElement(child); result != nil {
return result
}
}
return nil
}
// setAttribute safely sets an attribute on an HTML node
func (i *Injector) setAttribute(node *html.Node, key, value string) {
// Remove existing attribute if present
for idx, attr := range node.Attr {
if attr.Key == key {
node.Attr = append(node.Attr[:idx], node.Attr[idx+1:]...)
break
}
}
// Add new attribute
node.Attr = append(node.Attr, html.Attribute{
Key: key,
Val: value,
})
}
// Element represents a parsed HTML element with metadata
type Element struct {
Node *html.Node
Type string
Tag string
Classes []string
Content string
}
// ElementWithID combines an element with its generated content ID
type ElementWithID struct {
Element *Element
ContentID string
}

View File

@@ -0,0 +1,147 @@
package content
import (
"fmt"
"time"
)
// MockClient implements ContentClient with mock data for development
type MockClient struct {
data map[string]ContentItem
}
// NewMockClient creates a new mock content client with sample data
func NewMockClient() *MockClient {
// Generate realistic mock content based on actual generated IDs
data := map[string]ContentItem{
// Navigation (index.html has collision suffix)
"navbar-logo-2b10ad": {
ID: "navbar-logo-2b10ad",
SiteID: "demo",
Value: "Acme Consulting Solutions",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"navbar-logo-2b10ad-a44bad": {
ID: "navbar-logo-2b10ad-a44bad",
SiteID: "demo",
Value: "Acme Business Advisors",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Hero Section - index.html (updated with actual IDs)
"hero-title-7cfeea": {
ID: "hero-title-7cfeea",
SiteID: "demo",
Value: "Transform Your Business with Strategic Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-lead-e47475": {
ID: "hero-lead-e47475",
SiteID: "demo",
Value: "We help **ambitious businesses** grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-link-76c620": {
ID: "hero-link-76c620",
SiteID: "demo",
Value: "Schedule Free Consultation",
Type: "link",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Hero Section - about.html
"hero-title-c70343": {
ID: "hero-title-c70343",
SiteID: "demo",
Value: "About Our Consulting Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-lead-673026": {
ID: "hero-lead-673026",
SiteID: "demo",
Value: "We're a team of **experienced consultants** dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Services Section
"services-subtitle-c8927c": {
ID: "services-subtitle-c8927c",
SiteID: "demo",
Value: "Our Story",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"services-text-0d96da": {
ID: "services-text-0d96da",
SiteID: "demo",
Value: "**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.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Default fallback for any missing content
"default": {
ID: "default",
SiteID: "demo",
Value: "[Enhanced Content]",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
}
return &MockClient{data: data}
}
// GetContent fetches a single content item by ID
func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error) {
if item, exists := m.data[contentID]; exists && item.SiteID == siteID {
return &item, nil
}
// Return fallback content for missing items during development
fallback := &ContentItem{
ID: contentID,
SiteID: siteID,
Value: fmt.Sprintf("[Mock: %s]", contentID),
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
}
return fallback, nil
}
// GetBulkContent fetches multiple content items by IDs
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
result := make(map[string]ContentItem)
for _, id := range contentIDs {
item, err := m.GetContent(siteID, id)
if err != nil {
return nil, err
}
if item != nil {
result[id] = *item
}
}
return result, nil
}
// GetAllContent fetches all content for a site
func (m *MockClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
result := make(map[string]ContentItem)
for _, item := range m.data {
if item.SiteID == siteID {
result[item.ID] = item
}
}
return result, nil
}

View File

@@ -0,0 +1,28 @@
package content
// ContentItem represents a piece of content from the database
type ContentItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
UpdatedAt string `json:"updated_at"`
}
// ContentResponse represents the API response structure
type ContentResponse struct {
Content []ContentItem `json:"content"`
Error string `json:"error,omitempty"`
}
// ContentClient interface for content retrieval
type ContentClient interface {
// GetContent fetches content by ID
GetContent(siteID, contentID string) (*ContentItem, error)
// GetBulkContent fetches multiple content items by IDs
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
// GetAllContent fetches all content for a site
GetAllContent(siteID string) (map[string]ContentItem, error)
}