Documentation
¶
Overview ¶
Package contenturl gives content uniform URLs, /{lang}/{route}/{CODE}[/{slug}]: the page's language (every page has one, the default included), 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. ?v= selects a version (VersionParam).
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 AcceptLanguage(languages []string, header string) string
- func Canonical(routes Routes, languages []string, language string, link Link) (string, bool)
- func Path(languages []string, 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
- func Versioned(path string, code VersionCode) string
- 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
- func (s *Store) Versions(ctx context.Context, keys []VersionKey) (map[VersionKey]string, error)
- type VersionCode
- type VersionKey
- 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.
const VersionCodeLength = 4
VersionCodeLength is the length of a version code.
const VersionParam = "v"
VersionParam is the query parameter that selects a version on its content's page: /{lang}/{route}/{CODE}/{slug}?v={version code}. A page without it, or whose code names no live version, shows the language's default edition.
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.
var ErrInvalidVersionCode = fmt.Errorf("%w: not a version code", codes.ErrInvalid)
ErrInvalidVersionCode refuses what is not a version code.
Functions ¶
func AcceptLanguage ¶ added in v0.86.0
AcceptLanguage is the first of languages an Accept-Language header names, as a tag or a tag's primary subtag; "" when it names none.
func Canonical ¶
Canonical returns link's canonical path in language (the default when ""), with its slug there; false when link's kind has no route.
func Path ¶
Path is a content page's path, /{language}/{route}/{code}[/{slug}], under Prefix(languages, language); slug is the caller's for that language. Every path ContentKit answers is built here.
func Prefix ¶ added in v0.84.0
Prefix is the language of a page asked for in language on a site of languages, its default first: language, else the default. Every content page is under its language; contentkit-ui's languagePrefix mirrors this.
func Slugify ¶
Slugify turns a title into a URL slug: ASCII [a-z0-9] words joined by hyphens, diacritics dropped, at most 80 bytes cut at a word boundary. Text without Latin letters or digits gives "" (the URL then has no slug).
func Versioned ¶ added in v0.86.0
func Versioned(path string, code VersionCode) string
Versioned is path with ?v=code selecting a version; "" leaves path as is.
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 (an old link: it redirects)
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 ¶
type Resolved struct {
Link
Path string `json:"path"`
// VersionID is the live version ?v= names; "" when it names none, and the
// page shows its language's default edition.
VersionID string `json:"version_id,omitempty"`
}
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 missing language prefix, a wrong route, a non-canonical code spelling, a merged code, a missing or stale slug and a trailing slash all redirect; the query (?v= included) is kept.
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. Required: every
// content page is under one (/{lang}/{route}/...), which picks its slug;
// a request without one redirects to the language Accept-Language names,
// else the default.
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.
func (*Store) Versions ¶ added in v0.86.0
func (s *Store) Versions(ctx context.Context, keys []VersionKey) (map[VersionKey]string, error)
Versions answers the id of each key's version while it is live (published, past its publication time, its content live), as readers open it; keys naming none are left out.
type VersionCode ¶ added in v0.86.0
type VersionCode string
VersionCode is a version's code in canonical form: four characters of Alphabet, at least one a letter, unique within its content and fixed for the version's life.
func ParseVersionCode ¶ added in v0.86.0
func ParseVersionCode(s string) (VersionCode, error)
ParseVersionCode reads a version code with ParseCode's rules.
func (VersionCode) String ¶ added in v0.86.0
func (c VersionCode) String() string
type VersionKey ¶ added in v0.86.0
type VersionKey struct {
ContentID string
Code VersionCode
}
VersionKey names a version by its content and version code.
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 )