forge

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 18 Imported by: 0

README

Translations: English | 简体中文

Forge

Forge is an independent, pre-release fork of go-kratos/kratos. It explores deliberate breaking changes, faster standard-library-based internals, and a smaller long-term dependency surface. It is not affiliated with or endorsed by the go-kratos maintainers.

The project currently tracks v0 development and does not provide a stable API or compatibility guarantee. Read COMPATIBILITY.md before migrating and UPSTREAM.md for the source baseline and synchronization policy.

Features

  • API-first development with Protobuf and generated HTTP/gRPC code.
  • Unified transport layer for HTTP and gRPC.
  • Protocol-neutral asynchronous message contract with optional broker adapters.
  • Standard-library http.ServeMux routing with method patterns, path values, and Google AIP template support.
  • Composable middleware for recovery, logging, validation, tracing, metrics, auth, and more.
  • Pluggable registry, configuration, and encoding components.
  • Standard-library log/slog based logging with OpenTelemetry extensions in contrib packages.
  • Consistent metadata, errors, validation, OpenAPI, and code-generation workflows.
  • A contrib ecosystem for optional integrations such as registries, config stores, middleware, encodings, and observability.

Installation

Requirements
  • Go 1.27 RC (currently 1.27rc3; go.mod requires it, so Go 1.26 cannot build Forge)
  • protoc
  • protoc-gen-go
  • Buf or an equivalent protoc workflow
Add Forge
go get github.com/sylphylabs/forge@main

Forge intentionally does not ship a project-scaffolding CLI. Project creation, dependency upgrades, and execution use the standard Go toolchain.

Build the atomic Forge Protobuf generators from this checkout until the corresponding Buf plugins are published. Install ./protoc-gen-go-message only when a service consumes asynchronous messages:

cd cmd
GOWORK=off go install ./protoc-gen-go-errors ./protoc-gen-go-http ./protoc-gen-go-message ./protoc-gen-go-middleware

Generate and Run

Use the repository's Buf or protoc configuration to generate code, then run the service directly:

buf generate
go generate ./...
go run ./cmd/server -conf ./configs

Usage Example

package main

import (
	"github.com/sylphylabs/forge"
	"github.com/sylphylabs/forge/transport/grpc"
	"github.com/sylphylabs/forge/transport/http"
)

func main() {
	httpSrv := http.NewServer(http.WithAddress(":8000"))
	grpcSrv := grpc.NewServer(grpc.WithAddress(":9000"))

	app := forge.New(
		forge.WithName("helloworld"),
		forge.WithVersion("v1.0.0"),
		forge.WithServer(httpSrv, grpcSrv),
	)
	if err := app.Run(); err != nil {
		panic(err)
	}
}

Upstream Baseline

Forge started from Kratos v3. Existing Kratos users should treat the module-path change and all future Forge releases as an explicit migration, not as an in-place Forge upgrade.

Further Reading

Development

make test
make lint

See DEVELOPMENT.md for multi-module checks and Go 1.27 RC validation.

Security

Use a private GitHub security advisory in the Forge repository. Do not report Forge-specific vulnerabilities to the upstream Kratos project.

Acknowledgments

Forge preserves the complete Kratos Git history and original MIT copyright notice. The upstream Kratos project and its contributors created the foundation of this codebase.

The following projects influenced the original Forge design:

License

Forge is open-sourced software licensed under the MIT license.

Documentation

Overview

Package forge manages the lifecycle of a set of transport servers as one application.

New builds an App from options: identity (WithID, WithName, WithVersion, WithMetadata), the servers to run (WithServer), an optional service registry (WithRegistrar), and lifecycle hooks (WithBeforeStart, WithAfterStart, WithBeforeStop, WithAfterStop). App.Run starts every server, registers the instance, and blocks until a stop signal arrives or App.Stop is called; on the way out it deregisters, drains the servers within WithStopTimeout, and runs the AfterStop hooks within WithAfterStopTimeout. Run joins every error it observed rather than reporting only the first.

A Suite bundles options that belong together — an integration and its hooks — so an application adopts them with a single WithSuite call. Handlers and hooks recover the application's identity from the context via FromContext.

The package dictates no project layout and ships no scaffolding CLI. See docs/agent/application.md for the usage contract and docs/design/application-lifecycle.md for the lifecycle rationale.

Example (FromContext)

Example_fromContext mirrors the guide's identity snippet: inside a handler or hook, recover application identity from the context.

package main

import (
	"context"
	"fmt"

	"github.com/sylphylabs/forge"
)

func main() {
	app := forge.New(forge.WithName("helloworld"))
	ctx := forge.NewContext(context.Background(), app)

	if info, ok := forge.FromContext(ctx); ok {
		fmt.Println(info.Name())
		_ = info.Endpoints()
	}
}
Output:
helloworld
Example (Lifecycle)

Example_lifecycle mirrors "Lifecycle": hooks and the three independent timeouts are all construction options.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/sylphylabs/forge"
	"github.com/sylphylabs/forge/registry"

	forgehttp "github.com/sylphylabs/forge/transport/http"
)

// noopRegistrar stands in for a registry integration (contrib/registry/...).
type noopRegistrar struct{}

func (noopRegistrar) Register(context.Context, *registry.ServiceInstance) error   { return nil }
func (noopRegistrar) Deregister(context.Context, *registry.ServiceInstance) error { return nil }

// pool stands in for the guide's application-owned resource whose lifecycle
// the hooks manage.
type pool struct{}

func (pool) Ping(context.Context) error { return nil }
func (pool) Close() error               { return nil }

func main() {
	httpSrv := forgehttp.NewServer(forgehttp.WithAddress("127.0.0.1:0"))
	var reg noopRegistrar
	var dbPool pool

	app := forge.New(
		forge.WithName("helloworld"),
		forge.WithServer(httpSrv),
		forge.WithRegistrar(reg),
		forge.WithRegistrarTimeout(5*time.Second),
		forge.WithStopTimeout(15*time.Second),
		forge.WithAfterStopTimeout(5*time.Second),
		forge.WithBeforeStart(func(ctx context.Context) error { return dbPool.Ping(ctx) }),
		forge.WithAfterStop(func(_ context.Context) error { return dbPool.Close() }),
	)

	_ = app
	fmt.Println("constructed")
}
Output:
constructed
Example (MinimalService)

Example_minimalService mirrors "Minimal service". It has no Output comment, so `go test` compiles it without running it: the guide's version blocks in Run until a stop signal arrives.

package main

import (
	"github.com/sylphylabs/forge"

	forgegrpc "github.com/sylphylabs/forge/transport/grpc"

	forgehttp "github.com/sylphylabs/forge/transport/http"
)

func main() {
	httpSrv := forgehttp.NewServer(forgehttp.WithAddress(":8000"))
	grpcSrv := forgegrpc.NewServer(forgegrpc.WithAddress(":9000"))

	app := forge.New(
		forge.WithName("helloworld"),
		forge.WithVersion("v1.0.0"),
		forge.WithServer(httpSrv, grpcSrv),
	)
	if err := app.Run(); err != nil {
		panic(err)
	}
}
Example (Suite)
package main

import (
	"context"
	"fmt"

	"github.com/sylphylabs/forge"
)

// pool stands in for the guide's application-owned resource whose lifecycle
// the hooks manage.
type pool struct{}

func (pool) Ping(context.Context) error { return nil }
func (pool) Close() error               { return nil }

// closingSuite mirrors the guide's tracing suite: an integration bundles its
// options — here an AfterStop hook that shuts the integration down — so an
// application adopts them in one WithSuite call. The guide's version closes
// an OpenTelemetry TracerProvider; the shape is the same for any resource.
type closingSuite struct{ closer interface{ Close() error } }

func (s closingSuite) Options() []forge.Option {
	return []forge.Option{
		forge.WithAfterStop(func(_ context.Context) error {
			return s.closer.Close()
		}),
	}
}

func main() {
	app := forge.New(
		forge.WithName("helloworld"),
		forge.WithSuite(closingSuite{closer: pool{}}),
	)

	_ = app
	fmt.Println("constructed")
}
Output:
constructed

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppProbe

func AppProbe(info AppInfo) diagnosis.ProbeFunc

AppProbe returns a probe that reports info's identity as an AppSnapshot.

The probe reads info when it runs, not when it is built, so a snapshot taken after App.Run has started the servers includes the endpoints they bound. Register it under a name of your choosing — "app" by convention:

reg := diagnosis.NewRegistry()
app := forge.New(forge.WithName("checkout"), ...)
reg.Register("app", forge.AppProbe(app))

AppProbe panics if info is nil; a probe wired to nothing is a construction bug, surfaced at the offending line rather than as an error in every dump.

func NewContext

func NewContext(ctx context.Context, s AppInfo) context.Context

NewContext returns a new Context that carries value.

Types

type App

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

App is an application components lifecycle manager.

func New

func New(opts ...Option) *App

New create an application lifecycle manager.

func (*App) Endpoints

func (a *App) Endpoints() []string

Endpoints returns endpoints.

func (*App) Healthz

func (a *App) Healthz() bool

Healthz reports whether every server that exposes readiness through transport.Healthzer can accept new work. Servers without the capability make no claim and do not affect the result. App itself satisfies transport.Healthzer, so it can feed a health endpoint directly.

func (*App) ID

func (a *App) ID() string

ID returns app instance id.

func (*App) Metadata

func (a *App) Metadata() map[string]string

Metadata returns service metadata.

func (*App) Name

func (a *App) Name() string

Name returns service name.

func (*App) Run

func (a *App) Run() error

Run starts the application and blocks until it stops. It builds the registry instance, runs the BeforeStart hooks, starts every configured server, registers the instance with the registrar when one is configured, and then runs the AfterStart hooks. It returns when the application stops — because App.Stop was called, an exit signal from the WithSignal option arrived, or a server or hook failed. On the way out it runs the BeforeStop hooks, deregisters the instance, shuts the servers down within WithStopTimeout, and finally runs the AfterStop hooks within WithAfterStopTimeout. The returned error joins every failure observed along the way.

func (*App) Stop

func (a *App) Stop() error

Stop gracefully stops the application.

func (*App) Version

func (a *App) Version() string

Version returns app version.

type AppInfo

type AppInfo interface {
	ID() string
	Name() string
	Version() string
	Metadata() map[string]string
	Endpoints() []string
}

AppInfo is application context value.

func FromContext

func FromContext(ctx context.Context) (s AppInfo, ok bool)

FromContext returns the Transport value stored in ctx, if any.

type AppSnapshot

type AppSnapshot struct {
	// ID is the application instance id.
	ID string `json:"id"`
	// Name is the service name.
	Name string `json:"name"`
	// Version is the application version.
	Version string `json:"version"`
	// Metadata is the service metadata, if any.
	Metadata map[string]string `json:"metadata,omitempty"`
	// Endpoints lists the endpoints the application currently advertises.
	// Empty before the application has started its servers.
	Endpoints []string `json:"endpoints,omitempty"`
}

AppSnapshot is the value reported by an AppProbe: the application's identity as it stands at the moment the probe runs.

type Option

type Option func(o *options)

Option is an application option.

Options come in two shapes. Scalar options (WithID, WithName, WithVersion, WithContext, WithLogger, WithRegistrar, and the timeouts) set a single field: when the same option appears more than once, the one applied last wins. Collection options (WithServer, WithEndpoint, WithSignal, WithMetadata, and the lifecycle hooks) accumulate: every application of the option adds to what earlier applications contributed, so independent option lists — for example two Suite values — compose without overwriting each other.

func WithAfterStart

func WithAfterStart(fn func(context.Context) error) Option

WithAfterStart registers a func to run after the app starts.

func WithAfterStop

func WithAfterStop(fn func(context.Context) error) Option

WithAfterStop registers a func to run after the app stops.

func WithAfterStopTimeout

func WithAfterStopTimeout(t time.Duration) Option

WithAfterStopTimeout sets the total time allowed for all AfterStop hooks. A non-positive duration disables the deadline.

func WithBeforeStart

func WithBeforeStart(fn func(context.Context) error) Option

WithBeforeStart registers a func to run before the app starts.

func WithBeforeStop

func WithBeforeStop(fn func(context.Context) error) Option

WithBeforeStop registers a func to run before the app stops.

func WithContext

func WithContext(ctx context.Context) Option

WithContext sets the service context.

func WithEndpoint

func WithEndpoint(endpoints ...*url.URL) Option

WithEndpoint appends endpoints to the service endpoints.

func WithID

func WithID(id string) Option

WithID sets the service id.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the service logger.

func WithMetadata

func WithMetadata(md map[string]string) Option

WithMetadata merges md into the service metadata. Keys from later applications win over earlier ones.

func WithName

func WithName(name string) Option

WithName sets the service name.

func WithRegistrar

func WithRegistrar(r registry.Registrar) Option

WithRegistrar sets the service registrar.

func WithRegistrarTimeout

func WithRegistrarTimeout(t time.Duration) Option

WithRegistrarTimeout sets the registrar timeout.

func WithServer

func WithServer(srv ...transport.Server) Option

WithServer appends transport servers to the application.

func WithSignal

func WithSignal(sigs ...os.Signal) Option

WithSignal appends exit signals to the set the application stops on. When no WithSignal option is given, the application stops on SIGTERM, SIGQUIT, and SIGINT.

func WithStopTimeout

func WithStopTimeout(t time.Duration) Option

WithStopTimeout sets the app stop timeout.

func WithSuite

func WithSuite(s Suite) Option

WithSuite expands a Suite into a single Option. The suite's options apply in place, exactly where the returned Option appears in the caller's option list, in the order Options returned them. Each option keeps its usual semantics whether it came from a suite or was written directly: scalar options such as WithName, WithVersion, WithID, WithContext, and WithLogger set a single field and the one applied last wins, while collection options such as WithServer, WithEndpoint, WithMetadata, and the lifecycle hooks accumulate, so independent suites contributing to the same collection all take effect.

WithSuite calls Options once, immediately, so a suite is read at the point it is wired, not when the application is constructed. It panics right away if s is nil or if Options returns a nil element; a broken wiring fails at the offending line during construction rather than surfacing later.

func WithVersion

func WithVersion(version string) Option

WithVersion sets the service version.

type Suite

type Suite interface {
	// Options returns the options this suite contributes, in the order they
	// should apply. Every element must be non-nil.
	Options() []Option
}

Suite bundles application options that belong together. An integration — a service registry plus its lifecycle hooks, a logging setup plus the metadata it reads — implements Suite once, and an application adopts the whole bundle with a single WithSuite call instead of repeating each option.

A Suite carries no state of its own inside the application: everything it contributes goes through the returned options, so two applications may hold differently configured instances of the same Suite type without sharing anything. Suites compose: the slice returned by Options may itself contain options produced by WithSuite, and independent suites written without knowledge of each other stack in a single option list.

Directories

Path Synopsis
api module
Package auth carries the identity of the caller in flight.
Package auth carries the identity of the caller in flight.
cmd module
Package config loads configuration from pluggable sources, merges the results into one key tree, and keeps that tree current as sources change.
Package config loads configuration from pluggable sources, merges the results into one key tree, and keeps that tree current as sources change.
env
Package env sources configuration from process environment variables, optionally filtered and trimmed by prefix.
Package env sources configuration from process environment variables, optionally filtered and trimmed by prefix.
file
Package file sources configuration from a file or every non-hidden file in a directory, inferring each payload's format from the file extension.
Package file sources configuration from a file or every non-hidden file in a directory, inferring each payload's format from the file extension.
contrib
message/nats module
otel module
Package diagnosis gives components one shared way to expose internal state for inspection.
Package diagnosis gives components one shared way to expose internal state for inspection.
Package encoding defines the Codec contract — Marshal, Unmarshal, and a stable Name — and a process-wide registry that resolves a codec by the content subtype it serves.
Package encoding defines the Codec contract — Marshal, Unmarshal, and a stable Name — and a process-wide registry that resolves a codec by the content subtype it serves.
form
Package form provides the URL-encoded form encoding.Codec, handling both plain Go structs (via go-playground/form) and Protobuf messages (field-name aware, honoring json_name).
Package form provides the URL-encoded form encoding.Codec, handling both plain Go structs (via go-playground/form) and Protobuf messages (field-name aware, honoring json_name).
json
Package json provides the JSON encoding.Codec, backed by encoding/json.
Package json provides the JSON encoding.Codec, backed by encoding/json.
proto
Package proto defines the protobuf codec.
Package proto defines the protobuf codec.
protojson
Package protojson provides the ProtoJSON encoding.Codec for Protobuf messages, backed by google.golang.org/protobuf/encoding/protojson.
Package protojson provides the ProtoJSON encoding.Codec for Protobuf messages, backed by google.golang.org/protobuf/encoding/protojson.
xml
Package xml provides the XML encoding.Codec, backed by encoding/xml.
Package xml provides the XML encoding.Codec, backed by encoding/xml.
yaml
Package yaml provides the YAML encoding.Codec.
Package yaml provides the YAML encoding.Codec.
Package errors defines Forge's error contract: a portable representation that carries a failure's category, identity, and cause across process boundaries without losing meaning.
Package errors defines Forge's error contract: a portable representation that carries a failure's category, identity, and cause across process boundaries without losing meaning.
internal
backstop
Package backstop converts a panic caught at a transport boundary into the generic internal error every transport puts on the wire for one.
Package backstop converts a panic caught at a transport boundary into the generic internal error every transport puts on the wire for one.
e2e/service command
Command service is the Forge server used by the cross-service end-to-end test.
Command service is the Forge server used by the cross-service end-to-end test.
formtag
Package formtag holds the struct tag used to bind URL values.
Package formtag holds the struct tag used to bind URL values.
group
Package group provides a sample lazy load container.
Package group provides a sample lazy load container.
protojsonutil
Package protojsonutil centralizes the protojson marshal/unmarshal policy for packages that already carry a protobuf dependency (config, encoding/form).
Package protojsonutil centralizes the protojson marshal/unmarshal policy for packages that already carry a protobuf dependency (config, encoding/form).
Package log builds structured loggers on the standard library's log/slog.
Package log builds structured loggers on the standard library's log/slog.
Package metadata carries request-scoped key-value pairs across process boundaries, transport-neutrally.
Package metadata carries request-scoped key-value pairs across process boundaries, transport-neutrally.
Package middleware defines the two middleware contracts every Forge transport composes: unary and stream.
Package middleware defines the two middleware contracts every Forge transport composes: unary and stream.
circuitbreaker
Package circuitbreaker provides client middleware that stops calling an operation whose recent attempts have been failing, so a struggling dependency gets headroom to recover instead of more load.
Package circuitbreaker provides client middleware that stops calling an operation whose recent attempts have been failing, so a struggling dependency gets headroom to recover instead of more load.
governance
Package governance turns middleware parameters into dynamically observable values instead of construction-time constants.
Package governance turns middleware parameters into dynamically observable values instead of construction-time constants.
logging
Package logging provides middleware that writes one structured record per request: operation, transport kind, request summary, latency, and — on failure — the error's kind, reason, domain, and trace ID.
Package logging provides middleware that writes one structured record per request: operation, transport kind, request summary, latency, and — on failure — the error's kind, reason, domain, and trace ID.
metadata
Package metadata provides middleware that moves application metadata (package github.com/sylphylabs/forge/metadata) between the context and the transport headers, so request-scoped values propagate across process boundaries without transport-specific code.
Package metadata provides middleware that moves application metadata (package github.com/sylphylabs/forge/metadata) between the context and the transport headers, so request-scoped values propagate across process boundaries without transport-specific code.
ratelimit
Package ratelimit provides server middleware that sheds load when the service is over capacity, failing rejected requests fast with ErrLimitExceed (KindResourceExhausted) instead of queueing them into timeouts.
Package ratelimit provides server middleware that sheds load when the service is over capacity, failing rejected requests fast with ErrLimitExceed (KindResourceExhausted) instead of queueing them into timeouts.
recovery
Package recovery provides middleware that recovers a panicking handler, logs the panic value with its stack, and converts the panic into an error the transport can serve.
Package recovery provides middleware that recovers a panicking handler, logs the panic value with its stack, and converts the panic into an error the transport can serve.
retry
Package retry provides client middleware that re-invokes a failed unary call, with an injectable backoff curve — exponential full jitter by default — and a per-operation policy that can be governed at runtime.
Package retry provides client middleware that re-invokes a failed unary call, with an injectable backoff curve — exponential full jitter by default — and a per-operation policy that can be governed at runtime.
selector
Package selector provides middleware that applies other middleware conditionally, by matching the operation of the call in flight.
Package selector provides middleware that applies other middleware conditionally, by matching the operation of the call in flight.
throws
Package throws asserts at runtime that the error identities leaving a method are the ones its Protobuf throws declarations promised.
Package throws asserts at runtime that the error identities leaving a method are the ones its Protobuf throws declarations promised.
timeout
Package timeout provides server middleware that bounds handler execution time, with a per-operation deadline that can be governed at runtime.
Package timeout provides server middleware that bounds handler execution time, with a per-operation deadline that can be governed at runtime.
validate
Package validate provides server middleware that rejects invalid requests before the handler runs.
Package validate provides server middleware that rejects invalid requests before the handler runs.
Package registry defines the service registration and discovery contract.
Package registry defines the service registration and discovery contract.
Package selector picks one node from a discovered set for each request: load balancing on the client side of Forge's HTTP and gRPC transports.
Package selector picks one node from a discovered set for each request: load balancing on the client side of Forge's HTTP and gRPC transports.
filter
Package filter provides ready-made node filters for selector.Selector Select calls.
Package filter provides ready-made node filters for selector.Selector Select calls.
node/direct
Package direct wraps nodes with their statically declared weight: the weight discovery published is the weight the balancer sees, with no runtime feedback.
Package direct wraps nodes with their statically declared weight: the weight discovery published is the weight the balancer sees, with no runtime feedback.
node/ewma
Package ewma wraps nodes with exponentially weighted moving averages of their observed latency and success rate, so a balancer can prefer nodes that are currently fast and healthy over ones that are merely declared heavy.
Package ewma wraps nodes with exponentially weighted moving averages of their observed latency and success rate, so a balancer can prefer nodes that are currently fast and healthy over ones that are merely declared heavy.
p2c
Package p2c provides a "power of two choices" selector: each pick compares two random nodes by their EWMA-tracked latency and health and takes the better one, which balances load with O(1) work per pick.
Package p2c provides a "power of two choices" selector: each pick compares two random nodes by their EWMA-tracked latency and health and takes the better one, which balances load with O(1) work per pick.
random
Package random provides a uniformly random selector: every pick chooses among the candidates with equal probability, ignoring weights.
Package random provides a uniformly random selector: every pick chooses among the candidates with equal probability, ignoring weights.
wrr
Package wrr provides a weighted round robin selector: nodes are picked in proportion to their declared weights, with no runtime feedback.
Package wrr provides a weighted round robin selector: nodes are picked in proportion to their declared weights, with no runtime feedback.
Package transport defines the contract between the application lifecycle and the servers it runs, and the call-scoped view middleware gets of the transport underneath it.
Package transport defines the contract between the application lifecycle and the servers it runs, and the call-scoped view middleware gets of the transport underneath it.
grpc
Package grpc provides Forge's gRPC transport: a server wrapping google.golang.org/grpc with Forge's lifecycle and middleware contracts, and a client constructor that wires the same contracts into a *grpc.ClientConn.
Package grpc provides Forge's gRPC transport: a server wrapping google.golang.org/grpc with Forge's lifecycle and middleware contracts, and a client constructor that wires the same contracts into a *grpc.ClientConn.
http
Package http provides Forge's HTTP transport: a server that generated service code registers routes on, and a client that generated code calls through.
Package http provides Forge's HTTP transport: a server that generated service code registers routes on, and a client that generated code calls through.
http/healthz
Package healthz serves a readiness probe over HTTP for anything that implements transport.Healthzer — one server, or an App aggregating all of its servers.
Package healthz serves a readiness probe over HTTP for anything that implements transport.Healthzer — one server, or an App aggregating all of its servers.
http/transcoding
Package transcoding carries the Protobuf half of Forge's HTTP transport: Google HTTP transcoding, ProtoJSON projection, path and query binding, raw HTTP bodies, and stream body fields.
Package transcoding carries the Protobuf half of Forge's HTTP transport: Google HTTP transcoding, ProtoJSON projection, path and query binding, raw HTTP bodies, and stream body fields.
message
Package message defines the protocol-neutral contract for asynchronous message transports.
Package message defines the protocol-neutral contract for asynchronous message transports.

Jump to

Keyboard shortcuts

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