Documentation
¶
Overview ¶
Package contenturl gives content uniform URLs, [/{lang}]/{route}/{CODE}[/{slug}]: a short random code ContentKit assigns once per content record (unique per tenant across every kind), a decorative slug, and the host's route for the content kind. The server reads only the code; any other spelling of the path redirects to the canonical one.
Store registers codes and resolves them (and legacy aliases); Router maps content kinds to the host's routes, decides canonical redirects and serves a JSON lookup. Taxonomy nodes and posts are registered by ContentKit itself; hosts Put their own content.
Index ¶
- Constants
- Variables
- func Canonical(routes Routes, language string, link Link) (string, bool)
- func Path(language, route string, code Code, slug string) string
- func Slugify(title string) string
- func ValidSlug(s string) bool
- type Alias
- type AliasMatch
- type Code
- type Decision
- type Entry
- type Link
- type Options
- type Parsed
- type Resolved
- type Router
- func (r *Router) Decide(req *http.Request) (Decision, error)
- func (r *Router) DecideAlias(req *http.Request, source, legacyKind, key, language string) (Decision, error)
- func (r *Router) Handler() http.Handler
- func (r *Router) Middleware(next http.Handler) http.Handler
- func (r *Router) Path(link Link, language string) (string, bool)
- func (r *Router) Tenant() string
- type RouterOptions
- type Routes
- type Store
- func (s *Store) Links(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey]Link, error)
- func (s *Store) Merge(ctx context.Context, from, into contentref.ContentRef) error
- func (s *Store) Put(ctx context.Context, entries ...Entry) ([]Link, error)
- func (s *Store) PutAliases(ctx context.Context, aliases ...Alias) error
- func (s *Store) Resolve(ctx context.Context, code Code) (Link, error)
- func (s *Store) ResolveAlias(ctx context.Context, source, legacyKind, key string) (AliasMatch, error)
- func (s *Store) Tenant() string
- func (s *Store) WithSQLTx(tx *sql.Tx) *Store
- func (s *Store) WithTx(tx pgx.Tx) *Store
- type Visibility
Constants ¶
const Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
Alphabet is Crockford base32: digits and uppercase letters without I, L, O, U.
const CodeLength = 9
CodeLength is the length of a content code.
Variables ¶
var ( ErrInvalid = codes.ErrInvalid ErrNotFound = codes.ErrNotFound ErrConflict = codes.ErrConflict ErrInvalidCode = fmt.Errorf("%w: not a content code", codes.ErrInvalid) )
Errors; match with errors.Is.
Functions ¶
func Canonical ¶
Canonical returns link's canonical path under language ("" unprefixed), and false when link's kind has no route.
Types ¶
type Alias ¶
type Alias struct {
Source string
LegacyKind string
Key string
Locator string
contentref.ContentRef
}
Alias is an identifier of another system that resolves to content: a legacy site's id, token or name. Source names the system ("doujins-legacy"), LegacyKind the identifier space ("folder", "tag-name", "video"), Key the identifier as the host normalizes it (stored and matched verbatim). Locator is an optional host position inside the content, such as a page. Many aliases may name one content record.
type AliasMatch ¶
AliasMatch is what an alias resolves to.
type Code ¶
type Code string
Code is a content code in canonical form: nine characters of Alphabet, at least one a letter, so no code reads as a numeric id.
type Decision ¶
type Decision struct {
// Matched: the request names visible, routed content. When neither
// Matched nor Gone, the host serves the request as it would without the
// router (usually its 404).
Matched bool
// Gone: the request names routed content the host removed (410).
Gone bool
// Redirect: the path is not canonical; answer 301 to Location.
Redirect bool
Link Link
Language string
Path string // the canonical path
Location string // Path plus the request's query
Locator string // DecideAlias: the alias's position inside the content
}
Decision is the canonical verdict on a request.
type Entry ¶
type Entry struct {
contentref.ContentRef
Title string
Titles map[string]string
}
Entry is one content record to register. Title (any text) becomes the default slug through Slugify; Titles, keyed by language, the localized slugs. Titles nil keeps the stored localized slugs; an empty map clears them.
type Link ¶
type Link struct {
contentref.ContentRef
Code Code `json:"code"`
Slug string `json:"slug"`
Slugs map[string]string `json:"slugs,omitempty"`
}
Link is what a code resolves to: the content, its canonical code and slugs. After a merge the content and code are the surviving record's.
type Options ¶
type Options struct {
Pool *pgxpool.Pool
Schema string // the schema ContentKit's migrations were applied in
Tenant string
}
Options configures one tenant's Store.
type Parsed ¶
type Parsed struct {
Language string // "" when unprefixed
Route string
Code Code // canonical form
RawCode string // as written
Slug string // as written; "" when absent
}
Parsed is a content path split into its parts.
type Resolved ¶
Resolved is Handler's response: the link and its canonical path ("" when the kind has no route).
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router maps content kinds to the host's routes over a Store.
func NewRouter ¶
func NewRouter(store *Store, opts RouterOptions) (*Router, error)
NewRouter validates the options and returns the router.
func (*Router) Decide ¶
Decide parses the request path, resolves its code and compares the path with the canonical one. A wrong route, a non-canonical code spelling, a merged code, a missing or stale slug and a trailing slash all redirect.
func (*Router) DecideAlias ¶
func (r *Router) DecideAlias(req *http.Request, source, legacyKind, key, language string) (Decision, error)
DecideAlias resolves a legacy identifier for a host's legacy redirect: Matched with the canonical Path (Redirect is always true), Gone, or neither (unknown or hidden: 404). language picks the prefix and slug.
func (*Router) Handler ¶
Handler serves GET /{code}[?lang=xx]: the code's link and canonical path, for clients that hold only a code. Any Crockford spelling resolves; the response carries the canonical code. Unknown and hidden codes are 404 not_found, Gone content 410 gone, malformed codes 400 invalid_request. contentkit.Runtime.Handler serves it at /codes; mounted alone it goes under a prefix with http.StripPrefix.
func (*Router) Middleware ¶
Middleware canonicalizes content page requests (GET and HEAD): a non-canonical path is answered 301 with the canonical path and the query unchanged, Gone content with RouterOptions.Gone, and a canonical path reaches next with the Decision in its context (FromContext) and, when BaseURL is set, a canonical Link header. Other requests pass through.
type RouterOptions ¶
type RouterOptions struct {
// Routes maps content kinds to the host's routes. Required.
Routes Routes
// Languages are the language segments a path may start with
// (/{lang}/{route}/...). The prefix is kept in canonical paths and picks
// the localized slug.
Languages []string
// BaseURL ("https://example.com") makes Middleware send the canonical URL
// as a `Link: <url>; rel="canonical"` header. Empty: no header.
BaseURL string
// Visibility is the host's verdict on resolved content (drafts, removed
// content, viewer restrictions). nil: every registered code is Visible.
Visibility func(r *http.Request, link Link) (Visibility, error)
// Gone answers requests for Gone content. nil: a plain 410.
Gone http.Handler
// Logger receives resolution failures (5xx). nil: slog.Default().
Logger *slog.Logger
}
RouterOptions configures a Router.
type Routes ¶
Routes maps a content kind to the host's route: the first path segment of its pages ("video" -> "watch", "gallery" -> "g"). Several kinds may share a route; a kind without one has no URL.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is one tenant's content-code registry.
func (*Store) Links ¶
func (s *Store) Links(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey]Link, error)
Links returns the link of each registered reference, keyed by ref.Key(); unregistered references are absent. Use it to render lists.
func (*Store) Merge ¶
func (s *Store) Merge(ctx context.Context, from, into contentref.ContentRef) error
Merge makes from's code redirect to into's: both stay valid, from's resolves to into. Use it when duplicate content is folded into one record. Both must be registered (ErrNotFound); a merge back into from is ErrConflict.
func (*Store) Put ¶
Put gives every entry a code if it has none (codes never change) and sets its slugs from the titles. Call it in the transaction that creates the content and again when a title changes; it is idempotent, so a backfill calls it over existing rows. The result is aligned with entries.
func (*Store) PutAliases ¶
PutAliases records aliases, typically in the import transaction that writes their content. An alias is written once: repeating it is a no-op, pointing it at other content (or another locator) is ErrConflict and writes nothing. The content must be registered (ErrNotFound). Roll the transaction back on any error.
func (*Store) Resolve ¶
Resolve returns the link a code names, following a merge to the survivor. ErrNotFound when the tenant has no such code.
func (*Store) ResolveAlias ¶
func (s *Store) ResolveAlias(ctx context.Context, source, legacyKind, key string) (AliasMatch, error)
ResolveAlias returns what an alias names, following merges.
type Visibility ¶
type Visibility int
Visibility is the host's verdict on resolved content for one request. The zero value is Hidden, so an unset verdict fails closed.
const ( // Hidden content answers like an unknown code (the host's 404): no // redirect, so nothing about it (its slug) leaks. Hidden Visibility = iota // Visible content is served and canonicalized. Visible // Gone content was removed for good: 410, no redirect. Gone )