scaffold

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 19 Imported by: 0

README

scaffold

scaffold logo

tldr

Deploy an entire local environment for testing, development, or client side app stacks - all presented as a dead simple tool.


scaffold is a Go library for building ephemeral local infrastructure stacks around real containers. It is meant for quick-start environments used in tests, development tools, demos, and client applications that need to manage local services without carrying a full compose file or platform framework.

For instance - a complicated multi-service API service - with multiple APIs, backend databases, S3 object store, and some background processing - can be slowly built up as golang components and presented to the developer as a singular object that manages itself with an easy-to-use exposed interface. The resulting stack can also quickly be tied into a CLI tool for developer use.

scaffold is not only for tests. It is for ephemeral quick-start stacks used in testing, development, demos, and simplified local service management.

Toolbox

Several useful premade stacks and services live in scaffold-toolbox. There you can find common building blocks already made for you so you can easily build your own stack, or use them as examples for your own work.

Some example stacks supported are:

  • Postgres + Redis + API for ordinary application development.
  • Postgres + Qdrant + MinIO for local RAG and document search workflows.
  • LocalStack + app services for S3, SQS, SNS, DynamoDB, and other AWS services.
  • MySQL + Memcached + worker for cache-heavy backend development.

The Basics

A Service is the smallest useful unit in scaffold.

It represents one thing your local environment can start and stop: Postgres, Redis, Qdrant, MinIO, an app container, a model server, a mock API, or even another Stack.

At the lowest level, the interface is intentionally tiny:

type Service interface {
	Name() string
	Create(ctx context.Context) error
	Cleanup(ctx context.Context) error
	Logs(ctx context.Context) (logs.LogStreams, error)
}

That means scaffold services only need to clearly define four things:

  • What is this thing called?
  • How do I start it?
  • How do I clean it up?
  • How do I read its logs?

Everything else is optional.

The small interface is deliberate. Postgres and Redis are both services, but they do not expose the same useful behavior. Postgres might expose a *sql.DB, migrations, seed data, and connection strings. Redis might expose a client, seeded keys, and a TCP endpoint. A web app might expose HTTP URLs. A fake S3 service might expose buckets and an HTTP API.

If Service tried to model all of that, it would become awkward fast. Instead, scaffold standardizes lifecycle and composition, wherein Services keeps the helpers that are meaningful for its own domain for the developer to expand upon.

Richer behavior is added through optional interfaces. Services can expose environment variables, endpoints, Docker labels, generated Docker name prefixes, and shared Docker networks. Stacks and generated CLIs check for those capabilities when they exist, without requiring every service to implement them.

Containers are lower-level Docker primitives. They describe the image, tag, ports, environment, bind mounts, command, and optional Docker network for one running container. While support is built around Containers running, it is not required; merely helpful.

A Container object knows how to start a container, assign ports, join a network, and clean up container resources. It does not know what "ready" means. For Redis, ready might mean port 6379 accepts TCP. For a web server, ready might mean /healthz returns 200. For Postgres, ready might mean the database accepts connections and migrations have run.

Container primitives live in scaffold/container. It uses Docker's compatible API and tries DOCKER_HOST, the default Docker socket, and common Podman sockets, in that order.

Simple Service from a Container

For simple one-container services, use the ContainerService to quickly get started:

container, err := scaffoldcontainer.NewContainer(
	"web",
	"nginx",
	scaffoldcontainer.WithTag("alpine"),
	scaffoldcontainer.WithPort("80", ""),
)
if err != nil {
	return err
}

web, err := scaffold.FromContainer(
	container,
	scaffold.WithHTTPReady("80", "/", http.StatusOK, 30*time.Second),
	scaffold.WithEndpoint("web", "http", "80"),
)

Typed Services

For more specialized infrastructure, write a typed service. A Postgres service can return a *sql.DB and run SQL setup. A Redis service can return a Redis client and seed keys. A MinIO service can create buckets and upload objects. A Qdrant service can create collections and insert points.

A Stack is an ordered group of services. It is the local environment you actually want to bring up: "the RAG backend", "the SaaS backend", "the local AWS test setup", or "the CI/CD pipeline".

Service ordering follows two rules:

  • Services passed to the same WithServices call are created in parallel.
  • Separate WithServices calls are created in the order they are applied.

Cleanup follows the same grouping in reverse. Later service groups are cleaned up before earlier service groups, and services in the same group are cleaned up in parallel. If a service fails during startup, already-created services are cleaned up.

stack := scaffold.NewStack("rag-dev",
	scaffold.WithServices(postgres, qdrant, minio),
	scaffold.WithSharedNetwork(),
)

err := stack.Create(ctx)
if err != nil {
	return err
}
defer stack.Cleanup(context.WithoutCancel(ctx))

Use multiple calls when one service group depends on another. Here db and queue start together. After both are ready, api starts. During cleanup, api is cleaned up first, then db and queue are cleaned up together.

stack := scaffold.NewStack("app",
	scaffold.WithServices(db, queue),
	scaffold.WithServices(api),
)

Stacks are also services. This unlocks a lot of modularity; we can make increasingly complicated stacks comprised of other stacks. That lets you build small, named pieces and compose them without losing the simple lifecycle model.

For example, a RAG stack can own Postgres, Qdrant, and MinIO. An agent stack could in turn be built with this RAG stack in mind.

rag, err := presets.NewRAGStack("rag")
if err != nil {
	return err
}

app := scaffold.NewStack("agent-backend",
	scaffold.WithServices(rag),
	scaffold.WithServices(redis, ollama),
	scaffold.WithSharedNetwork(),
)

LogStreams

Logs follow the same composition model. A service returns named logs.LogStreams, and a stack recursively collects the streams from its children. The root stack name is not prefixed, but child service and child stack names become path segments:

api
data.postgres
data.redis

Call CollectLogs when you want selectable streams:

streams, err := scaffold.CollectLogs(ctx, app)
if err != nil {
	return err
}
defer streams.Close()

postgresLogs, ok := streams.GetStream("data", "postgres")

Call MergedLogs when you want one reader containing every stream:

logs, err := scaffold.MergedLogs(ctx, app)
if err != nil {
	return err
}
defer logs.Close()

Labels and running stacks

scaffold labels Docker resources so a Go-defined stack can find matching local infrastructure after application restart. Containers and shared stack networks are labeled when they are created. Explicitly created named volumes should use the same labels. Anonymous volumes created from image metadata generally cannot be labeled at creation time, though scaffold still tracks and removes the anonymous volumes it discovers on a container during cleanup.

Labels allow local infrastructure to outlives the Go process that created it, but still be worked with later. This allows scaffold to:

  • status can find containers, networks, and volumes for a defined stack.
  • down can remove resources created by another process.
  • nested stacks can inherit parent labels and still be discovered as one environment.
  • multiple local environments can be separated by stack names, run IDs, or custom inherited labels.

scaffold protects certain keyword labels for its own uses:

  • scaffold.managed-by - scaffold
  • scaffold.stack -
  • scaffold.service -
  • scaffold.run-id -

scaffold.stack is the stable identity. scaffold.run-id identifies a specific startup run. If no run id is specified, scaffold.run-id is generated when Create starts. Before Create, discovery uses the stable stack labels and does not filter by run id.

These labels are enough to ask whether a stack is already running:

stack := scaffold.NewStack("app",
	scaffold.WithServices(db, queue),
	scaffold.WithServices(api),
)

running, err := stack.IsRunning(ctx)
if err != nil {
	return err
}

You can also inspect the matching Docker resources:

resources, err := stack.Resources(ctx)
if err != nil {
	return err
}

fmt.Println(resources.Containers)
fmt.Println(resources.Networks)
fmt.Println(resources.Volumes)

RunningContainers is also available when you only care about containers:

containers, err := stack.RunningContainers(ctx)

Stacks labels are inheritable. Inherited labels are pushed down to child services and child stacks before they are created:

stack := scaffold.NewStack("app",
	scaffold.WithRunID("app-dev"),
	scaffold.WithInheritedLabel("key", "value"),
	scaffold.WithInheritedLabel("env", "local"),
	scaffold.WithServices(db, queue),
	scaffold.WithServices(api),
)

In this example, every labeled resource created by the stack receives key= value and env=local.

Generated names

WithNamePrefix makes Docker resource names easier to recognize in Docker Desktop and docker ps output:

stack := scaffold.NewStack("app",
	scaffold.WithNamePrefix("hlfshell-dev"),
	scaffold.WithServices(db, redis),
)

Services that support generated names receive the prefix hlfshell-dev-app. Nested stacks extend the prefix - a child stack named data inside the app stack receives hlfshell-dev-app-data for its own services.

Docker Compose

scaffold/compose can wrap a Docker Compose project as a Service. This is useful when a local environment already exists as Compose YAML, or when you want to ship a small Compose file inside a Go binary and still expose the same scaffold lifecycle, CLI, logs, and resource discovery behavior.

Compose support uses Docker Compose project labels, especially com.docker.compose.project, to discover containers, networks, and volumes. That means a fresh Go process can still answer status, collect logs, or run down for a previously started Compose project as long as it uses the same project name.

By default, Create runs:

docker compose up -d --wait --wait-timeout 120

The wait timeout is configurable. You can also add a final Go readiness check for application-specific validation after Compose reports that the project is running or healthy.

package dev

import (
	"context"
	"embed"
	"fmt"
	"net/http"
	"time"

	"github.com/hlfshell/scaffold/compose"
)

//go:embed compose.yaml
var composeFiles embed.FS

func NewDevEnvironment() (*compose.Compose, error) {
	contents, err := composeFiles.ReadFile("compose.yaml")
	if err != nil {
		return nil, err
	}

	return compose.New("app-compose",
		compose.WithProject("app-dev"),
		compose.WithEmbeddedFile("compose.yaml", contents),
		compose.WithWaitTimeout(3*time.Minute),
		compose.WithReadyCheck(func(ctx context.Context, project *compose.Compose) error {
			req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/healthz", nil)
			if err != nil {
				return err
			}

			res, err := http.DefaultClient.Do(req)
			if err != nil {
				return err
			}
			defer res.Body.Close()

			if res.StatusCode != http.StatusOK {
				return fmt.Errorf("unexpected status: %s", res.Status)
			}

			return nil
		}),
	)
}

WithEmbeddedFile is for Go's go:embed flow: the Compose YAML is compiled into the binary, written to a temporary file when Compose runs, and removed after the command exits. Use WithFile("./compose.yaml") when the file should be read from disk instead.

Helpers

Services can export environment variables and endpoints. A Stack collects those values after it has been created:

err := stack.Create(ctx)
if err != nil {
	return err
}
defer stack.Cleanup(context.WithoutCancel(ctx))

env := stack.Env()
endpoint, ok := stack.Endpoint("postgres")

You can also write the environment to a dotenv-style file:

err = stack.WriteEnvFile(".env.scaffold")

For CLI and demo workflows, Summary gives a short view of service groups and known endpoints:

fmt.Println(stack.Summary())

/*
Stack app

Services:
  1. db, redis
  2. api

Endpoints:
  api = http://localhost:49154
  db  = localhost:49155
*/

To create an easy function helper to start a stack, run your functon, and then clean up the stack afterwards (regardless if there's an error), use Run:

err := scaffold.Run(ctx, stack, func(ctx context.Context) error {
	return runIntegrationTests(ctx)
})

Writing services

Most simple container-backed services should start with FromContainer. It keeps the common case small: start one container, wait until it is ready, expose endpoints, and forward container logs.

When the service needs typed behavior, write a small Go wrapper around the thing your application needs. At minimum, a service implements:

type Service interface {
	Name() string
	Create(ctx context.Context) error
	Cleanup(ctx context.Context) error
	Logs(ctx context.Context) (logs.LogStreams, error)
}

For a specialized container-backed service, the usual pattern looks like this. The sketch omits constructors and connection-string details so the lifecycle shape stays visible:

type Postgres struct {
	container *scaffoldcontainer.Container
	db        *sql.DB
}

func (p *Postgres) Name() string {
	return "postgres"
}

func (p *Postgres) Create(ctx context.Context) error {
	if err := p.container.Start(ctx); err != nil {
		return err
	}

	port, ok := p.container.HostPort("5432")
	if !ok {
		return fmt.Errorf("postgres container did not publish port 5432")
	}

	db, err := sql.Open("postgres", postgresURL(port))
	if err != nil {
		return err
	}
	p.db = db

	return scaffold.WaitFunc(ctx, 30*time.Second, 50*time.Millisecond, func(ctx context.Context) error {
		return p.db.PingContext(ctx)
	})
}

func (p *Postgres) Cleanup(ctx context.Context) error {
	if p.db != nil {
		_ = p.db.Close()
	}

	return p.container.Cleanup(ctx)
}

func (p *Postgres) Logs(ctx context.Context) (logs.LogStreams, error) {
	stream, err := p.container.Logs(ctx)
	if err != nil {
		return nil, err
	}

	return logs.LogStreams{"postgres": stream}, nil
}

func (p *Postgres) DB() *sql.DB {
	return p.db
}

Add helpers that are specific to the service, not generic configuration blobs. Postgres should have SQL helpers. MinIO should have bucket and object helpers. Qdrant should have collection and point helpers. The point is to make local infrastructure easy to call from Go without hiding what is being started.

Waiters and Readiness Checks

scaffold includes small readiness helpers:

scaffold.WaitForTCP(ctx, "localhost", port, 10*time.Second)
scaffold.WaitForHTTP(ctx, url, 200, 10*time.Second)
scaffold.WaitFunc(ctx, 10*time.Second, 50*time.Millisecond, func(ctx context.Context) error {
	return db.PingContext(ctx)
})

Building images

container.BuildDockerfile builds a Dockerfile and returns the image id and Docker build logs. It is useful as a preflight check before constructing or starting a container from a local image.

image, logs, err := scaffoldcontainer.BuildDockerfile(ctx, "./Dockerfile")
if err != nil {
	fmt.Println(logs)
	return err
}

fmt.Println(logs)
fmt.Println(image)

The build context is the directory containing the Dockerfile. Build failures still return the logs Docker emitted before the failure.

Cleanup

Containers are killed and removed during cleanup. Anonymous Docker volumes discovered on the container are also removed. Stacks clean service groups up in reverse creation order.

Documentation

Index

Constants

View Source
const (
	LabelManagedBy = "scaffold.managed-by"
	LabelStack     = "scaffold.stack"
	LabelService   = "scaffold.service"
	LabelRunID     = "scaffold.run-id"
)

Variables

This section is empty.

Functions

func CollectLogs

func CollectLogs(ctx context.Context, service Service) (logs.LogStreams, error)

CollectLogs returns the named log streams exposed by a service. For stacks, this recursively returns streams for child services and containers.

func MergedLogs

func MergedLogs(ctx context.Context, service Service) (io.ReadCloser, error)

MergedLogs returns a single reader containing all logs exposed by a service. Use CollectLogs when callers need to choose individual named streams.

func Run

func Run(ctx context.Context, stack *Stack, fn func(context.Context) error) error

Run creates a stack with ctx, runs fn, and then cleans the stack up. Cleanup is attempted even if fn returns an error.

func WaitForHTTP

func WaitForHTTP(ctx context.Context, url string, statusCode int, timeout time.Duration) error

WaitForHTTP waits until the URL returns the expected HTTP status code, or until the timeout is reached.

func WaitForLogText

func WaitForLogText(ctx context.Context, reader io.Reader, text string, timeout time.Duration) error

WaitForLogText scans a reader until the requested text appears. This is useful for containers that only advertise readiness through logs.

func WaitForTCP

func WaitForTCP(ctx context.Context, host string, port string, timeout time.Duration) error

WaitForTCP waits until a TCP connection can be opened to host:port, or until the timeout is reached.

func WaitFunc

func WaitFunc(ctx context.Context, timeout time.Duration, interval time.Duration, fn func(context.Context) error) error

WaitFunc retries a function until it returns nil or the timeout is reached. The last error is wrapped into the timeout error.

Types

type Connectable

type Connectable[T any] interface {
	Connect() (T, error)
	ConnectWithTimeout(timeout time.Duration) (T, error)
}

Connectable is implemented by harnesses that can return a typed client or connection after the service is running.

type ContainerService

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

ContainerService is a small service wrapper around Container. It is for simple services that do not need a toolbox package or custom typed clients.

func FromContainer

func FromContainer(container *scaffoldcontainer.Container, options ...ContainerServiceOption) (*ContainerService, error)

FromContainer wraps a plain Docker container as a Service. The service name defaults to the container name. Use WithName when the container is unnamed or when the scaffold service should have a different logical name than the Docker container.

func (*ContainerService) Cleanup

func (s *ContainerService) Cleanup(ctx context.Context) error

func (*ContainerService) Container

func (*ContainerService) Create

func (s *ContainerService) Create(ctx context.Context) error

func (*ContainerService) Endpoints

func (s *ContainerService) Endpoints() map[string]string

func (*ContainerService) Logs

func (*ContainerService) Name

func (s *ContainerService) Name() string

func (*ContainerService) SetLabels

func (s *ContainerService) SetLabels(labels map[string]string)

func (*ContainerService) SetNamePrefix

func (s *ContainerService) SetNamePrefix(prefix string)

func (*ContainerService) SetNetwork

func (s *ContainerService) SetNetwork(name string)

type ContainerServiceOption

type ContainerServiceOption func(*ContainerService)

func WithEndpoint

func WithEndpoint(name string, scheme string, port string) ContainerServiceOption

WithEndpoint exposes a named endpoint built from a published container port.

func WithHTTPReady

func WithHTTPReady(port string, path string, statusCode int, timeout time.Duration) ContainerServiceOption

WithHTTPReady waits for an HTTP status code on the requested container port after the container starts.

func WithName

func WithName(name string) ContainerServiceOption

WithName sets the scaffold service name for a container-backed service. It is useful when the Docker container is unnamed or when the service name should differ from the Docker container name.

func WithTCPReady

func WithTCPReady(port string, timeout time.Duration) ContainerServiceOption

WithTCPReady waits until the requested container port accepts TCP connections.

type ContainerStatus

type ContainerStatus struct {
	ID      string
	Name    string
	Image   string
	State   string
	Status  string
	Labels  map[string]string
	Running bool
}

type EndpointProvider

type EndpointProvider interface {
	Endpoints() map[string]string
}

EndpointProvider is implemented by services that can expose named local endpoints after creation.

type EnvProvider

type EnvProvider interface {
	Env() map[string]string
}

EnvProvider is implemented by services that can export environment variables for applications, tests, or CLI commands.

type LabelAttachable

type LabelAttachable interface {
	SetLabels(labels map[string]string)
}

LabelAttachable is implemented by harnesses that can receive Docker labels from a parent stack before they are created.

type NamePrefixAttachable

type NamePrefixAttachable interface {
	SetNamePrefix(prefix string)
}

NamePrefixAttachable is implemented by harnesses that can apply a stack name prefix to Docker resources before they are created.

type NetworkAttachable

type NetworkAttachable interface {
	SetNetwork(name string)
}

NetworkAttachable is implemented by harnesses that can join a shared Docker network before they are created.

type NetworkStatus

type NetworkStatus struct {
	ID     string
	Name   string
	Driver string
	Scope  string
	Labels map[string]string
}

type ResourceStatus

type ResourceStatus struct {
	Containers []ContainerStatus
	Networks   []NetworkStatus
	Volumes    []VolumeStatus
}

type Service

type Service interface {
	Name() string
	Create(ctx context.Context) error
	Cleanup(ctx context.Context) error
	Logs(ctx context.Context) (logs.LogStreams, error)
}

Service is the minimum lifecycle interface used by Stack. Services only need a name, a create step, and a cleanup step to be composable.

type Stack

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

Stack is a simple ordered collection of service groups. Services passed in the same WithServices call are created in parallel. Multiple WithServices calls are created in the order they are applied.

func NewStack

func NewStack(name string, options ...StackOption) *Stack

NewStack builds a stack with the provided options. A stack can contain any service harness that implements the Service interface.

func (*Stack) Cleanup

func (s *Stack) Cleanup(ctx context.Context) error

Cleanup handles service groups in reverse creation order. Services that were created in the same group are cleaned up in parallel.

func (*Stack) Create

func (s *Stack) Create(ctx context.Context) error

Create creates each service group in the order it was added. Services in the same group are created in parallel. If a service fails, services that were already created are cleaned up.

func (*Stack) Down

func (s *Stack) Down(ctx context.Context) error

Down removes Docker resources that match this stack's labels. Unlike Cleanup, Down does not require this process to have created the stack. It is intended for CLI and cross-session cleanup.

func (*Stack) Endpoint

func (s *Stack) Endpoint(name string) (string, bool)

Endpoint returns a named endpoint from the stack.

func (*Stack) Endpoints

func (s *Stack) Endpoints() map[string]string

Endpoints returns named endpoints contributed by services in the stack.

func (*Stack) Env

func (s *Stack) Env() map[string]string

Env returns environment variables contributed by services in the stack. Later services overwrite earlier services if they use the same key.

func (*Stack) IsRunning

func (s *Stack) IsRunning(ctx context.Context) (bool, error)

IsRunning returns true if Docker has at least one running container that matches this stack's labels.

func (*Stack) Labels

func (s *Stack) Labels() map[string]string

Labels returns the labels that identify the stack in Docker. Service labels are added separately for each child service.

func (*Stack) Logs

func (s *Stack) Logs(ctx context.Context) (logs.LogStreams, error)

Logs returns named log streams for every service in the stack. Child stack and service names are used as path segments, so callers can choose streams such as "api" or "data.postgres".

func (*Stack) Name

func (s *Stack) Name() string

func (*Stack) Resources

func (s *Stack) Resources(ctx context.Context) (ResourceStatus, error)

Resources returns Docker containers, networks, and volumes that match this stack's labels. This is the broad discovery API for determining which Docker resources belong to a stack.

func (*Stack) RunningContainers

func (s *Stack) RunningContainers(ctx context.Context) ([]ContainerStatus, error)

RunningContainers returns Docker containers that match this stack's labels. This is how a defined Go stack can answer whether its matching local environment is already running.

func (*Stack) Service

func (s *Stack) Service(name string) (Service, bool)

Service returns a service by name if it exists in the stack; nil and false if not.

func (*Stack) Services

func (s *Stack) Services() []Service

func (*Stack) SetLabels

func (s *Stack) SetLabels(labels map[string]string)

SetLabels merges labels inherited from a parent stack. Child services and child stacks receive these labels before they are created.

func (*Stack) SetNamePrefix

func (s *Stack) SetNamePrefix(prefix string)

SetNamePrefix receives a parent stack's prefix. Child services receive that prefix plus this stack's name when the stack is created.

func (*Stack) SetNetwork

func (s *Stack) SetNetwork(name string)

SetNetwork pushes an inherited Docker network into this stack and its children. A stack with an inherited network does not own that network. Children services should adopt the network upon start.

func (*Stack) Summary

func (s *Stack) Summary() string

Summary returns a human-readable description of the stack service groups and known endpoints.

func (*Stack) WriteEnvFile

func (s *Stack) WriteEnvFile(path string) error

WriteEnvFile writes stack environment variables to a dotenv-style file.

type StackOption

type StackOption func(*Stack)

StackOption configures a stack at construction time.

func WithInheritedLabel

func WithInheritedLabel(label string, value string) StackOption

WithInheritedLabel adds a key/value inherited label. Inherited labels are pushed down to child services and child stacks.

func WithInheritedLabels

func WithInheritedLabels(labels map[string]string) StackOption

WithInheritedLabels adds multiple inherited labels to the stack.

func WithNamePrefix

func WithNamePrefix(prefix string) StackOption

WithNamePrefix prefixes Docker resource names for services that support SetNamePrefix. For stack "app" and prefix "dev", child service resources receive the prefix "dev-app".

func WithRunID

func WithRunID(runID string) StackOption

WithRunID sets the run identity used when finding existing Docker resources for this stack. If not set, a run id is generated when Create starts.

func WithServices

func WithServices(services ...Service) StackOption

WithServices adds a group of services to the stack. Services passed in the same call are created in parallel. Separate WithServices calls are created in call order.

func WithSharedNetwork

func WithSharedNetwork() StackOption

WithSharedNetwork creates a Docker network for the stack and attaches services that support SetNetwork before they are created.

type VolumeStatus

type VolumeStatus struct {
	Name       string
	Driver     string
	Mountpoint string
	Labels     map[string]string
}

Directories

Path Synopsis
Package cli turns a scaffold service or stack into a small command-line application.
Package cli turns a scaffold service or stack into a small command-line application.

Jump to

Keyboard shortcuts

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