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