graphql

package module
v0.0.0-...-25d3f34 Latest Latest
Warning

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

Go to latest
Published: Jan 16, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

GraphQL Extension for Forge

Production-ready GraphQL server extension powered by gqlgen, providing type-safe GraphQL APIs with automatic code generation, schema management, and full observability support.

Features

  • Type-safe GraphQL - Automatic code generation from GraphQL schemas
  • Multiple transports - HTTP (GET/POST), WebSocket subscriptions
  • Built-in optimization - Query complexity limits, automatic persisted queries (APQ)
  • DataLoader support - N+1 query optimization with batching and caching
  • Custom directives - Authentication, authorization, and custom logic
  • Apollo Federation v2 - Microservices composition ready
  • File uploads - Multipart form data support
  • GraphQL Playground - Interactive query exploration
  • Full observability - Logging, metrics, and tracing integration
  • DI integration - Access to Forge services via dependency injection

Installation

go get github.com/xraph/forge/extensions/graphql
go get github.com/99designs/[email protected]

Quick Start

1. Define Your GraphQL Schema

Create schema/schema.graphql:

directive @auth(requires: String) on FIELD_DEFINITION

type Query {
  hello(name: String!): String!
  version: String!
}

type Mutation {
  echo(message: String!): String!
}
2. Generate Code
go run github.com/99designs/gqlgen generate
3. Implement Resolvers

Edit schema.resolvers.go:

func (r *queryResolver) Hello(ctx context.Context, name string) (string, error) {
    r.logger.Debug("hello query called", forge.F("name", name))
    return fmt.Sprintf("Hello, %s!", name), nil
}
4. Register Extension
package main

import (
    "context"
    "github.com/xraph/forge"
    "github.com/xraph/forge/extensions/graphql"
)

func main() {
    // Create Forge app
    app := forge.NewApp(forge.DefaultAppConfig())

    // Register GraphQL extension
    gqlExt := graphql.NewExtension(
        graphql.WithEndpoint("/graphql"),
        graphql.WithPlayground(true),
        graphql.WithMaxComplexity(1000),
        graphql.WithQueryCache(true, 5*time.Minute),
    )
    
    if err := app.RegisterExtension(gqlExt); err != nil {
        log.Fatal(err)
    }

    // Start the app
    if err := app.Start(context.Background()); err != nil {
        log.Fatal(err)
    }

    // Run server
    if err := app.Run(context.Background(), ":8080"); err != nil {
        log.Fatal(err)
    }
}
5. Query Your API
# Query
curl -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ hello(name: \"World\") }"}'

# Or visit the playground
open http://localhost:8080/playground

Configuration

Full Configuration Example
config := graphql.DefaultConfig()
config.Endpoint = "/api/graphql"
config.PlaygroundEndpoint = "/api/playground"
config.EnablePlayground = true
config.EnableIntrospection = true

// Performance
config.MaxComplexity = 1000
config.MaxDepth = 15
config.QueryTimeout = 30 * time.Second

// DataLoader
config.EnableDataLoader = true
config.DataLoaderBatchSize = 100
config.DataLoaderWait = 10 * time.Millisecond

// Caching
config.EnableQueryCache = true
config.QueryCacheTTL = 5 * time.Minute
config.MaxCacheSize = 1000

// Security
config.EnableCORS = true
config.AllowedOrigins = []string{"https://example.com"}
config.MaxUploadSize = 10 * 1024 * 1024 // 10MB

// Observability
config.EnableMetrics = true
config.EnableTracing = true
config.EnableLogging = true
config.LogSlowQueries = true
config.SlowQueryThreshold = 1 * time.Second

ext := graphql.NewExtensionWithConfig(config)
Environment Variables/Config File
# config.yaml
extensions:
  graphql:
    endpoint: "/graphql"
    playground_endpoint: "/playground"
    enable_playground: true
    enable_introspection: true
    max_complexity: 1000
    max_depth: 15
    query_timeout: 30s
    enable_dataloader: true
    dataloader_batch_size: 100
    dataloader_wait: 10ms
    enable_query_cache: true
    query_cache_ttl: 5m
    max_cache_size: 1000
    enable_cors: true
    allowed_origins:
      - "https://example.com"
    max_upload_size: 10485760
    enable_metrics: true
    enable_tracing: true
    enable_logging: true
    log_slow_queries: true
    slow_query_threshold: 1s

Advanced Features

DataLoader for N+1 Query Optimization
import "github.com/xraph/forge/extensions/graphql/dataloader"

// Create a loader
config := dataloader.DefaultLoaderConfig()
loader := dataloader.NewLoader(config, func(ctx context.Context, keys []interface{}) ([]interface{}, []error) {
    // Batch load users by IDs
    ids := make([]int, len(keys))
    for i, key := range keys {
        ids[i] = key.(int)
    }
    
    users, err := db.GetUsersByIDs(ctx, ids)
    if err != nil {
        errs := make([]error, len(keys))
        for i := range errs {
            errs[i] = err
        }
        return nil, errs
    }
    
    // Return in same order as keys
    results := make([]interface{}, len(keys))
    for i, user := range users {
        results[i] = user
    }
    return results, make([]error, len(keys))
})

// Use in resolver
func (r *userResolver) Friends(ctx context.Context, obj *User) ([]*User, error) {
    friends, err := loader.LoadMany(ctx, obj.FriendIDs)
    if err != nil {
        return nil, err
    }
    return friends.([]*User), nil
}
Custom Directives

Define in schema:

directive @auth(requires: String) on FIELD_DEFINITION

type Query {
  adminOnly: String! @auth(requires: "admin")
  user: String! @auth
}

Implement directive:

import "github.com/xraph/forge/extensions/graphql/directives"

// Configure in gqlgen.yml
// directives:
//   auth:
//     skip_runtime: false

// Use built-in auth directive or implement custom
func (r *Resolver) Directive() generated.DirectiveRoot {
    return generated.DirectiveRoot{
        Auth: directives.Auth,
    }
}
Apollo Federation v2

Configure in gqlgen.yml:

federation:
  filename: generated/federation.go
  package: generated
  version: 2

Define federated schema:

extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])

type User @key(fields: "id") {
  id: ID!
  name: String!
}

extend type Product @key(fields: "id") {
  id: ID! @external
  reviews: [Review!]!
}
File Uploads
scalar Upload

type Mutation {
  uploadFile(file: Upload!): String!
}
func (r *mutationResolver) UploadFile(ctx context.Context, file graphql.Upload) (string, error) {
    content, err := io.ReadAll(file.File)
    if err != nil {
        return "", err
    }
    
    // Process file...
    return fmt.Sprintf("Uploaded %s (%d bytes)", file.Filename, len(content)), nil
}

Dependency Injection

Access Forge services in resolvers:

type Resolver struct {
    container forge.Container
    logger    forge.Logger
    metrics   forge.Metrics
    config    Config
}

func (r *queryResolver) GetData(ctx context.Context) (*Data, error) {
    // Resolve database from DI using helper function
    db, err := database.GetDatabase(r.container)
    if err != nil {
        return nil, err
    }
    
    // Use services
    r.logger.Info("fetching data")
    r.metrics.Counter("data_requests").Inc()
    
    return db.QueryData(ctx)
}

Observability

Logging
// Automatic operation logging
[DEBUG] graphql operation start {operation: "GetUser", type: "query"}
[DEBUG] graphql operation complete {operation: "GetUser", duration: "15ms"}

// Slow query logging
[WARN] slow query detected {operation: "ComplexQuery", duration: "1.5s", query: "..."}

// Error logging
[ERROR] graphql error {operation: "UpdateUser", error: "validation failed", path: ["updateUser"]}
Metrics

Automatically collected:

  • graphql_operation_duration_seconds - Operation latency histogram
  • graphql_operation_total - Total operations counter

Both tagged with:

  • operation - Operation name
  • type - query, mutation, or subscription
Tracing

Full distributed tracing support via OpenTelemetry integration.

Testing

func TestGraphQLQuery(t *testing.T) {
    container := forge.NewContainer()
    logger := forge.NewNoopLogger()
    metrics := forge.NewNoOpMetrics()
    config := graphql.DefaultConfig()

    server, err := graphql.NewGraphQLServer(config, logger, metrics, container)
    if err != nil {
        t.Fatal(err)
    }

    handler := server.HTTPHandler()

    query := `{"query":"{ hello(name: \"Test\") }"}`
    req := httptest.NewRequest("POST", "/graphql", strings.NewReader(query))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    
    handler.ServeHTTP(w, req)

    if w.Code != 200 {
        t.Errorf("expected 200, got %d", w.Code)
    }
}

Performance Best Practices

  1. Enable Query Caching - Use APQ for frequently executed queries
  2. Set Complexity Limits - Prevent resource exhaustion
  3. Use DataLoader - Batch and cache data loading
  4. Enable Compression - Reduce bandwidth
  5. Monitor Slow Queries - Optimize with logging and tracing

Troubleshooting

Schema Changes Not Reflected

Regenerate code after schema changes:

cd v2/extensions/graphql
go run github.com/99designs/gqlgen generate
Type Conflicts

Rename conflicting types in graphql.go or use autobind in gqlgen.yml:

autobind:
  - github.com/xraph/forge/extensions/graphql/models
High Memory Usage
  • Reduce MaxCacheSize
  • Lower DataLoaderBatchSize
  • Set reasonable MaxComplexity

Examples

See v2/examples/graphql-*/ for complete examples:

  • graphql-basic/ - Simple query and mutation
  • graphql-auth/ - Authentication and authorization
  • graphql-dataloader/ - DataLoader optimization
  • graphql-federation/ - Microservices composition
  • graphql-subscriptions/ - Real-time subscriptions

API Reference

Extension Methods
  • NewExtension(opts ...ConfigOption) forge.Extension
  • NewExtensionWithConfig(config Config) forge.Extension
Config Options
  • WithEndpoint(endpoint string)
  • WithPlayground(enable bool)
  • WithIntrospection(enable bool)
  • WithMaxComplexity(max int)
  • WithMaxDepth(max int)
  • WithTimeout(timeout time.Duration)
  • WithDataLoader(enable bool)
  • WithQueryCache(enable bool, ttl time.Duration)
  • WithCORS(origins ...string)
  • WithMetrics(enable bool)
  • WithTracing(enable bool)
  • WithRequireConfig(require bool)
  • WithConfig(config Config)

License

Part of the Forge framework - see main repository for license.

Documentation

Index

Constants

View Source
const (
	// ServiceKey is the DI key for the GraphQL service.
	ServiceKey = "graphql"
)

DI container keys for GraphQL extension services.

Variables

View Source
var (
	ErrNotInitialized     = errors.New("graphql: not initialized")
	ErrInvalidQuery       = errors.New("graphql: invalid query")
	ErrInvalidSchema      = errors.New("graphql: invalid schema")
	ErrTypeNotFound       = errors.New("graphql: type not found")
	ErrResolverNotFound   = errors.New("graphql: resolver not found")
	ErrExecutionFailed    = errors.New("graphql: execution failed")
	ErrComplexityExceeded = errors.New("graphql: query complexity exceeded")
	ErrDepthExceeded      = errors.New("graphql: query depth exceeded")
	ErrTimeout            = errors.New("graphql: query timeout")
	ErrInvalidConfig      = errors.New("graphql: invalid configuration")
)

Common GraphQL errors

Functions

func NewExtension

func NewExtension(opts ...ConfigOption) forge.Extension

NewExtension creates a new GraphQL extension

func NewExtensionWithConfig

func NewExtensionWithConfig(config Config) forge.Extension

NewExtensionWithConfig creates a new GraphQL extension with a complete config

Types

type ArgumentDefinition

type ArgumentDefinition struct {
	Name         string
	Description  string
	Type         string
	DefaultValue interface{}
}

ArgumentDefinition defines an argument

type Config

type Config struct {
	// Server settings
	Endpoint            string `json:"endpoint" yaml:"endpoint" mapstructure:"endpoint"`
	PlaygroundEndpoint  string `json:"playground_endpoint" yaml:"playground_endpoint" mapstructure:"playground_endpoint"`
	EnablePlayground    bool   `json:"enable_playground" yaml:"enable_playground" mapstructure:"enable_playground"`
	EnableIntrospection bool   `json:"enable_introspection" yaml:"enable_introspection" mapstructure:"enable_introspection"`

	// Schema
	AutoGenerateSchema bool   `json:"auto_generate_schema" yaml:"auto_generate_schema" mapstructure:"auto_generate_schema"`
	SchemaFile         string `json:"schema_file,omitempty" yaml:"schema_file,omitempty" mapstructure:"schema_file"`

	// Performance
	MaxComplexity       int           `json:"max_complexity" yaml:"max_complexity" mapstructure:"max_complexity"`
	MaxDepth            int           `json:"max_depth" yaml:"max_depth" mapstructure:"max_depth"`
	QueryTimeout        time.Duration `json:"query_timeout" yaml:"query_timeout" mapstructure:"query_timeout"`
	EnableDataLoader    bool          `json:"enable_dataloader" yaml:"enable_dataloader" mapstructure:"enable_dataloader"`
	DataLoaderBatchSize int           `json:"dataloader_batch_size" yaml:"dataloader_batch_size" mapstructure:"dataloader_batch_size"`
	DataLoaderWait      time.Duration `json:"dataloader_wait" yaml:"dataloader_wait" mapstructure:"dataloader_wait"`

	// Caching
	EnableQueryCache bool          `json:"enable_query_cache" yaml:"enable_query_cache" mapstructure:"enable_query_cache"`
	QueryCacheTTL    time.Duration `json:"query_cache_ttl" yaml:"query_cache_ttl" mapstructure:"query_cache_ttl"`
	MaxCacheSize     int           `json:"max_cache_size" yaml:"max_cache_size" mapstructure:"max_cache_size"`

	// Security
	EnableCORS     bool     `json:"enable_cors" yaml:"enable_cors" mapstructure:"enable_cors"`
	AllowedOrigins []string `json:"allowed_origins,omitempty" yaml:"allowed_origins,omitempty" mapstructure:"allowed_origins"`
	MaxUploadSize  int64    `json:"max_upload_size" yaml:"max_upload_size" mapstructure:"max_upload_size"`

	// Observability
	EnableMetrics      bool          `json:"enable_metrics" yaml:"enable_metrics" mapstructure:"enable_metrics"`
	EnableTracing      bool          `json:"enable_tracing" yaml:"enable_tracing" mapstructure:"enable_tracing"`
	EnableLogging      bool          `json:"enable_logging" yaml:"enable_logging" mapstructure:"enable_logging"`
	LogSlowQueries     bool          `json:"log_slow_queries" yaml:"log_slow_queries" mapstructure:"log_slow_queries"`
	SlowQueryThreshold time.Duration `json:"slow_query_threshold" yaml:"slow_query_threshold" mapstructure:"slow_query_threshold"`

	// Config loading flags
	RequireConfig bool `json:"-" yaml:"-" mapstructure:"-"`
}

Config contains configuration for the GraphQL extension

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns default GraphQL configuration

func (*Config) Validate

func (c *Config) Validate() error

Validate validates the configuration

type ConfigOption

type ConfigOption func(*Config)

ConfigOption is a functional option for Config

func WithCORS

func WithCORS(origins ...string) ConfigOption

func WithConfig

func WithConfig(config Config) ConfigOption

func WithDataLoader

func WithDataLoader(enable bool) ConfigOption

func WithEndpoint

func WithEndpoint(endpoint string) ConfigOption

func WithIntrospection

func WithIntrospection(enable bool) ConfigOption

func WithMaxComplexity

func WithMaxComplexity(max int) ConfigOption

func WithMaxDepth

func WithMaxDepth(max int) ConfigOption

func WithMetrics

func WithMetrics(enable bool) ConfigOption

func WithPlayground

func WithPlayground(enable bool) ConfigOption

func WithQueryCache

func WithQueryCache(enable bool, ttl time.Duration) ConfigOption

func WithRequireConfig

func WithRequireConfig(require bool) ConfigOption

func WithTimeout

func WithTimeout(timeout time.Duration) ConfigOption

func WithTracing

func WithTracing(enable bool) ConfigOption

type DataLoader

type DataLoader interface {
	Load(ctx context.Context, key interface{}) (interface{}, error)
	LoadMany(ctx context.Context, keys []interface{}) ([]interface{}, error)
	Prime(key interface{}, value interface{})
	Clear(key interface{})
	ClearAll()
}

DataLoader optimizes N+1 queries

type Error

type Error struct {
	Message    string                 `json:"message"`
	Path       []interface{}          `json:"path,omitempty"`
	Locations  []Location             `json:"locations,omitempty"`
	Extensions map[string]interface{} `json:"extensions,omitempty"`
}

Error represents a GraphQL error

type ExecutorFunc

type ExecutorFunc func(ctx context.Context, req *Request) (*Response, error)

ExecutorFunc executes a GraphQL operation

type Extension

type Extension struct {
	*forge.BaseExtension
	// contains filtered or unexported fields
}

Extension implements forge.Extension for GraphQL functionality. The extension is now a lightweight facade that loads config and registers services.

func (*Extension) Health

func (e *Extension) Health(ctx context.Context) error

Health checks the extension health. Service health is managed by Vessel through GraphQLService.Health().

func (*Extension) Register

func (e *Extension) Register(app forge.App) error

Register registers the GraphQL extension with the app. This method loads configuration and registers service constructors.

func (*Extension) Start

func (e *Extension) Start(ctx context.Context) error

Start starts the GraphQL extension and registers routes. Routes need the service, so we resolve it here.

func (*Extension) Stop

func (e *Extension) Stop(ctx context.Context) error

Stop marks the extension as stopped. The actual server is stopped by Vessel calling GraphQLService.Stop().

type FederationConfig

type FederationConfig struct {
	// Enable federation support
	Enabled bool
	// Version of Apollo Federation (1 or 2)
	Version int
}

FederationConfig holds federation-specific configuration

func DefaultFederationConfig

func DefaultFederationConfig() FederationConfig

DefaultFederationConfig returns default federation configuration

type FieldDefinition

type FieldDefinition struct {
	Name              string
	Description       string
	Type              string
	Args              []*ArgumentDefinition
	Resolver          FieldResolverFunc
	Deprecated        bool
	DeprecationReason string
}

FieldDefinition defines a field in a type

type FieldResolver

type FieldResolver interface {
	Resolve(ctx context.Context, source interface{}, args map[string]interface{}) (interface{}, error)
}

FieldResolver resolves a specific field

type FieldResolverFunc

type FieldResolverFunc func(ctx context.Context, args map[string]interface{}) (interface{}, error)

FieldResolverFunc handles field resolution (for dynamic registration)

type GraphQL

type GraphQL interface {
	// Schema management
	RegisterType(name string, obj interface{}) error
	RegisterQuery(name string, resolver FieldResolverFunc) error
	RegisterMutation(name string, resolver FieldResolverFunc) error
	RegisterSubscription(name string, resolver SubscriptionResolverFunc) error

	// Schema generation
	GenerateSchema() (string, error)
	GetSchema() *GraphQLSchema

	// Execution
	ExecuteQuery(ctx context.Context, query string, variables map[string]interface{}) (*Response, error)

	// HTTP handlers
	HTTPHandler() http.Handler
	PlaygroundHandler() http.Handler

	// Middleware
	Use(middleware Middleware)

	// Introspection
	EnableIntrospection(enable bool)

	// Health
	Ping(ctx context.Context) error
}

GraphQL represents a unified GraphQL server interface (wraps gqlgen)

func NewGraphQLServer

func NewGraphQLServer(config Config, logger forge.Logger, metrics forge.Metrics, container forge.Container) (GraphQL, error)

NewGraphQLServer creates a new GraphQL server with gqlgen

type GraphQLDirective

type GraphQLDirective struct {
	Name        string
	Description string
	Locations   []string
	Args        []*ArgumentDefinition
}

GraphQLDirective defines a GraphQL directive (wrapper for introspection)

type GraphQLSchema

type GraphQLSchema struct {
	Types         map[string]*TypeDefinition
	Queries       map[string]*FieldDefinition
	Mutations     map[string]*FieldDefinition
	Subscriptions map[string]*FieldDefinition
	Directives    []*GraphQLDirective
}

GraphQLSchema represents a GraphQL schema (wrapper for introspection)

type GraphQLService

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

GraphQLService wraps a GraphQL server implementation and provides lifecycle management. It implements vessel's di.Service interface so Vessel can manage its lifecycle.

func NewGraphQLService

func NewGraphQLService(config Config, container forge.Container, logger forge.Logger, metrics forge.Metrics) (*GraphQLService, error)

NewGraphQLService creates a new GraphQL service with the given configuration. This is the constructor that will be registered with the DI container.

func (*GraphQLService) ExecuteQuery

func (s *GraphQLService) ExecuteQuery(ctx context.Context, query string, variables map[string]interface{}) (*Response, error)

func (*GraphQLService) GenerateSchema

func (s *GraphQLService) GenerateSchema() (string, error)

func (*GraphQLService) GetSchema

func (s *GraphQLService) GetSchema() *GraphQLSchema

func (*GraphQLService) HTTPHandler

func (s *GraphQLService) HTTPHandler() http.Handler

func (*GraphQLService) Health

func (s *GraphQLService) Health(ctx context.Context) error

Health checks if the GraphQL service is healthy.

func (*GraphQLService) Name

func (s *GraphQLService) Name() string

Name returns the service name for Vessel's lifecycle management.

func (*GraphQLService) Ping

func (s *GraphQLService) Ping(ctx context.Context) error

func (*GraphQLService) PlaygroundHandler

func (s *GraphQLService) PlaygroundHandler() http.Handler

func (*GraphQLService) RegisterMutation

func (s *GraphQLService) RegisterMutation(name string, resolver FieldResolverFunc) error

func (*GraphQLService) RegisterQuery

func (s *GraphQLService) RegisterQuery(name string, resolver FieldResolverFunc) error

func (*GraphQLService) RegisterSubscription

func (s *GraphQLService) RegisterSubscription(name string, resolver SubscriptionResolverFunc) error

func (*GraphQLService) RegisterType

func (s *GraphQLService) RegisterType(name string, obj interface{}) error

func (*GraphQLService) Server

func (s *GraphQLService) Server() GraphQL

Server returns the underlying GraphQL server implementation.

func (*GraphQLService) Start

func (s *GraphQLService) Start(ctx context.Context) error

Start starts the GraphQL service. This is called automatically by Vessel during container.Start().

func (*GraphQLService) Stop

func (s *GraphQLService) Stop(ctx context.Context) error

Stop stops the GraphQL service. This is called automatically by Vessel during container.Stop().

func (*GraphQLService) Use

func (s *GraphQLService) Use(middleware Middleware)

type Location

type Location struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

Location represents an error location

type Middleware

type Middleware func(next ExecutorFunc) ExecutorFunc

Middleware wraps GraphQL execution

type Request

type Request struct {
	Query         string                 `json:"query"`
	OperationName string                 `json:"operationName,omitempty"`
	Variables     map[string]interface{} `json:"variables,omitempty"`
}

Request represents a GraphQL request

type Resolver

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

Resolver is the root resolver struct for GraphQL operations

func NewResolver

func NewResolver(container forge.Container, logger forge.Logger, metrics forge.Metrics, config Config) *Resolver

NewResolver creates a new resolver with dependencies

func (*Resolver) Mutation

func (r *Resolver) Mutation() generated.MutationResolver

func (*Resolver) Query

func (r *Resolver) Query() generated.QueryResolver

type Response

type Response struct {
	Data       interface{}            `json:"data,omitempty"`
	Errors     []Error                `json:"errors,omitempty"`
	Extensions map[string]interface{} `json:"extensions,omitempty"`
}

Response represents a GraphQL response

type SubscriptionResolverFunc

type SubscriptionResolverFunc func(ctx context.Context, args map[string]interface{}) (<-chan interface{}, error)

SubscriptionResolverFunc handles subscription events (for dynamic registration)

type TypeDefinition

type TypeDefinition struct {
	Name        string
	Description string
	Kind        string // OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT, SCALAR
	Fields      []*FieldDefinition
	Interfaces  []string
}

TypeDefinition defines a GraphQL type

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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