Powrót do strony głównej

Headless CMS na Go bez CGO i frameworków

Minimalny headless CMS na Go dla landingów bez zewnętrznych zależności. Używa modernc/sqlite, net/http, kanały dla Bitrix24. Pełne bezpieczeństwo i zero-config uruchomienie.

Tworzymy headless CMS na Go: 20 MB bez zależności
Advertisement 728x90

Minimalistyczny headless CMS w Go: architektura i implementacja

Deweloperzy landingów często potrzebują prostego backendu: REST API do zgłoszeń i aktualności, panel administracyjny, integracja z CRM typu Bitrix24. LightHeadless rozwiązuje ten problem bez frameworków, ORM i zewnętrznych magazynów — tylko standardowa biblioteka Go, modernc/sqlite oraz 125 testów. Binarka kompiluje się z CGO_ENABLED=0 i działa na dowolnym VPS.

Czysty Go SQLite bez CGO

Kluczowy wybór to modernc.org/sqlite zamiast mattn/go-sqlite3. To pełna implementacja SQLite w Go z kompatybilnym interfejsem database/sql, bez potrzeby kompilatora C.

import (
    "database/sql"
    _ "modernc.org/sqlite"
)

func Open(path string) (*sql.DB, error) {
    db, err := sql.Open("sqlite", path)
    if err != nil {
        return nil, err
    }

    // Tryb WAL dla współbieżnego dostępu
    pragmas := []string{
        "PRAGMA journal_mode=WAL",
        "PRAGMA busy_timeout=5000",
        "PRAGMA foreign_keys=ON",
        "PRAGMA synchronous=NORMAL",
    }
    for _, p := range pragmas {
        if _, err := db.Exec(p); err != nil {
            return nil, fmt.Errorf("pragma %q: %w", p, err)
        }
    }
    return db, nil
}

Tryb WAL zapewnia równoległe odczyty podczas zapisu — kluczowe dla jednoczesnej pracy API i panelu admina.

Google AdInline article slot

Routing i middleware na net/http

Odmowa Gin/Echo/Chi na rzecz http.ServeMux minimalizuje zależności i upraszcza debugowanie:

mux := http.NewServeMux()

// Publiczne API
mux.Handle("/api/leads", rateLimiter(http.HandlerFunc(h.CreateLead)))
mux.Handle("/api/news", http.HandlerFunc(h.ListNews))

// Panel administracyjny
mux.Handle("/admin/", authMiddleware(sessionStore, adminHandler))

Łańcuch middleware: logger → rateLimiter → auth → csrfCheck → handler. Każdy element to niezależna funkcja, przetestowana jednostkowo.

Asynchroniczna integracja z Bitrix24

Wysyłanie zgłoszeń do CRM odbywa się asynchronicznie przez kanały i pulę gorutyn. Brak Redis czy Celery:

Google AdInline article slot
type Worker struct {
    db      *sql.DB
    queue   chan Job
    webhook string
    wg      sync.WaitGroup
}

func NewWorker(db *sql.DB, webhook string, poolSize int) *Worker {
    w := &Worker{
        db:      db,
        queue:   make(chan Job, 100),
        webhook: webhook,
    }
    for i := 0; i < poolSize; i++ {
        w.wg.Add(1)
        go w.process()
    }
    return w
}

func (w *Worker) Enqueue(job Job) {
    select {
    case w.queue <- job:
    default:
        log.Printf("bitrix queue full, lead %d will be retried manually", job.LeadID)
    }
}

Bufor 100 zadań, wyłączanie graceful z timeoutem 30 sekund.

Ochrona CSRF za pomocą HMAC-SHA256

Panel administracyjny na formularzach HTML z HTMX chroniony jest tokenami synchronizującymi:

func generateCSRFToken(sessionID, secret string) string {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(sessionID))
    return hex.EncodeToString(mac.Sum(nil))
}

func validateCSRFToken(r *http.Request, sessionID, secret string) bool {
    token := r.FormValue("csrf_token")
    if token == "" {
        token = r.Header.Get("X-CSRF-Token")
    }
    expected := generateCSRFToken(sessionID, secret)
    return hmac.Equal([]byte(token), []byte(expected))
}

Token wbudowany w formularze i nagłówki HTMX. hmac.Equal eliminuje ataki czasowe.

Google AdInline article slot

Ograniczanie częstotliwości w pamięci

Publiczne API ogranicza 10 żądań/min z IP bez zewnętrznego magazynu:

type IPLimiter struct {
    visitors sync.Map
    mu       sync.Mutex
}

type entry struct {
    count   int
    resetAt time.Time
}

func (l *IPLimiter) Allow(ip string) bool {
    now := time.Now()
    val, _ := l.visitors.LoadOrStore(ip, &entry{resetAt: now.Add(time.Minute)})
    e := val.(*entry)

    l.mu.Lock()
    defer l.mu.Unlock()

    if now.After(e.resetAt) {
        e.count = 0
        e.resetAt = now.Add(time.Minute)
    }
    if e.count >= 10 {
        return false
    }
    e.count++
    return true
}

sync.Map + cykliczna czyszczenie co 5 minut.

Pierwsze uruchomienie bez konfiguracji

Serwer sam inicjuje bazę danych, generuje hasło administratora i wyświetla je w konsoli. Parametry przez flagi lub zmienne środowiskowe:

  • ./cms — domyślne (port=8080, db=./cms.db)
  • CMS_PORT=8080 CMS_DB_PATH=./data.db ./cms

Zabezpieczenia

| Zagrożenie | Ochrona |

|--------|--------|

| Wstrzykiwanie SQL | Zapytania parametryczne |

| XSS | html/template z automatycznym escapingiem |

| CSRF | Tokeny HMAC |

| Ataki brute force | Ograniczenie 10 żądań/min |

| Przechodzenie po ścieżkach | http.FileServer w sandboxzie |

Co ważne:

  • Binarka ~20 MB z statyką przez embed.FS, zero zależności czasu wykonania
  • WAL SQLite dla współbieżności bez blokad
  • Kanały Go zamiast kolejek Redis/Celery
  • HMAC CSRF + html/template dla bezpieczeństwa
  • CGO_ENABLED=0 — kompilacja krzyżowa z Windows na Linux

— Editorial Team

Advertisement 728x90

Czytaj dalej