redistore

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: Jan 14, 2026 License: MIT Imports: 13 Imported by: 0

README

redistore

codecov Go Report Card GoDoc Run Tests Trivy Security Scan

A session store backend for gorilla/sessions with Redis as the storage engine.

Features

  • Clean API - Single entry point with flexible option pattern
  • 🔧 Highly Configurable - 15+ options for fine-grained control
  • 🔒 Secure - Built on gorilla/sessions with secure cookie encoding
  • Fast - Redis-backed for high performance
  • 📦 Serialization - Support for Gob and JSON serializers
  • 🧪 Well Tested - Comprehensive test coverage

Requirements

Installation

go get github.com/boj/redistore/v2
For v1 (Legacy)
go get github.com/boj/redistore@v1

Note: v2 introduces a cleaner API with the Option Pattern. See MIGRATION.md for upgrade instructions.

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/boj/redistore/v2"
    "github.com/gorilla/sessions"
)

func main() {
    // Create a new store with options
    store, err := redistore.NewStore(
        redistore.KeysFromStrings("secret-key"),
        redistore.WithAddress("tcp", ":6379"),
    )
    if err != nil {
        panic(err)
    }
    defer store.Close()

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // Get a session
        session, err := store.Get(r, "session-key")
        if err != nil {
            log.Println(err.Error())
            return
        }

        // Set a value
        session.Values["foo"] = "bar"

        // Save session
        if err = sessions.Save(r, w); err != nil {
            log.Fatalf("Error saving session: %v", err)
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Documentation

Usage Examples

Basic Connection
store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithAddress("tcp", "localhost:6379"),
)
With Authentication
store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithAddress("tcp", "localhost:6379"),
    redistore.WithAuth("username", "password"),
)
Specific Database
store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithAddress("tcp", "localhost:6379"),
    redistore.WithDB("5"), // Use database 5
)
Using Redis URL
store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithURL("redis://:password@localhost:6379/0"),
)
Custom Configuration
store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithAddress("tcp", "localhost:6379"),
    redistore.WithMaxLength(8192),          // Max session size: 8KB
    redistore.WithKeyPrefix("myapp_"),      // Key prefix
    redistore.WithDefaultMaxAge(3600),      // Default TTL: 1 hour
    redistore.WithSerializer(redistore.JSONSerializer{}), // JSON serializer
)
Using a Custom Pool
import "github.com/gomodule/redigo/redis"

pool := &redis.Pool{
    MaxIdle:     100,
    IdleTimeout: 5 * time.Minute,
    Dial: func() (redis.Conn, error) {
        return redis.Dial("tcp", "localhost:6379")
    },
}

store, err := redistore.NewStore(
    redistore.KeysFromStrings("secret-key"),
    redistore.WithPool(pool),
)
Key Rotation

Support for encryption key rotation allows you to change keys without invalidating existing sessions:

// Keys are provided in pairs: authentication key, encryption key
// The first pair is used for encoding new sessions
// All pairs are tried for decoding existing sessions
store, err := redistore.NewStore(
    redistore.KeysFromStrings(
        "new-authentication-key", // 32 or 64 bytes recommended
        "new-encryption-key",     // 16, 24, or 32 bytes for AES
        "old-authentication-key", // Keep for existing sessions
        "old-encryption-key",     // Keep for existing sessions
    ),
    redistore.WithAddress("tcp", "localhost:6379"),
)

// Using Keys() with byte slices for production
authKey, _ := loadKeyFromSecureStorage("auth-key")
encryptKey, _ := loadKeyFromSecureStorage("encrypt-key")
store, err := redistore.NewStore(
    redistore.Keys(authKey, encryptKey),
    redistore.WithAddress("tcp", "localhost:6379"),
)

Key Sizes:

  • Authentication key: 32 or 64 bytes (HMAC)
  • Encryption key: 16 (AES-128), 24 (AES-192), or 32 bytes (AES-256)

Rotation Process:

  1. Add new key pair at the beginning
  2. Keep old keys for a transition period
  3. Remove old keys once all sessions have been renewed

Helper Functions:

  • KeysFromStrings(keys ...string) - Simplest way to provide keys from strings
  • Keys(keys ...[]byte) - For keys already as byte slices
  • Direct slice: [][]byte{[]byte("key")} - Original syntax still supported
Complete Example
package main

import (
    "log"
    "net/http"

    "github.com/boj/redistore/v2"
    "github.com/gorilla/sessions"
)

func main() {
    // Initialize store with custom configuration
    store, err := redistore.NewStore(
        redistore.KeysFromStrings("secret-key-123"),
        redistore.WithAddress("tcp", "localhost:6379"),
        redistore.WithDB("1"),
        redistore.WithMaxLength(8192),
        redistore.WithKeyPrefix("webapp_"),
        redistore.WithDefaultMaxAge(3600), // 1 hour
    )
    if err != nil {
        log.Fatal(err)
    }
    defer store.Close()

    http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
        session, _ := store.Get(r, "my-session")
        session.Values["user"] = "john_doe"
        session.Values["authenticated"] = true
        sessions.Save(r, w)
        w.Write([]byte("Session saved!"))
    })

    http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
        session, _ := store.Get(r, "my-session")
        user := session.Values["user"]
        if user != nil {
            w.Write([]byte("User: " + user.(string)))
        } else {
            w.Write([]byte("No user in session"))
        }
    })

    http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
        session, _ := store.Get(r, "my-session")
        session.Options.MaxAge = -1
        sessions.Save(r, w)
        w.Write([]byte("Session deleted!"))
    })

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Configuration Options

Connection Options (Required - Choose ONE)
Option Description
WithPool(pool) Use a custom Redis connection pool
WithAddress(network, address) Connect via network and address (e.g., "tcp", ":6379")
WithURL(url) Connect via Redis URL (e.g., "redis://localhost:6379/0")
Authentication Options
Option Description
WithAuth(username, password) Set username and password
WithPassword(password) Set password only
Redis Configuration
Option Default Description
WithDB(db) "0" Database index ("0"-"15")
WithDBNum(dbNum) 0 Database index as integer
WithPoolSize(size) 10 Connection pool size
WithIdleTimeout(timeout) 240s Connection idle timeout
Store Configuration
Option Default Description
WithMaxLength(length) 4096 Max session size in bytes (0 = unlimited)
WithKeyPrefix(prefix) "session_" Redis key prefix
WithDefaultMaxAge(age) 1200 Default TTL in seconds (20 minutes)
WithSerializer(s) GobSerializer Session serializer
WithSessionOptions(opts) - Full gorilla/sessions options
WithPath(path) "/" Cookie path
WithMaxAge(age) 30 days Cookie MaxAge

Serializers

Gob Serializer (Default)

Uses Go's encoding/gob package. Efficient binary format, suitable for complex Go types.

store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", ":6379"),
    // GobSerializer is the default, no need to specify
)
JSON Serializer

Uses encoding/json package. Human-readable, cross-language compatible.

store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", ":6379"),
    redistore.WithSerializer(redistore.JSONSerializer{}),
)
Custom Serializer

Implement the SessionSerializer interface:

type SessionSerializer interface {
    Serialize(ss *sessions.Session) ([]byte, error)
    Deserialize(d []byte, ss *sessions.Session) error
}

type MySerializer struct{}

func (s MySerializer) Serialize(ss *sessions.Session) ([]byte, error) {
    // Your implementation
}

func (s MySerializer) Deserialize(d []byte, ss *sessions.Session) error {
    // Your implementation
}

// Use it
store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", ":6379"),
    redistore.WithSerializer(MySerializer{}),
)

Session Management

Setting Values
session, _ := store.Get(r, "session-key")
session.Values["username"] = "john"
session.Values["role"] = "admin"
sessions.Save(r, w)
Getting Values
session, _ := store.Get(r, "session-key")
username := session.Values["username"]
if username != nil {
    fmt.Println(username.(string))
}
Flash Messages
// Add flash message
session.AddFlash("Welcome back!")
sessions.Save(r, w)

// Retrieve and clear flash messages
flashes := session.Flashes()
for _, flash := range flashes {
    fmt.Println(flash)
}
sessions.Save(r, w) // Save to clear flashes
Deleting Sessions
session, _ := store.Get(r, "session-key")
session.Options.MaxAge = -1
sessions.Save(r, w)

Post-Initialization Configuration

While the Option Pattern is recommended, you can still modify settings after creation:

store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", ":6379"),
)

// Modify after creation
store.SetMaxLength(16384)
store.SetKeyPrefix("app2_")
store.SetSerializer(redistore.JSONSerializer{})
store.SetMaxAge(86400 * 7) // 7 days

Error Handling

Configuration Errors
store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", ":6379"),
    redistore.WithURL("redis://localhost"), // ❌ Error: multiple connection options
)
if err != nil {
    // Error: "only one connection option can be specified"
    log.Fatal(err)
}
Connection Errors
store, err := redistore.NewStore(
    [][]byte{[]byte("secret-key")},
    redistore.WithAddress("tcp", "invalid:9999"),
)
if err != nil {
    // Error: "failed to connect to Redis: ..."
    log.Fatal(err)
}

Testing

Run the full test suite:

# Start Redis (required)
redis-server

# Run tests
go test -v

# With coverage
go test -v -coverprofile=coverage.out
go tool cover -html=coverage.out

Performance

  • Session Retrieval: ~1ms (local Redis)
  • Session Save: ~1-2ms (local Redis)
  • Memory: Minimal overhead, Redis handles storage
  • Concurrent Requests: Scales with Redis and connection pool size

Migration from v1

If you're using v1, please see MIGRATION.md for detailed upgrade instructions.

Quick comparison:

// v1
store, err := redistore.NewRediStore(10, "tcp", ":6379", "", "", []byte("key"))

// v2
store, err := redistore.NewStore(
    []byte("key"),
    redistore.WithAddress("tcp", ":6379"),
)

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new features
  4. Ensure all tests pass
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

Support

Documentation

Overview

Package redistore is a session store backend for gorilla/sessions

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Keys

func Keys(keys ...[]byte) [][]byte

Keys creates a key pairs slice from individual byte slices. This is a convenience function to simplify the creation of key pairs without having to write [][]byte{...}.

Example:

store, err := NewStore(
    Keys([]byte("auth-key"), []byte("encrypt-key")),
    WithAddress("tcp", ":6379"),
)

func KeysFromStrings

func KeysFromStrings(keys ...string) [][]byte

KeysFromStrings creates a key pairs slice from strings. This is the most convenient way to provide keys for development and testing.

Warning: For production use with sensitive keys, consider using Keys() with byte slices loaded from secure storage instead of hardcoded strings.

Example:

// Single key
store, err := NewStore(
    KeysFromStrings("secret-key"),
    WithAddress("tcp", ":6379"),
)

// Multiple keys for rotation
store, err := NewStore(
    KeysFromStrings(
        "new-auth-key",
        "new-encrypt-key",
        "old-auth-key",
        "old-encrypt-key",
    ),
    WithAddress("tcp", ":6379"),
)

Types

type GobSerializer

type GobSerializer struct{}

GobSerializer is a struct that provides methods for serializing and deserializing data using the Gob encoding format. Gob is a binary serialization format that is efficient and compact, making it suitable for encoding complex data structures in Go.

func (GobSerializer) Deserialize

func (s GobSerializer) Deserialize(d []byte, ss *sessions.Session) error

Deserialize decodes the given byte slice into the session's Values field. It uses the gob package to perform the decoding.

Parameters:

d - The byte slice to be deserialized.
ss - The session object where the deserialized data will be stored.

Returns:

An error if the deserialization fails, otherwise nil.

func (GobSerializer) Serialize

func (s GobSerializer) Serialize(ss *sessions.Session) ([]byte, error)

Serialize encodes the session values using gob encoding and returns the serialized byte slice. If the encoding process encounters an error, it returns nil and the error.

Parameters:

ss - A pointer to the session to be serialized.

Returns:

A byte slice containing the serialized session values, or nil if an
error occurred during encoding. The error encountered during encoding
is also returned.

type JSONSerializer

type JSONSerializer struct{}

JSONSerializer is a struct that provides methods for serializing and deserializing data to and from JSON format. It can be used to convert Go data structures into JSON strings and vice versa.

func (JSONSerializer) Deserialize

func (s JSONSerializer) Deserialize(d []byte, ss *sessions.Session) error

Deserialize takes a byte slice and a pointer to a sessions.Session, and attempts to deserialize the byte slice into the session's Values map. It returns an error if the deserialization process fails.

Parameters: - d: A byte slice containing the serialized session data. - ss: A pointer to the sessions.Session where the deserialized data will be stored.

Returns: - An error if the deserialization process fails, otherwise nil.

func (JSONSerializer) Serialize

func (s JSONSerializer) Serialize(ss *sessions.Session) ([]byte, error)

Serialize converts the session's values into a JSON-encoded byte slice. It returns an error if any of the session keys are not strings.

Parameters:

ss - A pointer to the session to be serialized.

Returns:

A byte slice containing the JSON-encoded session values, or an error if
serialization fails.

type Option

type Option func(*storeConfig) error

Option is a function type for configuring a RediStore.

func WithAddress

func WithAddress(network, address string) Option

WithAddress configures the RediStore to connect to Redis using network and address. This option is mutually exclusive with WithPool and WithURL.

Example:

WithAddress("tcp", "localhost:6379")
WithAddress("unix", "/tmp/redis.sock")

func WithAuth

func WithAuth(username, password string) Option

WithAuth sets the username and password for Redis authentication. Both username and password can be empty strings if not required.

func WithDB

func WithDB(db string) Option

WithDB sets the Redis database index to use. The db parameter should be a string representation of a number between 0 and 15. If empty, defaults to "0".

func WithDBNum

func WithDBNum(dbNum int) Option

WithDBNum sets the Redis database index using an integer. This is a convenience function equivalent to WithDB(strconv.Itoa(dbNum)).

func WithDefaultMaxAge

func WithDefaultMaxAge(age int) Option

WithDefaultMaxAge sets the default TTL (time-to-live) in seconds for sessions. This is used when session.Options.MaxAge is 0. Default is 1200 seconds (20 minutes).

func WithIdleTimeout

func WithIdleTimeout(timeout time.Duration) Option

WithIdleTimeout sets the idle timeout for connections in the pool. Default is 240 seconds. Only applies when using WithAddress or WithURL.

func WithKeyPrefix

func WithKeyPrefix(prefix string) Option

WithKeyPrefix sets the prefix for all Redis keys used by this store. Default is "session_". This is useful to avoid key collisions when using a single Redis instance for multiple applications.

func WithMaxAge

func WithMaxAge(age int) Option

WithMaxAge sets the MaxAge for session cookies in seconds. Default is sessionExpire (86400 * 30 = 30 days). This is a convenience function that modifies session options.

func WithMaxLength

func WithMaxLength(length int) Option

WithMaxLength sets the maximum size of session data in bytes. Default is 4096 bytes. Set to 0 for no limit (use with caution). Redis allows values up to 512MB.

func WithPassword

func WithPassword(password string) Option

WithPassword sets only the password for Redis authentication. This is a convenience function for Redis instances that don't use username.

func WithPath

func WithPath(path string) Option

WithPath sets the cookie path for sessions. Default is "/". This is a convenience function that modifies session options.

func WithPool

func WithPool(pool *redis.Pool) Option

WithPool configures the RediStore to use a custom Redis connection pool. This option is mutually exclusive with WithAddress and WithURL.

func WithPoolSize

func WithPoolSize(size int) Option

WithPoolSize sets the maximum number of idle connections in the pool. Default is 10. Only applies when using WithAddress or WithURL.

func WithSerializer

func WithSerializer(serializer SessionSerializer) Option

WithSerializer sets the session serializer. Default is GobSerializer. You can also use JSONSerializer or implement your own SessionSerializer.

func WithSessionOptions

func WithSessionOptions(opts *sessions.Options) Option

WithSessionOptions sets the default session options. This allows fine-grained control over cookie behavior.

func WithURL

func WithURL(url string) Option

WithURL configures the RediStore to connect to Redis using a URL. This option is mutually exclusive with WithPool and WithAddress.

Example:

WithURL("redis://localhost:6379/0")
WithURL("redis://:password@localhost:6379/1")

type RediStore

type RediStore struct {
	Pool          *redis.Pool
	Codecs        []securecookie.Codec
	Options       *sessions.Options // default configuration
	DefaultMaxAge int               // default Redis TTL for a MaxAge == 0 session
	// contains filtered or unexported fields
}

RediStore represents a session store backed by a Redis database. It provides methods to manage session data using Redis as the storage backend.

Fields:

Pool: A connection pool for Redis.
Codecs: A list of securecookie.Codec used to encode and decode session data.
Options: Default configuration options for sessions.
DefaultMaxAge: Default TTL (Time To Live) for sessions with MaxAge == 0.
maxLength: Maximum length of session data.
keyPrefix: Prefix to be added to all Redis keys used by this store.
serializer: Serializer used to encode and decode session data.
Example
// RedisStore
store, err := NewStore(
	[][]byte{[]byte("secret-key")},
	WithAddress("tcp", ":6379"),
	WithPoolSize(10),
)
if err != nil {
	panic(err)
}
defer func() {
	if err := store.Close(); err != nil {
		fmt.Printf("Error closing store: %v\n", err)
	}
}()

func NewStore

func NewStore(keyPairs [][]byte, opts ...Option) (*RediStore, error)

NewStore creates a new RediStore with the given options.

Parameters:

keyPairs - One or more key pairs for cookie encryption and authentication.
           Each key should be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256.
           Keys are used in pairs: authentication key and encryption key.
           Provide multiple pairs for key rotation (first pair is used for encoding,
           remaining pairs are used for decoding only).
opts - Configuration options. At least one connection option is required.

Connection Options (required, exactly one):

  • WithPool(pool) - Use a custom Redis connection pool
  • WithAddress(network, address) - Connect using network protocol and address
  • WithURL(url) - Connect using a Redis URL

Authentication Options:

  • WithAuth(username, password) - Set username and password
  • WithPassword(password) - Set password only

Redis Configuration Options:

  • WithDB(db) - Set database index (default "0")
  • WithDBNum(n) - Set database index as integer
  • WithPoolSize(size) - Set connection pool size (default 10)
  • WithIdleTimeout(timeout) - Set idle timeout (default 240s)

Store Configuration Options:

  • WithMaxLength(length) - Set max session size (default 4096)
  • WithKeyPrefix(prefix) - Set Redis key prefix (default "session_")
  • WithDefaultMaxAge(age) - Set default TTL (default 1200)
  • WithSerializer(s) - Set serializer (default GobSerializer)
  • WithSessionOptions(opts) - Set session options
  • WithPath(path) - Set cookie path (default "/")
  • WithMaxAge(age) - Set cookie MaxAge (default 30 days)

Example:

// Basic usage with single key (using helper function)
store, err := NewStore(
    KeysFromStrings("secret-key"),
    WithAddress("tcp", ":6379"),
)

// Using Keys() with byte slices
store, err := NewStore(
    Keys(
        []byte("authentication-key"), // 32 or 64 bytes
        []byte("encryption-key"),     // 16, 24, or 32 bytes
    ),
    WithAddress("tcp", ":6379"),
)

// With key rotation (old keys for decoding only)
store, err := NewStore(
    KeysFromStrings(
        "new-auth-key",
        "new-encrypt-key",
        "old-auth-key",    // For decoding existing sessions
        "old-encrypt-key",
    ),
    WithAddress("tcp", "localhost:6379"),
    WithDB("1"),
)

// With multiple options
store, err := NewStore(
    KeysFromStrings("secret-key"),
    WithAddress("tcp", "localhost:6379"),
    WithDB("1"),
    WithMaxLength(8192),
    WithKeyPrefix("myapp_"),
    WithSerializer(JSONSerializer{}),
)

// Using URL
store, err := NewStore(
    KeysFromStrings("secret-key"),
    WithURL("redis://:password@localhost:6379/0"),
)

// Without helper functions (direct slice)
store, err := NewStore(
    [][]byte{[]byte("secret-key")},
    WithAddress("tcp", ":6379"),
)

func (*RediStore) Close

func (s *RediStore) Close() error

Close closes the underlying *redis.Pool

func (*RediStore) Delete

func (s *RediStore) Delete(
	r *http.Request,
	w http.ResponseWriter,
	session *sessions.Session,
) error

Delete removes the session from redis, and sets the cookie to expire.

WARNING: This method should be considered deprecated since it is not exposed via the gorilla/sessions interface. Set session.Options.MaxAge = -1 and call Save instead. - July 18th, 2013

func (*RediStore) Get

func (s *RediStore) Get(r *http.Request, name string) (*sessions.Session, error)

Get returns a session for the given name after adding it to the registry.

See gorilla/sessions FilesystemStore.Get().

func (*RediStore) New

func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error)

New returns a session for the given name without adding it to the registry.

See gorilla/sessions FilesystemStore.New().

func (*RediStore) Save

func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error

Save adds a single session to the response.

func (*RediStore) SetKeyPrefix

func (s *RediStore) SetKeyPrefix(p string)

SetKeyPrefix sets the key prefix for all keys used in the RediStore. This is useful to avoid key name collisions when using a single Redis instance for multiple applications.

func (*RediStore) SetMaxAge

func (s *RediStore) SetMaxAge(v int)

SetMaxAge restricts the maximum age, in seconds, of the session record both in database and a browser. This is to change session storage configuration. If you want just to remove session use your session `s` object and change it's `Options.MaxAge` to -1, as specified in

http://godoc.org/github.com/gorilla/sessions#Options

Default is the one provided by this package value - `sessionExpire`. Set it to 0 for no restriction. Because we use `MaxAge` also in SecureCookie crypting algorithm you should use this function to change `MaxAge` value.

func (*RediStore) SetMaxLength

func (s *RediStore) SetMaxLength(l int)

SetMaxLength sets RediStore.maxLength if the `l` argument is greater or equal 0 maxLength restricts the maximum length of new sessions to l. If l is 0 there is no limit to the size of a session, use with caution. The default for a new RediStore is 4096. Redis allows for max. value sizes of up to 512MB (http://redis.io/topics/data-types) Default: 4096,

func (*RediStore) SetSerializer

func (s *RediStore) SetSerializer(ss SessionSerializer)

SetSerializer sets the session serializer for the RediStore. The serializer is responsible for encoding and decoding session data.

Parameters:

ss - The session serializer to be used.

type SessionSerializer

type SessionSerializer interface {
	Deserialize(d []byte, ss *sessions.Session) error
	Serialize(ss *sessions.Session) ([]byte, error)
}

SessionSerializer is an interface that defines methods for serializing and deserializing session data. Implementations of this interface should provide mechanisms to convert session data to and from byte slices.

Jump to

Keyboard shortcuts

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