contenturl

package
v0.85.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
const (
	AliasFound    = "found"
	AliasNotFound = "not_found" // unknown, or hidden from the request's visitor
	AliasGone     = "gone"      // its content was removed
)

Alias statuses.

View Source
const Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

Alphabet is Crockford base32: digits and uppercase letters without I, L, O, U.

View Source
const CodeLength = 9

CodeLength is the length of a content code.

View Source
const MaxAliases = 100

MaxAliases bounds an AliasLookup.

Variables

View Source
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

func Canonical(routes Routes, language string, link Link) (string, bool)

Canonical returns link's canonical path under language ("" unprefixed), and false when link's kind has no route.

func Path

func Path(language, route string, code Code, slug string) string

Path joins [/{language}]/{route}/{code}[/{slug}].

func Prefix added in v0.84.0

func Prefix(languages []string, language string) string

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.

func Slugify

func Slugify(title string) string

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 ValidSlug

func ValidSlug(s string) bool

ValidSlug reports whether s is a slug Slugify can produce ("" included).

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

type AliasMatch struct {
	Link
	Locator string
}

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.

func ParseCode

func ParseCode(s string) (Code, error)

ParseCode reads a code with Crockford's decoding rules: any letter case, O as 0, I and L as 1, hyphens ignored. U and every other character are refused, as are any length but nine and an all-digit result (a legacy numeric id such as /watch/346791971 is never a code).

func (Code) String

func (c Code) String() string

func (Code) Valid

func (c Code) Valid() bool

Valid reports whether c is in canonical form.

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.

func FromContext

func FromContext(ctx context.Context) (Decision, bool)

FromContext returns the decision Middleware matched for this 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 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.

func (Link) SlugFor

func (l Link) SlugFor(language string) string

SlugFor returns the slug for language, falling back to the default slug.

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.

func ParsePath

func ParsePath(path string, routes Routes, languages []string) (Parsed, bool)

ParsePath splits [/{language}]/{route}/{code}[/{slug}][/] where route is one of routes' values and language one of languages. ok is false for any other path, including an invalid code.

type Resolved

type Resolved struct {
	Link
	Path string `json:"path"`
}

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

func (r *Router) Decide(req *http.Request) (Decision, error)

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

func (r *Router) Handler() http.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

func (r *Router) Middleware(next http.Handler) http.Handler

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.

func (*Router) Path

func (r *Router) Path(link Link, language string) (string, bool)

Path returns link's canonical path in language: its slug there, under Prefix(language); false when its kind has no route.

func (*Router) Tenant added in v0.68.0

func (r *Router) Tenant() string

Tenant is the tenant whose codes the router resolves.

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

type Routes map[string]string

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.

func (Routes) Validate

func (r Routes) Validate(languages []string) error

Validate checks every route and language is one lowercase path segment and that no language is also a route.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is one tenant's content-code registry.

func New

func New(opts Options) (*Store, error)

New validates the options and returns the store.

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

func (s *Store) Put(ctx context.Context, entries ...Entry) ([]Link, error)

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

func (s *Store) Resolve(ctx context.Context, cs []Code) (map[Code]Link, error)

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

func (s *Store) ResolveAliases(ctx context.Context, keys []AliasKey) ([]*AliasMatch, error)

ResolveAliases returns what each alias names, following merges, in keys' order: nil for one the tenant does not have.

func (*Store) Tenant

func (s *Store) Tenant() string

Tenant is the tenant the store is pinned to.

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
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL