orale package - github.com/RobertWHurst/orale - Go Packages

orale

package module
v1.7.1 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2025 License: MIT Imports: 12 Imported by: 0

README

Órale (Oh-rah-leh)

A fantastic little config loader for Go that collects configuration from flags, environment variables, and configuration files, then marshals the values into your structs.

Órale is pronounced "Odelay" in English. The name is Mexican slang which translates roughly to "listen up" or "what's up?", and is pronounced like "Oh-rah-leh".



Features

  • 🎯 Single unified API - Load from flags, environment variables, and config files
  • 📝 Multiple sources - Configurable precedence: flags > environment > config files
  • 🔄 Automatic type conversion - String, int, bool, float, time.Duration, time.Time, and more
  • 🪆 Nested configuration - Support for nested structs and embedded fields
  • 📋 Slice support - Multi-value configuration (multiple flags, array values)
  • 🎨 Flexible naming - Automatically converts between camelCase, snake_case, and kebab-case
  • 🔀 Variable expansion - Reference environment variables, other config values, or file contents
  • ⚙️ Environment-specific configs - Load different configs for dev, staging, production
  • 🔧 Default values - Keep defaults in your structs, override only what you need

Installation

go get github.com/RobertWHurst/orale

Quick Start

package main

import (
    "fmt"
    "time"
    "github.com/RobertWHurst/orale"
)

type Config struct {
    Port    int           `config:"port"`
    Timeout time.Duration `config:"timeout"`
}

func main() {
    loader, err := orale.Load("myApp")
    if err != nil {
        panic(err)
    }

    config := Config{
        Port:    8080,  // defaults
        Timeout: time.Second * 30,
    }

    if err := loader.GetAll(&config); err != nil {
        panic(err)
    }

    fmt.Printf("Server starting on port %d with timeout %v\n", config.Port, config.Timeout)
}

Now you can configure it via:

  • Flag: ./myApp --port=3000 --timeout=5m
  • Environment: MY_APP__PORT=3000 MY_APP__TIMEOUT=5m
  • Config file (myApp.config.toml):
    port = 3000
    timeout = "5m"
    

Application Name

The application name you pass to Load() should be in camelCase. Orale uses this name to:

  • Generate the environment variable prefix by converting to uppercase with underscores (myAppMY_APP__)
  • Locate the config file by converting to lowercase with hyphens (myAppmy-app.config.toml)

For example, orale.Load("myApp") will look for environment variables like MY_APP__PORT and a config file named my-app.config.toml.

Configuration Sources

Orale loads configuration from three sources, with the following precedence (highest to lowest):

  1. Command-line flags - --key=value
  2. Environment variables - APP_NAME__KEY=value
  3. Configuration files - app-name.config.toml
Naming Conventions and Path Syntax

Orale automatically converts between different naming conventions and path delimiters:

Source Format Nesting Delimiter Example
Struct tags camelCase . (dot) config:"database.connectionUri"
Flags kebab-case -- (double dash) --database--connection-uri=...
Environment vars UPPER_SNAKE_CASE __ (double underscore) APP__DATABASE__CONNECTION_URI=...
TOML files snake_case [sections] [database] then connection_uri = ...

Important path rules:

  1. Flags use double-dashes for nesting:

    • --database--host=localhost
    • --server--tls--cert-path=/path/to/cert
    • The double-dash separates path segments
  2. Environment variables use double underscores for nesting:

    # Single level
    APP__PORT=8080
    
    # Nested
    APP__DATABASE__HOST=localhost
    APP__DATABASE__CONNECTION_POOL_SIZE=10
    
    # Deeply nested
    APP__SERVER__TLS__CERT_PATH=/path/to/cert
    
  3. TOML files use sections and snake_case:

    port = 8080
    
    [database]
    host = "localhost"
    connection_pool_size = 10
    
    [server.tls]
    cert_path = "/path/to/cert"
    
  4. All formats automatically convert to camelCase paths internally:

    • --server-portserverPort
    • APP__SERVER_PORTserverPort
    • TOML server_portserverPort
    • Important: Struct config tags must be in camelCase to match these converted paths:
      Port int `config:"serverPort"`  // Correct - matches converted paths
      Port int `config:"server_port"` // Wrong - won't match
      

Variable Expansion

Orale supports variable expansion in configuration values, allowing you to reference environment variables, other configuration values, or file contents. This is useful for building dynamic configuration values, referencing secrets, and avoiding duplication.

Variable Types

Orale supports three types of variable expansion:

Syntax Type Description Example
${VAR} Environment Variable Expands to the value of the environment variable VAR ${HOME}, ${DATABASE_PASSWORD}
%{key} Config Variable Expands to the value of another config key %{baseUrl}, %{database.host}
@{path} File Variable Expands to the contents of the file at path (strips trailing newline) @{/secrets/api-key}, @{./cert.pem}

Key features:

  • Variables can be combined in a single value
  • Expansion happens recursively (config variables can contain other variables)
  • Config variables support any naming convention (snake_case, kebab-case, camelCase)
  • Config variables can reference any value type (strings, integers, floats, bools)
  • File contents have trailing newlines automatically stripped
  • Circular references are detected and return an error
Environment Variables: ${VAR}

Reference environment variables using ${VAR} syntax:

type Config struct {
    DatabaseURL string `config:"databaseUrl"`
    APIKey      string `config:"apiKey"`
}

Configuration file (myApp.config.toml):

database_url = "postgres://${DB_HOST}:${DB_PORT}/mydb"
api_key = "${API_SECRET}"

Run with environment variables:

DB_HOST=localhost DB_PORT=5432 API_SECRET=secret123 ./myApp

Result:

  • config.DatabaseURL = "postgres://localhost:5432/mydb"
  • config.APIKey = "secret123"

You can also use environment variables directly:

# Environment variables (higher precedence than config files)
MY_APP__DATABASE_URL='postgres://${DB_HOST}:${DB_PORT}/mydb' \
MY_APP__API_KEY='${API_SECRET}' \
DB_HOST=localhost \
DB_PORT=5432 \
API_SECRET=secret123 \
./myApp
Config Variables: %{key}

Reference other configuration values using %{key} syntax. This is useful for building related URLs or avoiding duplication. Config variable paths support any naming convention - use whatever matches your configuration source (snake_case for TOML, kebab-case for flags, or direct camelCase):

type Config struct {
    BaseURL    string `config:"baseUrl"`
    APIURL     string `config:"apiUrl"`
    WebhookURL string `config:"webhookUrl"`
}

Configuration file (myApp.config.toml):

base_url = "https://api.example.com"
api_url = "%{base_url}/v1"
webhook_url = "%{base_url}/webhooks/incoming"

Result:

  • config.BaseURL = "https://api.example.com"
  • config.APIURL = "https://api.example.com/v1"
  • config.WebhookURL = "https://api.example.com/webhooks/incoming"

Nested paths: Use dots, double-underscores, or double-dashes to reference nested values:

[database]
host = "db.example.com"
port = 5432

[database]
# All of these work the same:
connection_string = "postgres://%{database.host}:%{database.port}/mydb"
# Or with double-underscore (TOML/env style):
connection_string = "postgres://%{database__host}:%{database__port}/mydb"
# Or with double-dash (flag style):
connection_string = "postgres://%{database--host}:%{database--port}/mydb"

Note: Config variables automatically convert non-string values (integers, floats, bools) to strings, so you can reference any configuration value.

File Variables: @{path}

Read file contents using @{path} syntax. This is ideal for reading secrets, certificates, or API keys from files:

type Config struct {
    APIKey      string `config:"apiKey"`
    Certificate string `config:"certificate"`
}

Configuration file (myApp.config.toml):

api_key = "@{/run/secrets/api-key}"
certificate = "@{/etc/ssl/certs/app-cert.pem}"

With files:

# /run/secrets/api-key
sk_live_abc123def456

# /etc/ssl/certs/app-cert.pem
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKZ...
-----END CERTIFICATE-----

Result:

  • config.APIKey = "sk_live_abc123def456"
  • config.Certificate = Full certificate content (trailing newline removed)

Security note: File variables are perfect for Docker secrets, Kubernetes mounted secrets, or any secrets management system that writes secrets to files.

Combining Variable Types

You can combine all three variable types in a single value:

type Config struct {
    ConnectionString string `config:"connectionString"`
}

Configuration file (myApp.config.toml):

[database]
host = "dbserver.example.com"

connection_string = "host=%{database.host};password=${DB_PASSWORD};cert=@{/secrets/db-cert.pem}"

Run with:

DB_PASSWORD=securepass123 ./myApp

Result:

config.ConnectionString = "host=dbserver.example.com;password=securepass123;cert=-----BEGIN CERTIFICATE-----..."
Practical Examples
Docker Secrets Pattern
# myApp.config.toml
[database]
host = "postgres"
port = 5432
name = "myapp"
user = "myapp"
password = "@{/run/secrets/db_password}"

[database]
url = "postgres://%{database.user}:%{database.password}@%{database.host}:%{database.port}/%{database.name}"
# docker-compose.yml
services:
  app:
    image: myapp:latest
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt
Multi-Environment Configuration
# myApp.production.config.toml
environment = "production"
base_url = "https://${DOMAIN}"
api_url = "%{baseUrl}/api"
cdn_url = "%{baseUrl}/cdn"

[security]
tls_cert = "@{/etc/letsencrypt/live/${DOMAIN}/fullchain.pem}"
tls_key = "@{/etc/letsencrypt/live/${DOMAIN}/privkey.pem}"
Development with Local Overrides
# myApp.config.toml (checked into git)
[database]
host = "localhost"
port = 5432

[api]
base_url = "%{protocol}://%{database.host}:${API_PORT}"
protocol = "http"
# .env file (not checked into git)
API_PORT=8080
Error Handling

Orale will return an error for:

  • Circular references: a = "%{b}" and b = "%{a}"
  • Missing closing brace: "${UNCLOSED"
  • Non-existent files: "@{/path/that/does/not/exist}"
  • Invalid UTF-8 in files: File contents must be valid UTF-8

Example:

loader, _ := orale.Load("myApp")
if err := loader.GetAll(&config); err != nil {
    // Handle expansion errors
    fmt.Printf("Configuration error: %v\n", err)
}

Type Conversions

Orale automatically converts configuration values to match your struct field types:

Basic Types
type Config struct {
    Name    string  `config:"name"`
    Port    int     `config:"port"`
    Enabled bool    `config:"enabled"`
    Ratio   float64 `config:"ratio"`
}
Time Types

time.Duration - Accepts human-readable strings or nanoseconds:

type Config struct {
    Timeout        time.Duration `config:"timeout"`
    RetryInterval  time.Duration `config:"retryInterval"`
}

Valid formats:

  • Strings: "5h", "30m", "1h30m", "500ms"
  • Numbers: 1000000000 (nanoseconds)

time.Time - Accepts various timestamp formats:

type Config struct {
    StartTime time.Time `config:"startTime"`
    Birthday  time.Time `config:"birthday"`
}

Valid formats:

  • RFC3339: "2025-01-15T10:30:00Z"
  • RFC3339Nano: "2025-01-15T10:30:00.123456789Z"
  • ISO Date: "2025-01-15"
  • Unix timestamp: 1736938200 (int or float)
Slices

Collect multiple values into slice fields:

Slices of Primitives
type Config struct {
    Servers []string `config:"servers"`
    Ports   []int    `config:"ports"`
}

Flags - Two approaches:

  1. Repeat the same flag multiple times:
./app --servers=host1 --servers=host2 --servers=host3
  1. Use indexed notation:
./app --servers--0=host1 --servers--1=host2 --servers--2=host3

Environment variables - Use indexed notation:

APP__SERVERS__0=host1
APP__SERVERS__1=host2
APP__SERVERS__2=host3

TOML - Use array syntax:

servers = ["host1", "host2", "host3"]
ports = [8080, 8081]
Slices of Structs

For slices of structs, you must use indexed notation to specify individual fields:

type Server struct {
    Host string `config:"host"`
    Port int    `config:"port"`
}

type Config struct {
    Servers []Server `config:"servers"`
}

Flags - Use indexed notation with nested fields:

./app --servers--0--host=localhost --servers--0--port=8080 \
      --servers--1--host=example.com --servers--1--port=9090

Environment variables - Use indexed notation with nested fields:

APP__SERVERS__0__HOST=localhost
APP__SERVERS__0__PORT=8080
APP__SERVERS__1__HOST=example.com
APP__SERVERS__1__PORT=9090

TOML - Use array of tables syntax:

[[servers]]
host = "localhost"
port = 8080

[[servers]]
host = "example.com"
port = 9090

Note: Indices can be sparse (e.g., servers.0, servers.4). Orale will create a slice with the appropriate length, and unset indices will have zero values.

Nested Structs

Nested structs create configuration paths using their config tag:

type TLSConfig struct {
    CertPath string `config:"certPath"`
    KeyPath  string `config:"keyPath"`
}

type ServerConfig struct {
    Host string    `config:"host"`
    Port int       `config:"port"`
    TLS  TLSConfig `config:"tls"`
}

type Config struct {
    Server ServerConfig `config:"server"`
}

The resulting paths are:

  • server.host
  • server.port
  • server.tls.certPath
  • server.tls.keyPath

Configure with:

# Flags (double-dash notation for nesting)
./app --server--host=0.0.0.0 --server--port=8080
./app --server--tls--cert-path=/path/to/cert --server--tls--key-path=/path/to/key

# Environment (double underscores)
APP__SERVER__HOST=0.0.0.0
APP__SERVER__PORT=8080
APP__SERVER__TLS__CERT_PATH=/path/to/cert
APP__SERVER__TLS__KEY_PATH=/path/to/key

# TOML (sections and nested sections)
[server]
host = "0.0.0.0"
port = 8080

[server.tls]
cert_path = "/path/to/cert"
key_path = "/path/to/key"
Pointers and Default Values
type Config struct {
    // Nil pointer - will be initialized if config is provided
    Database *DatabaseConfig `config:"database"`

    // Value with default - will be overridden if config is provided
    Port int `config:"port"` // default: 0
}

func main() {
    config := Config{
        Port: 8080, // default value
    }

    loader.GetAll(&config)
    // Port will be 8080 unless overridden by flags/env/file
}
Embedded Structs
type ServerConfig struct {
    Host string `config:"host"`
    Port int    `config:"port"`
}

type Config struct {
    ServerConfig // embedded - fields are promoted
    Debug bool `config:"debug"`
}

// Access: config.Host, config.Port, config.Debug

Configuration Files

Orale looks for TOML configuration files with the following naming pattern:

<app-name>.config.toml
<app-name>.<environment>.config.toml
File Discovery

Files are searched in the current directory and all parent directories, walking up the filesystem tree from the working directory to the root.

This allows you to place config files at the project root and run your app from subdirectories.

Environment-Specific Configs

Load different configs for different environments by setting the configEnvironment value:

Via flag:

./myApp --config-environment=production

Via environment variable:

MY_APP__CONFIG_ENVIRONMENT=production ./myApp

This will look for myApp.production.config.toml instead of myApp.config.toml.

Common patterns:

# Development (default - no environment specified)
./myApp  # loads myApp.config.toml

# Staging
./myApp --config-environment=staging  # loads myApp.staging.config.toml

# Production
MY_APP__CONFIG_ENVIRONMENT=production ./myApp  # loads myApp.production.config.toml

# QA
./myApp --config-environment=qa  # loads myApp.qa.config.toml
TOML Format
# Basic values
port = 8080
enabled = true
timeout = "5m"

# Nested configuration
[database]
host = "localhost"
port = 5432
connection_pool_size = 10

# Arrays
servers = ["host1:8080", "host2:8080", "host3:8080"]

# Time values
start_time = "2025-01-15T10:30:00Z"
retry_interval = "30s"

Advanced Usage

Loading Specific Paths (Sub-pathing)

You can load just a subset of your configuration using path strings with Get():

type DatabaseConfig struct {
    Host string `config:"host"`
    Port int    `config:"port"`
}

type ServerConfig struct {
    Port int `config:"port"`
}

type Config struct {
    Database DatabaseConfig `config:"database"`
    Server   ServerConfig   `config:"server"`
}

loader, _ := orale.Load("myApp")

// Load entire config
var fullConfig Config
loader.GetAll(&fullConfig)

// Load only database config
var dbConfig DatabaseConfig
loader.Get("database", &dbConfig)

// Load only server config
var serverConfig ServerConfig
loader.Get("server", &serverConfig)

Path syntax:

  • Paths use dot notation: "database", "server.tls", "database.connection"
  • Paths map to struct field config tags (in camelCase)

How sources map to paths:

Given the path "database.host":

# Flags (double-dash notation for nesting)
./app --database--host=localhost

# Environment (double-underscore notation for nesting)
APP__DATABASE__HOST=localhost

# TOML (sections for nesting)
[database]
host = "localhost"
Array and Slice Paths

Arrays and slices use dot notation with numeric indices:

type Config struct {
    Servers []ServerConfig `config:"servers"`
}

Configuration:

[[servers]]
host = "server1"
port = 8080

[[servers]]
host = "server2"
port = 8081

Internally, Orale resolves these as:

  • servers.0.host"server1"
  • servers.0.port8080
  • servers.1.host"server2"
  • servers.1.port8081

Sparse indices are supported: If you set non-contiguous indices (e.g., servers.0 and servers.4), Orale will create a slice with the appropriate length (5 in this case), and unset indices will have zero values.

You cannot directly load a single array element with Get(), but you can load the entire array and access elements in code.

Multiple Configuration Structs
type DatabaseConfig struct {
    Host string `config:"host"`
    Port int    `config:"port"`
}

type ServerConfig struct {
    Port    int           `config:"port"`
    Timeout time.Duration `config:"timeout"`
}

type Config struct {
    Database DatabaseConfig `config:"database"`
    Server   ServerConfig   `config:"server"`
}

Configure with namespaced keys:

[database]
host = "db.example.com"
port = 5432

[server]
port = 8080
timeout = "30s"
Must Variants

Panic instead of returning errors:

loader := orale.MustLoad("myApp")
loader.MustGetAll(&config)

Complete Example

package main

import (
    "fmt"
    "time"
    "github.com/RobertWHurst/orale"
)

type DatabaseConfig struct {
    Host               string `config:"host"`
    Port               int    `config:"port"`
    ConnectionPoolSize int    `config:"connectionPoolSize"`
}

type ServerConfig struct {
    Host    string        `config:"host"`
    Port    int           `config:"port"`
    Timeout time.Duration `config:"timeout"`
}

type Config struct {
    Database DatabaseConfig `config:"database"`
    Server   ServerConfig   `config:"server"`
    Debug    bool           `config:"debug"`
}

func main() {
    // Set defaults
    config := Config{
        Database: DatabaseConfig{
            Host:               "localhost",
            Port:               5432,
            ConnectionPoolSize: 5,
        },
        Server: ServerConfig{
            Host:    "0.0.0.0",
            Port:    8080,
            Timeout: 30 * time.Second,
        },
        Debug: false,
    }

    // Load configuration
    loader, err := orale.Load("myApp")
    if err != nil {
        panic(err)
    }

    if err := loader.GetAll(&config); err != nil {
        panic(err)
    }

    fmt.Printf("Database: %s:%d (pool: %d)\n",
        config.Database.Host,
        config.Database.Port,
        config.Database.ConnectionPoolSize)

    fmt.Printf("Server: %s:%d (timeout: %v)\n",
        config.Server.Host,
        config.Server.Port,
        config.Server.Timeout)

    fmt.Printf("Debug mode: %v\n", config.Debug)
}

Configuration file (myApp.config.toml):

debug = true

[database]
host = "db.example.com"
port = 5432
connection_pool_size = 20

[server]
host = "0.0.0.0"
port = 3000
timeout = "5m"

Override with environment:

MY_APP__SERVER__PORT=8080 MY_APP__DEBUG=false ./myApp

Override with flags:

./myApp --server--port=8080 --debug=false

API Reference

Load(appName string) (*Loader, error)

Creates a new configuration loader for the given application name.

The application name is used to:

  • Generate the environment variable prefix (e.g., "myApp"MY_APP__)
  • Locate configuration files (e.g., "myApp"myApp.config.toml)

Returns: A Loader instance or an error if configuration files cannot be parsed.

Example:

loader, err := orale.Load("myApp")
MustLoad(appName string) *Loader

Like Load, but panics on error instead of returning an error.

(*Loader).Get(path string, target interface{}) error

Loads configuration at the given path into the target struct.

Parameters:

  • path - Dot-separated path to configuration (camelCase)
    • Single level: "database", "server", "port"
    • Nested: "database.connection", "server.tls.certPath"
    • Paths must match struct field config tags (in camelCase)
  • target - Pointer to struct to populate

Examples:

// Load subset at path "database"
var dbConfig DatabaseConfig
loader.Get("database", &dbConfig)

// Load deeply nested path
var tlsConfig TLSConfig
loader.Get("server.tls", &tlsConfig)

Returns: Error if path doesn't exist or target is not a pointer.

(*Loader).GetAll(target interface{}) error

Loads all configuration into the target struct.

Example:

var config Config
loader.GetAll(&config)
(*Loader).MustGet(path string, target interface{})

Like Get, but panics on error instead of returning an error.

(*Loader).MustGetAll(target interface{})

Like GetAll, but panics on error instead of returning an error.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

[Include your license here]

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Test_SetArgs added in v1.2.0

func Test_SetArgs(args []string)

func Test_SetEnvironment added in v1.2.0

func Test_SetEnvironment(env []string)

func Test_SetWorkingDir added in v1.2.0

func Test_SetWorkingDir(dir string)

Types

type File

type File struct {
	// Path is the absolute path to the configuration file.
	Path string
	// Values is a map of configuration values loaded from the file. Note that
	// these values are flattened into paths separated by periods. Slice indexes
	// are represented by a period followed by the numeric index. The value is
	// always a slice of any. It's a slice because theoretically a configuration
	// file could have multiple values for the same path. This is not the case with
	// toml so as of now it's always a slice of length 1.
	Values map[string][]any
}

File represents a configuration file loaded from disk.

type Loader

type Loader struct {
	// FlagValues is a map of flag values by path.
	FlagValues map[string][]any
	// EnvironmentValues is a map of environment variable values by path.
	EnvironmentValues map[string][]any
	// ConfigurationFiles is a slice of configuration files.
	ConfigurationFiles []*File
	// contains filtered or unexported fields
}

Loader is a struct that contains all the values loaded from flags, environment variables, and configuration files. It can be used to marshal the values into a struct.

func Load

func Load(applicationName string) (*Loader, error)

Load loads configuration values from flags, environment variables, and configuration files. Flags are taken from `os.Args[1:]`. Environment variables are taken from `os.Environ()`. Configuration files are taken from the working directory and all parent directories. The configuration file name is the application name with the extension `.config.toml`.

func LoadFromValues

func LoadFromValues(programArgs []string, envVarPrefix string, envVars []string, configSearchStartPath string, configFileNames []string) (*Loader, error)

LoadFromValues works like Load, but allows the caller to specify configuration such as flag and environment values, as well as which path to start searching for configuration files and which configuration file names to look for.

func (*Loader) Get

func (l *Loader) Get(path string, target any) error

```go

type TestConfig struct {
	Database struct {
		ConnectionUri string `config:"connection_uri"`
	} `config:"database"`
	Server struct {
		Port int `config:"port"`
	} `config:"server"`
	Channels []struct {
		Name string `config:"name"`
		Id   int    `config:"id"`
	} `config:"channels"`
}

loader, err := orale.Load("my-app")
if err != nil {
	panic(err)
}

var testConfig TestConfig
if err := loader.Get("", &testConfig); err != nil {
	panic(err)
}

```

As you can see in the example above, the TestConfig struct is populated with the loaded configuration values. The property names of each field are specified by the `config` tag. If the `config` tag is not specified, the property name is converted to snake case. For example `ConnectionUri` becomes `connection_uri` path.

func (*Loader) GetAll added in v1.1.0

func (l *Loader) GetAll(target any) error

GetAll populates loaded configuration values into the target value. This should be a pointer type, usually a pointer to a struct. All loaded configuration values will be set into the target value. Any default values in the target value that are not present in the loaded configuration will be left unchanged.

func (*Loader) MustGet

func (l *Loader) MustGet(path string, target any)

MustGet is the same as Get except it panics if an error occurs.

func (*Loader) MustGetAll added in v1.1.0

func (l *Loader) MustGetAll(target any)

MustGetAll is the same as GetAll except it panics if an error occurs.

Jump to

Keyboard shortcuts

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