Documentation
¶
Index ¶
- Constants
- Variables
- func AbsURL(base, path string) string
- func Authenticate(auth AuthFunc) func(http.Handler) http.Handler
- func CORS(origin string) func(http.Handler) http.Handler
- func CSRF(auth AuthFunc) func(http.Handler) http.Handler
- func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler
- func ClearCookie(w http.ResponseWriter, c Cookie)
- func ConsentFor(r *http.Request, cat CookieCategory) bool
- func Excerpt(text string, maxLen int) string
- func GenerateSlug(input string) string
- func GrantConsent(w http.ResponseWriter, cats ...CookieCategory)
- func HasRole(userRoles []Role, required Role) bool
- func InMemoryCache(ttl time.Duration, opts ...Option) func(http.Handler) http.Handler
- func IsRole(userRoles []Role, required Role) bool
- func MaxBodySize(n int64) func(http.Handler) http.Handler
- func NewID() string
- func NewRole(name string) roleBuilder
- func Query[T any](ctx context.Context, db DB, query string, args ...any) ([]T, error)
- func QueryOne[T any](ctx context.Context, db DB, query string, args ...any) (T, error)
- func RateLimit(n int, d time.Duration, opts ...Option) func(http.Handler) http.Handler
- func ReadCookie(r *http.Request, name string) (string, bool)
- func Recoverer() func(http.Handler) http.Handler
- func RequestLogger() func(http.Handler) http.Handler
- func Require(errs ...error) error
- func RevokeConsent(w http.ResponseWriter)
- func RobotsTxt(cfg RobotsConfig, baseURL string) string
- func RobotsTxtHandler(cfg RobotsConfig, baseURL string) http.HandlerFunc
- func RunValidation(v any) error
- func SchemaFor(head Head, content any) string
- func SecurityHeaders() func(http.Handler) http.Handler
- func SetCookie(w http.ResponseWriter, c Cookie, value string)
- func SetCookieIfConsented(w http.ResponseWriter, r *http.Request, c Cookie, value string) bool
- func SignToken(user User, secret string, ttl time.Duration) (string, error)
- func TemplateFuncMap() template.FuncMap
- func URL(parts ...string) string
- func UniqueSlug(base string, exists func(string) bool) string
- func ValidateStruct(v any) error
- func WriteError(w http.ResponseWriter, r *http.Request, err error)
- func WriteSitemapFragment(w io.Writer, entries []SitemapEntry) error
- func WriteSitemapIndex(w io.Writer, fragmentURLs []string, lastMod time.Time) error
- type AIDocSummary
- type AIFeature
- type Alternate
- type App
- func (a *App) Content(v any, opts ...Option)
- func (a *App) Cookies(decls ...Cookie)
- func (a *App) CookiesManifestAuth(auth AuthFunc)
- func (a *App) Handle(pattern string, handler http.Handler)
- func (a *App) Handler() http.Handler
- func (a *App) Health()
- func (a *App) MCPModules() []MCPModule
- func (a *App) MustParseTemplate(path string) *template.Template
- func (a *App) Partials(dir string) *App
- func (a *App) Redirect(from, to string, code RedirectCode)
- func (a *App) RedirectManifestAuth(auth AuthFunc)
- func (a *App) RedirectStore() *RedirectStore
- func (a *App) Run(addr string) error
- func (a *App) SEO(opts ...SEOOption)
- func (a *App) Secret() []byte
- func (a *App) Use(mws ...func(http.Handler) http.Handler)
- type AppSchema
- type AuthFunc
- type Breadcrumb
- type CacheStore
- type ChangeFreq
- type Config
- type Context
- type Cookie
- type CookieCategory
- type CrawlerPolicy
- type DB
- type Error
- type EventDetails
- type EventProvider
- type FAQEntry
- type FAQProvider
- type FaviconLink
- type FeedConfig
- type FeedStore
- type From
- type Head
- type HeadAssets
- type Headable
- type HowToProvider
- type HowToStep
- type Image
- type LLMsEntry
- type LLMsStore
- type LLMsTemplateData
- type ListOptions
- type MCPField
- type MCPMeta
- type MCPModule
- type MCPOperation
- type Markdownable
- type MemoryRepo
- func (r *MemoryRepo[T]) Delete(_ context.Context, id string) error
- func (r *MemoryRepo[T]) FindAll(_ context.Context, opts ListOptions) ([]T, error)
- func (r *MemoryRepo[T]) FindByID(_ context.Context, id string) (T, error)
- func (r *MemoryRepo[T]) FindBySlug(_ context.Context, slug string) (T, error)
- func (r *MemoryRepo[T]) Save(_ context.Context, node T) error
- type Module
- func (m *Module[T]) MCPArchive(ctx Context, slug string) error
- func (m *Module[T]) MCPCreate(ctx Context, fields map[string]any) (any, error)
- func (m *Module[T]) MCPDelete(ctx Context, slug string) error
- func (m *Module[T]) MCPGet(ctx Context, slug string) (any, error)
- func (m *Module[T]) MCPList(ctx Context, status ...Status) ([]any, error)
- func (m *Module[T]) MCPMeta() MCPMeta
- func (m *Module[T]) MCPPublish(ctx Context, slug string) error
- func (m *Module[T]) MCPSchedule(ctx Context, slug string, at time.Time) error
- func (m *Module[T]) MCPSchema() []MCPField
- func (m *Module[T]) MCPUpdate(ctx Context, slug string, fields map[string]any) (any, error)
- func (m *Module[T]) Register(mux *http.ServeMux)
- func (m *Module[T]) Stop()
- type Node
- type OGDefaults
- type Option
- func AIIndex(features ...AIFeature) Option
- func At(prefix string) Option
- func Auth(opts ...Option) Option
- func Cache(ttl time.Duration) Option
- func CacheMaxEntries(n int) Option
- func Delete(r Role) Option
- func DisableFeed() Option
- func Feed(cfg FeedConfig) Option
- func HeadFunc[T any](fn func(Context, T) Head) Option
- func MCP(ops ...MCPOperation) Option
- func ManifestAuth(auth AuthFunc) Option
- func Middleware(mws ...func(http.Handler) http.Handler) Option
- func On[T any](signal Signal, h func(Context, T) error) Option
- func Read(r Role) Option
- func Redirects(from From, to string) Option
- func Repo[T any](r Repository[T]) Option
- func Social(features ...SocialFeature) Option
- func Templates(dir string) Option
- func TemplatesOptional(dir string) Option
- func TrustedProxy() Option
- func WithoutID() Option
- func Write(r Role) Option
- type OrganizationDetails
- type OrganizationProvider
- type RecipeDetails
- type RecipeProvider
- type RedirectCode
- type RedirectEntry
- type RedirectStore
- func (s *RedirectStore) Add(e RedirectEntry)
- func (s *RedirectStore) All() []RedirectEntry
- func (s *RedirectStore) Get(path string) (RedirectEntry, bool)
- func (s *RedirectStore) Len() int
- func (s *RedirectStore) Load(ctx context.Context, db DB) error
- func (s *RedirectStore) Remove(ctx context.Context, db DB, from string) error
- func (s *RedirectStore) Save(ctx context.Context, db DB, e RedirectEntry) error
- type Registrator
- type Repository
- type ReviewDetails
- type ReviewProvider
- type RobotsConfig
- type Role
- type SEOOption
- type SQLRepo
- func (r *SQLRepo[T]) Delete(ctx context.Context, id string) error
- func (r *SQLRepo[T]) FindAll(ctx context.Context, opts ListOptions) ([]T, error)
- func (r *SQLRepo[T]) FindByID(ctx context.Context, id string) (T, error)
- func (r *SQLRepo[T]) FindBySlug(ctx context.Context, slug string) (T, error)
- func (r *SQLRepo[T]) Save(ctx context.Context, item T) error
- type SQLRepoOption
- type Scheduler
- type ScriptTag
- type Signal
- type SitemapConfig
- type SitemapEntry
- type SitemapNode
- type SitemapPrioritiser
- type SitemapStore
- type SocialFeature
- type SocialOverrides
- type Status
- type TemplateData
- type TwitterCardType
- type TwitterMeta
- type User
- type Validatable
- type ValidationError
Examples ¶
Constants ¶
const ( Article = "Article" // blog posts and news articles Product = "Product" // e-commerce product pages FAQPage = "FAQPage" // frequently asked questions HowTo = "HowTo" // step-by-step guides Event = "Event" // events with dates and locations Recipe = "Recipe" // recipes with ingredients and steps Review = "Review" // reviews with star ratings Organization = "Organization" // company or about pages )
Rich result type constants for Head.Type. Each maps to a schema.org type used to generate JSON-LD structured data (see schema.go).
const CSRFCookieName = "forge_csrf"
CSRFCookieName is the name of the CSRF cookie set by CookieSession. Client-side AJAX code should read this cookie and send its value as the X-CSRF-Token request header on all non-safe methods (POST, PUT, PATCH, DELETE).
Variables ¶
var ( // ErrNotFound indicates the requested resource does not exist. → 404 ErrNotFound = newSentinel(http.StatusNotFound, "not_found", "Not found") // ErrGone indicates the resource existed but has been permanently removed. → 410 ErrGone = newSentinel(http.StatusGone, "gone", "This content has been removed") // ErrForbidden indicates the authenticated user lacks permission. → 403 ErrForbidden = newSentinel(http.StatusForbidden, "forbidden", "Forbidden") // ErrUnauth indicates the request requires authentication. → 401 ErrUnauth = newSentinel(http.StatusUnauthorized, "unauthorized", "Unauthorized") // ErrConflict indicates a state conflict (e.g. duplicate slug). → 409 ErrConflict = newSentinel(http.StatusConflict, "conflict", "Conflict") // ErrBadRequest indicates the request is malformed or unparseable. → 400 ErrBadRequest = newSentinel(http.StatusBadRequest, "bad_request", "Bad request") // ErrNotAcceptable indicates the requested content type is not supported. → 406 ErrNotAcceptable = newSentinel(http.StatusNotAcceptable, "not_acceptable", "Not acceptable") // ErrRequestTooLarge indicates the request body exceeds the allowed size. → 413 ErrRequestTooLarge = newSentinel(http.StatusRequestEntityTooLarge, "request_too_large", "Request too large") // ErrTooManyRequests indicates the client has exceeded the rate limit. → 429 ErrTooManyRequests = newSentinel(http.StatusTooManyRequests, "too_many_requests", "Too many requests") // ErrInternal indicates an unexpected server-side error. → 500 // The public message is intentionally generic; details are logged, not exposed. ErrInternal = newSentinel(http.StatusInternalServerError, "internal_server_error", "Internal server error") )
Sentinel errors for well-known HTTP failure conditions.
var GuestUser = User{}
GuestUser is the zero-value User representing an unauthenticated request. Forge sets ctx.User() to GuestUser when no authentication middleware has identified the caller.
Functions ¶
func AbsURL ¶ added in v1.1.4
AbsURL joins a base URL and a path into an absolute URL. It trims any trailing slash from base before joining, so both of the following produce the same result:
forge.AbsURL("https://example.com", "/posts/my-slug") → "https://example.com/posts/my-slug"
forge.AbsURL("https://example.com/", "/posts/my-slug") → "https://example.com/posts/my-slug"
The path argument is passed through URL first, so duplicate slashes are collapsed and a leading slash is guaranteed. Use AbsURL in Head() implementations when setting Head.Canonical, Head.Image.URL, or any other field that requires an absolute URL.
func (p *Post) Head() forge.Head {
return forge.Head{
Canonical: forge.AbsURL(siteBaseURL, forge.URL("/posts", p.Slug)),
}
}
func Authenticate ¶
Authenticate returns middleware that runs auth on every request and stores the resulting User in the request context so [Context.User] returns it.
Apply it globally before any module that enforces role checks via Auth, Read, or Write:
app.Use(forge.Authenticate(forge.BearerHMAC(secret)))
Unauthenticated requests — where auth returns false — pass through unchanged. ContextFrom then falls back to GuestUser, which is the correct behaviour for public read endpoints protected by forge.Read(forge.Guest).
Example ¶
ExampleAuthenticate demonstrates wiring bearer token and cookie session auth via AnyAuth so that both APIs and browser clients are supported. The first matching auth method wins on each request.
const secretStr = "example-secret-key-32-bytes!!!!!"
secretBytes := []byte(secretStr)
app := New(Config{
BaseURL: "https://example.com",
Secret: secretBytes,
})
app.Use(Authenticate(AnyAuth(
BearerHMAC(secretStr),
CookieSession("session", secretStr),
)))
_ = app.Handler()
func CORS ¶
CORS returns middleware that sets cross-origin resource sharing headers allowing requests from origin. On OPTIONS preflight requests it responds with 204 No Content without calling the next handler.
func CSRF ¶
CSRF returns middleware that validates the X-CSRF-Token request header against the forge_csrf cookie on non-safe HTTP methods (POST, PUT, PATCH, DELETE). It only activates when auth implements [csrfAware] and CSRF is enabled (i.e. CookieSession without WithoutCSRF).
The middleware also issues a new forge_csrf cookie when none is present, allowing JavaScript clients to read it and send it as X-CSRF-Token.
Apply CSRF after your auth middleware in the global chain or per-module:
app.Use(forge.CSRF(myAuth))
func Chain ¶
Chain applies a list of middleware to an http.Handler. The first middleware in the slice becomes the outermost wrapper (executed first on each request).
Chain(myHandler, RequestLogger(), Recoverer(), SecurityHeaders())
func ClearCookie ¶
func ClearCookie(w http.ResponseWriter, c Cookie)
ClearCookie expires c immediately by setting MaxAge to -1 and an Expires time in the past.
func ConsentFor ¶
func ConsentFor(r *http.Request, cat CookieCategory) bool
ConsentFor reports whether the request carries consent for the given category. Necessary always returns true regardless of the consent cookie.
func Excerpt ¶
Excerpt returns a plain-text summary truncated at the last word boundary within maxLen characters. A Unicode ellipsis ("…") is appended when the text is truncated. Use it to populate Head.Description.
forge.Excerpt(p.Body, 160)
func GenerateSlug ¶
GenerateSlug converts input into a URL-safe slug. The algorithm:
- Lowercase (Unicode-aware)
- Spaces, hyphens, and underscores become hyphens
- All other non-[a-z0-9] bytes are dropped
- Consecutive hyphens are collapsed to one
- Leading and trailing hyphens are trimmed
- Result is truncated to 200 bytes
Returns "untitled" if the result would be empty.
The implementation uses a byte loop — no regexp — to avoid allocations on the hot path.
func GrantConsent ¶
func GrantConsent(w http.ResponseWriter, cats ...CookieCategory)
GrantConsent writes the forge_consent cookie to w with the given categories. Necessary is always implicitly consented and is not stored in the cookie value. Subsequent calls overwrite the previous consent state.
func HasRole ¶
HasRole reports whether any role in userRoles has a level greater than or equal to the level of required. This is a hierarchical check: an Admin satisfies a check for Editor, Author, or Guest.
Unknown roles (not registered) have level 0 and never satisfy any check.
func InMemoryCache ¶
InMemoryCache returns middleware that caches successful GET responses in an LRU cache. Responses are keyed by method + full URL (including query parameters) + Accept header. Every response receives an X-Cache header (HIT or MISS).
Default capacity is 1000 entries. Use CacheMaxEntries to override. A background goroutine sweeps expired entries every 60 seconds.
func IsRole ¶
IsRole reports whether any role in userRoles exactly matches required. Unlike HasRole, this is not hierarchical — Admin does not satisfy Editor.
func MaxBodySize ¶
MaxBodySize returns middleware that limits the size of request bodies to n bytes. Requests exceeding the limit receive a 413 error response.
func NewID ¶
func NewID() string
NewID returns a new UUID v7 string. UUID v7 is time-ordered (48-bit millisecond timestamp) with 74 bits of cryptographic randomness, which keeps B-tree indexes compact while providing the same collision resistance as UUID v4. See Amendment S1.
Panics if crypto/rand is unavailable — this indicates an unrecoverable platform error and should never occur in practice.
func NewRole ¶
func NewRole(name string) roleBuilder
NewRole begins the registration of a custom role. Call [roleBuilder.Above] or [roleBuilder.Below] to position it, then [roleBuilder.Register] to commit it to the role registry.
r, err := forge.NewRole("publisher").Above(forge.Author).Below(forge.Editor).Register()
func Query ¶
Query executes a SQL query and scans the result rows into a slice of T. T may be a struct type or a pointer to a struct (e.g. *BlogPost). Columns are matched to fields by db struct tag first, then by lowercased field name. Unrecognised columns are discarded without error. Returns an empty (non-nil) slice when no rows match.
func QueryOne ¶
QueryOne executes a SQL query and returns the first scanned row as T. Returns ErrNotFound when no rows match.
func RateLimit ¶
RateLimit returns middleware that enforces a per-IP token bucket rate limit of n requests per duration d. Requests exceeding the limit receive a 429 Too Many Requests response with a Retry-After header.
Pass TrustedProxy when the application runs behind a reverse proxy so that the real client IP is read from X-Real-IP / X-Forwarded-For.
A background goroutine sweeps stale IP buckets every d to bound memory usage.
func ReadCookie ¶
ReadCookie returns the value of the named cookie from r, and whether it was present. Returns ("", false) when the cookie is absent.
func Recoverer ¶
Recoverer returns middleware that recovers from panics in downstream handlers. On panic it returns a 500 response via WriteError and logs the stack trace. The process is never crashed.
func RequestLogger ¶
RequestLogger returns middleware that logs each request using structured log/slog output. Fields: method, path, status, duration_ms, request_id.
RequestLogger calls ContextFrom before the next handler, which ensures X-Request-ID is set on the response prior to any downstream code running. It should be the outermost middleware in [app.Use].
func Require ¶
Require collects ValidationError values from errs into a single ValidationError. Nil values are silently skipped. Returns nil if every input is nil. Returns the first non-nil non-ValidationError error unchanged.
return forge.Require(
forge.Err("title", "required"),
forge.Err("body", "minimum 50 characters"),
)
func RevokeConsent ¶
func RevokeConsent(w http.ResponseWriter)
RevokeConsent clears the forge_consent cookie, withdrawing all non-Necessary consent. Subsequent calls to ConsentFor for non-Necessary categories return false until GrantConsent is called again.
func RobotsTxt ¶
func RobotsTxt(cfg RobotsConfig, baseURL string) string
RobotsTxt generates a well-formed robots.txt string from cfg.
The output always begins with a User-agent: * block. If cfg.Disallow contains paths, each becomes a Disallow directive; otherwise an empty Disallow line is emitted (allow all).
When cfg.AIScraper is AskFirst, individual User-agent / Disallow: / blocks are appended for each known AI training crawler, leaving the User-agent: * block permissive. When cfg.AIScraper is Disallow, the same is done for an extended crawler list.
When cfg.Sitemaps is true and baseURL is non-empty, a Sitemap directive is appended at the end pointing to <baseURL>/sitemap.xml.
func RobotsTxtHandler ¶
func RobotsTxtHandler(cfg RobotsConfig, baseURL string) http.HandlerFunc
RobotsTxtHandler returns an http.HandlerFunc that serves the robots.txt content generated from cfg.
The content is generated once at construction time — not per request — so the handler is safe to share across goroutines and incurs no per-request allocation.
Responses carry Content-Type: text/plain; charset=utf-8 and Cache-Control: max-age=86400 (one day).
func RunValidation ¶
RunValidation runs the full validation pipeline on v:
- ValidateStruct — struct-tag constraints (required, min, max, email, …)
- If tags pass and v implements Validatable, calls v.Validate()
If step 1 fails, step 2 is skipped — the caller receives only the tag errors. This matches Decision 10: "Tag validation runs before Validate(); if tags fail, Validate() is not called."
func SchemaFor ¶
SchemaFor generates one or two <script type="application/ld+json"> blocks for the given head and content value.
The primary block is determined by head.Type (Article, Product, FAQPage, HowTo, Event, Recipe, Review, Organization). An empty head.Type returns "". Unknown types return "". Types that require a provider interface (FAQPage, HowTo, Event, Recipe, Review, Organization) return "" when content does not implement the required interface.
A second BreadcrumbList block is appended (separated by "\n") when head.Breadcrumbs is non-empty.
SchemaFor never panics.
func SecurityHeaders ¶
SecurityHeaders returns middleware that sets a standard set of security response headers on every response:
- Strict-Transport-Security (2-year max-age, includeSubDomains)
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
func SetCookie ¶
func SetCookie(w http.ResponseWriter, c Cookie, value string)
SetCookie writes a Necessary cookie to w.
SetCookie panics if c.Category is not Necessary. This enforces Decision 5 at the point of misuse — before any response is sent in production. For non-Necessary categories use SetCookieIfConsented.
func SetCookieIfConsented ¶
SetCookieIfConsented writes a non-Necessary cookie to w only when the request carries consent for c.Category. Returns true when the cookie was set, false when skipped due to missing consent.
SetCookieIfConsented panics if c.Category is Necessary. Necessary cookies do not require consent and must use SetCookie instead.
func SignToken ¶
SignToken produces a signed token encoding the given User. Pass the token to the client (e.g. as a JSON response body); validate it later with BearerHMAC or CookieSession.
When ttl > 0 the token contains an expiry timestamp; [decodeToken] rejects tokens whose expiry has passed. Use ttl = 0 for tokens with no expiry.
The token format is: base64url(json(User)) + "." + base64url(hmac-sha256(secret, payload)). Roles are stored as strings for forward compatibility (Decision 15).
func TemplateFuncMap ¶
TemplateFuncMap returns a template.FuncMap containing all Forge template helper functions. Pass it to template.Template.Funcs before parsing:
tpl := template.New("page").Funcs(forge.TemplateFuncMap())
Available functions:
forge_meta — JSON-LD <script> block: {{forge_meta .Head .Content}}
forge_date — formatted date string: {{.PublishedAt | forge_date}}
forge_markdown — Markdown → HTML: {{.Body | forge_markdown}}
forge_excerpt — truncated excerpt: {{.Body | forge_excerpt 160}}
forge_csrf_token — hidden CSRF input: {{forge_csrf_token .Request}}
forge_rfc3339 — RFC 3339 timestamp: {{forge_rfc3339 .Head.Published}}
forge_llms_entries — AI doc entry links (LLMsTemplateData): {{forge_llms_entries .}}
markdown — full Markdown → HTML (tables, hr, language class): {{.Body | markdown}}
func URL ¶
URL joins path segments into a root-relative URL. It collapses duplicate slashes, ensures a leading slash, and trims any trailing slash (the root "/" is preserved).
forge.URL("/posts/", p.Slug) → "/posts/my-slug"
func UniqueSlug ¶
UniqueSlug returns base if exists(base) is false, otherwise tries base-2, base-3, … until exists returns false. Callers must ensure the namespace is finite; this function has no upper bound.
func ValidateStruct ¶
ValidateStruct runs struct-tag validation on v. v must be a struct or a pointer to a struct. Field constraints are parsed once per type and cached.
Returns a *ValidationError if any constraint fails, otherwise nil. Returns all field errors — does not short-circuit on the first failure.
func WriteError ¶
func WriteError(w http.ResponseWriter, r *http.Request, err error)
WriteError writes the correct HTTP error response for err. It should be the only error-to-HTTP translation in handler code — call it and return.
Behaviour by error type:
- *ValidationError → 422 with a JSON fields array
- forge.Error 4xx → the error's own status, code, and public message
- forge.Error 5xx → logged internally; generic 500 sent to client
- any other error → logged internally; generic 500 sent to client
The X-Request-ID header is echoed from the response (if already set by upstream middleware) or from the incoming request. A new ID is never generated here — that is ContextFrom's responsibility.
func WriteSitemapFragment ¶
func WriteSitemapFragment(w io.Writer, entries []SitemapEntry) error
WriteSitemapFragment writes a complete XML sitemap fragment to w. It streams the document via xml.NewEncoder — the full document is never held in memory. Returns the first write or encode error.
Entries with a zero [SitemapEntry.LastMod] omit the <lastmod> element. An empty entries slice produces a valid empty <urlset/>.
func WriteSitemapIndex ¶
WriteSitemapIndex writes a sitemap index document to w. Each URL in fragmentURLs becomes one <sitemap> entry. lastMod is written as a date-only string and omitted when zero. An empty fragmentURLs slice produces a valid empty <sitemapindex/>.
Types ¶
type AIDocSummary ¶
type AIDocSummary interface{ AISummary() string }
AIDocSummary is implemented by content types that provide a concise, human-readable summary optimised for AI consumption. The summary is used in /llms.txt entries and the summary: field of AIDoc output.
When a content type implements neither AIDocSummary nor Markdownable, Forge falls back to Head.Description.
type AIFeature ¶
type AIFeature int
AIFeature selects which AI indexing endpoints are enabled for a module. Pass one or more AIFeature constants to AIIndex.
const ( // LLMsTxt enables the /llms.txt compact content index for the module. // Only Published items appear. Regenerated on every publish event. LLMsTxt AIFeature = 1 // LLMsTxtFull enables the /llms-full.txt full markdown corpus for the module. // Each Published item is rendered as a full document with a header. // Only Published items appear. Regenerated on every publish event. LLMsTxtFull AIFeature = 2 // AIDoc enables per-item /{prefix}/{slug}.aidoc endpoints. Each endpoint // returns the item in token-efficient AIDoc format (text/plain). // Only Published items are served; non-Published items return 404. AIDoc AIFeature = 3 )
type Alternate ¶
type Alternate struct {
Locale string // BCP 47 language tag, e.g. "en-GB"
URL string // absolute URL for this locale
}
Alternate is an hreflang entry for internationalised pages. Reserved for v2 — Forge always generates an empty Alternates slice in v1.
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is the central registry for a Forge application. It couples the HTTP router, global middleware, and all content modules into a single value.
Create an App with New, wire in modules with App.Content, add global middleware with App.Use, then serve with App.Run or App.Handler.
Optional cross-cutting features are configured directly on the App:
- App.SEO — robots.txt, sitemap index, AI-crawler policy
- App.Cookies + App.CookiesManifestAuth — typed cookie compliance manifest
- App.Redirect + App.RedirectStore + App.RedirectManifestAuth — redirect rules
App is not safe for concurrent configuration: set it up in main before calling Run or Handler, then treat it as read-only.
func New ¶
New creates a new App from cfg.
New calls MustConfig on cfg automatically, so it panics at startup if BaseURL is empty or not a valid absolute URL, or if Secret is shorter than 16 bytes. Configuration errors are always caught at process start, never at first request.
Default timeouts are applied if the corresponding Config fields are zero: ReadTimeout 5 s, WriteTimeout 10 s, IdleTimeout 120 s.
func (*App) Content ¶
Content registers a content module with the App.
If v implements Registrator (which *Module does), its Register method is called directly and opts are ignored. This is the idiomatic path:
posts := forge.NewModule[*Post](&Post{}, forge.Repo(repo), forge.At("/posts"))
app.Content(posts)
If v does not implement Registrator, Content calls NewModule[any](v, opts...) and registers the result. In this case forge.Repo must be supplied as a repoOption[any] — type safety is lost. Prefer the Registrator path for all production code.
func (*App) Cookies ¶
Cookies registers cookie declarations for the compliance manifest at /.well-known/cookies.json. Call once at startup with all cookies the application may set.
Duplicate declarations (same Name) are silently deduplicated; the first declaration with a given name wins.
Optionally pass ManifestAuth to restrict the manifest endpoint to authenticated requests:
app.Cookies(
forge.Cookie{Name: "session", Category: forge.Necessary, ...},
forge.Cookie{Name: "prefs", Category: forge.Preferences, ...},
)
func (*App) CookiesManifestAuth ¶
CookiesManifestAuth sets the AuthFunc that guards /.well-known/cookies.json. Call before App.Handler or App.Run.
app.CookiesManifestAuth(forge.BearerHMAC(secret, forge.Editor))
func (*App) Handle ¶
Handle registers a raw http.Handler at the given pattern on the App's internal mux. The pattern follows the same rules as http.ServeMux.
Use Handle for endpoints that are not managed by a Module:
app.Handle("GET /healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
func (*App) Handler ¶
Handler returns the composed http.Handler that serves all registered routes behind the global middleware stack.
When Config.HTTPS is true, an HTTP→HTTPS redirect middleware is prepended before all user-supplied middleware.
Handler is called automatically by App.Run. Call it directly when you need to hand the handler to your own server (e.g. for testing or embedding):
srv := &http.Server{Handler: app.Handler()}
func (*App) Health ¶ added in v1.0.6
func (a *App) Health()
Health mounts GET /_health on the App's mux.
The endpoint always returns HTTP 200 with Content-Type application/json. Framework versions are read from the binary's embedded build info and included in the response. The forge core version uses the key "forge"; companion modules use a key derived from their sub-path (e.g. "forge_mcp"). When build info is unavailable, only {"status":"ok"} is returned.
Call Health before App.Handler or App.Run:
app.Health()
// GET /_health → {"status":"ok","forge":"1.1.6","forge_mcp":"1.0.5"}
func (*App) MCPModules ¶ added in v1.1.0
MCPModules returns all content modules registered with MCP. forge-mcp calls this once in its New constructor to build its resource and tool registry. The returned slice is the App's live internal slice and must not be modified by the caller.
func (*App) MustParseTemplate ¶ added in v1.2.0
MustParseTemplate parses the HTML template at path and registers TemplateFuncMap, the forge:head partial, and any partials configured via App.Partials. Panics on any error.
Use this for custom route handlers that need access to the same shared partials as module templates:
app.Partials("templates/partials")
homeTpl := app.MustParseTemplate("templates/home.html")
app.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
homeTpl.Execute(w, data)
}))
func (*App) Partials ¶ added in v1.2.0
Partials sets the directory from which shared HTML partial templates are loaded. Every *.html file in dir is registered into each module's template set (list.html and show.html) at App.Run time, making them available via:
{{template "nav" .}}
Each partial file must use {{define "name"}}...{{end}} syntax. Any name except "forge:head" may be used. Files are registered in alphabetical order.
Partials returns the App so multiple calls can be chained:
app.Partials("templates/partials")
Use App.MustParseTemplate to parse custom handler templates (e.g. a home page) with the same partials and forge:head registered.
Example ¶
ExampleApp_Partials demonstrates registering a shared partials directory so that nav, footer, and other common HTML fragments are available in every module template and in custom handler templates parsed via MustParseTemplate.
app := New(MustConfig(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
}))
// Any *.html file in templates/partials is injected into every module
// template set and into templates parsed via MustParseTemplate.
app.Partials("templates/partials")
_ = app.Handler()
func (*App) Redirect ¶
func (a *App) Redirect(from, to string, code RedirectCode)
Redirect registers a manual redirect rule. Chain collapse is applied automatically: if from already redirects to an intermediate path and this call adds a rule for that intermediate path, the chain is collapsed (A→B + B→C = A→C). Maximum collapse depth is 10 (Decision 24).
To issue a 301 Moved Permanently:
app.Redirect("/old-path", "/new-path", forge.Permanent)
To issue a 410 Gone (pass an empty destination):
app.Redirect("/removed", "", forge.Gone)
func (*App) RedirectManifestAuth ¶
RedirectManifestAuth sets the AuthFunc that guards /.well-known/redirects.json. Call before App.Handler or App.Run.
app.RedirectManifestAuth(forge.BearerHMAC(secret, forge.Editor))
func (*App) RedirectStore ¶
func (a *App) RedirectStore() *RedirectStore
RedirectStore returns the App's RedirectStore, which can be used to load persisted redirects from a database at startup, or to save/remove entries at runtime:
if err := app.RedirectStore().Load(ctx, db); err != nil {
log.Fatal(err)
}
func (*App) Run ¶
Run starts the HTTP server on addr (e.g. ":8080") and blocks until SIGINT or SIGTERM is received.
On receiving a signal, Run initiates a graceful shutdown with a 5-second deadline, waits for active connections to drain, and returns nil. Non-shutdown errors from ListenAndServe are returned directly.
if err := app.Run(":8080"); err != nil {
log.Fatal(err)
}
func (*App) SEO ¶
SEO applies one or more app-level SEO options.
Call SEO before App.Handler or App.Run so the configuration is applied before routes are registered. SEO may be called multiple times; later calls override earlier values for the same option type.
app.SEO(&forge.RobotsConfig{AIScraper: forge.AskFirst, Sitemaps: true})
func (*App) Secret ¶ added in v1.1.0
Secret returns the HMAC signing secret from the application configuration. It is intended for use by forge-mcp and other companion packages that must verify tokens minted with SignToken but cannot access Config directly.
func (*App) Use ¶
Use appends one or more global middleware to the App's middleware stack.
Middleware is applied in the order it is added: the first call to Use wraps the outermost layer. Use may be called multiple times; all calls are additive.
app.Use(forge.RequestLogger(), forge.Recoverer(), forge.SecurityHeaders())
type AppSchema ¶ added in v1.1.9
type AppSchema struct {
// Type is the JSON-LD @type, e.g. "Organization" or "WebSite".
Type string
// Name is the human-readable name of the organisation or site.
Name string
// URL is the canonical URL of the organisation or site's home page.
URL string
// Logo is the absolute URL of the organisation's logo image.
Logo string
}
AppSchema registers app-level JSON-LD structured data emitted in every page's <head> by forge:head. Use it to declare site-wide Organisation or WebSite metadata once rather than per content type.
Apply via App.SEO:
app.SEO(&forge.AppSchema{
Type: "Organization",
Name: "Acme Corp",
URL: "https://acme.com",
Logo: "https://acme.com/logo.png",
})
Example ¶
ExampleAppSchema demonstrates registering app-level JSON-LD structured data. The block is emitted automatically by forge:head on every page.
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.SEO(&AppSchema{
Type: "Organization",
Name: "Acme Corp",
URL: "https://example.com",
Logo: "https://example.com/logo.png",
})
_ = app.Handler()
type AuthFunc ¶
type AuthFunc interface {
// contains filtered or unexported methods
}
AuthFunc authenticates an incoming HTTP request and returns the identified User and whether authentication succeeded. Use BearerHMAC, CookieSession, BasicAuth, or AnyAuth to obtain an AuthFunc. Implement this interface to provide a custom authentication scheme.
The unexported authenticate method is intentional: it prevents accidental direct calls and allows future additions to the interface without breaking existing implementations (consistent with Option and Signal).
func AnyAuth ¶
AnyAuth returns an AuthFunc that tries each provided AuthFunc in order and returns the first successful result. If none match, it returns GuestUser.
AnyAuth forwards [productionWarner] and [csrfAware] capability calls to any child that implements them.
func BasicAuth ¶
BasicAuth returns an AuthFunc that validates HTTP Basic Auth credentials. On success it returns a synthetic User with ID and Name set to the username and Roles set to Guest.
BasicAuth should not be used in production. Consider BearerHMAC or CookieSession for production use. See Amendment S7.
func BearerHMAC ¶
BearerHMAC returns an AuthFunc that validates HMAC-signed bearer tokens from the Authorization header (format: "Bearer <token>"). Generate tokens with SignToken.
func CookieSession ¶
CookieSession returns an AuthFunc that reads a named cookie containing a signed user token (same format as BearerHMAC). CSRF protection is enabled by default — pass WithoutCSRF to opt out (strongly discouraged).
The CSRF cookie is named CSRFCookieName. See [Amendment S6].
type Breadcrumb ¶
type Breadcrumb struct {
Label string // human-readable label
URL string // root-relative or absolute URL
}
Breadcrumb is a single step in a breadcrumb trail. Build slices using the Crumb constructor and the Crumbs helper.
func Crumb ¶
func Crumb(label, url string) Breadcrumb
Crumb returns a single Breadcrumb entry. Use with Crumbs to build Head.Breadcrumbs:
forge.Crumbs(
forge.Crumb("Home", "/"),
forge.Crumb("Posts", "/posts"),
forge.Crumb(p.Title, "/posts/"+p.Slug),
)
func Crumbs ¶
func Crumbs(crumbs ...Breadcrumb) []Breadcrumb
Crumbs collects Breadcrumb entries for use in Head.Breadcrumbs.
type CacheStore ¶
type CacheStore struct {
// contains filtered or unexported fields
}
CacheStore is a thread-safe LRU cache of HTTP responses used by InMemoryCache and by Module for signal-triggered invalidation. Use NewCacheStore to create one.
func NewCacheStore ¶
func NewCacheStore(ttl time.Duration, max int) *CacheStore
NewCacheStore returns a CacheStore with the given TTL per entry and maximum entry count. When the store is full, the least-recently-used entry is evicted.
func (*CacheStore) Flush ¶
func (c *CacheStore) Flush()
Flush removes all entries from the cache immediately. Used by Module to invalidate the cache after a write operation (create, update, delete).
func (*CacheStore) Sweep ¶
func (c *CacheStore) Sweep()
Sweep removes all expired entries. Called periodically by InMemoryCache background goroutine and available for use by Module.
type ChangeFreq ¶
type ChangeFreq string
ChangeFreq is the value of a sitemap <changefreq> element, indicating how frequently the page content is likely to change.
const ( // Always signals the URL changes with every access. Use for live data. Always ChangeFreq = "always" // Hourly signals the URL is updated approximately once per hour. Hourly ChangeFreq = "hourly" // Daily signals the URL is updated approximately once per day. Daily ChangeFreq = "daily" // Weekly signals the URL is updated approximately once per week. // This is the default when [SitemapConfig.ChangeFreq] is empty. Weekly ChangeFreq = "weekly" // Monthly signals the URL is updated approximately once per month. Monthly ChangeFreq = "monthly" // Yearly signals the URL is updated approximately once per year. Yearly ChangeFreq = "yearly" // Never signals the URL is permanently archived and will not change. Never ChangeFreq = "never" )
type Config ¶
type Config struct {
// BaseURL is the canonical URL of the site, e.g. "https://example.com"
// (no trailing slash). Required.
BaseURL string
// Secret is the HMAC signing key used by [BearerHMAC], [CookieSession], and
// [SignToken]. It must be at least 16 bytes. Required.
//
// When Auth is nil, Secret is used to validate Bearer tokens automatically
// via [BearerHMAC]. Set [Config.Auth] to override.
Secret []byte
// Auth is the [AuthFunc] used to authenticate requests. When nil, Forge
// defaults to [BearerHMAC] using [Config.Secret]. Set this explicitly to
// use [CookieSession], [AnyAuth], or a custom [AuthFunc].
//
// Example — cookie sessions instead of bearer tokens:
//
// Auth: forge.CookieSession("session", secret)
//
// Example — both bearer tokens and cookie sessions:
//
// Auth: forge.AnyAuth(
// forge.BearerHMAC(secret),
// forge.CookieSession("session", secret),
// )
Auth AuthFunc
// Version is the application version string. It is an optional field for
// application authors who want to track their own release version; forge
// itself does not use this field in any built-in endpoint.
Version string
// DB is the database connection used by content modules.
// It accepts *sql.DB, *sql.Tx, or any value that satisfies [DB].
// Optional — leave nil to use in-memory repositories only.
DB DB
// HTTPS forces an HTTP→HTTPS redirect for all plain-HTTP requests when
// true. The App handler checks r.TLS and the X-Forwarded-Proto header so
// this works correctly behind a reverse proxy. Optional.
HTTPS bool
// ReadTimeout is the maximum time to read the full request, including the
// body. Defaults to 5 s. Optional.
ReadTimeout time.Duration
// WriteTimeout is the maximum time to write the full response. Defaults to
// 10 s. Optional.
WriteTimeout time.Duration
// IdleTimeout is the maximum keep-alive idle time between requests.
// Defaults to 120 s. Optional.
IdleTimeout time.Duration
}
Config holds the application-wide configuration passed to New.
BaseURL and Secret are required; all other fields are optional.
Timeouts default to 5 s (read), 10 s (write), and 120 s (idle) when left as zero. Set them explicitly to override.
func MustConfig ¶
MustConfig validates cfg and returns it unchanged.
Panics with a descriptive message if:
- Config.BaseURL is empty or not a valid absolute URL
- Config.Secret is fewer than 16 bytes
Typical usage:
app := forge.New(forge.MustConfig(forge.Config{
BaseURL: os.Getenv("BASE_URL"),
Secret: []byte(os.Getenv("SECRET")),
}))
type Context ¶
type Context interface {
context.Context
// User returns the authenticated identity for this request.
// Returns [GuestUser] (zero value) for unauthenticated requests.
User() User
// Locale returns the BCP 47 language tag for this request.
// Always "en" in v1; i18n support is planned for v2 (Decision 11).
Locale() string
// SiteName returns the configured site name. Always "" in v1 until
// wired in forge.go (Step 11).
SiteName() string
// RequestID returns the UUID v7 assigned to this request for
// end-to-end traceability. Set as X-Request-ID on the response.
RequestID() string
// Request returns the underlying *http.Request.
Request() *http.Request
// Response returns the http.ResponseWriter for this request.
Response() http.ResponseWriter
}
Context is the request-scoped value passed to every Forge hook and handler. It embeds context.Context for full compatibility with stdlib and third-party libraries, while exposing Forge-specific accessors without key-based lookups.
forge.Context is always non-nil — Forge guarantees this before any user code is called. The internal implementation is [contextImpl] (unexported). Use ContextFrom in production and NewTestContext in tests.
func ContextFrom ¶
func ContextFrom(w http.ResponseWriter, r *http.Request) Context
ContextFrom builds a Context from a live HTTP request. It:
- Derives the RequestID from X-Request-ID response header, then request header, generating a fresh UUID v7 if neither is present
- Writes the final RequestID to the X-Request-ID response header
- Reads the authenticated User from the request's context (set by auth middleware); uses GuestUser if absent
- Sets Locale to "en" (i18n deferred to v2)
- Sets SiteName to "" (wired in forge.go, Step 11)
func NewBackgroundContext ¶
NewBackgroundContext returns a Context for use in background goroutines such as the scheduled-publishing ticker. It has no HTTP lifecycle and never times out:
- Request() returns a synthetic GET / request backed by context.Background
- Response() returns a *httptest.ResponseRecorder (discards output)
- User is GuestUser; Locale is "en"; RequestID is a generated UUID v7
siteName should be the hostname portion of [Config.BaseURL] (e.g. "example.com").
func NewContextWithUser ¶ added in v1.1.0
NewContextWithUser returns a Context for use in background goroutines or non-HTTP transports (e.g. stdio MCP) that require a real User identity. Unlike NewTestContext, this function may appear in production code. Unlike NewBackgroundContext, the User is caller-supplied rather than hardcoded to GuestUser.
- Request() returns a synthetic GET / request backed by context.Background
- Response() returns a *httptest.ResponseRecorder (discards output)
- Locale is "en"; SiteName is ""; RequestID is a generated UUID v7
func NewTestContext ¶
NewTestContext returns a Context suitable for unit tests. It requires no running HTTP server:
- Request() returns a synthetic GET / request
- Response() returns a *httptest.ResponseRecorder
- Locale is "en", SiteName is "", RequestID is a generated UUID v7
Pass GuestUser (or a zero User) for unauthenticated test scenarios.
type Cookie ¶
type Cookie struct {
// Name is the cookie name as set on the wire.
Name string
// Category classifies the cookie for consent enforcement.
Category CookieCategory
// Path scopes the cookie to a URL prefix. Defaults to "/" if empty.
Path string
// Domain optionally scopes the cookie to a domain.
Domain string
// Secure restricts the cookie to HTTPS connections.
Secure bool
// HttpOnly prevents JavaScript from accessing the cookie.
HttpOnly bool
// SameSite controls cross-site request behaviour.
// Defaults to http.SameSiteStrictMode when zero.
SameSite http.SameSite
// MaxAge is the cookie lifetime in seconds.
// 0 = session cookie; negative = delete immediately.
MaxAge int
// Purpose is a human-readable description for the compliance manifest.
Purpose string
}
Cookie declares a typed cookie with its category, attributes, and purpose.
Category determines which set API is legal (Decision 5):
- Necessary: use SetCookie — no consent required
- Preferences, Analytics, Marketing: use SetCookieIfConsented
Purpose is a human-readable description included in the compliance manifest at /.well-known/cookies.json.
type CookieCategory ¶
type CookieCategory string
CookieCategory classifies a cookie by its GDPR consent requirement. The category determines which set API is legal (Decision 5).
const ( // Necessary cookies are required for the site to function. // They do not require user consent and must be set with [SetCookie]. Necessary CookieCategory = "necessary" // Preferences cookies remember user settings (e.g. language, theme). // They require consent and must be set with [SetCookieIfConsented]. Preferences CookieCategory = "preferences" // Analytics cookies collect anonymous usage statistics. // They require consent and must be set with [SetCookieIfConsented]. Analytics CookieCategory = "analytics" // Marketing cookies track users across sites for advertising purposes. // They require consent and must be set with [SetCookieIfConsented]. Marketing CookieCategory = "marketing" )
type CrawlerPolicy ¶
type CrawlerPolicy string
CrawlerPolicy controls how AI web crawlers are treated in the generated robots.txt. The zero value is Allow.
const ( // Allow permits all crawlers, including AI training scrapers. // This is the zero-value default. Allow CrawlerPolicy = "allow" // Disallow blocks all known AI training crawlers by adding individual // User-agent / Disallow: / entries for each identified bot. Disallow CrawlerPolicy = "disallow" // AskFirst blocks known AI training crawlers while permitting AI // assistants that respect the robots.txt contract. Recommended for // sites that wish to be indexed by AI search but not scraped for // training. AskFirst CrawlerPolicy = "ask-first" )
type DB ¶
type DB interface {
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
DB is satisfied by *sql.DB, *sql.Tx, and any pgx adapter such as forgepgx.Wrap(pool). Users pass a concrete implementation to forge.Config — they do not implement DB directly.
type Error ¶
type Error interface {
error
// Code returns a machine-readable error identifier (e.g. "not_found").
Code() string
// HTTPStatus returns the HTTP status code that should be sent to the client.
HTTPStatus() int
// Public returns a message that is safe to expose to API clients.
Public() string
}
Error is implemented by all Forge errors. Callers should use errors.As to inspect the concrete type — never type-assert directly against a sentinel.
type EventDetails ¶
type EventDetails struct {
StartDate time.Time
EndDate time.Time
Location string // venue name
Address string // street address or city
}
EventDetails carries the extra fields required for Event rich results.
type EventProvider ¶
type EventProvider interface{ EventDetails() EventDetails }
EventProvider is implemented by content types that supply event structured data.
type FAQProvider ¶
type FAQProvider interface{ FAQEntries() []FAQEntry }
FAQProvider is implemented by content types that supply FAQ structured data. Return a non-empty slice to enable FAQPage JSON-LD generation via SchemaFor.
type FaviconLink ¶ added in v1.3.0
type FaviconLink struct {
Rel string // "icon", "apple-touch-icon", etc.
Type string // MIME type, e.g. "image/png"; omitted when empty
Sizes string // e.g. "32x32"; omitted when empty
Href string // URL to the icon file
}
FaviconLink declares a single <link> element for a favicon or touch icon. Rel is required ("icon", "apple-touch-icon"). Type and Sizes are optional; omit them by leaving the fields empty.
type FeedConfig ¶
type FeedConfig struct {
// Title is the channel title shown in feed readers.
// Defaults to the capitalised prefix (e.g. "Posts").
Title string
// Description is the channel description.
// Defaults to the site hostname when empty.
Description string
// Language is the BCP 47 language code for the feed.
// Defaults to "en".
Language string
}
FeedConfig configures the RSS 2.0 feed for a content module. Pass it to Feed to enable feed generation for the module.
All fields are optional. Title defaults to the capitalised module prefix (e.g. "/posts" → "Posts"). Language defaults to "en".
type FeedStore ¶
type FeedStore struct {
// contains filtered or unexported fields
}
FeedStore holds pre-built RSS item fragments from all Feed-enabled content modules. It is shared across modules via App.Content and provides per-module and aggregate /feed.xml HTTP handlers.
All public methods are safe for concurrent use.
func NewFeedStore ¶
NewFeedStore constructs a FeedStore for the given site hostname and base URL. Called by App.Content when the first Feed-enabled module is registered.
func (*FeedStore) HasFeeds ¶
HasFeeds reports whether at least one module has registered a feed fragment. Used by App.Handler to decide whether to mount GET /feed.xml.
func (*FeedStore) IndexHandler ¶
IndexHandler returns an http.Handler that serves a merged RSS 2.0 feed of all Published items from every Feed-enabled module, sorted by pubDate descending, at /feed.xml.
func (*FeedStore) ModuleHandler ¶
ModuleHandler returns an http.Handler that serves the RSS 2.0 feed for the given module prefix at /{prefix}/feed.xml.
The channel title comes from FeedConfig.Title (falling back to the capitalised prefix). Language defaults to "en".
func (*FeedStore) Set ¶
func (s *FeedStore) Set(prefix string, cfg FeedConfig, items []rssItem)
Set stores the RSS items and config for the given module prefix. Passing nil items registers the prefix without content (used at startup). Called by regenerateFeed on every publish event and by setFeedStore at startup.
type From ¶
type From string
From is the old URL prefix supplied to the Redirects module option. Wrapping in a named type makes call sites self-documenting:
forge.Redirects(forge.From("/posts"), "/articles")
type Head ¶
type Head struct {
Title string // page title; used in <title>, og:title, and JSON-LD
Description string // meta description; recommended max 160 characters
Author string // author name; used in <meta name="author"> and JSON-LD
Published time.Time // publication date; zero value omits date tags
Modified time.Time // last-modified date; zero value omits date tags
Image Image // primary image; zero URL omits all image tags
Type string // rich result type (Article, Product, etc.); empty omits JSON-LD
Canonical string // canonical URL; empty omits the canonical tag
Tags []string // content tags; used for article:tag meta and RSS categories
Breadcrumbs []Breadcrumb // breadcrumb trail; empty omits BreadcrumbList JSON-LD
Alternates []Alternate // hreflang entries; always empty in v1
Social SocialOverrides // per-item social sharing overrides; zero value uses defaults
NoIndex bool // true renders <meta name="robots" content="noindex">
}
Head carries all SEO and social metadata for a content page. Define it on your content type via the Headable interface. Forge uses the Head to populate HTML <head> tags, JSON-LD structured data, sitemaps, RSS feeds, and AI endpoints.
All fields are optional: the zero value is safe and produces a minimal page header.
type HeadAssets ¶ added in v1.3.0
type HeadAssets struct {
Preconnect []string // <link rel="preconnect" href="…">
Stylesheets []string // <link rel="stylesheet" href="…">
Favicons []FaviconLink // <link rel="icon" …> / <link rel="apple-touch-icon" …>
Scripts []ScriptTag // <script …>
}
HeadAssets is an SEOOption that injects static linked assets — preconnect hints, stylesheets, favicons, and scripts — into the forge:head partial on every page.
Apply it via App.SEO:
app.SEO(&forge.HeadAssets{
Preconnect: []string{"https://fonts.googleapis.com"},
Stylesheets: []string{"https://fonts.googleapis.com/css2?family=Inter&display=swap"},
Favicons: []forge.FaviconLink{
{Rel: "icon", Type: "image/png", Sizes: "32x32", Href: "/favicon-32.png"},
},
Scripts: []forge.ScriptTag{
{Src: "/static/app.js", Defer: true},
},
})
Assets are emitted in order: preconnect → stylesheets → favicons → scripts.
Example ¶
ExampleHeadAssets demonstrates injecting site-wide static assets — preconnect hints, stylesheets, favicons, and scripts — into forge:head on every page via app.SEO.
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.SEO(&HeadAssets{
Preconnect: []string{"https://fonts.googleapis.com"},
Stylesheets: []string{"https://fonts.googleapis.com/css2?family=Inter&display=swap", "/static/app.css"},
Favicons: []FaviconLink{
{Rel: "icon", Type: "image/png", Sizes: "32x32", Href: "/favicon-32.png"},
{Rel: "apple-touch-icon", Href: "/apple-touch-icon.png"},
},
Scripts: []ScriptTag{
{Src: "/static/app.js", Defer: true},
},
})
_ = app.Handler()
type Headable ¶
type Headable interface{ Head() Head }
Headable is implemented by content types that provide their own SEO metadata. Module[T] calls Head() automatically when building HTML responses, sitemaps, RSS feeds, and AI endpoints — no HeadFunc option required. HeadFunc takes priority over Headable when both are present.
type HowToProvider ¶
type HowToProvider interface{ HowToSteps() []HowToStep }
HowToProvider is implemented by content types that supply step-by-step structured data for HowTo rich results.
type HowToStep ¶
type HowToStep struct {
Name string // short label for the step
Text string // full instruction text
}
HowToStep is a single step in a HowTo or Recipe structured data block.
type Image ¶
type Image struct {
URL string // absolute or root-relative
Alt string // accessibility and SEO description
Width int // pixels; required for og:image:width
Height int // pixels; required for og:image:height
}
Image is a typed image reference. Width and Height are required for optimal Open Graph rendering and Twitter Card display. The zero value (empty URL) renders no image tags — safe to leave unset.
type LLMsEntry ¶
type LLMsEntry struct {
// Title is the content item's title. Required.
Title string
// URL is the canonical URL for this item. Required.
URL string
// Summary is a short plain-text description. Optional; omitted when empty.
Summary string
}
LLMsEntry is a single compact entry in /llms.txt output. Fields map to the llmstxt.org compact format: - [Title](URL): Summary
type LLMsStore ¶
type LLMsStore struct {
// contains filtered or unexported fields
}
LLMsStore holds compact and full content fragments for /llms.txt and /llms-full.txt. Thread-safe. Analogous to SitemapStore.
Created by App.Content when the first module registers with AIIndex. Passed to each module via setAIRegistry.
func NewLLMsStore ¶
NewLLMsStore creates an LLMsStore for the given site name.
func (*LLMsStore) CompactHandler ¶
CompactHandler returns an http.Handler that serves the /llms.txt endpoint. The built-in format follows the llmstxt.org convention: site name header followed by per-item entries as "- [Title](URL): Summary". Responses are gzip-compressed when the client sends Accept-Encoding: gzip and the body exceeds [gzipMinBytes] (Amendment A17).
func (*LLMsStore) FullHandler ¶
FullHandler returns an http.Handler that serves the /llms-full.txt endpoint. The corpus header identifies the site name, generation date, and item count. Each item is rendered as a full document separated by "---". Responses are gzip-compressed when the client sends Accept-Encoding: gzip and the body exceeds [gzipMinBytes] (Amendment A17).
func (*LLMsStore) HasCompact ¶
HasCompact reports whether any module registered with LLMsTxt.
func (*LLMsStore) HasFull ¶
HasFull reports whether any module registered with LLMsTxtFull.
func (*LLMsStore) SetCompact ¶
SetCompact stores compact entries for the given module prefix. Called by Module.regenerateAI after every publish event.
type LLMsTemplateData ¶
type LLMsTemplateData struct {
// SiteName is the hostname of the site (e.g. "example.com").
SiteName string
// Description is a one-line site description. Empty by default;
// set manually in custom templates.
Description string
// Entries contains all compact entries across all registered modules.
Entries []LLMsEntry
// GeneratedAt is the generation date in YYYY-MM-DD format.
GeneratedAt string
// ItemCount is the total number of Published items across all modules.
ItemCount int
}
LLMsTemplateData is the data value passed to custom llms.txt templates. Create templates/llms.txt in your template directory to override the built-in format:
# {{.SiteName}}
> {{.Description}}
## All Content
{{forge_llms_entries .}}
type ListOptions ¶
type ListOptions struct {
// Page is one-based. Values ≤ 0 are treated as page 1.
Page int
// PerPage is the maximum number of items per page.
// A value of 0 means return all items.
PerPage int
// OrderBy is the Go field name to sort by (e.g. "Title").
// Sorting applies only to exported string fields; other types are ignored.
OrderBy string
// Desc reverses the sort order when true.
Desc bool
// Status restricts results to items whose Status field matches one of the
// given values. An empty or nil slice means return all statuses.
Status []Status
}
ListOptions controls pagination and ordering for FindAll queries.
func (ListOptions) Offset ¶
func (o ListOptions) Offset() int
Offset returns the zero-based row offset for the page described by o.
type MCPField ¶ added in v1.1.0
type MCPField struct {
Name string // Go field name
JSONName string // lowercase snake_case name used in MCP messages
Type string // "string" | "number" | "boolean" | "datetime"
Required bool
MinLength int // 0 = no constraint
MaxLength int // 0 = no constraint
Enum []string // nil = no constraint
}
MCPField describes a single field in a content type's MCP schema, derived automatically from the Go struct type and forge: struct tags. Returned by [MCPModule.MCPSchema].
type MCPMeta ¶ added in v1.1.0
type MCPMeta struct {
Prefix string // URL prefix, e.g. "/posts"
TypeName string // content type name, e.g. "BlogPost"
Operations []MCPOperation // MCPRead and/or MCPWrite
}
MCPMeta describes the MCP registration of a content module. Returned by [MCPModule.MCPMeta].
type MCPModule ¶ added in v1.1.0
type MCPModule interface {
// MCPMeta returns the module's MCP registration metadata.
MCPMeta() MCPMeta
// MCPSchema returns the field schema derived from the content type's
// struct tags.
MCPSchema() []MCPField
// MCPList returns all items matching the given statuses (all statuses if
// none are given).
MCPList(ctx Context, status ...Status) ([]any, error)
// MCPGet returns the item with the given slug.
// MCPGet does not filter by lifecycle status — it returns the item
// regardless of status. Callers are responsible for enforcing lifecycle
// rules (e.g. forge-mcp checks that the item is Published before
// including it in a resources/read response).
MCPGet(ctx Context, slug string) (any, error)
// MCPCreate creates a new item from the given fields map.
MCPCreate(ctx Context, fields map[string]any) (any, error)
// MCPUpdate applies a partial update to the item with the given slug.
MCPUpdate(ctx Context, slug string, fields map[string]any) (any, error)
// MCPPublish transitions the item with the given slug to Published.
MCPPublish(ctx Context, slug string) error
// MCPSchedule sets the item with the given slug to publish at the given time.
MCPSchedule(ctx Context, slug string, at time.Time) error
// MCPArchive transitions the item with the given slug to Archived.
MCPArchive(ctx Context, slug string) error
// MCPDelete permanently deletes the item with the given slug.
MCPDelete(ctx Context, slug string) error
}
MCPModule is implemented by any Module[T] that has been registered with MCP. forge-mcp reads this interface to build MCP resources and tools without accessing Module internals directly.
All methods receive a Context carrying the authenticated user. Callers must construct the Context with the appropriate Role before calling any mutating method — the MCPModule implementation enforces roles and validation identically to the HTTP layer.
type MCPOperation ¶
type MCPOperation string
MCPOperation is an option flag for the MCP function. Only MCPRead and MCPWrite are defined.
const ( // MCPRead signals that this module should be exposed as a read-only MCP // resource. The forge-mcp server will include it in resources/list and // resources/read responses. See [MCPModule]. MCPRead MCPOperation = "read" // MCPWrite signals that this module should be exposed as a read+write MCP // resource. The forge-mcp server will generate tools for create, update, // publish, schedule, archive, and delete operations. See [MCPModule]. MCPWrite MCPOperation = "write" )
type Markdownable ¶
type Markdownable interface{ Markdown() string }
Markdownable is implemented by content types that render directly to Markdown. When T implements Markdownable, Module serves text/markdown responses without requiring forge.Templates to be configured. The Markdown body is also used in AIDoc output and /llms-full.txt corpus entries.
type MemoryRepo ¶
type MemoryRepo[T any] struct { // contains filtered or unexported fields }
MemoryRepo is a thread-safe in-memory implementation of Repository. It is intended for unit tests and prototyping — not production use. Fields named ID and Slug are located via cached reflection on first use.
func NewMemoryRepo ¶
func NewMemoryRepo[T any]() *MemoryRepo[T]
NewMemoryRepo returns an empty MemoryRepo[T] ready for use.
func (*MemoryRepo[T]) Delete ¶
func (r *MemoryRepo[T]) Delete(_ context.Context, id string) error
Delete removes the item with the given ID. Returns ErrNotFound if absent.
func (*MemoryRepo[T]) FindAll ¶
func (r *MemoryRepo[T]) FindAll(_ context.Context, opts ListOptions) ([]T, error)
FindAll returns items in insertion order, with optional sorting and pagination from opts. When opts.PerPage is 0, all items are returned.
func (*MemoryRepo[T]) FindByID ¶
func (r *MemoryRepo[T]) FindByID(_ context.Context, id string) (T, error)
FindByID returns the item with the given ID, or ErrNotFound.
func (*MemoryRepo[T]) FindBySlug ¶
func (r *MemoryRepo[T]) FindBySlug(_ context.Context, slug string) (T, error)
FindBySlug returns the first item whose Slug field matches slug, or ErrNotFound.
type Module ¶
type Module[T any] struct { // contains filtered or unexported fields }
Module is the core routing and lifecycle unit for a content type T. T must embed Node — its struct must have exported ID, Slug, and Status fields. Use NewModule to construct; Registration onto a ServeMux is done via [Register]. App.Content handles both steps automatically.
func NewModule ¶
NewModule constructs a Module for content type T.
proto is a representative value of T (typically a nil pointer: (*Post)(nil)) used to derive the default URL prefix and to detect capabilities.
Required options (supplied automatically by App.Content):
- Repo: provides the Repository[T]
Optional options:
- At: override URL prefix (default: "/"+lowercase(TypeName)+"s")
- Auth: set per-operation role requirements
- Cache: enable per-module LRU response cache
- Middleware: wrap all routes with the given middleware
- On: register signal handlers
Panics if no Repo option is present — this is a programming error caught at startup, never at request time.
Example ¶
ExampleNewModule demonstrates creating a typed content module and registering it with an App. This is the idiomatic two-step path: NewModule[T] preserves full type safety and ensures all App-level wiring (sitemap, feed, AI) runs.
secret := []byte("example-secret-key-32-bytes!!!!!")
repo := NewMemoryRepo[*examplePost]()
m := NewModule(&examplePost{},
At("/posts"),
Repo(repo),
Auth(
Read(Guest),
Write(Author),
Delete(Editor),
),
Cache(5*time.Minute),
AIIndex(LLMsTxt, AIDoc),
)
app := New(Config{
BaseURL: "https://example.com",
Secret: secret,
})
app.Content(m)
_ = app.Handler()
func (*Module[T]) MCPArchive ¶ added in v1.1.0
MCPArchive transitions the item with the given slug to Archived, fires AfterArchive, and triggers derived-content rebuild.
func (*Module[T]) MCPCreate ¶ added in v1.1.0
MCPCreate creates a new content item from the supplied fields map. A new ID is always generated; the slug is auto-derived when absent. The item is validated before persistence. AfterCreate signals are dispatched asynchronously.
func (*Module[T]) MCPDelete ¶ added in v1.1.0
MCPDelete permanently removes the item with the given slug, fires AfterDelete, and triggers derived-content rebuild.
func (*Module[T]) MCPGet ¶ added in v1.1.0
MCPGet returns the item with the given slug regardless of its lifecycle status. The caller is responsible for enforcing visibility rules.
func (*Module[T]) MCPList ¶ added in v1.1.0
MCPList returns all content items matching the given statuses. If no statuses are provided, items of all statuses are returned.
func (*Module[T]) MCPMeta ¶ added in v1.1.0
MCPMeta returns the MCP registration metadata for this module.
func (*Module[T]) MCPPublish ¶ added in v1.1.0
MCPPublish transitions the item with the given slug to Published, sets PublishedAt to now, fires AfterPublish, and triggers derived-content rebuild.
func (*Module[T]) MCPSchedule ¶ added in v1.1.0
MCPSchedule sets the item with the given slug to Scheduled and records the time at which it will be automatically published.
func (*Module[T]) MCPSchema ¶ added in v1.1.0
MCPSchema derives the field schema for this module's content type from Go struct fields and forge: struct tags. The embedded forge.Node fields Slug, Status, PublishedAt, and ScheduledAt are included; ID, CreatedAt, and UpdatedAt are omitted because they are managed by the framework.
func (*Module[T]) MCPUpdate ¶ added in v1.1.0
MCPUpdate applies a partial update to the item with the given slug. Fields present in the map overlay the existing item; absent fields are preserved. Node.ID, Node.Slug, and Node.Status are always restored after the merge — use the dedicated lifecycle methods to change status.
func (*Module[T]) Register ¶
Register mounts the five standard routes for this module onto mux. Called automatically by App.Content.
GET /{prefix} → list
GET /{prefix}/{slug} → show
POST /{prefix} → create
PUT /{prefix}/{slug} → update
DELETE /{prefix}/{slug} → delete
func (*Module[T]) Stop ¶ added in v1.0.5
func (m *Module[T]) Stop()
Stop terminates background goroutines started by this module (cache sweep ticker and any pending debounce timer). It is called automatically by App.Run during graceful shutdown. Stop is idempotent — calling it more than once is safe.
type Node ¶
type Node struct {
// ID is the UUID v7 primary key. Set by the storage layer on insert;
// immutable thereafter. See [NewID] and Amendment S1.
ID string
// Slug is the URL-safe identifier used in all public URLs. Unique within
// a module. Auto-generated from the first required string field if not
// set explicitly. May be changed; the old URL should redirect.
Slug string
// Status is the lifecycle state. Forge enforces this on every public
// endpoint. See Decision 14.
Status Status
// PublishedAt is the time the content was first published. Zero until
// the first transition to Published.
PublishedAt time.Time `db:"published_at"`
// ScheduledAt is the time at which a Scheduled item will be published.
// Nil for all other lifecycle states.
ScheduledAt *time.Time `db:"scheduled_at"`
// CreatedAt is set by the storage layer on insert and never updated.
CreatedAt time.Time `db:"created_at"`
// UpdatedAt is set by the storage layer on every Save.
UpdatedAt time.Time `db:"updated_at"`
}
Node is the base type embedded by every Forge content type. It carries the stable UUID identity, the URL slug, and the full content lifecycle.
Content types must embed Node as a value (not a pointer):
type BlogPost struct {
forge.Node
Title string `forge:"required"`
Body string `forge:"required,min=50"`
}
Never store a Node by pointer inside your content type — the storage and validation layers require a contiguous struct layout.
func (*Node) GetPublishedAt ¶
GetPublishedAt returns the time this node was first published. The zero time indicates the node has never been published.
func (*Node) GetSlug ¶
GetSlug returns the URL slug for this node. Satisfies the SitemapNode constraint, enabling generic sitemap generation without reflection.
type OGDefaults ¶ added in v1.1.9
type OGDefaults struct {
// Image is the fallback og:image used when a content item's Head.Image.URL
// is empty. Width and Height are recommended for optimal Twitter Card display.
Image Image
// TwitterSite is the twitter:site handle for the site (e.g. "@mycompany").
// Always emitted on every page; not overridable per item.
TwitterSite string
// TwitterCreator is the fallback twitter:creator handle used when the
// content item's Head.Social.Twitter.Creator is empty.
TwitterCreator string
}
OGDefaults sets app-level Open Graph and Twitter Card fallback values. Apply via App.SEO; values are merged into every page's Head by forge:head when the content item does not supply its own.
- Image — fallback og:image when [Head.Image].URL is empty.
- TwitterSite — twitter:site handle (e.g. "@mycompany"); always app-level, emitted on every page.
- TwitterCreator — fallback twitter:creator when [Head.Social].Twitter.Creator is empty.
Example:
app.SEO(&forge.OGDefaults{
Image: forge.Image{URL: "https://example.com/og.png", Width: 1200, Height: 630},
TwitterSite: "@mycompany",
TwitterCreator: "@editor",
})
Example ¶
ExampleOGDefaults demonstrates setting app-level Open Graph and Twitter Card fallback values. These are merged into every page's Head by forge:head when the content item does not supply its own image or Twitter creator handle.
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.SEO(&OGDefaults{
Image: Image{URL: "https://example.com/og-default.png", Width: 1200, Height: 630},
TwitterSite: "@mycompany",
TwitterCreator: "@editor",
})
_ = app.Handler()
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures a Module or App at registration time. Option values are created by functions such as Read, Write, Delete, At, Cache, and forge.On. They are consumed during module or app setup and have no effect after App.Run is called.
var WithoutCSRF Option = withoutCSRFOption{}
WithoutCSRF is an Option passed to CookieSession to disable automatic CSRF protection. This is strongly discouraged for production use.
func AIIndex ¶
AIIndex returns an Option that enables AI indexing endpoints for a module. Pass one or more AIFeature constants to select which endpoints are registered.
app.Content(&BlogPost{},
forge.At("/posts"),
forge.AIIndex(forge.LLMsTxt, forge.LLMsTxtFull, forge.AIDoc),
)
Example ¶
ExampleAIIndex demonstrates enabling AI indexing on a content module. LLMsTxt registers the module in /llms.txt, LLMsTxtFull produces a full markdown corpus at /llms-full.txt, and AIDoc adds /{slug}/aidoc endpoints.
repo := NewMemoryRepo[*examplePost]()
m := NewModule(&examplePost{},
At("/posts"),
Repo(repo),
AIIndex(LLMsTxt, LLMsTxtFull, AIDoc),
)
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.Content(m)
_ = app.Handler()
func At ¶
At returns an Option that sets the URL prefix for a module. The prefix must start with "/" and must not end with "/". Example: forge.At("/posts")
func Auth ¶
Auth returns an Option that sets the minimum role for each HTTP operation on this module. Accepts Read, Write, and Delete role options.
forge.Auth(
forge.Read(forge.Guest),
forge.Write(forge.Author),
forge.Delete(forge.Editor),
)
Example ¶
ExampleAuth demonstrates declaring role-based access for read, write, and delete operations on a content module.
repo := NewMemoryRepo[*examplePost]()
m := NewModule(&examplePost{},
At("/posts"),
Repo(repo),
Auth(
Read(Guest),
Write(Author),
Delete(Editor),
),
)
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.Content(m)
_ = app.Handler()
func Cache ¶
Cache returns an Option that enables a per-module LRU response cache with the given TTL. Cached entries are flushed automatically on any create, update, or delete operation. The cache holds at most 1000 entries (LRU eviction).
func CacheMaxEntries ¶
CacheMaxEntries returns an Option that configures InMemoryCache to hold at most n entries, evicting the least-recently-used entry when full. The default is 1000 entries.
func Delete ¶
Delete returns an Option that restricts delete access to users whose role satisfies the required role. Wired in Step 10 (module.go).
func DisableFeed ¶ added in v1.0.5
func DisableFeed() Option
DisableFeed returns an Option that explicitly opts a module out of RSS feed generation. This is a defensive marker for modules where a feed endpoint would be inappropriate (e.g. admin-only or API-only modules).
func Feed ¶
func Feed(cfg FeedConfig) Option
Feed returns an Option that enables RSS 2.0 feed generation for the module. The feed is served at /{prefix}/feed.xml and regenerated on every publish event. An aggregate feed at /feed.xml merges all Published items from every Feed-enabled module, sorted by publish date descending.
app.Content(&Post{},
forge.At("/posts"),
forge.Feed(forge.FeedConfig{Title: "Blog", Description: "Latest posts"}),
)
func HeadFunc ¶
HeadFunc returns an Option that overrides a content type's Head method at the module level. The function receives the current request context and the content item; its return value takes precedence over the content type's own Head() implementation.
app.Content(&BlogPost{},
forge.At("/posts"),
forge.HeadFunc(func(ctx forge.Context, p *BlogPost) forge.Head {
return forge.Head{Title: p.Title + " — " + ctx.SiteName()}
}),
)
func MCP ¶
func MCP(ops ...MCPOperation) Option
MCP marks a module as an MCP (Model Context Protocol) resource. Pass MCPRead to expose content as resources, MCPWrite to also generate write tools. See MCPModule for the interface implemented by Module.
Example:
app.Content(&BlogPost{},
forge.At("/posts"),
forge.MCP(forge.MCPRead, forge.MCPWrite),
)
func ManifestAuth ¶
ManifestAuth returns an Option that restricts the /.well-known/cookies.json endpoint to requests that pass the given AuthFunc.
A 401 Unauthorized response is returned for unauthenticated requests. Omit ManifestAuth to make the endpoint publicly accessible.
func Middleware ¶
Middleware returns an Option that wraps every route in this module with the provided middleware. Applied in the same order as Chain (index 0 is outermost).
func On ¶
On registers a typed signal handler as a module Option. The handler receives the content value as its concrete type T — no type assertion required at the call site.
Example:
forge.On(forge.BeforeCreate, func(ctx forge.Context, p *Post) error {
p.Author = ctx.User().Name
return nil
})
Example ¶
ExampleOn demonstrates registering a typed signal handler on a content module. The handler fires after a post is published and receives the full forge.Context and the typed item.
repo := NewMemoryRepo[*examplePost]()
m := NewModule(&examplePost{},
At("/posts"),
Repo(repo),
On(AfterPublish, func(_ Context, p *examplePost) error {
_ = p.Title // access typed fields
return nil
}),
)
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.Content(m)
_ = app.Handler()
func Read ¶
Read returns an Option that restricts read (list + show) access to users whose role satisfies the required role. Wired in Step 10 (module.go).
func Redirects ¶
Redirects returns a module Option that registers a 301 prefix redirect from old to to. Use it when renaming a module's URL prefix so all inbound links are preserved automatically:
app.Content(&BlogPost{},
forge.At("/articles"),
forge.Redirects(forge.From("/posts"), "/articles"),
)
func Repo ¶
func Repo[T any](r Repository[T]) Option
Repo returns an Option that provides the Repository for a Module. This is called internally by App.Content. In unit tests pass a MemoryRepo:
forge.Repo(forge.NewMemoryRepo[*Post]())
func Social ¶
func Social(features ...SocialFeature) Option
Social returns an Option that documents which social sharing tag sets a module emits. The forge:head partial always renders Open Graph and Twitter Card tags when [Head.Title] is non-empty — Social() is declarative metadata that makes intent explicit at the call site.
app.Content(&BlogPost{},
forge.At("/posts"),
forge.Social(forge.OpenGraph, forge.TwitterCard),
)
To customise per-item Twitter output, set [Head.Social] on the content type's Head() method:
func (p *BlogPost) Head() forge.Head {
return forge.Head{
// ...
Social: forge.SocialOverrides{
Twitter: forge.TwitterMeta{
Card: forge.SummaryLargeImage,
Creator: "@alice",
},
},
}
}
Example ¶
ExampleSocial demonstrates enabling Open Graph and Twitter Card metadata on a content module. Head fields (Title, Description, Image) are sourced from the content type's Head() method automatically (Amendment A28).
repo := NewMemoryRepo[*examplePost]()
m := NewModule(&examplePost{},
At("/posts"),
Repo(repo),
Social(OpenGraph, TwitterCard),
)
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.Content(m)
_ = app.Handler()
func Templates ¶
Templates returns an Option that sets the directory containing HTML templates for a module. The directory must contain list.html and show.html; if either file is absent App.Run returns an error before the server starts.
Template files are parsed once at startup. The expected layout is:
{dir}/list.html — rendered for GET /{prefix}
{dir}/show.html — rendered for GET /{prefix}/{slug}
{dir}/errors/404.html — (optional) custom error page for 404 responses
Use TemplatesOptional during development when template files are added incrementally.
func TemplatesOptional ¶
TemplatesOptional returns an Option that sets the template directory but treats absent files as a silent no-op. HTML content negotiation is only enabled for a handler when its corresponding template file is found.
Use this during development when templates are added incrementally.
func TrustedProxy ¶
func TrustedProxy() Option
TrustedProxy returns an Option for RateLimit that reads the real client IP from X-Real-IP or X-Forwarded-For headers instead of r.RemoteAddr. Use this when the application runs behind a reverse proxy (nginx, Caddy, load balancer).
type OrganizationDetails ¶
OrganizationDetails carries the extra fields required for Organization rich results.
type OrganizationProvider ¶
type OrganizationProvider interface{ OrganizationDetails() OrganizationDetails }
OrganizationProvider is implemented by content types that supply organization structured data.
type RecipeDetails ¶
RecipeDetails carries the extra fields required for Recipe rich results.
type RecipeProvider ¶
type RecipeProvider interface{ RecipeDetails() RecipeDetails }
RecipeProvider is implemented by content types that supply recipe structured data.
type RedirectCode ¶
type RedirectCode int
RedirectCode is the HTTP status code issued for a redirect entry. Use Permanent (301) for URL changes that search engines should follow and update, and Gone (410) for content that has been intentionally removed. 410 signals de-indexing significantly faster than 404.
const ( // Permanent issues a 301 Moved Permanently response. // Use when the resource has moved to a new URL and the change is final. Permanent RedirectCode = http.StatusMovedPermanently // Gone issues a 410 Gone response. // Use when the resource has been intentionally removed. // Pass an empty string as the destination to [App.Redirect]. Gone RedirectCode = http.StatusGone )
type RedirectEntry ¶
type RedirectEntry struct {
From string // absolute path to match
To string // destination path; empty = 410 Gone
Code RedirectCode // Permanent (301) or Gone (410)
IsPrefix bool // prefix-rewrite semantics (Decision 17 amendment)
}
RedirectEntry describes a single redirect rule. Obtain entries via App.Redirect or the Redirects module option; do not construct them directly in production code unless building a custom migration tool.
- From is the absolute request path that triggers the rule, e.g. "/posts/hello".
- To is the destination path. An empty To with Code == Gone issues 410.
- IsPrefix, when true, matches any path whose prefix equals From and rewrites the suffix onto To at request time — a single entry covers an entire renamed module prefix with zero per-request allocations beyond the destination string concatenation.
type RedirectStore ¶
type RedirectStore struct {
// contains filtered or unexported fields
}
RedirectStore holds the runtime redirect table. Exact lookups are O(1) map reads; prefix lookups iterate a short slice sorted longest-first, ending on the first match. The store is safe for concurrent use.
func NewRedirectStore ¶
func NewRedirectStore() *RedirectStore
NewRedirectStore returns an empty RedirectStore ready for use.
func (*RedirectStore) Add ¶
func (s *RedirectStore) Add(e RedirectEntry)
Add registers e in the store. For exact entries, if e.To is already the From of an existing entry the chain is collapsed (A→B + B→C = A→C). The maximum collapse depth is 10; exceeding it panics with a descriptive message (Decision 24). Gone entries are never collapsed through — a Gone destination is terminal.
For prefix entries (e.IsPrefix == true) the entry is appended to the prefix slice which is then re-sorted descending by len(From) to ensure longest-prefix-first lookup.
func (*RedirectStore) All ¶
func (s *RedirectStore) All() []RedirectEntry
All returns a deterministically sorted slice of all registered entries (exact + prefix), sorted ascending by From. Intended for manifest serialisation.
func (*RedirectStore) Get ¶
func (s *RedirectStore) Get(path string) (RedirectEntry, bool)
Get returns the RedirectEntry matching path, or (RedirectEntry{}, false) when no rule applies. Exact entries are checked first; if no exact match is found the prefix slice is scanned longest-first.
func (*RedirectStore) Len ¶
func (s *RedirectStore) Len() int
Len returns the total number of registered entries (exact + prefix).
func (*RedirectStore) Load ¶
func (s *RedirectStore) Load(ctx context.Context, db DB) error
Load reads all rows from the forge_redirects table and registers them via RedirectStore.Add. Chain collapse and validation rules are applied during load. The forge_redirects table must exist — see the README for the schema.
func (*RedirectStore) Remove ¶
Remove deletes the entry with the given from path from the forge_redirects table. The forge_redirects table must exist — see the README for the schema.
func (*RedirectStore) Save ¶
func (s *RedirectStore) Save(ctx context.Context, db DB, e RedirectEntry) error
Save upserts e into the forge_redirects table. The forge_redirects table must exist — see the README for the schema.
type Registrator ¶
Registrator is implemented by any value that can register its HTTP routes on a http.ServeMux. *Module satisfies this interface automatically.
Pass a pre-built *Module to App.Content to register it:
posts := forge.NewModule[*Post](&Post{}, forge.Repo(repo))
app.Content(posts)
type Repository ¶
type Repository[T any] interface { FindByID(ctx context.Context, id string) (T, error) FindBySlug(ctx context.Context, slug string) (T, error) FindAll(ctx context.Context, opts ListOptions) ([]T, error) Save(ctx context.Context, node T) error Delete(ctx context.Context, id string) error }
Repository is the storage interface for a content type. Implement it to provide a custom storage backend. Use NewMemoryRepo for in-process testing and prototyping.
type ReviewDetails ¶
ReviewDetails carries the extra fields required for Review rich results.
type ReviewProvider ¶
type ReviewProvider interface{ ReviewDetails() ReviewDetails }
ReviewProvider is implemented by content types that supply review structured data.
type RobotsConfig ¶
type RobotsConfig struct {
// Disallow lists URL paths to block for all crawlers (e.g. "/admin").
Disallow []string
// Sitemaps appends a Sitemap directive pointing to <baseURL>/sitemap.xml
// when true. Requires a non-empty baseURL on [App].
Sitemaps bool
// AIScraper sets the AI crawler policy. Defaults to [Allow] when zero.
AIScraper CrawlerPolicy
}
RobotsConfig configures the auto-generated robots.txt. Pass a pointer to App.SEO to register the /robots.txt endpoint:
app.SEO(&forge.RobotsConfig{
AIScraper: forge.AskFirst,
Sitemaps: true,
})
Example ¶
ExampleRobotsConfig demonstrates configuring robots.txt with an explicit disallow list, automatic sitemap inclusion, and an AI crawler policy of AskFirst — which disallows known AI training crawlers by name.
app := New(Config{
BaseURL: "https://example.com",
Secret: []byte("example-secret-key-32-bytes!!!!!"),
})
app.SEO(&RobotsConfig{
Disallow: []string{"/admin"},
Sitemaps: true,
AIScraper: AskFirst,
})
_ = app.Handler()
type Role ¶
type Role string
Role is a named permission level. The four built-in roles cover most applications; custom roles can be registered via NewRole.
Roles are stored as plain strings in tokens and sessions. The numeric level is derived at runtime via a registry lookup, not stored with the role name.
const ( // Guest is the implicit role for unauthenticated requests (level 1). Guest Role = "guest" // Author can create and manage their own content (level 2). Author Role = "author" // Editor can manage all content (level 3). Editor Role = "editor" // Admin has full access including app configuration (level 4). Admin Role = "admin" )
Built-in role constants in ascending permission order.
type SEOOption ¶
type SEOOption interface {
// contains filtered or unexported methods
}
SEOOption is implemented by any value that modifies the app-level SEO configuration. Pass SEOOption values to App.SEO:
app.SEO(&forge.RobotsConfig{AIScraper: forge.AskFirst, Sitemaps: true})
type SQLRepo ¶
type SQLRepo[T any] struct { // contains filtered or unexported fields }
SQLRepo is a production Repository[T] backed by forge.DB. T must embed forge.Node — its fields are mapped to SQL columns via `db` struct tags, falling back to lowercase field names (the same rules as Query and QueryOne).
All queries use $N positional placeholders (PostgreSQL / pgx compatible). Use the Table option to override the automatically derived table name.
func NewSQLRepo ¶
func NewSQLRepo[T any](db DB, opts ...SQLRepoOption) *SQLRepo[T]
NewSQLRepo returns a SQLRepo[T] ready for use. The table name is derived automatically from T (e.g. BlogPost → "blog_posts"); pass Table to override.
T must be a pointer type and must match the proto passed to NewModule:
repo := forge.NewSQLRepo[*Post](db) m := forge.NewModule((*Post)(nil), forge.Repo(repo))
Using a value type (NewSQLRepo[Post]) will compile but will not satisfy Repository[*Post] — the type parameters must match throughout.
func (*SQLRepo[T]) Delete ¶
Delete removes the item with the given id. Returns ErrNotFound if no row was deleted.
func (*SQLRepo[T]) FindAll ¶
func (r *SQLRepo[T]) FindAll(ctx context.Context, opts ListOptions) ([]T, error)
FindAll returns items matching opts. Status filter, ordering, and pagination are translated to SQL WHERE / ORDER BY / LIMIT OFFSET clauses.
func (*SQLRepo[T]) FindByID ¶
FindByID returns the item with the given id, or ErrNotFound.
func (*SQLRepo[T]) FindBySlug ¶
FindBySlug returns the item with the given slug, or ErrNotFound.
type SQLRepoOption ¶
type SQLRepoOption interface {
// contains filtered or unexported methods
}
SQLRepoOption configures a SQLRepo. Obtain values via Table.
func Table ¶
func Table(name string) SQLRepoOption
Table returns a SQLRepoOption that overrides the automatically derived table name for a SQLRepo. Use it when the default snake_case plural derivation does not produce the correct name.
repo := forge.NewSQLRepo[BlogPost](db, forge.Table("posts"))
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler drives the Scheduled→Published transition for all content modules registered with App.Content. A single background goroutine runs with an adaptive timer: after each tick the timer is reset to fire at the soonest remaining ScheduledAt across all modules, falling back to 60 seconds when no scheduled items exist.
The Scheduler is created and started by App.Run and stopped as part of graceful shutdown. Applications do not create Schedulers directly.
func (*Scheduler) Start ¶
Start spawns the scheduler goroutine. The goroutine exits when ctx is cancelled. Call Scheduler.Wait after cancellation to block until the goroutine has fully exited.
func (*Scheduler) Wait ¶
func (s *Scheduler) Wait()
Wait blocks until the goroutine started by Scheduler.Start has exited. It should be called after cancelling the context passed to Start to ensure clean shutdown.
type ScriptTag ¶ added in v1.3.0
type ScriptTag struct {
Src string // external script URL; empty means inline
Body template.JS // inline JavaScript body; used when Src is empty
Async bool // adds async attribute (external scripts only)
Defer bool // adds defer attribute (external scripts only)
}
ScriptTag declares a single <script> element. Src loads an external script; Body inlines a JavaScript body when Src is empty. Body is typed as html/template.JS — convert a string literal with template.JS("…") to mark it as safe for emission inside a <script> block; never use this with user-supplied content. Async and Defer are only emitted for external scripts (Src non-empty).
type Signal ¶
type Signal string
Signal identifies a lifecycle event fired by a content module. Handlers are registered with On and receive the content value as their concrete type T — no type assertion required.
const ( // BeforeCreate fires before a new content item is persisted. // Return an error to abort the operation. BeforeCreate Signal = "before_create" // AfterCreate fires after a new content item has been persisted. // Runs asynchronously — errors and panics are logged, never returned. AfterCreate Signal = "after_create" // BeforeUpdate fires before an existing content item is updated. // Return an error to abort the operation. BeforeUpdate Signal = "before_update" // AfterUpdate fires after a content item has been updated. // Runs asynchronously — errors and panics are logged, never returned. AfterUpdate Signal = "after_update" // BeforeDelete fires before a content item is deleted. // Return an error to abort the operation. BeforeDelete Signal = "before_delete" // AfterDelete fires after a content item has been deleted. // Runs asynchronously — errors and panics are logged, never returned. AfterDelete Signal = "after_delete" // AfterPublish fires after a content item transitions to Published. // Runs asynchronously — triggers sitemap and feed regeneration. AfterPublish Signal = "after_publish" // AfterUnpublish fires after a content item is moved out of Published status. // Runs asynchronously — triggers sitemap and feed regeneration. AfterUnpublish Signal = "after_unpublish" // AfterArchive fires after a content item transitions to Archived. // Runs asynchronously — triggers sitemap and feed regeneration. AfterArchive Signal = "after_archive" // SitemapRegenerate is fired internally after AfterPublish, AfterUnpublish, // AfterArchive, and AfterDelete. It is debounced to coalesce burst changes // into a single sitemap and feed rebuild. SitemapRegenerate Signal = "sitemap_regenerate" )
Lifecycle signals fired by content modules.
type SitemapConfig ¶
type SitemapConfig struct {
// ChangeFreq is the expected update frequency for URLs in this module.
// Defaults to [Weekly] when empty.
ChangeFreq ChangeFreq
// Priority is the relative importance of URLs in this module, in the range
// 0.0–1.0. Defaults to 0.5 when zero or negative.
Priority float64
}
SitemapConfig configures the per-module sitemap fragment. Pass it to App.Content as an option alongside At, Cache, and similar options.
app.Content(posts, forge.SitemapConfig{ChangeFreq: forge.Weekly, Priority: 0.8})
ChangeFreq defaults to Weekly when zero. Priority defaults to 0.5 when zero or negative.
type SitemapEntry ¶
type SitemapEntry struct {
// Loc is the canonical URL of the page.
Loc string
// LastMod is the date-time the content was last modified. It is formatted
// as a date-only string (YYYY-MM-DD) in the output. Zero value is omitted.
LastMod time.Time
// ChangeFreq is the expected update frequency. Defaults to [Weekly].
ChangeFreq ChangeFreq
// Priority is the relative importance, 0.0–1.0. Defaults to 0.5.
Priority float64
}
SitemapEntry is a single URL entry in a sitemap fragment. A zero LastMod is omitted from the XML output.
func SitemapEntries ¶
func SitemapEntries[T SitemapNode](items []T, baseURL string, cfg SitemapConfig) []SitemapEntry
SitemapEntries builds a slice of SitemapEntry values from items, applying the rules in cfg. Only Published items are included.
Loc is taken from [Head.Canonical]; if empty it falls back to strings.TrimRight(baseURL, "/") + "/" + item.GetSlug().
ChangeFreq defaults to Weekly when cfg.ChangeFreq is empty. Priority is taken from SitemapPrioritiser if implemented, then from cfg.Priority if positive, otherwise defaults to 0.5.
type SitemapNode ¶
type SitemapNode interface {
Headable
GetSlug() string
GetPublishedAt() time.Time
GetStatus() Status
}
SitemapNode is the type constraint for SitemapEntries. It is satisfied by any pointer to a struct that embeds Node and implements Headable. All Forge content types that embed Node satisfy this constraint automatically after Amendment A2.
type SitemapPrioritiser ¶
type SitemapPrioritiser interface {
SitemapPriority() float64
}
SitemapPrioritiser may be implemented by content types to provide a per-item priority override in the sitemap. When not implemented, [SitemapConfig.Priority] is used (defaulting to 0.5).
type SitemapStore ¶
type SitemapStore struct {
// contains filtered or unexported fields
}
SitemapStore holds the latest generated sitemap fragments in memory. Forge populates it automatically via the debouncer on every publish/unpublish event. It is safe for concurrent use by multiple goroutines.
func NewSitemapStore ¶
func NewSitemapStore() *SitemapStore
NewSitemapStore returns an initialised, empty SitemapStore.
func (*SitemapStore) Get ¶
func (s *SitemapStore) Get(path string) ([]byte, bool)
Get returns the stored bytes for path and whether the path exists.
func (*SitemapStore) Handler ¶
func (s *SitemapStore) Handler() http.Handler
Handler returns an http.Handler that serves stored fragment bytes by request path. Responds with 404 when the path has no stored fragment. Content-Type is set to application/xml; charset=utf-8.
func (*SitemapStore) IndexHandler ¶
func (s *SitemapStore) IndexHandler(baseURL string) http.Handler
IndexHandler returns an http.Handler that generates the sitemap index on each request from all currently stored fragment paths. baseURL is prepended to each path to form the full fragment URL (e.g. "https://example.com/posts/sitemap.xml").
func (*SitemapStore) Paths ¶
func (s *SitemapStore) Paths() []string
Paths returns a sorted slice of all stored fragment paths. Used by SitemapStore.IndexHandler to enumerate fragments when building the index.
func (*SitemapStore) Set ¶
func (s *SitemapStore) Set(path string, data []byte)
Set stores a copy of data keyed by path (e.g. "/posts/sitemap.xml"). Subsequent calls replace the previous value for the same path.
type SocialFeature ¶
type SocialFeature int
SocialFeature selects which social sharing meta tags forge:head emits for a module. Use the predefined constants OpenGraph and TwitterCard.
const ( // OpenGraph enables Open Graph meta tags (og:title, og:description, // og:image, og:type, og:url, and article:* for Article content). OpenGraph SocialFeature = 1 // TwitterCard enables Twitter Card meta tags (twitter:card, twitter:title, // twitter:description, twitter:image, twitter:creator). TwitterCard SocialFeature = 2 )
type SocialOverrides ¶
type SocialOverrides struct {
Twitter TwitterMeta // Twitter Card overrides for this item
}
SocialOverrides carries per-item social sharing overrides. Set on [Head.Social] to customise Open Graph and Twitter Card output.
type Status ¶
type Status string
Status is the content lifecycle state. All content types embed Node and therefore always carry a Status. Forge enforces lifecycle rules on all public endpoints — non-Published content is never publicly visible.
const ( // Draft is the default state for newly created content. Not publicly visible. Draft Status = "draft" // Published content is publicly visible and included in sitemaps, feeds, // and AI indexes. Published Status = "published" // Scheduled content will be automatically transitioned to Published at // [Node.ScheduledAt]. Not publicly visible until the transition fires. Scheduled Status = "scheduled" // Archived content has been retired. Not publicly visible. Does not appear // in sitemaps or feeds. Returns 410 Gone from public endpoints. Archived Status = "archived" )
type TemplateData ¶
type TemplateData[T any] struct { // Content is the page payload — a single item for show templates, // a slice for list templates. Content T // Head carries all SEO and social metadata for this page, merged from // the content type's Head() method and any module-level HeadFunc. Head Head // User is the authenticated user for this request. Zero value ([GuestUser]) // when the request is unauthenticated. User User // Request is the live *http.Request for this response. Use it in // templates for URL introspection, query parameters, or helpers that // require the request (e.g. [forge_csrf_token]). Request *http.Request // SiteName is the hostname extracted from [Config.BaseURL] at module // registration time (e.g. "example.com"). Uses the hostname rather than // [Context.SiteName] because SiteName() always returns "" in v1. SiteName string // OGDefaults holds the app-level Open Graph and Twitter Card fallback // values set via [App.SEO]. forge:head uses OGDefaults.TwitterSite to emit // twitter:site on every page; image and creator fallbacks are already merged // into [Head] before TemplateData is constructed. OGDefaults *OGDefaults // AppSchema is a pre-rendered <script type="application/ld+json"> block for // the app-level structured data set via [App.SEO] with [AppSchema]. It is // emitted automatically by the forge:head partial on every page. // The value is safe HTML produced by [renderAppSchema]; it is empty when // no [AppSchema] was registered. AppSchema template.HTML // HeadAssets holds the app-level static assets (preconnect hints, // stylesheets, favicons, and scripts) set via [App.SEO] with [HeadAssets]. // forge:head emits them on every page in order: preconnect → stylesheets → // favicons → scripts. Nil when no [HeadAssets] was registered. HeadAssets *HeadAssets }
TemplateData is the value passed to every HTML template rendered by Forge. T is the content type for show handlers (e.g. *BlogPost) or a slice type for list handlers (e.g. []*BlogPost).
Show handler:
TemplateData[*BlogPost]{
Content: post,
Head: post.Head(), // merged with module HeadFunc when set
User: ctx.User(),
Request: r,
SiteName: "example.com",
}
List handler:
TemplateData[[]*BlogPost]{
Content: posts,
Head: forge.Head{Title: "All Posts"},
User: ctx.User(),
Request: r,
SiteName: "example.com",
}
In templates:
{{template "forge:head" .Head}}
<h1>{{.Content.Title}}</h1>
<p>Welcome, {{.User.Name}}</p>
func NewTemplateData ¶
func NewTemplateData[T any](ctx Context, content T, head Head, siteName string) TemplateData[T]
NewTemplateData constructs a TemplateData[T] for the given context, content, merged head, and site name.
siteName should be the hostname extracted from [Config.BaseURL] (e.g. "example.com"), set once at module registration.
type TwitterCardType ¶
type TwitterCardType string
TwitterCardType is the value of the twitter:card meta property. Use the predefined constants Summary, SummaryLargeImage, AppCard, PlayerCard.
const ( Summary TwitterCardType = "summary" // small card with title and description SummaryLargeImage TwitterCardType = "summary_large_image" // large image above the title AppCard TwitterCardType = "app" // deep-link to a mobile app PlayerCard TwitterCardType = "player" // inline video or audio player )
type TwitterMeta ¶
type TwitterMeta struct {
Card TwitterCardType // overrides the default card type; empty uses a sensible default
Creator string // @handle of the content author; populates twitter:creator
}
TwitterMeta carries per-item Twitter Card overrides. Set on [Head.Social] to customise Twitter Card output for a specific content item.
type User ¶
type User struct {
// ID is the user's stable UUID. Empty for unauthenticated guests.
ID string
// Name is the display name. Empty for unauthenticated guests.
Name string
// Roles is the set of roles held by this user. Forge's hierarchical
// permission checks ([HasRole], [IsRole]) operate on this slice.
Roles []Role
}
User represents an authenticated identity. The zero value is an unauthenticated guest — equivalent to GuestUser. See Amendment R3.
The User type is declared here (context.go) rather than auth.go because [Context.User] returns it and context.go is in a lower dependency layer than auth.go. auth.go adds authentication machinery on top of this type.
func VerifyBearerToken ¶ added in v1.1.0
VerifyBearerToken extracts and verifies the HMAC-signed bearer token from r's Authorization header. It returns the authenticated User and true on success, or GuestUser and false if the header is absent, malformed, or the signature is invalid. secret must be the same value used to sign the token with SignToken. This is the public counterpart to the unexported authenticate method on BearerHMAC and is intended for use outside the forge package (e.g. forge-mcp SSE transport) where AuthFunc is not directly callable.
type Validatable ¶
type Validatable interface {
Validate() error
}
Validatable is implemented by content types that have business-rule validation beyond struct-tag constraints. RunValidation calls Validate() after tag validation passes — if tags fail, Validate() is not called.
func (p *BlogPost) Validate() error {
if p.Status == forge.Published && len(p.Tags) == 0 {
return forge.Err("tags", "required when publishing")
}
return nil
}
type ValidationError ¶
type ValidationError struct {
// contains filtered or unexported fields
}
ValidationError is returned when one or more fields fail validation. It implements forge.Error with HTTP status 422.
Create with Err for a single field, or Require to collect several.
func Err ¶
func Err(field, message string) *ValidationError
Err returns a ValidationError for a single field. The returned error implements forge.Error and will produce a 422 response with field details.
return forge.Err("title", "required")
func (*ValidationError) Code ¶
func (e *ValidationError) Code() string
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error returns a human-readable summary of all validation failures.
func (*ValidationError) HTTPStatus ¶
func (e *ValidationError) HTTPStatus() int
func (*ValidationError) Public ¶
func (e *ValidationError) Public() string