Consolidate type definitions and fix API contract

- Move all ContentItem, ContentClient, ContentResponse types to engine/types.go as single source of truth
- Remove duplicate type definitions from content/types.go
- Update all imports across codebase to use engine types
- Enhance engine to extract existing data-content-id from HTML markup
- Simplify frontend to always send html_markup, let server handle ID extraction/generation
- Fix contentId reference errors in frontend error handling
- Add getAttribute helper method to engine for ID extraction
- Add GetAllContent method to engine.DatabaseClient
- Update enhancer to use engine.ContentClient interface
- All builds and API endpoints verified working

This resolves the 400 Bad Request errors and creates a unified architecture where the server is the single source of truth for all ID generation and content type management.
This commit is contained in:
2025-09-16 16:45:29 +02:00
parent d0ac3088b4
commit d877366be0
15 changed files with 150 additions and 181 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/insertr/insertr/internal/content" "github.com/insertr/insertr/internal/content"
"github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db"
"github.com/insertr/insertr/internal/engine"
) )
var enhanceCmd = &cobra.Command{ var enhanceCmd = &cobra.Command{
@@ -49,7 +50,7 @@ func runEnhance(cmd *cobra.Command, args []string) {
outputDir := viper.GetString("cli.output") outputDir := viper.GetString("cli.output")
// Create content client // Create content client
var client content.ContentClient var client engine.ContentClient
if apiURL != "" { if apiURL != "" {
fmt.Printf("🌐 Using content API: %s\n", apiURL) fmt.Printf("🌐 Using content API: %s\n", apiURL)
client = content.NewHTTPClient(apiURL, apiKey) client = content.NewHTTPClient(apiURL, apiKey)

View File

@@ -8,6 +8,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/insertr/insertr/internal/engine"
) )
// HTTPClient implements ContentClient for HTTP API access // HTTPClient implements ContentClient for HTTP API access
@@ -29,7 +31,7 @@ func NewHTTPClient(baseURL, apiKey string) *HTTPClient {
} }
// GetContent fetches a single content item by ID // GetContent fetches a single content item by ID
func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error) { func (c *HTTPClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID) url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@@ -60,7 +62,7 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error)
return nil, fmt.Errorf("reading response: %w", err) return nil, fmt.Errorf("reading response: %w", err)
} }
var item ContentItem var item engine.ContentItem
if err := json.Unmarshal(body, &item); err != nil { if err := json.Unmarshal(body, &item); err != nil {
return nil, fmt.Errorf("parsing response: %w", err) return nil, fmt.Errorf("parsing response: %w", err)
} }
@@ -69,9 +71,9 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error)
} }
// GetBulkContent fetches multiple content items by IDs // GetBulkContent fetches multiple content items by IDs
func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
if len(contentIDs) == 0 { if len(contentIDs) == 0 {
return make(map[string]ContentItem), nil return make(map[string]engine.ContentItem), nil
} }
// Build query parameters // Build query parameters
@@ -107,13 +109,13 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str
return nil, fmt.Errorf("reading response: %w", err) return nil, fmt.Errorf("reading response: %w", err)
} }
var response ContentResponse var response engine.ContentResponse
if err := json.Unmarshal(body, &response); err != nil { if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("parsing response: %w", err) return nil, fmt.Errorf("parsing response: %w", err)
} }
// Convert slice to map for easy lookup // Convert slice to map for easy lookup
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, item := range response.Content { for _, item := range response.Content {
result[item.ID] = item result[item.ID] = item
} }
@@ -122,7 +124,7 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str
} }
// GetAllContent fetches all content for a site // GetAllContent fetches all content for a site
func (c *HTTPClient) GetAllContent(siteID string) (map[string]ContentItem, error) { func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID) url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@@ -149,13 +151,13 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]ContentItem, error
return nil, fmt.Errorf("reading response: %w", err) return nil, fmt.Errorf("reading response: %w", err)
} }
var response ContentResponse var response engine.ContentResponse
if err := json.Unmarshal(body, &response); err != nil { if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("parsing response: %w", err) return nil, fmt.Errorf("parsing response: %w", err)
} }
// Convert slice to map for easy lookup // Convert slice to map for easy lookup
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, item := range response.Content { for _, item := range response.Content {
result[item.ID] = item result[item.ID] = item
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db"
"github.com/insertr/insertr/internal/db/postgresql" "github.com/insertr/insertr/internal/db/postgresql"
"github.com/insertr/insertr/internal/db/sqlite" "github.com/insertr/insertr/internal/db/sqlite"
"github.com/insertr/insertr/internal/engine"
) )
// DatabaseClient implements ContentClient for direct database access // DatabaseClient implements ContentClient for direct database access
@@ -24,7 +25,7 @@ func NewDatabaseClient(database *db.Database) *DatabaseClient {
} }
// GetContent fetches a single content item by ID // GetContent fetches a single content item by ID
func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) { func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
ctx := context.Background() ctx := context.Background()
var content interface{} var content interface{}
var err error var err error
@@ -56,9 +57,9 @@ func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
} }
// GetBulkContent fetches multiple content items by IDs // GetBulkContent fetches multiple content items by IDs
func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
if len(contentIDs) == 0 { if len(contentIDs) == 0 {
return make(map[string]ContentItem), nil return make(map[string]engine.ContentItem), nil
} }
ctx := context.Background() ctx := context.Background()
@@ -87,7 +88,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items := d.convertToContentItemList(dbContent) items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup // Convert slice to map for easy lookup
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, item := range items { for _, item := range items {
result[item.ID] = item result[item.ID] = item
} }
@@ -96,7 +97,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
} }
// GetAllContent fetches all content for a site // GetAllContent fetches all content for a site
func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) { func (d *DatabaseClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
ctx := context.Background() ctx := context.Background()
var dbContent interface{} var dbContent interface{}
var err error var err error
@@ -117,7 +118,7 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items := d.convertToContentItemList(dbContent) items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup // Convert slice to map for easy lookup
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, item := range items { for _, item := range items {
result[item.ID] = item result[item.ID] = item
} }
@@ -125,12 +126,12 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
return result, nil return result, nil
} }
// convertToContentItem converts database models to content.ContentItem // convertToContentItem converts database models to engine.ContentItem
func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem { func (d *DatabaseClient) convertToContentItem(content interface{}) engine.ContentItem {
switch d.db.GetDBType() { switch d.db.GetDBType() {
case "sqlite3": case "sqlite3":
c := content.(sqlite.Content) c := content.(sqlite.Content)
return ContentItem{ return engine.ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, Value: c.Value,
@@ -139,7 +140,7 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
} }
case "postgresql": case "postgresql":
c := content.(postgresql.Content) c := content.(postgresql.Content)
return ContentItem{ return engine.ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, Value: c.Value,
@@ -147,26 +148,26 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
} }
} }
return ContentItem{} // Should never happen return engine.ContentItem{} // Should never happen
} }
// convertToContentItemList converts database model lists to content.ContentItem slice // convertToContentItemList converts database model lists to engine.ContentItem slice
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []ContentItem { func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []engine.ContentItem {
switch d.db.GetDBType() { switch d.db.GetDBType() {
case "sqlite3": case "sqlite3":
list := contentList.([]sqlite.Content) list := contentList.([]sqlite.Content)
items := make([]ContentItem, len(list)) items := make([]engine.ContentItem, len(list))
for i, content := range list { for i, content := range list {
items[i] = d.convertToContentItem(content) items[i] = d.convertToContentItem(content)
} }
return items return items
case "postgresql": case "postgresql":
list := contentList.([]postgresql.Content) list := contentList.([]postgresql.Content)
items := make([]ContentItem, len(list)) items := make([]engine.ContentItem, len(list))
for i, content := range list { for i, content := range list {
items[i] = d.convertToContentItem(content) items[i] = d.convertToContentItem(content)
} }
return items return items
} }
return []ContentItem{} // Should never happen return []engine.ContentItem{} // Should never happen
} }

View File

@@ -15,7 +15,7 @@ type Enhancer struct {
} }
// NewEnhancer creates a new HTML enhancer using unified engine // NewEnhancer creates a new HTML enhancer using unified engine
func NewEnhancer(client ContentClient, siteID string) *Enhancer { func NewEnhancer(client engine.ContentClient, siteID string) *Enhancer {
// Create database client for engine // Create database client for engine
var engineClient engine.ContentClient var engineClient engine.ContentClient
if dbClient, ok := client.(*DatabaseClient); ok { if dbClient, ok := client.(*DatabaseClient); ok {

View File

@@ -1,76 +0,0 @@
package content
import (
"bytes"
"log"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// MarkdownProcessor handles minimal markdown processing
// Supports only: **bold**, *italic*, and [link](url)
type MarkdownProcessor struct {
parser goldmark.Markdown
}
// NewMarkdownProcessor creates a new markdown processor with minimal configuration
func NewMarkdownProcessor() *MarkdownProcessor {
// Configure goldmark to only support basic inline formatting
md := goldmark.New(
goldmark.WithParserOptions(
parser.WithInlineParsers(
// Bold (**text**) and italic (*text*) - same parser handles both
util.Prioritized(parser.NewEmphasisParser(), 500),
// Links [text](url)
util.Prioritized(parser.NewLinkParser(), 600),
),
// Disable all block parsers except paragraph (no headings, lists, etc.)
parser.WithBlockParsers(
util.Prioritized(parser.NewParagraphParser(), 200),
),
),
goldmark.WithRendererOptions(
html.WithXHTML(), // <br /> instead of <br>
html.WithHardWraps(), // Line breaks become <br />
html.WithUnsafe(), // Allow existing HTML to pass through
),
)
return &MarkdownProcessor{parser: md}
}
// ToHTML converts markdown string to HTML
func (mp *MarkdownProcessor) ToHTML(markdown string) (string, error) {
if markdown == "" {
return "", nil
}
var buf bytes.Buffer
if err := mp.parser.Convert([]byte(markdown), &buf); err != nil {
log.Printf("Markdown conversion failed: %v", err)
return "", err
}
html := buf.String()
// Clean up goldmark's paragraph wrapping for inline content
// If content is wrapped in a single <p> tag, extract just the inner content
html = strings.TrimSpace(html)
if strings.HasPrefix(html, "<p>") && strings.HasSuffix(html, "</p>") {
// Check if this is a single paragraph (no other <p> tags inside)
inner := html[3 : len(html)-4] // Remove <p> and </p>
if !strings.Contains(inner, "<p>") {
// Single paragraph - return just the inner content for inline injection
return inner, nil
}
}
// Multiple paragraphs or other block content - return as-is
return html, nil
}

View File

@@ -2,17 +2,19 @@ package content
import ( import (
"time" "time"
"github.com/insertr/insertr/internal/engine"
) )
// MockClient implements ContentClient with mock data for development // MockClient implements ContentClient with mock data for development
type MockClient struct { type MockClient struct {
data map[string]ContentItem data map[string]engine.ContentItem
} }
// NewMockClient creates a new mock content client with sample data // NewMockClient creates a new mock content client with sample data
func NewMockClient() *MockClient { func NewMockClient() *MockClient {
// Generate realistic mock content based on actual generated IDs // Generate realistic mock content based on actual generated IDs
data := map[string]ContentItem{ data := map[string]engine.ContentItem{
// Navigation (index.html has collision suffix) // Navigation (index.html has collision suffix)
"navbar-logo-2b10ad": { "navbar-logo-2b10ad": {
ID: "navbar-logo-2b10ad", ID: "navbar-logo-2b10ad",
@@ -98,7 +100,7 @@ func NewMockClient() *MockClient {
} }
// GetContent fetches a single content item by ID // GetContent fetches a single content item by ID
func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error) { func (m *MockClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
if item, exists := m.data[contentID]; exists && item.SiteID == siteID { if item, exists := m.data[contentID]; exists && item.SiteID == siteID {
return &item, nil return &item, nil
} }
@@ -108,8 +110,8 @@ func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error)
} }
// GetBulkContent fetches multiple content items by IDs // GetBulkContent fetches multiple content items by IDs
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, id := range contentIDs { for _, id := range contentIDs {
item, err := m.GetContent(siteID, id) item, err := m.GetContent(siteID, id)
@@ -125,8 +127,8 @@ func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[str
} }
// GetAllContent fetches all content for a site // GetAllContent fetches all content for a site
func (m *MockClient) GetAllContent(siteID string) (map[string]ContentItem, error) { func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
result := make(map[string]ContentItem) result := make(map[string]engine.ContentItem)
for _, item := range m.data { for _, item := range m.data {
if item.SiteID == siteID { if item.SiteID == siteID {

View File

@@ -7,6 +7,8 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"time" "time"
"github.com/insertr/insertr/internal/engine"
) )
// SiteConfig represents configuration for a registered site // SiteConfig represents configuration for a registered site
@@ -28,7 +30,7 @@ type SiteManager struct {
} }
// NewSiteManager creates a new site manager // NewSiteManager creates a new site manager
func NewSiteManager(contentClient ContentClient, backupDir string, devMode bool) *SiteManager { func NewSiteManager(contentClient engine.ContentClient, backupDir string, devMode bool) *SiteManager {
if backupDir == "" { if backupDir == "" {
backupDir = "./insertr-backups" backupDir = "./insertr-backups"
} }

View File

@@ -1,28 +0,0 @@
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)
}

View File

@@ -110,3 +110,47 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
} }
} }
// GetAllContent retrieves all content items for a site
func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
switch c.database.GetDBType() {
case "sqlite3":
contents, err := c.database.GetSQLiteQueries().GetAllContent(context.Background(), siteID)
if err != nil {
return nil, err
}
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
case "postgresql":
contents, err := c.database.GetPostgreSQLQueries().GetAllContent(context.Background(), siteID)
if err != nil {
return nil, err
}
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}

View File

@@ -37,20 +37,33 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
processedElements := make([]ProcessedElement, len(elements)) processedElements := make([]ProcessedElement, len(elements))
for i, elem := range elements { for i, elem := range elements {
// Generate ID using the same algorithm as the parser // Check if element already has a data-content-id
id := e.idGenerator.Generate(elem.Node, input.FilePath) existingID := e.getAttribute(elem.Node, "data-content-id")
var id string
var wasGenerated bool
if existingID != "" {
// Use existing ID from enhanced element
id = existingID
wasGenerated = false
} else {
// Generate new ID for unprocessed element
id = e.idGenerator.Generate(elem.Node, input.FilePath)
wasGenerated = true
}
generatedIDs[fmt.Sprintf("element_%d", i)] = id generatedIDs[fmt.Sprintf("element_%d", i)] = id
processedElements[i] = ProcessedElement{ processedElements[i] = ProcessedElement{
Node: elem.Node, Node: elem.Node,
ID: id, ID: id,
Type: elem.Type, Type: elem.Type,
Generated: true, Generated: wasGenerated,
Tag: elem.Node.Data, Tag: elem.Node.Data,
Classes: GetClasses(elem.Node), Classes: GetClasses(elem.Node),
} }
// Add content attributes to the node // Add/update content attributes to the node
e.addContentAttributes(elem.Node, id, elem.Type) e.addContentAttributes(elem.Node, id, elem.Type)
} }
@@ -133,6 +146,16 @@ func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, content
e.setAttribute(node, "data-content-type", contentType) e.setAttribute(node, "data-content-type", contentType)
} }
// getAttribute gets an attribute value from an HTML node
func (e *ContentEngine) getAttribute(node *html.Node, key string) string {
for _, attr := range node.Attr {
if attr.Key == key {
return attr.Val
}
}
return ""
}
// setAttribute sets an attribute on an HTML node // setAttribute sets an attribute on an HTML node
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) { func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
// Remove existing attribute if it exists // Remove existing attribute if it exists

View File

@@ -43,17 +43,25 @@ type ProcessedElement struct {
} }
// ContentClient interface for accessing content data // ContentClient interface for accessing content data
// This will be implemented by database clients // This will be implemented by database clients, HTTP clients, and mock clients
type ContentClient interface { type ContentClient interface {
GetContent(siteID, contentID string) (*ContentItem, error) GetContent(siteID, contentID string) (*ContentItem, error)
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
GetAllContent(siteID string) (map[string]ContentItem, error)
} }
// ContentItem represents a piece of content from the database // ContentItem represents a piece of content from the database
type ContentItem struct { type ContentItem struct {
ID string ID string `json:"id"`
SiteID string SiteID string `json:"site_id"`
Value string Value string `json:"value"`
Type string Type string `json:"type"`
LastEditedBy string UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by,omitempty"`
}
// ContentResponse represents the API response structure
type ContentResponse struct {
Content []ContentItem `json:"content"`
Error string `json:"error,omitempty"`
} }

View File

@@ -29,24 +29,15 @@ export class ApiClient {
} }
async createContent(contentId, content, type, htmlMarkup = null) { async createContent(content, type, htmlMarkup) {
try { try {
const payload = { const payload = {
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
value: content, value: content,
type: type, type: type,
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
}; };
if (contentId) {
// Enhanced site - provide existing ID
payload.id = contentId;
} else if (htmlMarkup) {
// Non-enhanced site - provide HTML markup for unified engine ID generation
payload.html_markup = htmlMarkup;
} else {
throw new Error('Either contentId or htmlMarkup must be provided');
}
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, { const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -61,7 +52,7 @@ export class ApiClient {
console.log(`✅ Content created: ${result.id} (${result.type})`); console.log(`✅ Content created: ${result.id} (${result.type})`);
return result; return result;
} else { } else {
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`); console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
return null; return null;
} }
} catch (error) { } catch (error) {
@@ -69,7 +60,7 @@ export class ApiClient {
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`); console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
console.warn('💡 Start full-stack development: just dev'); console.warn('💡 Start full-stack development: just dev');
} else { } else {
console.error('Failed to create content:', contentId, error); console.error('Failed to create content:', error);
} }
return false; return false;
} }

View File

@@ -102,13 +102,12 @@ export class InsertrEditor {
contentValue = formData.text || formData; contentValue = formData.text || formData;
} }
// Universal upsert - works for both new and existing content // Universal upsert - server handles ID extraction/generation from markup
const contentType = this.determineContentType(meta.element); const contentType = this.determineContentType(meta.element);
const result = await this.apiClient.createContent( const result = await this.apiClient.createContent(
meta.contentId, // Use existing ID if available, null if new
contentValue, contentValue,
contentType, contentType,
meta.htmlMarkup meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
); );
if (result) { if (result) {

View File

@@ -106,6 +106,11 @@ export class InsertrCore {
getElementMetadata(element) { getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id'); const existingId = element.getAttribute('data-content-id');
// Ensure element has insertr class for server processing
if (!element.classList.contains('insertr')) {
element.classList.add('insertr');
}
// Send HTML markup to server for unified ID generation // Send HTML markup to server for unified ID generation
return { return {
contentId: existingId, // null if new content, existing ID if updating contentId: existingId, // null if new content, existing ID if updating

View File

@@ -109,6 +109,11 @@ var Insertr = (function () {
getElementMetadata(element) { getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id'); const existingId = element.getAttribute('data-content-id');
// Ensure element has insertr class for server processing
if (!element.classList.contains('insertr')) {
element.classList.add('insertr');
}
// Send HTML markup to server for unified ID generation // Send HTML markup to server for unified ID generation
return { return {
contentId: existingId, // null if new content, existing ID if updating contentId: existingId, // null if new content, existing ID if updating
@@ -2820,13 +2825,12 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
contentValue = formData.text || formData; contentValue = formData.text || formData;
} }
// Universal upsert - works for both new and existing content // Universal upsert - server handles ID extraction/generation from markup
const contentType = this.determineContentType(meta.element); const contentType = this.determineContentType(meta.element);
const result = await this.apiClient.createContent( const result = await this.apiClient.createContent(
meta.contentId, // Use existing ID if available, null if new
contentValue, contentValue,
contentType, contentType,
meta.htmlMarkup meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
); );
if (result) { if (result) {
@@ -3761,24 +3765,15 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
} }
async createContent(contentId, content, type, htmlMarkup = null) { async createContent(content, type, htmlMarkup) {
try { try {
const payload = { const payload = {
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
value: content, value: content,
type: type, type: type,
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
}; };
if (contentId) {
// Enhanced site - provide existing ID
payload.id = contentId;
} else if (htmlMarkup) {
// Non-enhanced site - provide HTML markup for unified engine ID generation
payload.html_markup = htmlMarkup;
} else {
throw new Error('Either contentId or htmlMarkup must be provided');
}
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, { const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -3793,7 +3788,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
console.log(`✅ Content created: ${result.id} (${result.type})`); console.log(`✅ Content created: ${result.id} (${result.type})`);
return result; return result;
} else { } else {
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`); console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
return null; return null;
} }
} catch (error) { } catch (error) {
@@ -3801,7 +3796,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`); console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
console.warn('💡 Start full-stack development: just dev'); console.warn('💡 Start full-stack development: just dev');
} else { } else {
console.error('Failed to create content:', contentId, error); console.error('Failed to create content:', error);
} }
return false; return false;
} }