Help us shape the future of Go! Take the 2026 Go Developer Survey

redirects

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 24 Imported by: 0

README

Redirects

Sends visitors from old addresses to new ones, counts the hits of every redirect and lists the paths visitors asked for that do not exist.

It is a site feature: one instance serves the whole site, for example after moving an existing website to WebThings. It has no templates or template functions.

Add it to the site

app.Use(redirects.Module())

The instance gets the key redirects and the title Redirects, translated in the admin; its section is at /admin/redirects (/admin is the default admin path). Named chooses another key and title:

app.Use(redirects.Module(redirects.Named("old", "Old addresses")))

In the admin

The list shows every redirect with its old path, new address, status, hits and last hit, in the site's timezone. Redirects again marks a redirect whose new address redirects once more.

  • New redirect asks for the old path, the new address and the kind of redirect: 301 Moved permanently (the default), 302 Found (temporary), 307 Temporary redirect or 308 Permanent redirect. The new address is a path on this site, such as /en/news, or a full URL. A redirect that would lead back to its own path, directly or through other redirects, is refused. The edit screen warns when the new address redirects again, and when other redirects now take two steps to arrive.
  • Move to trash, on a redirect's edit screen, moves it to the trash: it stops redirecting at once and its old path is free for a new redirect. Restoring it is refused when another redirect uses that path by then, or when it would lead back to its own path.
  • Test a path shows where a path leads, step by step, or that it belongs to the website itself.
  • Missed paths lists the paths that ended in "not found", most requested first. Add redirect opens a new redirect with the old path filled in; a path leaves the list once it has a redirect. Clear list empties it.
  • Import takes CSV lines pasted into a text box: the old path, the new address and, optionally, the status (301 when empty).
from,to,status
/about-us.html,/en/about,301
/blog/2019/summer-party,/en/news/summer-party
https://old.example.com/contact.php,/en/contact,302

A first line whose first field is from is skipped. The fields are separated by commas, semicolons or tabs, whichever the first line uses most, and a byte order mark at the start, as spreadsheet programs write, is ignored. A line whose old path already has a redirect replaces it. The import saves every line or, when one is wrong, none: the screen then lists the lines to correct. One import takes at most 5000 lines, and the box holds at most 262,144 characters: split a long list of long addresses over several imports.

Editors' rights are set in the groups' permission matrix, like those of any module instance.

Redirects from the editors

With this module, the admin also offers redirects where a page or an item, such as a post, leaves its address; see Links to a page:

  • Unpublish and redirect, in the editor of a page that published content links to, takes the page offline and redirects its addresses, those of the pages below it and the linked addresses below those to a page the editor chooses. In the editor of a post, event, album, person, directory entry or category of Publishing or Directory that published content links to, it does the same with the item's address.
  • Redirect the old addresses to the new ones is a box with Publish this revision when the revision gives a page a new Web address or another parent page, or a post or an event a new Web address. It is ticked by default, and redirects the old addresses of the page and of the pages below it, or of the item, to their new ones.
  • Saving a published album, person, directory entry or category of Publishing or Directory with a new Web address redirects its old address to the new one.
  • Create redirect, in the report under Pages › Broken links, redirects an address that links lead to and where the site has no page.

These are 301 redirects, listed here with the others. A redirect from the same old path is replaced. A redirect from the new address, which would hide the page there, such as one left from moving the page away earlier, is moved to the trash, so a page can get its old web address back. Making them needs the create right in this module's row of the permission matrix; without it, the report says "You need the right to create redirects."

How it works

The module wraps the public site. A GET or HEAD request whose path has a redirect is redirected before the site handles it, and before the site's output cache, so a redirect also wins over a page at the same address, at once, also while that page is in the cache. The query string is kept when the new address has none: /old?utm_source=mail goes to /en/new?utm_source=mail.

Paths are compared after normalising: the host, query and fragment are dropped, a leading slash is added, and trailing slashes and dot segments are removed. Editors can therefore paste full old URLs: https://old.example.com/a/?b=1 becomes /a. Case counts, so /About and /about are different paths.

Never redirected are the home page / and the paths WebThings serves itself: the admin, visitor actions (/_/), /media/, /assets/ and /theme/, with everything below them, /sitemap.xml, /robots.txt, the endpoints for operations, /healthz, /readyz and /metrics, and, while Config.API turns it on, the content API (/api/v1). A redirect from one of these paths is refused.

Missed paths are GET requests that the site answered with 404. A 404 sent with X-Robots-Tag: noindex or none, such as a mistyped registration code, is not counted. The module keeps the 1000 most requested; the admin shows the first 100.

Hits and missed paths are counted in memory and written to the database in batches: at most every 30 seconds, whenever the admin lists them, and when the app closes, so a redirect never waits for the database. The redirects themselves are kept in memory too: a change applies at once in the process that made it, and other processes on the same database pick it up within a minute.

From Go

redirects.Of(app, "redirects") returns the instance once the app has started, for example to add the redirects of a migration in code:

moved := redirects.Of(app, "redirects")

_, err := moved.Save(ctx, redirects.Redirect{
    From:   "/about-us.html",
    To:     "/en/about",
    Status: 301,
})

Import reads the same CSV as the admin from any io.Reader, such as a file, and refuses one over 1 MiB or 5000 lines whole. Resolve and Trace tell where a path leads, by the saved redirects: a path that is never redirected has none, as Save and Import refuse it. Missed returns the missed paths. Delete, Undelete and Purge move a redirect to the trash, bring it back or remove it for good.

Redirect(ctx, address, target) saves a 301 from address to target, as the editors do: it replaces the redirect from the same path and moves a redirect from target to the trash. It refuses what Save refuses, such as a reserved path or a redirect that leads back to its own path. Redirected(ctx, address) returns where the redirect of a path sends visitors, and false when it has none. With moved as above:

if err := moved.Redirect(ctx, "/en/about", "/en/about-us"); err != nil {
    return err
}

// "/en/about-us", true
target, ok := moved.Redirected(ctx, "/en/about")

Together they are the module's links.Redirector, which it gives the app as Parts.Redirects; see Links to pages.

  • Stats counts the pages visitors do find.

Documentation

Overview

Package redirects sends visitors from old URLs to new ones, for example after moving a site to WebThings. Editors manage the redirects in the admin: an old path, a new address (a path on this site or a full URL) and the kind of redirect (301, 302, 307 or 308). Each redirect counts its hits. Add the module with:

app.Use(redirects.Module())

Its admin section is at <AdminPath>/redirects; redirects.Module(redirects.Named("old", "Old addresses")) chooses another key and title.

The module wraps the public site. GET and HEAD requests whose path matches a redirect are redirected before the site handles them; the query string is kept when the new address has none. Paths are compared after normalising: host, query and fragment are dropped, a leading slash is added, trailing slashes and dot segments are removed, and case is kept. The home page "/" and the paths WebThings serves itself (webthings.App.CorePaths: the admin, visitor actions (/_/), media, assets, theme files, sitemap.xml, robots.txt, /healthz, /readyz and /metrics) are never redirected.

Requests that end in "not found" are counted as missed paths; the admin lists the most frequent ones so editors can add redirects for them. A "not found" that asks not to be indexed (X-Robots-Tag: noindex or none), such as a mistyped registration code, is not a missed page and is not counted.

The admin can import redirects as CSV, one per line: old path, new address and, optionally, the status (301 when empty). A first line starting with "from" is skipped, and semicolons may separate the fields instead of commas. An import saves all lines or, when one is wrong, none.

Deleting a redirect moves it to the admin's trash: it stops redirecting and its path is free for a new redirect. Restoring it fails while another redirect uses the path.

The module also redirects for the page editor (webthings.Parts.Redirects, see Service.Redirect): when a page that published content links to is unpublished, or a revision that changes its address is published, the editor offers to redirect the addresses it leaves, and the report of broken links offers to redirect an address that links lead to.

The module has no public templates or template functions.

Index

Constants

This section is empty.

Variables

View Source
var Statuses = []int{301, 302, 307, 308}

Statuses are the HTTP statuses a redirect can answer with.

Functions

func Module

func Module(opts ...Option) webthings.Module

Module adds the Redirects instance, with the key "redirects" and the title "Redirects" (translated in the admin) unless Named says otherwise. A site needs one: it redirects every old path.

func Normalize

func Normalize(address string) (string, error)

Normalize turns an old address into the path redirects are matched on: the host, query and fragment are dropped, a leading slash is added and trailing slashes and dot segments are removed. "https://old.example/a/?b=1" becomes "/a".

Types

type EditView

type EditView struct {
	Title   string
	BackURL string
	// BackLabel names the list BackURL leads to, as a translation key.
	BackLabel string
	// Warnings point out chains of redirects.
	Warnings []string
	// Saved is set for an existing redirect, which has hits.
	Saved   bool
	Hits    int64
	LastHit string
	Form    form.RenderModel
	Errors  form.FieldErrors
	Delete  *form.RenderModel
}

EditView is the data of templates/admin/edit.gohtml: the form of one redirect.

type ImportError

type ImportError struct {
	Lines []LineError
}

ImportError lists the lines an import refused; nothing was saved.

func (*ImportError) Error

func (e *ImportError) Error() string

Error lists the refused lines with their reasons.

type ImportView

type ImportView struct {
	Title   string
	BackURL string
	// Problems lists the refused lines; nothing was imported.
	Problems []string
	Form     form.RenderModel
	Errors   form.FieldErrors
}

ImportView is the data of templates/admin/import.gohtml: the form that imports redirects from CSV.

type IndexRow

type IndexRow struct {
	URL     string
	From    string
	To      string
	Status  int
	Hits    int64
	LastHit string
	// Chained is set when To leads to another redirect.
	Chained bool
}

IndexRow is one redirect in the list, with how often it was used.

type IndexView

type IndexView struct {
	Title     string
	NewURL    string
	ImportURL string
	MissedURL string
	// TrashURL shows the deleted redirects in the admin's trash.
	TrashURL string
	// TestURL is where the test box sends ?test=<path>.
	TestURL string
	// TestForm is the test box, a form that gets TestURL.
	TestForm form.RenderModel
	Test     *TestResult
	Rows     []IndexRow
	// Pager pages through long lists.
	Pager admin.Pager
}

IndexView is the data of templates/admin/index.gohtml: the redirects, with a box to test where a path leads.

type LineError

type LineError struct {
	Line int
	Err  error
}

LineError is a CSV line an import refused.

type Miss

type Miss struct {
	Path    string    `db:"path"`
	Hits    int64     `db:"hits"`
	LastHit time.Time `db:"last_hit_at"`
}

Miss is a path that was not found, with how often and when it was last requested.

type MissedRow

type MissedRow struct {
	Path    string
	Hits    int64
	LastHit string
	// AddURL opens a new redirect for the path; empty without the right.
	AddURL string
}

MissedRow is one path that was not found, with how often it was asked for.

type MissedView

type MissedView struct {
	Title   string
	BackURL string
	Rows    []MissedRow
	Clear   *form.RenderModel
}

MissedView is the data of templates/admin/missed.gohtml: the paths visitors asked for that the site does not have.

type Option

type Option func(*options)

Option configures the instance.

func Named

func Named(key, title string) Option

Named gives the instance its own key, used in URLs of the admin, and title, shown in the admin, instead of "redirects" and "Redirects".

type Redirect

type Redirect struct {
	ID string `db:"id"`
	// From is the normalised old path, for example "/old-page".
	From string `db:"from_path"`
	// To is a path on the site, such as "/en/news", or an absolute URL.
	To      string     `db:"to_url"`
	Status  int        `db:"status"`
	Hits    int64      `db:"hits"`
	LastHit *time.Time `db:"last_hit_at"`
	// CreatedAt is when the redirect was added, CreatedBy by whom (empty for
	// the system).
	CreatedAt time.Time `db:"created_at"`
	CreatedBy db.Ref    `db:"created_by"`
	// UpdatedAt and UpdatedBy are the last change by an editor; hits do not
	// count as changes.
	UpdatedAt time.Time `db:"updated_at"`
	UpdatedBy db.Ref    `db:"updated_by"`
	// DeletedAt is set while the redirect is in the trash, with DeletedBy.
	DeletedAt *time.Time `db:"deleted_at"`
	DeletedBy db.Ref     `db:"deleted_by"`
}

Redirect sends requests for From to To.

type Service

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

Service is one Redirects instance, for the website's own code.

func Of

func Of(app *webthings.App, key string) *Service

Of returns the Redirects instance with key from a started app, or nil.

func (*Service) ClearMissed

func (s *Service) ClearMissed(ctx context.Context) error

ClearMissed empties the list of missed paths.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, id string) error

Delete moves a redirect to the trash: it stops redirecting and its path is free for a new redirect. An unknown one, or one already in the trash, is db.ErrNotFound.

func (*Service) Flush

func (s *Service) Flush(ctx context.Context) error

Flush writes the hits and misses counted in memory to the database.

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (Redirect, error)

Get returns one redirect, or db.ErrNotFound when it does not exist or is in the trash.

func (*Service) Import

func (s *Service) Import(ctx context.Context, source io.Reader) (int, error)

Import reads CSV lines "old path,new address[,status]" and saves them, replacing redirects with the same old path. It saves all lines or, when a line is wrong, none; then the error is an *ImportError. A source over 1 MiB or 5000 lines is refused whole. It returns the number of redirects saved.

func (*Service) List

func (s *Service) List(ctx context.Context) ([]Redirect, error)

List returns all redirects ordered by old path, without those in the trash.

func (*Service) Missed

func (s *Service) Missed(ctx context.Context, limit int) ([]Miss, error)

Missed returns the most requested paths that were not found and have no redirect yet.

func (*Service) Purge

func (s *Service) Purge(ctx context.Context, id string) error

Purge removes a redirect in the trash for good; one that is not in the trash is db.ErrNotFound.

func (*Service) Redirect

func (s *Service) Redirect(ctx context.Context, address, target string) error

Redirect sends the visitors of address to target, a path on this site or a full URL, with a permanent redirect (301) from now on: it replaces the redirect of address when there is one, and moves a redirect of target to the trash, as it would hide the page that is there now. It is the module's links.Redirector (webthings.Parts.Redirects): the admin redirects with it the addresses that links lead to when a page leaves them. A reserved address, or one target leads back to, is refused, as by Save.

func (*Service) Redirected

func (s *Service) Redirected(ctx context.Context, address string) (string, bool)

Redirected returns where the redirect of address, a path on this site, sends visitors; ok is false when no redirect has it. It is part of the module's links.Redirector, with Redirect.

func (*Service) Resolve

func (s *Service) Resolve(ctx context.Context, requestPath string) (Redirect, bool)

Resolve returns the redirect for a request path, if there is one. No redirect is saved from a reserved path (see Save), and requests to the paths the app serves itself never reach the site's middleware.

func (*Service) Save

func (s *Service) Save(ctx context.Context, redirect Redirect) (Redirect, error)

Save validates and stores a redirect: a new one when ID is empty, otherwise it changes From, To and Status of an existing one. A redirect that would lead back to its own path, directly or through other redirects, is refused.

func (*Service) Trace

func (s *Service) Trace(ctx context.Context, requestPath string) []Redirect

Trace follows a path through the redirects, at most a few steps, and returns each redirect on the way.

func (*Service) Trashed

func (s *Service) Trashed(ctx context.Context, limit, offset int) ([]Redirect, int, error)

Trashed returns redirects in the trash, most recently deleted first: at most limit of them from offset (all for a limit of 0), and how many the trash holds in all.

func (*Service) Undelete

func (s *Service) Undelete(ctx context.Context, id string) error

Undelete brings a redirect back from the trash. When another redirect uses its path now, or it would lead back to its own path through the redirects made meanwhile, it stays in the trash (errTaken, errLoop). One that is not in the trash is db.ErrNotFound.

type TestResult

type TestResult struct {
	Path     string
	Reserved bool
	Steps    []IndexRow
}

TestResult is what the test box found for a path.

Jump to

Keyboard shortcuts

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