Documentation
¶
Overview ¶
Package cms embeds Sigocu, a schema-driven content manager, into an existing Go HTTP application without replacing its website templates or styles.
Integration ¶
Declare editable collections and singleton settings with Model and Field, then call New with the application's existing database/sql connection pool. Run CMS.Migrate in the application's migration stage and CMS.CheckSchema on subsequent web-process startups. Neither New nor model registration changes existing application tables. The host retains ownership of its pool and server.
CMS.Mount registers the admin on an existing http.ServeMux. The default path is /admin; set Config.BasePath to use another unused prefix. Mount is optional: CMS implements http.Handler and can be passed to another router directly, preserving its configured path. Admin assets are embedded in the executable and served only through the admin routes.
Keep the website's design ¶
A schema defines editable data and generated admin forms. It does not generate or modify the public website. Read values with CMS.Singleton, CMS.Get, CMS.GetBySlug, or CMS.List and pass them into the application's existing templates or frontend response objects. Existing HTML, CSS, components, and routes remain controlled by the host. Only values chosen by the developer become editable; adding a schema does not discover hardcoded template text. Record.Decode binds schema values to an existing Go view struct using normal JSON tags, so template field names can stay the same.
An unsaved singleton returns ErrNotFound. The host can retain its existing text as a fallback while content is introduced. Plain strings should remain escaped template data; use RenderRichText to render a Markdown field safely. Go read methods are trusted application APIs and do not mount a public REST endpoint or authorize website visitors automatically.
Authentication and media ¶
Built-in SQL-backed authentication is enabled unless Config.Authenticate is provided. Create the first administrator with CMS.BootstrapAdmin in an explicit application command. There are no default credentials. Supplying a host authenticator disables built-in login; Config.Authorize can adapt the host's permissions as well.
Images require a MediaStorage adapter. The media/local subpackage provides persistent filesystem storage. CMS.Mount also mounts the configured public image path when media is enabled; applications mounting CMS manually can mount CMS.MediaHandler separately. Private attachments are outside this handler's scope. The host owns persistence, backups, and adapter shutdown.
Consistency and limits ¶
CMS.Update and CMS.Delete require an expected revision and return ErrConflict for stale writes. Query uses bounded cursor pagination and defaults to metadata-only results. Select Fields explicitly for public lists. Saves are immediately visible; drafts, publishing workflows, relations, and historical versions are not implemented in this core.
See the integration guide, runnable example, and publishing guide for complete host-application and release workflows.
Index ¶
- Variables
- func RenderRichText(source string) (template.HTML, error)
- type Action
- type CMS
- func (c *CMS) BasePath() string
- func (c *CMS) BootstrapAdmin(ctx context.Context, username, password string) error
- func (c *CMS) CheckSchema(ctx context.Context) error
- func (c *CMS) CleanExpiredSessions(ctx context.Context) error
- func (c *CMS) Create(ctx context.Context, actor Principal, model string, input Values) (Record, error)
- func (c *CMS) CreateUser(ctx context.Context, actor Principal, username, password, role string) (string, error)
- func (c *CMS) Delete(ctx context.Context, actor Principal, model, id string, expected int64) error
- func (c *CMS) DisableUser(ctx context.Context, actor Principal, id string, disabled bool) error
- func (c *CMS) Get(ctx context.Context, model, id string) (Record, error)
- func (c *CMS) GetBySlug(ctx context.Context, model, slug string) (Record, error)
- func (c *CMS) ImageURL(id string) string
- func (c *CMS) List(ctx context.Context, model string, q Query) (Page, error)
- func (c *CMS) MediaHandler() http.Handler
- func (c *CMS) Migrate(ctx context.Context) error
- func (c *CMS) Mount(mux *http.ServeMux)
- func (c *CMS) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (c *CMS) SetPassword(ctx context.Context, actor Principal, id, password string) error
- func (c *CMS) SetRole(ctx context.Context, actor Principal, id, role string) error
- func (c *CMS) Singleton(ctx context.Context, model string) (Record, error)
- func (c *CMS) Update(ctx context.Context, actor Principal, model, id string, expected int64, ...) (Record, error)
- type Config
- type Dialect
- type Field
- type FieldType
- type ImageValue
- type ListOptions
- type Media
- type MediaStorage
- type Model
- type Page
- type Principal
- type Query
- type Record
- type Repository
- type Resource
- type ValidationErrors
- type Values
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotFound indicates that a requested model, record, slug, or image is missing. ErrNotFound = errors.New("cms: not found") // ErrConflict indicates a stale revision or a duplicate record/slug key. ErrConflict = errors.New("cms: record changed or key already exists") // ErrUnauthenticated indicates that no usable editor identity was provided. ErrUnauthenticated = errors.New("cms: authentication required") // ErrForbidden indicates that the actor lacks permission for an action. ErrForbidden = errors.New("cms: permission denied") // ErrInvalid indicates malformed configuration or invalid content values. ErrInvalid = errors.New("cms: invalid input") // ErrTooLarge indicates a record or request exceeding a configured package limit. ErrTooLarge = errors.New("cms: payload too large") )
var ErrRateLimited = errors.New("cms: too many login attempts")
ErrRateLimited indicates that the bounded local-login allowance was exhausted.
Functions ¶
Types ¶
type Action ¶
type Action string
Action identifies an operation for an authorization policy.
const ( // Read allows reading admin content, including collection lists. Read Action = "read" // Create allows creating records. Create Action = "create" // Update allows modifying records. Update Action = "update" // Delete allows deleting collection records. Delete Action = "delete" // Upload allows adding media. Upload Action = "upload" // ManageUsers allows local account administration through trusted Go APIs. ManageUsers Action = "manage_users" )
type CMS ¶
type CMS struct {
// contains filtered or unexported fields
}
CMS manages content and implements http.Handler for its admin subtree. A constructed instance is safe for concurrent use. The host owns its server, database pool, and media adapter lifecycle.
func New ¶
New compiles immutable models and templates. It performs no database I/O. Without Authenticate, Sigocu's own SQL-backed login is enabled.
Example ¶
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
cms "github.com/rubberpirate/sigocu"
_ "modernc.org/sqlite"
)
func main() {
// The host chooses its driver and owns this connection pool.
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
admin, err := cms.New(cms.Config{
DB: db, Dialect: cms.SQLite, BasePath: "/cms",
Models: []cms.Model{{
Name: "homepage", Singleton: true,
Fields: []cms.Field{{Name: "heading", Type: cms.Text, Required: true}},
}},
})
if err != nil {
panic(err)
}
if err = admin.Migrate(context.Background()); err != nil {
panic(err)
}
// Bootstrap the first editor in a separate application command, or provide
// Config.Authenticate to use the host's existing login.
mux := http.NewServeMux()
admin.Mount(mux)
fmt.Println(admin.BasePath())
}
Output: /cms
func (*CMS) BootstrapAdmin ¶
BootstrapAdmin creates the first local administrator. It never resets an existing account and is intentionally unavailable as an HTTP endpoint.
func (*CMS) CheckSchema ¶
CheckSchema verifies the installed schema without performing DDL. Call it on web-process startup when migrations run in a separate deployment stage.
func (*CMS) CleanExpiredSessions ¶
CleanExpiredSessions removes one bounded batch; no background worker is started.
func (*CMS) Create ¶
func (c *CMS) Create(ctx context.Context, actor Principal, model string, input Values) (Record, error)
Create authorizes actor and creates a validated record with revision 1. For a singleton, only one record can exist. Duplicate IDs/slugs return ErrConflict. Actor must come from trusted host code, not a browser-supplied identity.
func (*CMS) CreateUser ¶
func (c *CMS) CreateUser(ctx context.Context, actor Principal, username, password, role string) (string, error)
CreateUser is a trusted application API; actor is supplied by host code.
func (*CMS) Delete ¶
Delete authorizes actor and deletes a collection record at the expected revision. Singletons cannot be deleted; media references do not delete files.
func (*CMS) DisableUser ¶
DisableUser changes an account's disabled flag and revokes its sessions. Actor must have ManageUsers permission. It does not delete account data.
func (*CMS) Get ¶
Get retrieves a complete record by model and ID. Missing records return ErrNotFound. This is a trusted host API; it does not authorize website visitors.
func (*CMS) GetBySlug ¶
GetBySlug retrieves a complete record using its model's configured SlugField. It returns ErrNotFound when the slug or slug-enabled model is unavailable.
func (*CMS) ImageURL ¶
ImageURL returns an image's public path, or an empty string for an invalid ID or disabled media. It does not query SQL to confirm the image exists.
func (*CMS) List ¶
List retrieves a bounded page, ordered by descending creation time and ID. Values are omitted unless Fields or AllFields is requested. Pass NextCursor as After with the same model and projection to continue; no count query runs.
Example ¶
package main
import (
"context"
"database/sql"
"fmt"
cms "github.com/rubberpirate/sigocu"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
admin, err := cms.New(cms.Config{DB: db, Dialect: cms.SQLite, Models: []cms.Model{{
Name: "posts", Fields: []cms.Field{{Name: "title", Type: cms.Text, Required: true}, {Name: "excerpt", Type: cms.Textarea}},
}}})
if err != nil {
panic(err)
}
ctx := context.Background()
if err = admin.Migrate(ctx); err != nil {
panic(err)
}
// A trusted application command supplies the actor; browser input must not.
actor := cms.Principal{ID: "seed-command", Roles: []string{"admin"}}
if _, err = admin.Create(ctx, actor, "posts", cms.Values{"title": "First post", "excerpt": "A short introduction."}); err != nil {
panic(err)
}
page, err := admin.List(ctx, "posts", cms.Query{Fields: []string{"excerpt"}})
if err != nil {
panic(err)
}
fmt.Println(page.Items[0].Title)
fmt.Println(page.Items[0].Values["excerpt"])
}
Output: First post A short introduction.
func (*CMS) MediaHandler ¶
MediaHandler serves finalized images publicly under MediaBasePath. Mount calls this automatically; callers using ServeHTTP directly may mount it themselves.
func (*CMS) Migrate ¶
Migrate creates and verifies CMS-owned SQL tables without changing application tables. Run it before serving traffic, or in a designated deployment stage. Custom repositories own their migrations and cannot use this method.
func (*CMS) Mount ¶
Mount registers the exact base path and its subtree on the host's mux. Like ServeMux.Handle, it panics if the host already registered these patterns. Mount is optional: CMS also implements http.Handler directly.
func (*CMS) ServeHTTP ¶
func (c *CMS) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP serves only the configured admin path and its subtree. Pass the original path without stripping BasePath. Website routes and public media are separate; Mount is a convenience for registering the relevant handlers.
func (*CMS) SetPassword ¶
SetPassword revokes all sessions for the account in the same transaction.
func (*CMS) SetRole ¶
SetRole changes a local account's role to admin, editor, or viewer and revokes its sessions. Actor must have ManageUsers permission.
func (*CMS) Singleton ¶
Singleton retrieves a singleton model's saved record. Before its first save, it returns ErrNotFound; field defaults are not persisted implicitly on reads.
func (*CMS) Update ¶
func (c *CMS) Update(ctx context.Context, actor Principal, model, id string, expected int64, input Values) (Record, error)
Update authorizes actor and applies a partial update at the expected revision. Omitted and unregistered stored fields are preserved. A stale revision or conflicting slug returns ErrConflict without replacing the current record.
type Config ¶
type Config struct {
// DB is borrowed from the host; CMS never closes or retunes it.
DB *sql.DB
// Dialect is required when DB is provided.
Dialect Dialect
// Repository replaces built-in SQL content storage. It requires Authenticate
// and CSRFKey and currently cannot be combined with media storage.
Repository Repository
// BasePath defaults to /admin and must be a clean, non-root absolute path.
BasePath string
// Models register the editable schemas. New copies their definitions.
Models []Model
// Authenticate reads a trusted host session or request context. A non-nil
// callback replaces local login; an empty principal ID is unauthenticated.
Authenticate func(*http.Request) (Principal, error)
// Authorize replaces the default admin/editor/viewer policy when non-nil.
Authorize func(context.Context, Principal, Action, Resource) error
// Media stores image bytes; image fields require this optional adapter.
Media MediaStorage
// MediaBasePath defaults to /media/sigocu, outside the admin path.
MediaBasePath string
// InsecureCookies allows HTTP development. Leave false behind production HTTPS.
InsecureCookies bool
// SessionTTL defaults to 12 hours; allowed values range from one minute to 30 days.
SessionTTL time.Duration
// DBTimeout defaults to two seconds, capped by earlier caller deadlines.
DBTimeout time.Duration
// CSRFKey is optional with SQL; SQL migrations persist a shared key by default.
// Custom repositories without SQL must supply exactly 32 random bytes.
CSRFKey []byte
// Logger defaults to slog.Default.
Logger *slog.Logger
}
Config defines immutable models, storage, routing, and authentication. Provide either DB or Repository. With DB and no Authenticate callback, Sigocu's own login is enabled. All callbacks must be safe for concurrent use.
type Dialect ¶
type Dialect string
Dialect selects SQL syntax; the application still chooses and imports its driver.
type Field ¶
type Field struct {
Name string
Label string
Help string
Type FieldType
Required bool
// Default is validated at construction and applied on creation, not on reads.
Default any
// MaxLength counts Unicode characters; encoded records are also limited to 256 KiB.
MaxLength int
// Min and Max bound Integer/Decimal values using exact whole-number limits.
Min *int64
Max *int64
Choices []string
}
Field declares one editable schema value. The public website decides how to render it; the schema only controls validation and the admin form.
type FieldType ¶
type FieldType string
FieldType selects a field's value validation and generated admin input.
const ( // Text is a single-line string. Text FieldType = "text" // Textarea is a multiline plain-text string. Textarea FieldType = "textarea" // RichText stores Markdown; RenderRichText produces safe formatted HTML. RichText FieldType = "richtext" // Slug is a lowercase ASCII URL key, optionally indexed with Model.SlugField. Slug FieldType = "slug" // Integer stores an exact signed 64-bit integer. Integer FieldType = "integer" // Decimal stores an exact plain decimal string without floating-point conversion. Decimal FieldType = "decimal" // Boolean stores true or false; required booleans may still be false. Boolean FieldType = "boolean" // Date stores a YYYY-MM-DD calendar date. Date FieldType = "date" // DateTime stores an RFC3339 instant normalized to UTC. DateTime FieldType = "datetime" // URL stores an HTTP(S) URL or a site-relative path. URL FieldType = "url" // Select stores one of Field.Choices, or an empty optional value. Select FieldType = "select" // Image stores an ImageValue and requires configured media storage. Image FieldType = "image" )
type ImageValue ¶
ImageValue refers to reusable media and its contextual alternative text.
type ListOptions ¶
type ListOptions struct {
Limit int
BeforeTime int64
BeforeID string
Fields []string
AllFields bool
}
ListOptions is the validated repository-level query. Implementations return at most Limit+1 rows and use BeforeTime/BeforeID as an exclusive cursor boundary. BeforeTime uses UTC Unix microseconds. No field projection means metadata only.
type Media ¶
type Media struct {
ID string
Key string
ContentType string
Size int64
Width int
Height int
CreatedAt time.Time
}
Media describes a finalized uploaded image. Content stores its ID through an ImageValue; the storage key is owned by the configured MediaStorage adapter.
type MediaStorage ¶
type MediaStorage interface {
Put(context.Context, string, io.Reader) error
Open(context.Context, string) (io.ReadCloser, error)
Delete(context.Context, string) error
}
MediaStorage owns persistent image bytes. Put must either finalize the complete object or clean up its partial output. Keys are generated by CMS, not editors.
type Model ¶
type Model struct {
Name string
Label string
TitleField string
SlugField string
Singleton bool
// Fields appear in this order in generated admin forms.
Fields []Field
// Validate checks the merged values after field validation. Return
// ValidationErrors for editor-facing field errors. The callback may run concurrently.
Validate func(Values) error
}
Model registers an editable collection or a singleton settings schema. Names follow [a-z][a-z0-9_]{0,62}; they need not match application table names.
type Principal ¶
Principal is a trusted editor identity obtained by an authenticator or host application code. Browser input must not choose its own ID or Roles.
type Query ¶
type Query struct {
// Limit defaults to 25 and must not exceed 100.
Limit int
// After is a Page.NextCursor for the same model and field projection.
After string
// Fields names the values to project in SQL; omitted fields are not fetched.
Fields []string
// AllFields explicitly loads full documents, including preserved unregistered values.
AllFields bool
}
Query defaults to metadata-only results. Fields names select document values; AllFields explicitly requests the complete document.
type Record ¶
type Record struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug,omitempty"`
Values Values `json:"values,omitempty"`
Revision int64 `json:"revision"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Record contains content plus identity and concurrency metadata. Values is nil for metadata-only lists. Title is a bounded display copy, not a replacement for the full title value stored in Values.
func (Record) Decode ¶
Decode copies schema values into dst using standard JSON field names/tags. Pass a non-nil pointer, normally to an existing website view struct. Record metadata is excluded; unrepresented fields are ignored and missing fields leave existing destination values intact. As with encoding/json, a type error may partially update dst: decode into a temporary value when retaining fallbacks. Interface-valued numbers use json.Number to preserve integer precision.
Metadata-only records have nil Values and return ErrInvalid; request Fields or AllFields when listing. Content strings remain untrusted template data. Render Markdown with RenderRichText instead of binding raw strings to template.HTML.
Example ¶
package main
import (
"fmt"
cms "github.com/rubberpirate/sigocu"
_ "modernc.org/sqlite"
)
func main() {
// The website already renders this view struct in its own template.
type homeView struct {
Heading string `json:"hero_title"`
ButtonText string `json:"button_text"`
ButtonURL string `json:"button_url"`
}
// In a website handler, obtain record using admin.Singleton(ctx, "homepage").
record := cms.Record{Values: cms.Values{
"hero_title": "My latest work",
"button_text": "View projects",
"button_url": "/projects",
}}
var home homeView
if err := record.Decode(&home); err != nil {
panic(err)
}
// Pass home into the existing template. Its elements, classes and CSS stay
// owned by the website; only these values now come from the CMS.
fmt.Println(home.Heading)
fmt.Println(home.ButtonText, home.ButtonURL)
}
Output: My latest work View projects /projects
type Repository ¶
type Repository interface {
Get(ctx context.Context, model, id string) (Record, error)
GetBySlug(ctx context.Context, model, slug string) (Record, error)
List(ctx context.Context, model string, options ListOptions) ([]Record, error)
Create(ctx context.Context, model string, record Record) error
Update(ctx context.Context, model string, record Record, expectedRevision int64) error
Delete(ctx context.Context, model, id string, expectedRevision int64) error
}
Repository implementations must enforce slug uniqueness and atomic revisions. List returns at most Limit+1 entries ordered by descending creation time and ID. Projection names are validated by CMS before calling List.
type Resource ¶
Resource identifies the collection and optional record for an authorization decision. Media operations use Model "media"; user management may omit Model.
type ValidationErrors ¶
ValidationErrors maps field names to editor-facing errors.
func (ValidationErrors) Error ¶
func (e ValidationErrors) Error() string
func (ValidationErrors) Unwrap ¶
func (e ValidationErrors) Unwrap() error