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 Prefix(languages []string, language string) string
- func Slugify(title string) string
- func ValidSlug(s string) bool
- type AliasKey
- type AliasLookup
- type AliasMatch
- type AliasResult
- type AliasResults
- type Code
- type Decision
- type Entry
- type Link
- type Options
- type Parsed
- type Resolved
- type Router
- 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) Resolve(ctx context.Context, cs []Code) (map[Code]Link, error)
- func (s *Store) ResolveAliases(ctx context.Context, keys []AliasKey) ([]*AliasMatch, error)
- func (s *Store) Tenant() string
- type Visibility
Constants ¶
const ( AliasFound = "found" AliasNotFound = "not_found" // unknown, or hidden from the request's visitor AliasGone = "gone" // its content was removed )
Alias statuses.
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.
const MaxAliases = 100
MaxAliases bounds an AliasLookup.
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.
func Prefix ¶ added in v0.84.0
Prefix is the path prefix of pages in language on a site of languages, its default first: none for the default language, which pages serve unprefixed, and the language for the others.
Types ¶
type AliasKey ¶ added in v0.83.0
type AliasKey struct {
Source string `json:"source"`
Kind string `json:"kind"`
Key string `json:"key"`
}
AliasKey names another system's identifier: the system (an import batch's source), the identifier's space and the identifier.
type AliasLookup ¶ added in v0.83.0
type AliasLookup struct {
Aliases []AliasKey `json:"aliases"`
}
AliasLookup is legacy identifiers to resolve, at most MaxAliases.
type AliasMatch ¶
AliasMatch is what an alias resolves to.
type AliasResult ¶ added in v0.83.0
type AliasResult struct {
AliasKey
// Status is found, not_found or gone.
Status string `json:"status"`
// Resolved is a found alias's link and canonical path.
Resolved *Resolved `json:"resolved,omitempty"`
// Locator is a found alias's position inside its content, such as a page.
Locator string `json:"locator,omitempty"`
}
AliasResult is what one alias resolves to.
type AliasResults ¶ added in v0.83.0
type AliasResults struct {
Aliases []AliasResult `json:"aliases"`
}
AliasResults answers an AliasLookup in its order.
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
}
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) 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. POST /aliases resolves legacy identifiers the same way, in batches. The engine serves them as the codes route group (/v1/codes).
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 site's languages, its default first: the segments a
// path may start with (/{lang}/{route}/...). A request's prefix is kept
// in its canonical path and picks the localized slug; Path and the codes
// route leave the default language unprefixed (Prefix).
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 each resolved link, in order
// (drafts, removed content, viewer restrictions). nil: every registered
// code is Visible.
Visibility func(r *http.Request, links []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; it is idempotent. The result is aligned with entries.
func (*Store) Resolve ¶
Resolve returns the link each code names, following merges to the survivor; a code the tenant does not have is absent. A code not in canonical form is ErrInvalidCode.
func (*Store) ResolveAliases ¶ added in v0.83.0
ResolveAliases returns what each alias names, following merges, in keys' order: nil for one the tenant does not have.
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 )