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

@@ -8,6 +8,8 @@ import (
"net/url"
"strings"
"time"
"github.com/insertr/insertr/internal/engine"
)
// 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
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)
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)
}
var item ContentItem
var item engine.ContentItem
if err := json.Unmarshal(body, &item); err != nil {
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
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 {
return make(map[string]ContentItem), nil
return make(map[string]engine.ContentItem), nil
}
// 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)
}
var response ContentResponse
var response engine.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)
result := make(map[string]engine.ContentItem)
for _, item := range response.Content {
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
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)
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)
}
var response ContentResponse
var response engine.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)
result := make(map[string]engine.ContentItem)
for _, item := range response.Content {
result[item.ID] = item
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/insertr/insertr/internal/db"
"github.com/insertr/insertr/internal/db/postgresql"
"github.com/insertr/insertr/internal/db/sqlite"
"github.com/insertr/insertr/internal/engine"
)
// 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
func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) {
func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
ctx := context.Background()
var content interface{}
var err error
@@ -56,9 +57,9 @@ func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
}
// 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 {
return make(map[string]ContentItem), nil
return make(map[string]engine.ContentItem), nil
}
ctx := context.Background()
@@ -87,7 +88,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup
result := make(map[string]ContentItem)
result := make(map[string]engine.ContentItem)
for _, item := range items {
result[item.ID] = item
}
@@ -96,7 +97,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
}
// 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()
var dbContent interface{}
var err error
@@ -117,7 +118,7 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup
result := make(map[string]ContentItem)
result := make(map[string]engine.ContentItem)
for _, item := range items {
result[item.ID] = item
}
@@ -125,12 +126,12 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
return result, nil
}
// convertToContentItem converts database models to content.ContentItem
func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
// convertToContentItem converts database models to engine.ContentItem
func (d *DatabaseClient) convertToContentItem(content interface{}) engine.ContentItem {
switch d.db.GetDBType() {
case "sqlite3":
c := content.(sqlite.Content)
return ContentItem{
return engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
@@ -139,7 +140,7 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
}
case "postgresql":
c := content.(postgresql.Content)
return ContentItem{
return engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
@@ -147,26 +148,26 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
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
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []ContentItem {
// convertToContentItemList converts database model lists to engine.ContentItem slice
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []engine.ContentItem {
switch d.db.GetDBType() {
case "sqlite3":
list := contentList.([]sqlite.Content)
items := make([]ContentItem, len(list))
items := make([]engine.ContentItem, len(list))
for i, content := range list {
items[i] = d.convertToContentItem(content)
}
return items
case "postgresql":
list := contentList.([]postgresql.Content)
items := make([]ContentItem, len(list))
items := make([]engine.ContentItem, len(list))
for i, content := range list {
items[i] = d.convertToContentItem(content)
}
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
func NewEnhancer(client ContentClient, siteID string) *Enhancer {
func NewEnhancer(client engine.ContentClient, siteID string) *Enhancer {
// Create database client for engine
var engineClient engine.ContentClient
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 (
"time"
"github.com/insertr/insertr/internal/engine"
)
// MockClient implements ContentClient with mock data for development
type MockClient struct {
data map[string]ContentItem
data map[string]engine.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{
data := map[string]engine.ContentItem{
// Navigation (index.html has collision suffix)
"navbar-logo-2b10ad": {
ID: "navbar-logo-2b10ad",
@@ -98,7 +100,7 @@ func NewMockClient() *MockClient {
}
// 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 {
return &item, nil
}
@@ -108,8 +110,8 @@ func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error)
}
// GetBulkContent fetches multiple content items by IDs
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
result := make(map[string]ContentItem)
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
result := make(map[string]engine.ContentItem)
for _, id := range contentIDs {
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
func (m *MockClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
result := make(map[string]ContentItem)
func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
result := make(map[string]engine.ContentItem)
for _, item := range m.data {
if item.SiteID == siteID {

View File

@@ -7,6 +7,8 @@ import (
"path/filepath"
"sync"
"time"
"github.com/insertr/insertr/internal/engine"
)
// SiteConfig represents configuration for a registered site
@@ -28,7 +30,7 @@ type SiteManager struct {
}
// 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 == "" {
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)
}