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:
85
insertr-cli/assets/editor/insertr-editor.js
Normal file
85
insertr-cli/assets/editor/insertr-editor.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
76
insertr-cli/cmd/enhance.go
Normal file
76
insertr-cli/cmd/enhance.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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{
|
||||||
@@ -21,8 +23,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
164
insertr-cli/pkg/content/client.go
Normal file
164
insertr-cli/pkg/content/client.go
Normal 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
|
||||||
|
}
|
||||||
215
insertr-cli/pkg/content/enhancer.go
Normal file
215
insertr-cli/pkg/content/enhancer.go
Normal 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)
|
||||||
|
}
|
||||||
204
insertr-cli/pkg/content/injector.go
Normal file
204
insertr-cli/pkg/content/injector.go
Normal 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
|
||||||
|
}
|
||||||
147
insertr-cli/pkg/content/mock.go
Normal file
147
insertr-cli/pkg/content/mock.go
Normal 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
|
||||||
|
}
|
||||||
28
insertr-cli/pkg/content/types.go
Normal file
28
insertr-cli/pkg/content/types.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user