contenturl

package
v0.70.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 16 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 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.

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

type AliasMatch struct {
	Link
	Locator string
}

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.

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
	Locator  string // DecideAlias: the alias's position inside the content
}

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) 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

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. contentkit.Runtime.Handler serves it at /codes; mounted alone it goes under a prefix with http.StripPrefix.

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 under language ("" unprefixed); 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 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

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. 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

func (s *Store) PutAliases(ctx context.Context, aliases ...Alias) error

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

func (s *Store) Resolve(ctx context.Context, code Code) (Link, error)

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.

func (*Store) Tenant

func (s *Store) Tenant() string

Tenant is the tenant the store is pinned to.

func (*Store) WithSQLTx

func (s *Store) WithSQLTx(tx *sql.Tx) *Store

WithSQLTx returns a store that runs in a database/sql transaction opened on the pgx stdlib driver; the caller commits.

func (*Store) WithTx

func (s *Store) WithTx(tx pgx.Tx) *Store

WithTx returns a store that runs in tx; the caller commits.

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