diff --git a/cmd/serve.go b/cmd/serve.go index d0e8064..71c6171 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -141,6 +141,9 @@ func runServe(cmd *cobra.Command, args []string) { // Site enhancement endpoint apiRouter.HandleFunc("/enhance", contentHandler.EnhanceSite).Methods("POST") + // Static library serving (for demo sites) + router.HandleFunc("/insertr.js", contentHandler.ServeInsertrJS).Methods("GET") + // Handle CORS preflight requests explicitly contentRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS") diff --git a/insertr.yaml b/insertr.yaml index efc5e37..ce457c4 100644 --- a/insertr.yaml +++ b/insertr.yaml @@ -33,6 +33,7 @@ server: cli: site_id: "demo" # Default site ID for CLI operations output: "./dist" # Default output directory for enhanced files + inject_demo_gate: true # Inject demo gate in development mode if no gates exist # API client configuration (for CLI remote mode) api: diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 851e4ba..01002a8 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -5,8 +5,10 @@ import ( "database/sql" "encoding/json" "fmt" + "io" "log" "net/http" + "os" "strconv" "strings" "time" @@ -717,3 +719,30 @@ func (h *ContentHandler) generateContentID(ctx *ElementContext) string { idGenerator := parser.NewIDGenerator() return idGenerator.Generate(virtualNode, "api-generated") } + +// ServeInsertrJS handles GET /insertr.js - serves the insertr JavaScript library +func (h *ContentHandler) ServeInsertrJS(w http.ResponseWriter, r *http.Request) { + // Path to the built insertr.js file + jsPath := "lib/dist/insertr.js" + + // Check if file exists + if _, err := os.Stat(jsPath); os.IsNotExist(err) { + http.Error(w, "insertr.js not found - run 'just build-lib' to build the library", http.StatusNotFound) + return + } + + // Open and serve the file + file, err := os.Open(jsPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to open insertr.js: %v", err), http.StatusInternalServerError) + return + } + defer file.Close() + + // Set appropriate headers + w.Header().Set("Content-Type", "application/javascript") + w.Header().Set("Cache-Control", "no-cache") // For development + + // Copy file contents to response + io.Copy(w, file) +} diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 0a94037..8183096 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -3,6 +3,7 @@ package api import ( "log" "net/http" + "strings" "time" ) @@ -11,24 +12,8 @@ func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - // Allow localhost and 127.0.0.1 on common development ports - allowedOrigins := []string{ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:8080", - "http://127.0.0.1:8080", - } - - // Check if origin is allowed - originAllowed := false - for _, allowed := range allowedOrigins { - if origin == allowed { - originAllowed = true - break - } - } - - if originAllowed { + // In development mode, allow all localhost origins including demo sites + if isLocalhostOrigin(origin) { w.Header().Set("Access-Control-Allow-Origin", origin) } else { // Fallback to wildcard for development (can be restricted in production) @@ -97,8 +82,8 @@ func CORSPreflightHandler(w http.ResponseWriter, r *http.Request) { // Allow localhost and 127.0.0.1 on common development ports allowedOrigins := []string{ "http://localhost:3000", - "http://127.0.0.1:3000", "http://localhost:8080", + "http://127.0.0.1:3000", "http://127.0.0.1:8080", } @@ -125,3 +110,20 @@ func CORSPreflightHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +// isLocalhostOrigin checks if an origin is a localhost/127.0.0.1 development origin +func isLocalhostOrigin(origin string) bool { + if origin == "" { + return false + } + + // Allow localhost and 127.0.0.1 on any port for development + return strings.HasPrefix(origin, "http://localhost:") || + strings.HasPrefix(origin, "https://localhost:") || + strings.HasPrefix(origin, "http://127.0.0.1:") || + strings.HasPrefix(origin, "https://127.0.0.1:") || + origin == "http://localhost" || + origin == "https://localhost" || + origin == "http://127.0.0.1" || + origin == "https://127.0.0.1" +} diff --git a/internal/content/injector.go b/internal/content/injector.go index 29d8eaa..1f3cf86 100644 --- a/internal/content/injector.go +++ b/internal/content/injector.go @@ -209,17 +209,17 @@ func (i *Injector) AddContentAttributes(node *html.Node, contentID string, conte i.addClass(node, "insertr") } -// InjectEditorAssets adds editor JavaScript to HTML document +// InjectEditorAssets adds editor JavaScript to HTML document and injects demo gate if needed func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, libraryScript string) { - // TODO: Implement script injection strategy when we have CDN hosting - // For now, script injection is disabled since HTML files should include their own script tags - // Future options: - // 1. Inject CDN script tag: - // 2. Inject local script tag for development: - // 3. Continue with inline injection for certain use cases + // Inject demo gate if no gates exist and add script for functionality + if isDevelopment { + i.InjectDemoGateIfNeeded(doc) + i.InjectEditorScript(doc) + } - // Currently disabled to avoid duplicate scripts - return + // TODO: Implement CDN script injection for production + // Production options: + // 1. Inject CDN script tag: } // findHeadElement finds the element in the document @@ -310,3 +310,196 @@ type ElementWithID struct { Element *Element ContentID string } + +// InjectDemoGateIfNeeded injects a demo gate element if no .insertr-gate elements exist +func (i *Injector) InjectDemoGateIfNeeded(doc *html.Node) { + // Check if any .insertr-gate elements already exist + if i.hasInsertrGate(doc) { + return + } + + // Find the body element + bodyNode := i.findBodyElement(doc) + if bodyNode == nil { + log.Printf("Warning: Could not find body element to inject demo gate") + return + } + + // Create demo gate HTML structure + gateHTML := `
+ +
` + + // Parse the gate HTML and inject it into the body + gateDoc, err := html.Parse(strings.NewReader(gateHTML)) + if err != nil { + log.Printf("Error parsing demo gate HTML: %v", err) + return + } + + // Extract and inject the gate element + if gateDiv := i.extractElementByClass(gateDoc, "insertr-demo-gate"); gateDiv != nil { + if gateDiv.Parent != nil { + gateDiv.Parent.RemoveChild(gateDiv) + } + bodyNode.AppendChild(gateDiv) + log.Printf("✅ Demo gate injected: Edit button added to top-right corner") + } +} + +// InjectEditorScript injects the insertr.js library and initialization script +func (i *Injector) InjectEditorScript(doc *html.Node) { + // Find the head element for the script tag + headNode := i.findHeadElement(doc) + if headNode == nil { + log.Printf("Warning: Could not find head element to inject editor script") + return + } + + // Create script element that loads insertr.js from our server + scriptHTML := fmt.Sprintf(` +`, i.siteID, i.siteID) + + // Parse and inject the script + scriptDoc, err := html.Parse(strings.NewReader(scriptHTML)) + if err != nil { + log.Printf("Error parsing editor script HTML: %v", err) + return + } + + // Extract and inject all script elements + if err := i.injectAllScriptElements(scriptDoc, headNode); err != nil { + log.Printf("Error injecting script elements: %v", err) + return + } + + log.Printf("✅ Insertr.js library and initialization script injected") +} + +// injectAllScriptElements finds and injects all script elements from parsed HTML +func (i *Injector) injectAllScriptElements(doc *html.Node, targetNode *html.Node) error { + scripts := i.findAllScriptElements(doc) + + for _, script := range scripts { + // Remove from original parent + if script.Parent != nil { + script.Parent.RemoveChild(script) + } + // Add to target node + targetNode.AppendChild(script) + } + + return nil +} + +// findAllScriptElements recursively finds all script elements +func (i *Injector) findAllScriptElements(node *html.Node) []*html.Node { + var scripts []*html.Node + + if node.Type == html.ElementNode && node.Data == "script" { + scripts = append(scripts, node) + } + + for child := node.FirstChild; child != nil; child = child.NextSibling { + childScripts := i.findAllScriptElements(child) + scripts = append(scripts, childScripts...) + } + + return scripts +} + +// hasInsertrGate checks if document has .insertr-gate elements +func (i *Injector) hasInsertrGate(node *html.Node) bool { + if node.Type == html.ElementNode { + for _, attr := range node.Attr { + if attr.Key == "class" && strings.Contains(attr.Val, "insertr-gate") { + return true + } + } + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + if i.hasInsertrGate(child) { + return true + } + } + return false +} + +// findBodyElement finds the element +func (i *Injector) findBodyElement(node *html.Node) *html.Node { + if node.Type == html.ElementNode && node.Data == "body" { + return node + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + if result := i.findBodyElement(child); result != nil { + return result + } + } + return nil +} + +// extractElementByClass finds element with specific class +func (i *Injector) extractElementByClass(node *html.Node, className string) *html.Node { + if node.Type == html.ElementNode { + for _, attr := range node.Attr { + if attr.Key == "class" && strings.Contains(attr.Val, className) { + return node + } + } + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + if result := i.extractElementByClass(child, className); result != nil { + return result + } + } + return nil +} + +// extractElementByTag finds element with specific tag +func (i *Injector) extractElementByTag(node *html.Node, tagName string) *html.Node { + if node.Type == html.ElementNode && node.Data == tagName { + return node + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + if result := i.extractElementByTag(child, tagName); result != nil { + return result + } + } + return nil +} diff --git a/justfile b/justfile index 83c2a7a..a49f799 100644 --- a/justfile +++ b/justfile @@ -104,22 +104,22 @@ demo site="": if [ ! -d "./test-sites/simple/dan-eden-portfolio-demo" ]; then echo "🔧 Dan Eden demo not ready - auto-enhancing now..." just build - ./insertr auto-enhance test-sites/simple/dan-eden-portfolio --output test-sites/simple/dan-eden-portfolio-temp - ./insertr enhance test-sites/simple/dan-eden-portfolio-temp --output test-sites/simple/dan-eden-portfolio-demo --site-id dan-eden + ./insertr auto-enhance test-sites/simple/dan-eden-portfolio --output test-sites/simple/dan-eden-portfolio-temp --config test-sites/simple/dan-eden-portfolio/insertr.yaml + ./insertr enhance test-sites/simple/dan-eden-portfolio-temp --output test-sites/simple/dan-eden-portfolio-demo --config test-sites/simple/dan-eden-portfolio/insertr.yaml rm -rf test-sites/simple/dan-eden-portfolio-temp fi echo "🚀 Starting Dan Eden portfolio demo..." - just demo-site "dan-eden" "./test-sites/simple/dan-eden-portfolio-demo" "3001" + just demo-site "dan-eden" "./test-sites/simple/dan-eden-portfolio-demo" "3000" elif [ "{{site}}" = "simple" ]; then if [ ! -d "./test-sites/simple/test-simple-demo" ]; then echo "🔧 Simple demo not ready - auto-enhancing now..." just build - ./insertr auto-enhance test-sites/simple/test-simple --output test-sites/simple/test-simple-temp - ./insertr enhance test-sites/simple/test-simple-temp --output test-sites/simple/test-simple-demo --site-id simple + ./insertr auto-enhance test-sites/simple/test-simple --output test-sites/simple/test-simple-temp --config test-sites/simple/test-simple/insertr.yaml + ./insertr enhance test-sites/simple/test-simple-temp --output test-sites/simple/test-simple-demo --config test-sites/simple/test-simple/insertr.yaml rm -rf test-sites/simple/test-simple-temp fi echo "🚀 Starting simple test site demo..." - just demo-site "simple" "./test-sites/simple/test-simple-demo" "3002" + just demo-site "simple" "./test-sites/simple/test-simple-demo" "3000" else echo "❌ Unknown demo site: {{site}}" echo "" diff --git a/test-sites/simple/dan-eden-portfolio/insertr.yaml b/test-sites/simple/dan-eden-portfolio/insertr.yaml new file mode 100644 index 0000000..f56946f --- /dev/null +++ b/test-sites/simple/dan-eden-portfolio/insertr.yaml @@ -0,0 +1,27 @@ +# Insertr Configuration for Dan Eden Portfolio Demo Site +# Specific configuration for the Dan Eden portfolio demo + +# Global settings +dev_mode: true # Development mode for demos + +# Database configuration +database: + path: "./insertr.db" # Shared database with main config + +# Demo-specific configuration +demo: + site_id: "dan-eden" # Unique site ID for Dan Eden demo + inject_demo_gate: true # Auto-inject demo gate if no gates exist + mock_auth: true # Use mock authentication for demos + api_endpoint: "http://localhost:8080/api/content" + demo_port: 3000 # Port for live-server + +# CLI enhancement configuration +cli: + site_id: "dan-eden" # Site ID for this demo + output: "./dan-eden-demo" # Output directory for enhanced files + inject_demo_gate: true # Inject demo gate in development mode + +# Authentication configuration (for demo) +auth: + provider: "mock" # Mock auth for demos \ No newline at end of file diff --git a/test-sites/simple/test-simple/insertr.yaml b/test-sites/simple/test-simple/insertr.yaml new file mode 100644 index 0000000..85c15b9 --- /dev/null +++ b/test-sites/simple/test-simple/insertr.yaml @@ -0,0 +1,27 @@ +# Insertr Configuration for Simple Demo Site +# Specific configuration for the simple test site demo + +# Global settings +dev_mode: true # Development mode for demos + +# Database configuration +database: + path: "./insertr.db" # Shared database with main config + +# Demo-specific configuration +demo: + site_id: "simple" # Unique site ID for simple demo + inject_demo_gate: true # Auto-inject demo gate if no gates exist + mock_auth: true # Use mock authentication for demos + api_endpoint: "http://localhost:8080/api/content" + demo_port: 3000 # Port for live-server + +# CLI enhancement configuration +cli: + site_id: "simple" # Site ID for this demo + output: "./simple-demo" # Output directory for enhanced files + inject_demo_gate: true # Inject demo gate in development mode + +# Authentication configuration (for demo) +auth: + provider: "mock" # Mock auth for demos \ No newline at end of file