rux

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 1 Imported by: 0

README

Rux

GitHub go.mod Go version Actions Status GitHub tag (latest SemVer) GoDoc Coverage Status Go Report Card

Simple and fast web framework for build golang HTTP applications.

中文说明

v2 Highlights

rux v2 is a clean-room rewrite focused on extreme performance:

  • High-performance Radix Tree routing — per-method tree, lock-free hot path
  • Zero-allocation static routes (one map lookup per request)
  • Inline Params [16]Param in Context for low-allocation dynamic routing
  • Auto-freeze on first ServeHTTP — the routing tables become read-only at runtime
  • Auto HEAD → GET mirror at freeze time (no manual r.HEAD boilerplate)
  • Pre-merged middleware chains (no per-request append)
  • Same high-level API as v1 — Router, Group, Resource, Controller, GET/POST/... all unchanged.

See _benchmarks/v2-results.txt for measured numbers, and docs/MIGRATION-v1-to-v2.md for breaking changes.

Features

Routing & request handling

  • Fast route match (Radix Tree, per-method, lock-free hot path), route groups
  • Path params and named routing
  • Route / group / global middleware
  • RESTful and Controller style structs
  • Generic http.Handler middleware
  • Static file / directory / embed.FS serving
  • NotFound, NotAllowed, Error, and panic handlers

Built-in batteries (server/, pkg/*)

  • Production-ready Server with sane timeouts, graceful shutdown, lifecycle hooks, and /healthz + /readyz endpoints (docs)
  • Echo Server: httpbin-style debug endpoints (/anything, /status/{code}, /delay, /redirect, /cookies, /basic-auth, /bytes, /uuid, /download, /upload, …)
  • Render package (pkg/render): stateless helpers + status-aware Responder for JSON / XML / Text / HTML / Binary / Auto, with a pluggable TemplateRenderer interface (no template engine bundled)
  • Server-Sent Events (pkg/sse): Stream / StreamWith with lifecycle hooks, default :connected frame, optional keepalive, plus a Hub for keyed push and broadcast to active clients

GoDoc

Install

go get github.com/gookit/rux/v2

Quick start

package main

import (
	"fmt"

	"github.com/gookit/rux/v2"
)

func main() {
	r := rux.New()

	// Add Routes:
	r.GET("/", func(c *rux.Context) {
		c.Text(200, "hello")
	})
	r.GET("/hello/{name}", func(c *rux.Context) {
		c.Text(200, "hello "+c.Param("name"))
	})
	r.POST("/post", func(c *rux.Context) {
		c.Text(200, "hello")
	})
	// add multi method support for a route path
	r.Add("/post[/{id}]", func(c *rux.Context) {
		if c.Param("id") == "" {
			// do create post
			c.Text(200, "created")
			return
		}

		id := c.Params().Int("id")
		// do update post
		c.Text(200, "updated "+fmt.Sprint(id))
	}, rux.POST, rux.PUT)

	// Start server
	r.Listen(":8080")
	// can also
	// http.ListenAndServe(":8080", r)
}

Route Group

r.Group("/articles", func() {
    r.GET("", func(c *rux.Context) {
        c.Text(200, "view list")
    })
    r.POST("", func(c *rux.Context) {
        c.Text(200, "create ok")
    })
    r.GET(`/{id}`, func(c *rux.Context) {
        c.Text(200, "view detail, id: "+c.Param("id"))
    })
})

Path Params

In v2 the path-param syntax is {name} (named) or *name (wildcard).

Regex constraints such as {id:\d+} are no longer supported — validate inside the handler or with a small middleware (see the migration guide).

// can access by: "/blog/123"
r.GET(`/blog/{id}`, func(c *rux.Context) {
    id := c.Params().Int("id")
    if id <= 0 {
        c.AbortWithStatus(400)
        return
    }
    c.Text(200, fmt.Sprintf("view detail, id: %d", id))
})

optional params, like /about[.html] or /posts[/{id}]:

// can access by: "/blog/my-article" or "/blog/my-article.html"
r.GET(`/blog/{title}[.html]`, func(c *rux.Context) {
    c.Text(200, "view detail, title: "+c.Param("title"))
})

r.Add("/posts[/{id}]", func(c *rux.Context) {
    if c.Param("id") == "" {
        // do create post
        c.Text(200, "created")
        return
    }

    id := c.Params().Int("id")
    // do update post
    c.Text(200, "updated "+fmt.Sprint(id))
}, rux.POST, rux.PUT)
Wildcards

Catch-all wildcards capture everything past the prefix:

r.GET("/files/*path", func(c *rux.Context) {
    c.Text(200, "serve: "+c.Param("path"))
})

Use Middleware

rux support use middleware, allow:

  • global middleware
  • group middleware
  • route middleware

Call priority: global middleware -> group middleware -> route middleware

Examples:

package main

import (
	"fmt"

	"github.com/gookit/rux/v2"
)

func main() {
	r := rux.New()

	// add global middleware
	r.Use(func(c *rux.Context) {
	    // do something ...
	})

	// add middleware for the route
	route := r.GET("/middle", func(c *rux.Context) { // main handler
		c.WriteString("-O-")
	}, func(c *rux.Context) { // middle 1
        c.WriteString("a")
        c.Next() // Notice: call Next()
        c.WriteString("A")
        // if call Abort(), will abort at the end of this middleware run
        // c.Abort()
    })

	// add more by Use()
	route.Use(func(c *rux.Context) { // middle 2
		c.WriteString("b")
		c.Next()
		c.WriteString("B")
	})

	// now, access the URI /middle
	// will output: ab-O-BA
}
  • Call sequence: middle 1 -> middle 2 -> main handler -> middle 2 -> middle 1
  • Flow chart:
        +-----------------------------+
        | middle 1                    |
        |  +----------------------+   |
        |  | middle 2             |   |
 start  |  |  +----------------+  |   | end
------->|  |  |  main handler  |  |   |--->----
        |  |  |________________|  |   |
        |  |______________________|   |
        |_____________________________|

more please see middleware_test.go middleware tests

Use http.Handler

rux is support generic http.Handler interface middleware

You can use rux.WrapHTTPHandler() convert http.Handler as rux.HandlerFunc

package main

import (
	"net/http"

	"github.com/gookit/rux/v2"
	// here we use gorilla/handlers, it provides some generic handlers.
	"github.com/gorilla/handlers"
)

func main() {
	r := rux.New()

	// create a simple generic http.Handler
	h0 := http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
		w.Header().Set("new-key", "val")
	})

	r.Use(rux.WrapHTTPHandler(h0), rux.WrapHTTPHandler(handlers.ProxyHeaders()))

	r.GET("/", func(c *rux.Context) {
		c.Text(200, "hello")
	})
	// add routes ...

    // Wrap our server with our gzip handler to gzip compress all responses.
    http.ListenAndServe(":8000", handlers.CompressHandler(r))
}

More Usage

Static Assets
package main

import (
	"embed"
	"net/http"

	"github.com/gookit/rux/v2"
)

//go:embed static
var embAssets embed.FS

func main() {
	r := rux.New()

	// one file
	r.StaticFile("/site.js", "testdata/site.js")

	// allow any files in the directory.
	r.StaticDir("/static", "testdata")

	// file type limit in the directory
	r.StaticFiles("/assets", "testdata", "css|js")

	// go 1.16+: use embed assets. access: /embed/static/some.html
	r.StaticFS("/embed", http.FS(embAssets))
}
Name Route

In rux, you can add a named route, and you can get the corresponding route instance(rux.Route) from the router according to the name.

Examples:

	r := rux.New()

	// Method 1
	myRoute := rux.NewNamedRoute("name1", "/path4/some/{id}", emptyHandler, "GET")
	r.AddRoute(myRoute)

	// Method 2
	rux.AddNamed("name2", "/", func(c *rux.Context) {
		c.Text(200, "hello")
	})

	// Method 3
	r.GET("/hi", func(c *rux.Context) {
		c.Text(200, "hello")
	}).NamedTo("name3", r)

	// get route by name
	myRoute = r.GetRoute("name1")
Redirect

redirect to other page

r.GET("/", func(c *rux.Context) {
    c.AbortThen().Redirect("/login", 302)
})

// Or
r.GET("/", func(c *rux.Context) {
    c.Redirect("/login", 302)
    c.Abort()
})

r.GET("/", func(c *rux.Context) {
    c.Back()
    c.Abort()
})
Cookies

you can quick operate cookies by FastSetCookie() DelCookie()

Note: You must set or delete cookies before writing BODY content

r.GET("/setcookie", func(c *rux.Context) {
    c.FastSetCookie("rux_cookie2", "test-value2", 3600)
    c.SetCookie("rux_cookie", "test-value1", 3600, "/", c.Req.URL.Host, false, true)
	c.WriteString("hello, in " + c.URL().Path)
})

// FastSetCookie accepts optional func(*http.Cookie) callbacks to override
// the developer-friendly defaults — handy for HTTPS / SameSite.
r.GET("/setsecure", func(c *rux.Context) {
    c.FastSetCookie("session", "v", 3600, func(ck *http.Cookie) {
        ck.Secure = true
        ck.SameSite = http.SameSiteStrictMode
    })
})

r.GET("/delcookie", func(c *rux.Context) {
	val := ctx.Cookie("rux_cookie") // "test-value1"
	c.DelCookie("rux_cookie", "rux_cookie2")
})
Multi Domains

code is refer from julienschmidt/httprouter

package main

import (
	"log"
	"net/http"

	"github.com/gookit/rux/v2"
)

type HostSwitch map[string]http.Handler

// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Check if a http.Handler is registered for the given host.
	// If yes, use it to handle the request.
	if router := hs[r.Host]; router != nil {
		router.ServeHTTP(w, r)
	} else {
		// Handle host names for which no handler is registered
		http.Error(w, "Forbidden", 403) // Or Redirect?
	}
}

func main() {
	// Initialize a router as usual
	router := rux.New()
	router.GET("/", Index)
	router.GET("/hello/{name}", func(c *rux.Context) {})

	// Make a new HostSwitch and insert the router (our http handler)
	// for example.com and port 12345
	hs := make(HostSwitch)
	hs["example.com:12345"] = router

	// Use the HostSwitch to listen and serve on port 12345
	log.Fatal(http.ListenAndServe(":12345", hs))
}
RESETFul Style
package main

import (
	"log"
	"net/http"

	"github.com/gookit/rux/v2"
)

type Product struct {
}

// Uses middlewares [optional]
func (Product) Uses() map[string][]rux.HandlerFunc {
	return map[string][]rux.HandlerFunc{
		// function name: handlers
		"Delete": []rux.HandlerFunc{
			handlers.HTTPBasicAuth(map[string]string{"test": "123"}),
			handlers.GenRequestID(),
		},
	}
}

// all products [optional]
func (p *Product) Index(c *rux.Context) { }

// create product [optional]
func (p *Product) Create(c *rux.Context) { }

// save new product [optional]
func (p *Product) Store(c *rux.Context) { }

// show product with {id} [optional]
func (p *Product) Show(c *rux.Context) { }

// edit product [optional]
func (p *Product) Edit(c *rux.Context) { }

// save edited product [optional]
func (p *Product) Update(c *rux.Context) { }

// delete product [optional]
func (p *Product) Delete(c *rux.Context) { }

func main() {
	router := rux.New()

	// methods	Path	Action	Route Name
    // GET	/product	index	product_index
    // GET	/product/create	create	product_create
    // POST	/product	store	product_store
    // GET	/product/{id}	show	product_show
    // GET	/product/{id}/edit	edit	product_edit
    // PUT/PATCH	/product/{id}	update	product_update
    // DELETE	/product/{id}	delete	product_delete
    // resetful style
	router.Resource("/", new(Product))

	log.Fatal(http.ListenAndServe(":12345", router))
}
Controller Style
package main

import (
	"log"
	"net/http"

	"github.com/gookit/rux/v2"
)

// News controller
type News struct {
}

func (n *News) AddRoutes(g *rux.Router) {
	g.GET("/", n.Index)
	g.POST("/", n.Create)
	g.PUT("/", n.Edit)
}

func (n *News) Index(c *rux.Context) { }

func (n *News) Create(c *rux.Context) { }

func (n *News) Edit(c *rux.Context) { }

func main() {
	router := rux.New()

	// controller style
	router.Controller("/news", new(News))

	log.Fatal(http.ListenAndServe(":12345", router))
}
Build URL
package main

import (
	"log"
	"net/http"

	"github.com/gookit/rux/v2"
)

func main() {
	// Initialize a router as usual
	router := rux.New()
	router.GET(`/news/{category_id}/{new_id}/detail`, func(c *rux.Context) {
		var u = make(url.Values)
        u.Add("username", "admin")
        u.Add("password", "12345")

		b := rux.NewBuildRequestURL()
        // b.Scheme("https")
        // b.Host("www.mytest.com")
        b.Queries(u)
        b.Params(rux.M{"{category_id}": "100", "{new_id}": "20"})
		// b.Path("/dev")
        // println(b.Build().String())

        println(c.Router().BuildRequestURL("new_detail", b).String())
		// result:  /news/100/20/detail?username=admin&password=12345
		// get current route name
		if c.MustGet(rux.CTXCurrentRouteName) == "new_detail" {
            // post data etc....
        }
	}).NamedTo("new_detail", router)

	// Use the HostSwitch to listen and serve on port 12345
	log.Fatal(http.ListenAndServe(":12345", router))
}

Request Binding and Validation

pkg/binding supports form, query, header, JSON and XML binding. Validation is an extension hook and is disabled by default, so rux does not pull in a validation library unless your application chooses one.

To integrate gookit/validate, install an adapter once at startup:

package main

import (
	"github.com/gookit/rux/v2/pkg/binding"
	"github.com/gookit/validate"
)

type validateAdapter struct{}

func (validateAdapter) Validate(obj any) error {
	v := validate.New(obj)
	if v.Validate() {
		return nil
	}
	return v.Errors.OneError()
}

func main() {
	binding.Validator = validateAdapter{}
}

After the adapter is installed, c.Bind, c.BindJSON, binding.Auto, and other built-in binders validate the bound object automatically. See _examples/validate for a runnable example.

Production-Ready Server

Package server wraps a rux.Router with sensible HTTP timeouts, graceful shutdown, lifecycle hooks, and built-in /healthz / /readyz endpoints. It is the recommended way to run rux in containers / k8s.

package main

import (
	"context"
	"log"

	"github.com/gookit/rux/v2"
	"github.com/gookit/rux/v2/server"
)

func main() {
	s := server.New(false) // false = no debug logging
	s.Addr = ":8080"

	s.GET("/", func(c *rux.Context) {
		c.Text(200, "hello")
	})

	// Optional liveness/readiness endpoints under /healthz and /readyz.
	s.MountHealthChecks()

	// Optional lifecycle hooks (warm caches, validate config, etc.).
	s.PreStart = append(s.PreStart, func(ctx context.Context) error {
		return nil
	})

	if err := s.Run(); err != nil {
		log.Fatal(err)
	}
}

What Run() does for you:

  • ListenAndServe (or TLS variant when TLSCertFile/TLSKeyFile are set)
  • Wait for SIGINT / SIGTERM (configurable via StopSignals)
  • On signal: flip /readyz to 503 → wait DrainDelay so the upstream LB can drain → call http.Server.Shutdown bounded by ShutdownTimeout
  • Run PreShutdown / PostShutdown hooks in order

Defaults tuned for container deployments:

Field Default Purpose
ReadHeaderTimeout 2s slowloris defense
ReadTimeout 10s full request read budget
WriteTimeout 30s response write budget
IdleTimeout 120s keep-alive idle close
DrainDelay 5s LB drain window after stop signal
ShutdownTimeout 25s bound on graceful shutdown
Echo Server (httpbin-style)

server.NewEchoServer() builds a Server with httpbin-style debug endpoints pre-mounted: /anything, /get|post|put|patch|delete, /status/{code}, /delay/{n}, /redirect/{n}, /cookies, /basic-auth/{u}/{p}, /bytes/{n}, /uuid, /download/{filename}, POST /upload, and a /*path catch-all. Useful for local debugging, integration tests, and as a /debug subtree inside larger apps via server.MountEchoRoutes(r).

go run ./_examples/echo-server
# then:
curl http://127.0.0.1:18080/anything
curl -F "file=@./README.md" http://127.0.0.1:18080/upload

See docs/echo-server.md for the full endpoint table and usage recipes.

Server-Sent Events

pkg/sse wraps the SSE wire format and lifecycle so handlers only have to drive the producer. The Hooks struct exposes OnConnect / OnDisconnect / OnSend / OnError callbacks for auth, logging, filtering, and metrics — any field may be nil.

import "github.com/gookit/rux/v2/pkg/sse"

s.GET("/events", func(c *rux.Context) {
    _ = sse.Stream(c, &sse.Hooks{
        OnConnect:    func(c *rux.Context) error { /* auth check */ return nil },
        OnDisconnect: func(c *rux.Context, reason error) { /* audit */ },
    }, func(send sse.SendFunc, done <-chan struct{}) error {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-done:
                return nil
            case t := <-ticker.C:
                if err := send(sse.Event{Data: t.Format(time.RFC3339)}); err != nil {
                    return err
                }
            }
        }
    })
})

OnConnect runs before the SSE headers are written, so a rejecting hook can issue any 4xx via c.Resp (e.g. http.Error(c.Resp, "no token", 401)).

Stream emits a leading : connected\n\n comment frame by default (suppress with StreamWith and SendConnected: false).

For keepalives use StreamWith and set KeepaliveInterval:

sse.StreamWith(c, &sse.Options{
    Hooks: myHooks,
    SendConnected: true,
    KeepaliveInterval: 30 * time.Second, // ": keepalive\n\n" every 30s
}, producer)

Two different timeouts — both matter:

Timer Defeated by
server.Server.WriteTimeout (default 30s) Must set = 0. Heartbeats do NOT save you — this bounds the whole response lifetime.
Proxy / NAT idle timeout (nginx 60s, ALB 60s, …) KeepaliveInterval ≤ that value.

Keyed push with Hub. For business-driven pushes (notify user X, broadcast to all) use sse.NewHub — an in-memory registry keyed by ID (e.g. user ID), multi-connection-per-id (multi-tab fan-out), with non-blocking per-client buffer + dropped-event counter + OnDrop hook:

hub := sse.NewHub(64) // per-client buffer size

s.GET("/events", func(c *rux.Context) {
    uid := authUserID(c)
    _ = sse.Stream(c, nil, sse.HubProducer(hub, uid))
})

// elsewhere, business code:
delivered, dropped := hub.Send("user-42", sse.Event{Name: "notify", Data: "..."})
hub.Broadcast(sse.Event{Name: "announce", Data: "..."})
hub.SetOnDrop(func(c *sse.Client, _ sse.Event) {
    log.Printf("slow client %s, dropped=%d", c.ID, c.Dropped())
})

See _examples/sse-server for the full setup (subscribe + push + broadcast + stats endpoints with a tiny HTML demo client).

Migrating from v1

If you are upgrading from rux v1.x, please read docs/MIGRATION-v1-to-v2.md for a complete list of breaking changes. The high-level API surface is largely unchanged and most basic applications need no source edits.

Performance

rux v2 targets sub-200 ns/op for typical dynamic routes and 0 alloc/op for static and most parametrized routes. See _benchmarks/v2-results.txt for the benchmark numbers measured on the current branch.

Help

  • lint
golint ./...
  • format check
# list error files
gofmt -s -l ./
# fix format and write to file
gofmt -s -w some.go
  • unit test
go test -cover ./...

Gookit Packages

  • gookit/ini Go config management, use INI files
  • gookit/rux Simple and fast request router for golang HTTP
  • gookit/gcli build CLI application, tool library, running CLI commands
  • gookit/slog Concise and extensible go log library
  • gookit/event Lightweight event manager and dispatcher implements by Go
  • gookit/cache Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached.
  • gookit/config Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags
  • gookit/color A command-line color library with true color support, universal API methods and Windows support
  • gookit/filter Provide filtering, sanitizing, and conversion of golang data
  • gookit/validate Use for data validation and filtering; rux can integrate it through pkg/binding without depending on it directly.
  • gookit/goutil Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more
  • More please see https://github.com/gookit

See also

License

MIT

Documentation

Overview

Package rux is a high-performance HTTP router for Go.

This package is the public API surface; all implementation lives in internal/core. Public symbols are type aliases / function vars over the internal types so users get a single coherent import path.

Source: https://github.com/gookit/rux

Index

Constants

View Source
const (
	GET     = core.GET
	HEAD    = core.HEAD
	POST    = core.POST
	PUT     = core.PUT
	PATCH   = core.PATCH
	DELETE  = core.DELETE
	OPTIONS = core.OPTIONS
	CONNECT = core.CONNECT
	TRACE   = core.TRACE
)

HTTP method constants.

View Source
const (
	ContentType        = core.ContentType
	ContentBinary      = core.ContentBinary
	ContentDisposition = core.ContentDisposition
)

Content-Type and disposition header constants.

View Source
const (
	CTXAllowedMethods = core.CTXAllowedMethods
	CTXRecoverResult  = core.CTXRecoverResult
)

Context keys exposed by the dispatcher.

Variables

View Source
var (
	IndexAction  = core.IndexAction
	CreateAction = core.CreateAction
	StoreAction  = core.StoreAction
	ShowAction   = core.ShowAction
	EditAction   = core.EditAction
	UpdateAction = core.UpdateAction
	DeleteAction = core.DeleteAction

	RESTFulActions = core.RESTFulActions
)

REST action names.

View Source
var (
	New                = core.New
	NewBuildRequestURL = core.NewBuildRequestURL
	Debug              = core.Debug
	IsDebug            = core.IsDebug
	AnyMethods         = core.AnyMethods
	AllMethods         = core.AllMethods
	MethodsString      = core.MethodsString
)

Constructor and lifecycle helpers.

View Source
var (
	StrictLastSlash        = core.StrictLastSlash
	UseEncodedPath         = core.UseEncodedPath
	HandleMethodNotAllowed = core.HandleMethodNotAllowed
	HandleFallbackRoute    = core.HandleFallbackRoute
	InterceptAll           = core.InterceptAll
)

Router options.

View Source
var (
	WrapH               = core.WrapH
	HTTPHandler         = core.HTTPHandler
	WrapHTTPHandler     = core.WrapHTTPHandler
	WrapHF              = core.WrapHF
	HTTPHandlerFunc     = core.HTTPHandlerFunc
	WrapHTTPHandlerFunc = core.WrapHTTPHandlerFunc
)

Middleware adapters (wrap http.Handler / http.HandlerFunc as HandlerFunc).

Functions

This section is empty.

Types

type BuildRequestURL

type BuildRequestURL = core.BuildRequestURL

Public types — all aliased to the internal/core implementation.

type Context

type Context = core.Context

Public types — all aliased to the internal/core implementation.

type ControllerFace

type ControllerFace = core.ControllerFace

Public types — all aliased to the internal/core implementation.

type HandlerFunc

type HandlerFunc = core.HandlerFunc

Public types — all aliased to the internal/core implementation.

type HandlersChain

type HandlersChain = core.HandlersChain

Public types — all aliased to the internal/core implementation.

type M

type M = core.M

Public types — all aliased to the internal/core implementation.

type Param

type Param = core.Param

Public types — all aliased to the internal/core implementation.

type Params

type Params = core.Params

Public types — all aliased to the internal/core implementation.

type Renderer

type Renderer = core.Renderer

Public types — all aliased to the internal/core implementation.

type Route

type Route = core.Route

Public types — all aliased to the internal/core implementation.

type RouteInfo

type RouteInfo = core.RouteInfo

Public types — all aliased to the internal/core implementation.

type Router

type Router = core.Router

Public types — all aliased to the internal/core implementation.

type Validator

type Validator = core.Validator

Public types — all aliased to the internal/core implementation.

Directories

Path Synopsis
internal
pkg
binding
Package binding provide some common binder for binding http.Request data to strcut
Package binding provide some common binder for binding http.Request data to strcut
sse
Package sse provides Server-Sent Events helpers for rux handlers.
Package sse provides Server-Sent Events helpers for rux handlers.
Package server provides a production-ready HTTP server wrapping a rux.Router.
Package server provides a production-ready HTTP server wrapping a rux.Router.

Jump to

Keyboard shortcuts

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