registry

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Mar 31, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package registry handles plugin discovery and lifecycle management.

The registry scans for installed plugins in the standard location (~/.finfocus/plugins/) and manages their metadata and availability.

Plugin Directory Structure

Plugins are organized as:

~/.finfocus/plugins/<name>/<version>/
├── finfocus-plugin-<name>     # Plugin binary
└── plugin.manifest.json         # Optional manifest

Discovery Process

  1. Scan plugin directories for valid binaries
  2. Validate optional manifest files
  3. Register discovered plugins for use

Platform Detection

Executable detection is platform-aware, checking Unix permissions or Windows .exe extensions as appropriate.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrAssetNotInChecksums is returned when the asset name is not listed in the checksums file.
	ErrAssetNotInChecksums = errors.New("asset not listed in checksums file")

	// ErrMalformedChecksums is returned when the checksums file contains no valid entries.
	ErrMalformedChecksums = errors.New("checksums file contains no valid entries")

	// ErrChecksumMismatch is returned when the computed hash does not match the expected hash.
	ErrChecksumMismatch = errors.New("checksum mismatch")
)

Sentinel errors for checksum verification.

View Source
var ErrMetadataNotFound = errors.New("metadata file not found")

ErrMetadataNotFound is returned when plugin.metadata.json does not exist.

Functions

func CompareVersions

func CompareVersions(v1, v2 string) (int, error)

CompareVersions compares two versions. Returns:

-1 if v1 < v2
 0 if v1 == v2

CompareVersions compares two semantic version strings and returns -1, 0, or 1. It ignores a leading 'v' prefix on each version before parsing. v1 and v2 are the version strings to compare. The returned int is -1 if v1 < v2, 0 if v1 == v2, and 1 if v1 > v2. An error is returned if either input is not a valid semantic version.

func ExtractArchive

func ExtractArchive(archivePath, destDir string) error

ExtractArchive extracts an archive to the destination directory. ExtractArchive extracts the archive at archivePath into destDir. It supports archives with .tar.gz, .tgz, and .zip extensions. archivePath is the path to the archive file to extract. destDir is the directory where extracted files will be written. An error is returned if the archive format is unsupported or if extraction fails.

func IsValidVersion

func IsValidVersion(version string) bool

IsValidVersion reports whether the provided string is a valid semantic version. A leading "v" is ignored; the function returns true if the remainder parses as a semantic version, false otherwise.

func ListPluginsFromRegistry

func ListPluginsFromRegistry() ([]string, error)

ListPluginsFromRegistry returns the names of all plugins contained in the embedded registry. It loads the embedded registry and collects each plugin key into a slice. The returned error is non-nil if the embedded registry cannot be loaded or parsed.

func ParseChecksumsFile added in v0.3.0

func ParseChecksumsFile(data []byte, assetName string) (string, error)

ParseChecksumsFile parses a SHA256SUMS-format byte slice and returns the hash for the named asset. It supports GNU (two-space), BSD (single-space), and binary-mode (*-prefixed filename) formats. Blank lines and lines starting with '#' are skipped. Lines with invalid hash lengths or non-hex characters are also skipped. Returns ErrMalformedChecksums if no valid entries are found, or ErrAssetNotInChecksums if the asset name is not present.

func ParseGitHubURL

func ParseGitHubURL(url string) (string, string, error)

ParseGitHubURL extracts owner and repo from a GitHub URL.

ParseGitHubURL extracts the owner and repository name from a GitHub URL of the form "github.com/owner/repo". It returns the captured owner and repo strings. If the input does not match the expected GitHub URL format, it returns a non-nil error.

func ParseRegionFromBinaryName added in v0.3.0

func ParseRegionFromBinaryName(binaryPath string) (string, bool)

ParseRegionFromBinaryName extracts a region string from a binary filename. It looks for common cloud region patterns like "us-east-1", "eu-west-1", etc. File extensions (e.g., ".exe") are stripped before matching. Returns the region and true if found, or empty string and false otherwise.

func ReadPluginMetadata added in v0.3.0

func ReadPluginMetadata(dir string) (map[string]string, error)

ReadPluginMetadata reads plugin.metadata.json from the given directory.

func SatisfiesConstraint

func SatisfiesConstraint(version string, constraint *VersionConstraint) (bool, error)

SatisfiesConstraint reports whether the provided semantic version satisfies the given VersionConstraint. It accepts a version string (a leading "v" is ignored) and evaluates it against the parsed constraint. If the constraint or its parsed representation is nil, an error is returned. If the version cannot be parsed as a semantic version, an error describing the invalid version is returned. The boolean result is true when the version satisfies the constraint, false otherwise.

func ValidateBinary

func ValidateBinary(binaryPath string) error

ValidateBinary verifies that the file at binaryPath exists and is a runnable binary for the current platform. On Windows this requires the path to end with the `.exe` extension (case-insensitive). On non-Windows platforms this requires at least one executable permission bit to be set. Returns an error if the path does not exist, points to a directory, lacks the required extension or executable bits, or if other file stat errors occur.

func ValidateRegistryEntry

func ValidateRegistryEntry(entry RegistryEntry) error

ValidateRegistryEntry checks that the required fields of a RegistryEntry are present and well-formed. It returns an error describing the first validation failure encountered or nil if the entry is valid.

The following validations are performed:

  • `Name` must not be empty.
  • `Repository` must not be empty and must match the "owner/repo" format.
  • If `SecurityLevel` is set, it must be one of "official", "community", or "experimental".

ValidateRegistryEntry validates required fields and formats of a RegistryEntry. It checks that Name and Repository are present, that Repository matches the "owner/repo" pattern, and that SecurityLevel (if set) is one of "official", "community", or "experimental". On failure it returns an error describing which field is invalid and includes the entry name when available. On success it returns nil.

func VerifyChecksum added in v0.3.0

func VerifyChecksum(ctx context.Context, filePath, expectedHash string) error

VerifyChecksum computes the SHA256 hash of the file at filePath and compares it against expectedHash. The expectedHash is normalized to lowercase before comparison. Returns nil if the hashes match, or an error wrapping ErrChecksumMismatch if they differ.

func WritePluginMetadata added in v0.3.0

func WritePluginMetadata(dir string, metadata map[string]string) error

WritePluginMetadata writes the provided metadata map as indented JSON to a file named plugin.metadata.json inside dir. The file is written with permission mode 0600 and a trailing newline is appended.

dir is the target directory for the metadata file. metadata is the key/value map to encode.

It returns an error if the metadata cannot be marshaled to JSON or if the file cannot be written.

Types

type AssetNamingHints

type AssetNamingHints struct {
	// AssetPrefix is the project name prefix used in asset filenames
	// (e.g., "finfocus-plugin-aws-public" instead of just "aws-public")
	AssetPrefix string
	// Region specifies a region suffix to match (e.g., "us-east-1")
	Region string
	// VersionPrefix if false, version in asset name has no "v" prefix
	VersionPrefix bool
}

AssetNamingHints provides hints for matching assets with non-standard naming conventions.

type EmbeddedRegistry

type EmbeddedRegistry struct {
	SchemaVersion string                   `json:"schema_version"`
	Plugins       map[string]RegistryEntry `json:"plugins"`
}

EmbeddedRegistry represents the embedded plugin registry catalog.

func GetEmbeddedRegistry

func GetEmbeddedRegistry() (*EmbeddedRegistry, error)

GetEmbeddedRegistry returns the parsed embedded registry catalog. It initializes and parses the embedded registry data on first call in a thread-safe manner. GetEmbeddedRegistry parses and returns the embedded plugin registry, initializing it once on first use. It returns the parsed *EmbeddedRegistry and a non-nil error if parsing the embedded registry failed; the initialization is performed exactly once and is safe for concurrent use.

type FallbackInfo added in v0.2.3

type FallbackInfo struct {
	// Release is the GitHub release that was selected
	Release *GitHubRelease
	// Asset is the platform-compatible asset from the release
	Asset *ReleaseAsset
	// WasFallback is true if the selected version differs from the requested version
	WasFallback bool
	// RequestedVersion is the original version that was requested (empty if @latest)
	RequestedVersion string
	// FallbackReason explains why fallback occurred (e.g., "no compatible assets")
	FallbackReason string
}

FallbackInfo contains metadata about release selection, including whether a fallback occurred during version resolution.

type GitHubClient

type GitHubClient struct {
	HTTPClient *http.Client
	BaseURL    string
	// contains filtered or unexported fields
}

GitHubClient provides GitHub API access for releases.

func NewGitHubClient

func NewGitHubClient() *GitHubClient

NewGitHubClient creates and returns a GitHubClient configured to access the GitHub API. The client has an HTTP client with a 5-minute timeout (for large plugin downloads) and BaseURL set to https://api.github.com. It initializes the authentication token by reading NewGitHubClient creates and returns a GitHubClient configured for the GitHub API. It sets BaseURL to "https://api.github.com", constructs an HTTP client using the package downloadTimeout for request timeouts, and initializes the client's token from the GITHUB_TOKEN environment variable or, if unset, by attempting to obtain it from the `gh` CLI.

func (*GitHubClient) DownloadAsset

func (c *GitHubClient) DownloadAsset(
	ctx context.Context,
	url, destPath string,
	progress func(downloaded, total int64),
) error

DownloadAsset downloads a release asset to a local file.

func (*GitHubClient) FindReleaseWithAsset added in v0.2.2

func (c *GitHubClient) FindReleaseWithAsset(
	ctx context.Context,
	owner, repo, version, projectName string,
	hints *AssetNamingHints,
) (*GitHubRelease, *ReleaseAsset, error)

FindReleaseWithAsset attempts to find a release with a matching platform asset. If version is specified, it first tries that exact version. If no asset is found for the requested version, or if version is empty, it falls back to searching through recent stable releases to find one with a compatible asset.

Parameters:

  • owner, repo: GitHub repository owner and name
  • version: specific version to try first (empty string for latest)
  • projectName: project name used in asset filenames
  • hints: optional naming hints for asset matching

Returns the release and asset that matched, or an error if none found.

func (*GitHubClient) FindReleaseWithFallbackInfo added in v0.2.3

func (c *GitHubClient) FindReleaseWithFallbackInfo(
	ctx context.Context,
	owner, repo, version, projectName string,
	hints *AssetNamingHints,
) (*FallbackInfo, error)

FindReleaseWithFallbackInfo attempts to find a release with a matching platform asset, returning detailed information about whether a fallback occurred.

If version is specified, it first tries that exact version. If no asset is found for the requested version, or if version is empty, it falls back to searching through recent stable releases to find one with a compatible asset.

Parameters:

  • owner, repo: GitHub repository owner and name
  • version: specific version to try first (empty string for latest)
  • projectName: project name used in asset filenames
  • hints: optional naming hints for asset matching

Returns FallbackInfo containing:

  • Release and Asset that matched
  • WasFallback: true if the returned version differs from the requested version
  • RequestedVersion: the original version that was requested
  • FallbackReason: explanation if fallback occurred

func (*GitHubClient) GetLatestRelease

func (c *GitHubClient) GetLatestRelease(ctx context.Context, owner, repo string) (*GitHubRelease, error)

GetLatestRelease returns the latest release for a repository.

func (*GitHubClient) GetReleaseByTag

func (c *GitHubClient) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*GitHubRelease, error)

GetReleaseByTag returns a specific release by tag name.

func (*GitHubClient) ListStableReleases added in v0.2.2

func (c *GitHubClient) ListStableReleases(ctx context.Context, owner, repo string, limit int) ([]GitHubRelease, error)

ListStableReleases fetches all releases and returns only stable (non-draft, non-prerelease) releases sorted by creation order (newest first, as returned by GitHub API).

type GitHubRelease

type GitHubRelease struct {
	TagName    string         `json:"tag_name"`
	Name       string         `json:"name"`
	Draft      bool           `json:"draft"`
	Prerelease bool           `json:"prerelease"`
	Assets     []ReleaseAsset `json:"assets"`
}

GitHubRelease represents release metadata from GitHub API.

type InstallOptions

type InstallOptions struct {
	Force            bool              // Reinstall even if version exists
	NoSave           bool              // Don't add to config file
	PluginDir        string            // Custom plugin directory (default: ~/.finfocus/plugins)
	FallbackToLatest bool              // Automatically install latest stable version if requested version lacks assets
	NoFallback       bool              // Disable fallback behavior entirely (fail if requested version lacks assets)
	Metadata         map[string]string // User-supplied metadata (e.g., region=us-west-2), stored as plugin.metadata.json
	SkipChecksum     bool              // SkipChecksum bypasses SHA256 checksum verification during installation
}

InstallOptions configures plugin installation behavior.

type InstallResult

type InstallResult struct {
	Name             string
	Version          string
	Path             string
	FromURL          bool
	Repository       string
	WasFallback      bool   // True if installed version differs from requested
	RequestedVersion string // Original version requested (empty if @latest)
}

InstallResult contains the result of a plugin installation.

type Installer

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

Installer handles plugin installation from registry or URLs.

func NewInstaller

func NewInstaller(pluginDir string) *Installer

NewInstaller creates a new Installer configured to install plugins into pluginDir. If pluginDir is empty, it defaults to "$HOME/.finfocus/plugins"; if the home directory cannot be determined, the default is "./.finfocus/plugins" relative to NewInstaller creates an Installer configured to install plugins into pluginDir. If pluginDir is empty, it defaults to $HOME/.finfocus/plugins; when the user home directory cannot be determined it falls back to the current directory. The returned Installer contains an initialized GitHub client.

func NewInstallerWithClient

func NewInstallerWithClient(client *GitHubClient, pluginDir string) *Installer

NewInstallerWithClient creates a new Installer using the provided GitHub client. If pluginDir is empty, it defaults to $HOME/.finfocus/plugins; if the home directory cannot be determined it falls back to the current directory. NewInstallerWithClient creates an Installer that uses the provided GitHub client and a resolved plugin directory. If pluginDir is empty, it defaults to "$HOME/.finfocus/plugins"; if the user home directory cannot be determined it falls back to the current directory ("./") and uses "./.finfocus/plugins". NewInstallerWithClient creates an Installer that uses the provided GitHub client and plugin directory. If pluginDir is empty, it is resolved to $HOME/.finfocus/plugins; if the user's home directory cannot be determined, it falls back to the current working directory. The returned Installer's client field is set to the provided client and its pluginDir field to the resolved path.

func (*Installer) Install

func (i *Installer) Install(
	ctx context.Context,
	specifier string,
	opts InstallOptions,
	progress func(msg string),
) (*InstallResult, error)

Install installs a plugin from a specifier (name or URL with optional version).

func (*Installer) Remove

func (i *Installer) Remove(name string, opts RemoveOptions, progress func(msg string)) error

Remove removes an installed plugin.

func (*Installer) RemoveOtherVersions added in v0.2.2

func (i *Installer) RemoveOtherVersions(
	name string,
	keepVersion string,
	pluginDir string,
	progress func(msg string),
) (*RemoveOtherVersionsResult, error)

RemoveOtherVersions removes all versions of a plugin except the specified one. This is used for cleanup after upgrade/install operations. It scans the plugin directory for version subdirectories and removes any that don't match the keepVersion parameter.

func (*Installer) Update

func (i *Installer) Update(
	ctx context.Context,
	name string,
	opts UpdateOptions,
	progress func(msg string),
) (*UpdateResult, error)

Update updates an installed plugin to the latest or specified version.

type Manifest

type Manifest struct {
	Name        string            `json:"name"`
	Version     string            `json:"version"`
	Description string            `json:"description"`
	Author      string            `json:"author"`
	Providers   []string          `json:"providers"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}

Manifest represents the optional plugin.manifest.json metadata file. It provides additional plugin information and validation data.

func LoadManifest

func LoadManifest(path string) (*Manifest, error)

LoadManifest loads and parses a plugin manifest JSON file from the specified path. It returns an error if the file doesn't exist or contains invalid JSON.

type PluginInfo

type PluginInfo struct {
	Name     string            `json:"name"`
	Version  string            `json:"version"`
	Path     string            `json:"path"`
	Metadata map[string]string `json:"metadata,omitempty"`
}

PluginInfo contains metadata about a discovered plugin.

func (PluginInfo) Region added in v0.3.0

func (p PluginInfo) Region() string

Region returns the plugin's region from metadata, or empty string if universal.

type PluginSpecifier

type PluginSpecifier struct {
	Name    string
	Version string
	IsURL   bool
	Owner   string
	Repo    string
}

PluginSpecifier represents a parsed plugin specifier (name or URL with optional version).

func ParsePluginSpecifier

func ParsePluginSpecifier(spec string) (*PluginSpecifier, error)

ParsePluginSpecifier parses a plugin specifier string. Formats:

  • "kubecost" - registry plugin, latest version
  • "kubecost@v1.0.0" - registry plugin, specific version
  • "github.com/owner/repo" - GitHub URL, latest version

ParsePluginSpecifier parses a plugin specifier string into a PluginSpecifier. It accepts either a registry name or a GitHub URL with an optional version suffix separated by `@` (for example: `kubecost`, `kubecost@v1.0.0`, `github.com/owner/repo`, or `github.com/owner/repo@v1.0.0`).

The `spec` parameter is the plugin specifier to parse. If `spec` is empty, the function returns an error. When the input is a GitHub URL, the returned PluginSpecifier has IsURL set to true, Owner and Repo populated from the URL, and Name derived from the repository name with a leading `finfocus-plugin-` prefix removed (if present). When the input is a registry name, IsURL is false and Name is set to the given name. The Version field is set if a `@version` suffix is provided; otherwise it is empty.

The function returns an error if `spec` is empty or if a GitHub URL does not match the expected `github.com/owner/repo` format.

type Registry

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

Registry manages plugin discovery and lifecycle operations. It scans plugin directories and provides client connections to active plugins.

func NewDefault

func NewDefault() *Registry

NewDefault creates a new Registry with default configuration from config.PluginDir and using ProcessLauncher for plugin execution.

func (*Registry) GetLatestPlugin

func (r *Registry) GetLatestPlugin(name string) (PluginInfo, bool, []string, error)

GetLatestPlugin returns the latest version of a specific plugin. Returns (PluginInfo{}, false, warnings) if plugin not found or all versions are invalid.

func (*Registry) ListLatestPlugins

func (r *Registry) ListLatestPlugins() ([]PluginInfo, []string, error)

ListLatestPlugins scans the plugin directory and returns only the latest version of each plugin. Plugins with same name in different locations are treated as duplicates and the latest version across all locations is selected. Returns warnings for invalid or corrupted plugins.

func (*Registry) ListPlugins

func (r *Registry) ListPlugins() ([]PluginInfo, error)

ListPlugins scans the plugin directory and returns metadata for all discovered plugins. It returns an empty list if the plugin directory doesn't exist.

func (*Registry) Open

func (r *Registry) Open(
	ctx context.Context,
	onlyName string,
) ([]*pluginhost.Client, func(), error)

Open launches plugin processes and returns active gRPC clients with a cleanup function. If onlyName is non-empty, only that specific plugin is opened.

type RegistryAssetHints

type RegistryAssetHints struct {
	// AssetPrefix is the project name prefix used in asset filenames
	// (e.g., "finfocus-plugin-aws-public" for assets named
	// "finfocus-plugin-aws-public_0.0.6_Linux_x86_64.tar.gz")
	AssetPrefix string `json:"asset_prefix,omitempty"`
	// DefaultRegion is the region suffix to use when downloading (e.g., "us-east-1")
	DefaultRegion string `json:"default_region,omitempty"`
	// VersionPrefix indicates if the version in asset names has a "v" prefix
	VersionPrefix bool `json:"version_prefix,omitempty"`
}

RegistryAssetHints provides hints for asset naming conventions specific to a plugin.

type RegistryEntry

type RegistryEntry struct {
	Name               string              `json:"name"`
	Description        string              `json:"description"`
	Repository         string              `json:"repository"`
	Author             string              `json:"author"`
	License            string              `json:"license"`
	Homepage           string              `json:"homepage"`
	SupportedProviders []string            `json:"supported_providers"`
	Capabilities       []string            `json:"capabilities"`
	SecurityLevel      string              `json:"security_level"`
	MinSpecVersion     string              `json:"min_spec_version"`
	AssetHints         *RegistryAssetHints `json:"asset_hints,omitempty"`
}

RegistryEntry represents a plugin in the embedded registry.

func GetAllPluginEntries

func GetAllPluginEntries() ([]RegistryEntry, error)

GetAllPluginEntries retrieves all plugin entries present in the embedded registry catalog. It returns a slice containing every RegistryEntry found in the embedded catalog. If the embedded registry cannot be loaded or parsed, an error is returned.

func GetPlugin

func GetPlugin(name string) (*RegistryEntry, error)

GetPlugin looks up a plugin by name in the embedded registry and returns its RegistryEntry. The name parameter is the plugin identifier to look up. It returns a pointer to the RegistryEntry if the plugin exists. If parsing the embedded registry fails, an error from GetEmbeddedRegistry is returned. If the plugin is not present in the registry, an error indicating the plugin was not found is returned.

type ReleaseAsset

type ReleaseAsset struct {
	Name               string `json:"name"`
	Size               int64  `json:"size"`
	BrowserDownloadURL string `json:"browser_download_url"`
	ContentType        string `json:"content_type"`
}

ReleaseAsset represents a downloadable asset.

func FindChecksumAsset added in v0.3.0

func FindChecksumAsset(release *GitHubRelease) *ReleaseAsset

FindChecksumAsset searches a release's assets for a checksums file. It performs a case-insensitive match for the asset name "checksums.txt". Returns the first matching asset, or nil if no checksums asset is found.

func FindPlatformAsset

func FindPlatformAsset(release *GitHubRelease, projectName string) (*ReleaseAsset, error)

FindPlatformAsset locates the release asset matching the current OS and architecture for the given project. It tries multiple naming conventions to handle different GoReleaser configurations:

  • Standard: {project}_{version}_{os}_{arch}.{ext}
  • Capitalized OS: {project}_{version}_{Os}_{arch}.{ext}
  • x86_64 arch: {project}_{version}_{os}_x86_64.{ext}
  • Region-specific: {project}_{version}_{Os}_{arch}_{region}.{ext}

release is the GitHubRelease to search; projectName is the project name used in asset filenames. It returns the matching ReleaseAsset or an error that includes the available asset names when no match is found.

func FindPlatformAssetWithHints

func FindPlatformAssetWithHints(
	release *GitHubRelease,
	projectName string,
	hints *AssetNamingHints,
) (*ReleaseAsset, error)

FindPlatformAssetWithHints locates the release asset with custom naming hints.

type RemoveOptions

type RemoveOptions struct {
	KeepConfig bool   // Don't remove from config file
	PluginDir  string // Custom plugin directory
}

RemoveOptions configures plugin removal behavior.

type RemoveOtherVersionsResult added in v0.2.2

type RemoveOtherVersionsResult struct {
	PluginName      string
	KeptVersion     string
	RemovedVersions []string
	BytesFreed      int64
}

RemoveOtherVersionsResult contains the result of removing other plugin versions.

type UpdateOptions

type UpdateOptions struct {
	DryRun       bool   // Show what would be updated without changes
	Version      string // Specific version to update to (empty = latest)
	PluginDir    string // Custom plugin directory
	SkipChecksum bool   // SkipChecksum bypasses SHA256 checksum verification during update
}

UpdateOptions configures plugin update behavior.

type UpdateResult

type UpdateResult struct {
	Name        string
	OldVersion  string
	NewVersion  string
	Path        string
	WasUpToDate bool
}

UpdateResult contains the result of a plugin update.

type VersionConstraint

type VersionConstraint struct {
	Raw        string
	Constraint *semver.Constraints
}

VersionConstraint represents a semantic version constraint for dependencies.

func ParseVersionConstraint

func ParseVersionConstraint(s string) (*VersionConstraint, error)

ParseVersionConstraint parses a version constraint string. Supported formats:

  • ">=1.0.0" - Greater than or equal
  • "<2.0.0" - Less than
  • ">=1.0.0,<2.0.0" - Range (AND)
  • "~1.2.3" - Patch-level changes (>=1.2.3,<1.3.0)

ParseVersionConstraint parses a semantic version constraint string and returns a VersionConstraint containing the original raw string and the parsed semver.Constraints.

If s is empty, an error is returned. If parsing fails, an error describing the invalid constraint and wrapping the underlying semver parse error is returned.

Jump to

Keyboard shortcuts

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