features

package
v0.8.6 Latest Latest
Warning

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

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

README

Feature Flags Extension

Enterprise-grade feature flags and A/B testing for Forge applications.

Features

  • Multiple Providers - Local, LaunchDarkly, Unleash, Flagsmith
  • Boolean Flags - Simple on/off toggles
  • Multi-Variant Flags - String, number, JSON flags
  • User Targeting - Target specific users or groups
  • Percentage Rollouts - Gradual feature rollouts
  • Real-time Updates - Automatic flag refresh
  • Caching - Local caching for performance
  • A/B Testing - Experimentation support

Installation

go get github.com/xraph/forge/extensions/features

Quick Start

Basic Usage
package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/forge/extensions/features"
)

func main() {
    app := forge.NewApp(forge.AppConfig{
        Extensions: []forge.Extension{
            features.NewExtension(
                features.WithEnabled(true),
                features.WithLocalFlags(map[string]features.FlagConfig{
                    "new-checkout": {
                        Key:         "new-checkout",
                        Name:        "New Checkout Flow",
                        Description: "Enable new checkout experience",
                        Type:        "boolean",
                        Enabled:     true,
                    },
                    "max-cart-items": {
                        Key:         "max-cart-items",
                        Name:        "Maximum Cart Items",
                        Type:        "number",
                        Value:       100,
                    },
                }),
            ),
        },
    })

    // Get features service using helper
    featuresService := features.MustGetFromApp(app)

    // Check if feature is enabled
    router := app.Router()
    router.POST("/checkout", func(ctx forge.Context) error {
        userCtx := features.NewUserContext(ctx.UserID()).
            WithEmail(ctx.User().Email)

        if featuresService.IsEnabled(ctx.Context(), "new-checkout", userCtx) {
            return handleNewCheckout(ctx)
        }
        return handleLegacyCheckout(ctx)
    })

    app.Run()
}

Configuration

YAML Configuration
extensions:
  features:
    enabled: true
    provider: "local"
    refresh_interval: 30s
    enable_cache: true
    cache_ttl: 5m

    # Local provider (for development)
    local:
      flags:
        new-checkout:
          key: "new-checkout"
          name: "New Checkout Flow"
          type: "boolean"
          enabled: true
          targeting:
            - attribute: "email"
              operator: "contains"
              values: ["@company.com"]
              value: true
          rollout:
            percentage: 50
            attribute: "user_id"

        theme:
          key: "theme"
          name: "UI Theme"
          type: "string"
          value: "dark"

    # Default flag values
    default_flags:
      new-checkout: false
      max-cart-items: 100
Programmatic Configuration
features.NewExtension(
    features.WithEnabled(true),
    features.WithProvider("local"),
    features.WithRefreshInterval(30 * time.Second),
    features.WithCache(true, 5 * time.Minute),
    features.WithLocalFlags(map[string]features.FlagConfig{
        // ... flags
    }),
)

Usage Examples

Boolean Flags
// Check if feature is enabled
userCtx := features.NewUserContext("user-123").
    WithEmail("[email protected]").
    WithGroups([]string{"beta-testers"})

if service.IsEnabled(ctx, "new-feature", userCtx) {
    // New feature enabled
}

// With custom default
enabled := service.IsEnabledWithDefault(ctx, "experimental-feature", userCtx, false)
String Flags
// Get string flag
theme := service.GetString(ctx, "theme", userCtx, "light")

// Use in templates
return ctx.Render("dashboard.html", map[string]interface{}{
    "theme": theme,
})
Number Flags
// Get number flag
maxItems := service.GetInt(ctx, "max-cart-items", userCtx, 50)
pageSize := service.GetInt(ctx, "page-size", userCtx, 20)
JSON Flags
// Get complex configuration
config := service.GetJSON(ctx, "api-config", userCtx, map[string]interface{}{
    "timeout": 30,
    "retry": 3,
})
Get All Flags
// Get all flags for a user
allFlags, err := service.GetAllFlags(ctx, userCtx)
if err != nil {
    return err
}

// Return to frontend
return ctx.JSON(200, map[string]interface{}{
    "flags": allFlags,
})

Targeting Rules

User Targeting
flags:
  vip-features:
    targeting:
      - attribute: "email"
        operator: "in"
        values: ["[email protected]", "[email protected]"]
        value: true
      
      - attribute: "group"
        operator: "in"
        values: ["vip", "enterprise"]
        value: true
Percentage Rollout
flags:
  new-ui:
    rollout:
      percentage: 25  # 25% of users
      attribute: "user_id"  # Consistent hashing on user_id
Combined Targeting
flags:
  beta-feature:
    targeting:
      # Enable for beta testers
      - attribute: "group"
        operator: "in"
        values: ["beta-testers"]
        value: true
    
    rollout:
      # Plus 10% of regular users
      percentage: 10
      attribute: "user_id"

Providers

Local Provider (Development)
features.NewExtension(
    features.WithProvider("local"),
    features.WithLocalFlags(localFlags),
)
LaunchDarkly (Production)
features.NewExtension(
    features.WithLaunchDarkly(os.Getenv("LAUNCHDARKLY_SDK_KEY")),
)
Unleash
features.NewExtension(
    features.WithUnleash(
        "https://unleash.company.com",
        os.Getenv("UNLEASH_API_TOKEN"),
        "my-app",
    ),
)
Flagsmith
features.NewExtension(
    features.WithFlagsmith(
        "https://flagsmith.company.com",
        os.Getenv("FLAGSMITH_ENV_KEY"),
    ),
)

Middleware Integration

// Feature flag middleware
func FeatureFlagMiddleware(flagKey string, service *features.Service) forge.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userCtx := features.NewUserContext(getUserID(r))
            
            if !service.IsEnabled(r.Context(), flagKey, userCtx) {
                http.Error(w, "Feature not available", http.StatusNotFound)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

// Use in routes
router.Use(FeatureFlagMiddleware("api-v2", featuresService))

Best Practices

1. Use Descriptive Flag Names
// ✅ Good
"new-checkout-flow"
"enable-email-notifications"
"max-upload-size"

// ❌ Bad
"flag1"
"new-feature"
"temp"
2. Always Provide Defaults
// ✅ Good - provides fallback
enabled := service.IsEnabledWithDefault(ctx, "feature", userCtx, false)

// ❌ Bad - panics if flag not found
enabled := service.IsEnabled(ctx, "feature", userCtx)
3. Use User Context
// ✅ Good - provides targeting context
userCtx := features.NewUserContext(user.ID).
    WithEmail(user.Email).
    WithGroups(user.Groups).
    WithAttribute("plan", user.Plan)

// ❌ Bad - no targeting possible
userCtx := nil
4. Cache Flag Values (When Appropriate)
// For frequently checked flags in hot paths
var cachedFlag atomic.Bool

go func() {
    ticker := time.NewTicker(10 * time.Second)
    for range ticker.C {
        enabled := service.IsEnabled(ctx, "hot-feature", userCtx)
        cachedFlag.Store(enabled)
    }
}()
5. Clean Up Old Flags
// Remove flags after full rollout
// Document flag lifecycle in code comments

// TODO: Remove this flag after 2025-06-01
if service.IsEnabled(ctx, "temp-feature", userCtx) {
    // ...
}

Testing

func TestFeatureFlag(t *testing.T) {
    // Create test provider
    provider := providers.NewLocalProvider(
        features.LocalProviderConfig{
            Flags: map[string]features.FlagConfig{
                "test-flag": {
                    Key:     "test-flag",
                    Type:    "boolean",
                    Enabled: true,
                },
            },
        },
        nil,
    )

    service := features.NewService(provider, logger)

    userCtx := features.NewUserContext("test-user")
    enabled := service.IsEnabled(context.Background(), "test-flag", userCtx)

    assert.True(t, enabled)
}

Performance Considerations

  • Caching: Enable caching for production (5-minute TTL recommended)
  • Refresh Interval: 30 seconds for real-time, 5 minutes for less critical
  • User Context: Only include necessary attributes
  • Hot Paths: Cache flag values for high-traffic code paths

Migration from Hardcoded Flags

// Before
const enableNewFeature = true

if enableNewFeature {
    // ...
}

// After
if service.IsEnabled(ctx, "new-feature", userCtx) {
    // ...
}

License

MIT License - see LICENSE file for details

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewExtension

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

NewExtension creates a new feature flags extension.

func NewExtensionWithConfig

func NewExtensionWithConfig(config Config) forge.Extension

NewExtensionWithConfig creates a new extension with complete config.

Types

type Config

type Config struct {
	// Enabled determines if feature flags extension is enabled
	Enabled bool `json:"enabled" yaml:"enabled"`

	// Provider specifies the backend provider
	// Options: "local", "launchdarkly", "unleash", "flagsmith", "posthog"
	Provider string `json:"provider" yaml:"provider"`

	// RefreshInterval is how often to refresh flags from remote provider
	RefreshInterval time.Duration `json:"refresh_interval" yaml:"refresh_interval"`

	// EnableCache enables local caching of flag values
	EnableCache bool `json:"enable_cache" yaml:"enable_cache"`

	// CacheTTL is the cache TTL for flag values
	CacheTTL time.Duration `json:"cache_ttl" yaml:"cache_ttl"`

	// Local provider settings
	Local LocalProviderConfig `json:"local" yaml:"local"`

	// LaunchDarkly provider settings
	LaunchDarkly LaunchDarklyConfig `json:"launchdarkly" yaml:"launchdarkly"`

	// Unleash provider settings
	Unleash UnleashConfig `json:"unleash" yaml:"unleash"`

	// Flagsmith provider settings
	Flagsmith FlagsmithConfig `json:"flagsmith" yaml:"flagsmith"`

	// PostHog provider settings
	PostHog PostHogConfig `json:"posthog" yaml:"posthog"`

	// DefaultFlags are default flag values (used as fallback)
	DefaultFlags map[string]any `json:"default_flags" yaml:"default_flags"`
}

Config holds feature flags extension configuration.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the default feature flags configuration.

type ConfigOption

type ConfigOption func(*Config)

ConfigOption configures the feature flags extension.

func WithCache

func WithCache(enabled bool, ttl time.Duration) ConfigOption

WithCache enables caching with TTL.

func WithConfig

func WithConfig(config Config) ConfigOption

WithConfig sets the complete config.

func WithDefaultFlags

func WithDefaultFlags(flags map[string]any) ConfigOption

WithDefaultFlags sets default flag values.

func WithEnabled

func WithEnabled(enabled bool) ConfigOption

WithEnabled sets whether feature flags is enabled.

func WithFlagsmith

func WithFlagsmith(apiURL, environmentKey string) ConfigOption

WithFlagsmith configures Flagsmith provider.

func WithLaunchDarkly

func WithLaunchDarkly(sdkKey string) ConfigOption

WithLaunchDarkly configures LaunchDarkly provider.

func WithLocalFlags

func WithLocalFlags(flags map[string]FlagConfig) ConfigOption

WithLocalFlags sets local provider flags.

func WithPostHog

func WithPostHog(apiKey, host string) ConfigOption

WithPostHog configures PostHog provider.

func WithProvider

func WithProvider(provider string) ConfigOption

WithProvider sets the feature flags provider.

func WithRefreshInterval

func WithRefreshInterval(interval time.Duration) ConfigOption

WithRefreshInterval sets the refresh interval.

func WithUnleash

func WithUnleash(url, apiToken, appName string) ConfigOption

WithUnleash configures Unleash provider.

type EvaluationResult

type EvaluationResult struct {
	// FlagKey is the flag key
	FlagKey string `json:"flag_key"`

	// Value is the evaluated value
	Value any `json:"value"`

	// VariationIndex is the variation index (if applicable)
	VariationIndex int `json:"variation_index,omitempty"`

	// Reason is the evaluation reason
	Reason string `json:"reason"`

	// RuleID is the ID of the rule that matched (if applicable)
	RuleID string `json:"rule_id,omitempty"`
}

EvaluationResult holds the result of a flag evaluation.

type Extension

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

Extension implements forge.Extension for feature flags.

func (*Extension) Dependencies

func (e *Extension) Dependencies() []string

Dependencies returns extension dependencies.

func (*Extension) Health

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

Health checks the extension health.

func (*Extension) Register

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

Register registers the extension with the app.

func (*Extension) Service

func (e *Extension) Service() *Service

Service returns the feature flags service.

func (*Extension) Start

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

Start starts the extension.

func (*Extension) Stop

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

Stop stops the extension gracefully.

type FlagConfig

type FlagConfig struct {
	// Key is the unique flag identifier
	Key string `json:"key" yaml:"key"`

	// Name is the human-readable name
	Name string `json:"name" yaml:"name"`

	// Description describes what this flag controls
	Description string `json:"description" yaml:"description"`

	// Type is the flag value type: "boolean", "string", "number", "json"
	Type string `json:"type" yaml:"type"`

	// Enabled is the default enabled state for boolean flags
	Enabled bool `json:"enabled" yaml:"enabled"`

	// Value is the default value for non-boolean flags
	Value any `json:"value" yaml:"value"`

	// Targeting defines user/group targeting rules
	Targeting []TargetingRule `json:"targeting" yaml:"targeting"`

	// Rollout defines percentage-based rollout
	Rollout *RolloutConfig `json:"rollout" yaml:"rollout"`
}

FlagConfig defines a single feature flag.

type FlagEvaluationEvent

type FlagEvaluationEvent struct {
	// FlagKey is the flag key
	FlagKey string `json:"flag_key"`

	// UserContext is the user context
	UserContext *UserContext `json:"user_context"`

	// Value is the evaluated value
	Value any `json:"value"`

	// Default indicates if the default value was used
	Default bool `json:"default"`

	// Reason is the evaluation reason
	Reason string `json:"reason"`

	// Timestamp is when the evaluation occurred
	Timestamp int64 `json:"timestamp"`
}

FlagEvaluationEvent represents a flag evaluation event for analytics.

type FlagValue

type FlagValue struct {
	// Key is the flag key
	Key string `json:"key"`

	// Value is the flag value
	Value any `json:"value"`

	// Enabled indicates if the flag is enabled (for boolean flags)
	Enabled bool `json:"enabled"`

	// Variation is the variation key/name
	Variation string `json:"variation,omitempty"`

	// Reason is the evaluation reason
	Reason string `json:"reason,omitempty"`
}

FlagValue represents a feature flag value with metadata.

type FlagsmithConfig

type FlagsmithConfig struct {
	// APIURL is the Flagsmith API URL
	APIURL string `json:"api_url" yaml:"api_url"`

	// EnvironmentKey is the Flagsmith environment key
	EnvironmentKey string `json:"environment_key" yaml:"environment_key"`
}

FlagsmithConfig for Flagsmith provider.

type LaunchDarklyConfig

type LaunchDarklyConfig struct {
	// SDKKey is the LaunchDarkly SDK key
	SDKKey string `json:"sdk_key" yaml:"sdk_key"`

	// Timeout for API requests
	Timeout time.Duration `json:"timeout" yaml:"timeout"`
}

LaunchDarklyConfig for LaunchDarkly provider.

type LocalProviderConfig

type LocalProviderConfig struct {
	// Flags are the static flag definitions
	Flags map[string]FlagConfig `json:"flags" yaml:"flags"`
}

LocalProviderConfig for local in-memory provider.

type PostHogConfig

type PostHogConfig struct {
	// APIKey is the PostHog project API key
	APIKey string `json:"api_key" yaml:"api_key"`

	// Host is the PostHog host (default: https://app.posthog.com)
	Host string `json:"host" yaml:"host"`

	// PersonalAPIKey for admin operations (optional)
	PersonalAPIKey string `json:"personal_api_key" yaml:"personal_api_key"`

	// EnableLocalEvaluation enables local flag evaluation (faster)
	EnableLocalEvaluation bool `json:"enable_local_evaluation" yaml:"enable_local_evaluation"`

	// PollingInterval for flag refresh (default: 30s)
	PollingInterval time.Duration `json:"polling_interval" yaml:"polling_interval"`
}

PostHogConfig for PostHog provider.

type Provider

type Provider interface {
	// Name returns the provider name
	Name() string

	// Initialize initializes the provider
	Initialize(ctx context.Context) error

	// IsEnabled checks if a boolean flag is enabled for a user/context
	IsEnabled(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue bool) (bool, error)

	// GetString gets a string flag value
	GetString(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue string) (string, error)

	// GetInt gets an integer flag value
	GetInt(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue int) (int, error)

	// GetFloat gets a float flag value
	GetFloat(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue float64) (float64, error)

	// GetJSON gets a JSON flag value
	GetJSON(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue any) (any, error)

	// GetAllFlags gets all flags for a user/context
	GetAllFlags(ctx context.Context, userCtx *UserContext) (map[string]any, error)

	// Refresh refreshes flags from remote source
	Refresh(ctx context.Context) error

	// Close closes the provider
	Close() error

	// Health checks provider health
	Health(ctx context.Context) error
}

Provider defines the interface for feature flag providers.

func NewFlagsmithProvider

func NewFlagsmithProvider(config FlagsmithConfig, defaults map[string]any) (Provider, error)

NewFlagsmithProvider creates a new Flagsmith provider NOTE: Temporarily disabled until dependencies are resolved.

func NewLaunchDarklyProvider

func NewLaunchDarklyProvider(config LaunchDarklyConfig, defaults map[string]any) (Provider, error)

NewLaunchDarklyProvider creates a new LaunchDarkly provider NOTE: Temporarily disabled until dependencies are resolved.

func NewLocalProvider

func NewLocalProvider(config LocalProviderConfig, defaults map[string]any) Provider

NewLocalProvider creates a new local in-memory provider.

func NewPostHogProvider

func NewPostHogProvider(config PostHogConfig, defaults map[string]any) (Provider, error)

NewPostHogProvider creates a new PostHog provider NOTE: Temporarily disabled until dependencies are resolved.

func NewUnleashProvider

func NewUnleashProvider(config UnleashConfig, defaults map[string]any) (Provider, error)

NewUnleashProvider creates a new Unleash provider NOTE: Temporarily disabled until dependencies are resolved.

type RolloutConfig

type RolloutConfig struct {
	// Percentage of users to enable (0-100)
	Percentage int `json:"percentage" yaml:"percentage"`

	// Attribute to use for consistent hashing (default: "user_id")
	Attribute string `json:"attribute" yaml:"attribute"`
}

RolloutConfig defines percentage-based rollout.

type Service

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

Service provides high-level feature flag operations.

func Get added in v0.7.4

func Get(container forge.Container) (*Service, error)

Get retrieves the features service from the container. Returns error if features service is not registered.

func GetFromApp added in v0.7.4

func GetFromApp(app forge.App) (*Service, error)

GetFromApp is a convenience helper to get features service from an App.

func MustGet added in v0.7.4

func MustGet(container forge.Container) *Service

MustGet retrieves the features service from the container. Panics if features service is not registered. Use this during application startup where failure should halt execution.

func MustGetFromApp added in v0.7.4

func MustGetFromApp(app forge.App) *Service

MustGetFromApp is a convenience helper to get features service from an App. Panics if features service is not registered. Use this during application startup where failure should halt execution.

func NewService

func NewService(provider Provider, logger forge.Logger) *Service

NewService creates a new feature flags service.

func (*Service) GetAllFlags

func (s *Service) GetAllFlags(ctx context.Context, userCtx *UserContext) (map[string]any, error)

GetAllFlags gets all feature flags for a user/context.

func (*Service) GetFloat

func (s *Service) GetFloat(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue float64) float64

GetFloat gets a float feature flag value.

func (*Service) GetInt

func (s *Service) GetInt(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue int) int

GetInt gets an integer feature flag value.

func (*Service) GetJSON

func (s *Service) GetJSON(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue any) any

GetJSON gets a JSON feature flag value.

func (*Service) GetString

func (s *Service) GetString(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue string) string

GetString gets a string feature flag value.

func (*Service) IsEnabled

func (s *Service) IsEnabled(ctx context.Context, flagKey string, userCtx *UserContext) bool

IsEnabled checks if a boolean feature flag is enabled.

func (*Service) IsEnabledWithDefault

func (s *Service) IsEnabledWithDefault(ctx context.Context, flagKey string, userCtx *UserContext, defaultValue bool) bool

IsEnabledWithDefault checks if a boolean feature flag is enabled with a custom default.

func (*Service) Refresh

func (s *Service) Refresh(ctx context.Context) error

Refresh refreshes flags from remote source.

type TargetingRule

type TargetingRule struct {
	// Attribute is the user attribute to match (e.g., "user_id", "email", "group")
	Attribute string `json:"attribute" yaml:"attribute"`

	// Operator is the comparison operator: "equals", "contains", "in", "not_in"
	Operator string `json:"operator" yaml:"operator"`

	// Values are the values to match against
	Values []string `json:"values" yaml:"values"`

	// Value is the flag value for matching users
	Value any `json:"value" yaml:"value"`
}

TargetingRule defines targeting rules for a flag.

type UnleashConfig

type UnleashConfig struct {
	// URL is the Unleash API URL
	URL string `json:"url" yaml:"url"`

	// APIToken is the Unleash API token
	APIToken string `json:"api_token" yaml:"api_token"`

	// AppName is the application name
	AppName string `json:"app_name" yaml:"app_name"`

	// Environment is the environment name
	Environment string `json:"environment" yaml:"environment"`

	// InstanceID is a unique instance identifier
	InstanceID string `json:"instance_id" yaml:"instance_id"`
}

UnleashConfig for Unleash provider.

type UserContext

type UserContext struct {
	// UserID is the unique user identifier
	UserID string `json:"user_id"`

	// Email is the user email
	Email string `json:"email,omitempty"`

	// Name is the user name
	Name string `json:"name,omitempty"`

	// Groups are user group memberships
	Groups []string `json:"groups,omitempty"`

	// Attributes are custom attributes for targeting
	Attributes map[string]any `json:"attributes,omitempty"`

	// IP is the user IP address
	IP string `json:"ip,omitempty"`

	// Country is the user country code
	Country string `json:"country,omitempty"`
}

UserContext holds user/context information for flag evaluation.

func NewUserContext

func NewUserContext(userID string) *UserContext

NewUserContext creates a new user context for flag evaluation.

func (*UserContext) GetAttribute

func (uc *UserContext) GetAttribute(key string) (any, bool)

GetAttribute gets a custom attribute.

func (*UserContext) GetAttributeString

func (uc *UserContext) GetAttributeString(key string) (string, error)

GetAttributeString gets a string attribute.

func (*UserContext) HasGroup

func (uc *UserContext) HasGroup(group string) bool

HasGroup checks if user is in a group.

func (*UserContext) WithAttribute

func (uc *UserContext) WithAttribute(key string, value any) *UserContext

WithAttribute sets a custom attribute.

func (*UserContext) WithCountry

func (uc *UserContext) WithCountry(country string) *UserContext

WithCountry sets the user country.

func (*UserContext) WithEmail

func (uc *UserContext) WithEmail(email string) *UserContext

WithEmail sets the user email.

func (*UserContext) WithGroups

func (uc *UserContext) WithGroups(groups []string) *UserContext

WithGroups sets the user groups.

func (*UserContext) WithIP

func (uc *UserContext) WithIP(ip string) *UserContext

WithIP sets the user IP address.

func (*UserContext) WithName

func (uc *UserContext) WithName(name string) *UserContext

WithName sets the user name.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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