Help us shape the future of Go! Take the 2026 Go Developer Survey

prettyconverter

package module
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 14 Imported by: 0

README

go-pretty-converter

Go Reference CI Go Report Card

The only Markdown-to-book toolchain that checks its own work.

Transform a directory of MDX files into a PDF, an EPUB 3, and a Kindle (MOBI/AZW3) book — via headless Chrome and Calibre — with an automatic quality audit (pretty-converter analyze, plus a post-render check on every build) that catches overflow, broken links, low-contrast text, and clipped headings before your readers do. Pandoc and most Markdown→PDF tools convert and stop there; go-pretty-converter converts, then tells you what's still broken.

Library + CLI. Use it as a composable Go library or as a standalone command-line tool.

Fast. A 3,000-document book becomes a 3,535-page print-ready PDF in ~23 seconds — and validating those 3,000 documents takes 142 ms. Measured, reproducible numbers in BENCHMARKS.md.

Install

CLI (binary)
go install github.com/sazardev/go-pretty-converter/cmd/pretty-converter@latest
Library
go get github.com/sazardev/go-pretty-converter
Requirements
  • Go 1.26+
  • Chrome or Chromium — optional, only for PDF output. If none is found on your system, pretty-converter automatically downloads and caches a small headless-only Chrome build the first time you run it (like Playwright/Puppeteer do). Already have Chrome installed? It's used as-is, nothing is downloaded. Prefer to control this yourself? Pass --chrome-path /path/to/chrome or set PRETTY_CONVERTER_CHROME_PATH. Auto-download currently covers linux/amd64, darwin/amd64, darwin/arm64, and windows/amd64 — on linux/arm64 (no official build exists yet) install Chromium via your package manager and point --chrome-path at it.
  • Calibre — optional, only for Kindle output (--format kindle / pretty-converter kindle). Not bundled or auto-downloaded — install it from calibre-ebook.com so ebook-convert is on your PATH, or point --calibre-path / PRETTY_CONVERTER_CALIBRE_PATH at it.

Quick start

CLI
# Scaffold a new book project (interactive wizard)
pretty-converter init my-book

# Build a PDF
pretty-converter build --source my-book --out my-book.pdf

# Watch for changes and rebuild
pretty-converter watch --source my-book --out my-book.pdf

# Build a Kindle-ready ebook (needs Calibre's ebook-convert on PATH)
pretty-converter kindle --source my-book --out my-book.mobi

# Validate MDX files
pretty-converter check --source my-book

# Flag content that will render poorly on PDF/EPUB/Kindle (errors, warnings, improvements)
pretty-converter analyze --source my-book
Library
package main

import (
	"context"
	"log"

	prettyconverter "github.com/sazardev/go-pretty-converter"
)

func main() {
	pdf, err := prettyconverter.New(
		prettyconverter.WithSourceDir("./docs"),
		prettyconverter.WithOutputFile("output.pdf"),
		prettyconverter.WithTitle("My Documentation"),
		prettyconverter.WithAuthor("Jane Doe"),
	)
	if err != nil {
		log.Fatal(err)
	}

	if err := pdf.Build(context.Background()); err != nil {
		log.Fatal(err)
	}
}

How it works

MD/MDX files → Parse frontmatter & markdown → Transpile components → Compose HTML → Render PDF
  1. Parse — goldmark parses .md/.mdx files with YAML frontmatter. Fenced code blocks (```go, ```python, ...) are syntax-highlighted via Chroma, using a style paired to each theme's tone (e.g. Dracula for the dark theme, the Gruvbox style for the gruvbox theme, GitHub's light style everywhere else)
  2. Transpile — custom components (<DeepDive>, <Warning>, <Axiom>) become styled HTML
  3. Compose — HTML assembled with embedded template + CSS + auto-generated Table of Contents
  4. Render — headless Chrome prints to PDF with headers, footers, and PDF bookmarks, then an automatic quality audit checks the result for overflowing content, broken images, low-contrast text, near-empty output, dead links, duplicate ids, broken TOC entries, unloaded fonts, at-risk page breaks, and headings at risk of being clipped by the print engine (see pretty-converter build's Warnings output, or render.RenderToPDFWithAudit in the library API)

Documents are sorted by their [X.Y.Z] frontmatter ID, not filename.

A .md/.mdx file doesn't strictly need a --- frontmatter block either: if one is missing entirely, id and title are generated automatically from the filename, the same convention .txt uses below (02-getting -started.mdx → id [2.0.0], title "Getting Started"; no numeric prefix → the next free major version, so it never collides with an explicitly numbered doc). The content itself still gets full markdown rendering — components, raw HTML, everything — unlike .txt. A --- block that is present but fails to parse as YAML is still a hard error, so a typo in real frontmatter doesn't silently get treated as "no frontmatter".

.txt files are also accepted for freeform writing with zero setup: they have no frontmatter, so id and title are generated automatically from the filename (03-field-notes.txt → id [3.0.0], title "Field Notes"; no numeric prefix → the next free major version, so it never collides with an explicitly numbered doc). Content is treated as literal text — blank lines become paragraphs, single line breaks become <br> — and is always HTML-escaped, so .txt loses component/raw-HTML customization but in exchange never executes anything the author typed. Good for a quick note dropped into an otherwise structured .mdx book.

Trust model

MDX is parsed with raw HTML passthrough enabled, and custom components don't escape their inner content — this lets authors embed arbitrary HTML/CSS for rich documents, but it also means a .md or .mdx file can contain a <script> tag that will execute during rendering. By default, headless Chrome's network access is blocked while rendering (see WithNetworkAccess), so scripts can't exfiltrate data or fetch remote content — but they still run. Only build PDFs from source files you trust. See SECURITY.md for details.

Performance

go-pretty-converter keeps the Go-side pipeline (parse + validate + compose) sub-second even on large books; headless Chrome's print step is what drives PDF wall time. Measured on a Ryzen 5 3600 with Chrome 152:

Source docs check epub PDF pages PDF wall time --fast wall time
100 55 ms 64 ms 121 0.55 s 0.43 s
1,000 112 ms 156 ms 1,179 2.46 s 1.37 s
3,000 221 ms 362 ms 3,533 12.19 s 6.86 s

--fast (shorthand for --no-header --no-page-numbers --no-outline --no-tagged-pdf) cuts wall time 40-44% on books over ~500 docs — page numbers/header alone cost more than bookmarks and accessibility tagging combined. Full breakdown, hardware, and a reproducible recipe: BENCHMARKS.md. To measure your own machine:

PRETTY_CONVERTER_CHROME_PATH=/usr/bin/chromium go run ./scripts/benchmark --color=false

MDX format

---
id: "[1.0.0]"
title: "Getting Started"
subtitle: "A simple introduction"
tags: [example, intro]
difficulty: "beginner"
status: complete
completeness: 100
depends_on: []
---

# Welcome to Your Book

This is the first chapter.

## Variables

You can use {{key}} syntax for variable substitution: running {{product}} v{{version}}.

Required frontmatter fields: id (format [X.Y.Z]), title.

Built-in components

Component Usage Appearance
<DeepDive> <DeepDive title="Details">...</DeepDive> Blue info panel
<Warning> <Warning title="Note">...</Warning> Orange warning panel
<Axiom> <Axiom>...</Axiom> Green italic quote

Register custom components via WithComponent():

prettyconverter.WithComponent("Callout", func(attrs map[string]string, inner string) string {
	level := attrs["level"]
	return fmt.Sprintf(`<div class="callout callout-%s">%s</div>`, level, inner)
})

Configuration

Create a go-pretty-converter.yml in your project:

title: "My Book"
subtitle: "A Complete Guide"
author: "Jane Doe"
source: book
output: out.pdf
theme: default

css: custom.css
template: custom-template.html

vars:
  product: "go-pretty-converter"
  version: "1.0"

lint:
  require_frontmatter: [id, title]
  require_id_format: "[X.Y.Z]"
  no_duplicate_ids: true
  max_heading_depth: 3

render:
  timeout: 60s
  paper: a4
  margin_top: 20mm
  margin_bottom: 20mm
  margin_left: 15mm
  margin_right: 15mm
  header_title: "{{title}}"

Library API

// Constructor with functional options
pdf, err := prettyconverter.New(opts...)

// All-in-one build pipeline
pdf.Build(ctx)

// Step-by-step pipeline
docs, _ := pdf.ParseDir()
errs := pdf.ValidateDoc(doc)
html, _ := pdf.ComposeHTML(docs)
pdf.Render(html)

// Quality audit from the most recent Build/Render call (nil if neither ran yet)
audit := pdf.LastAudit()

// Validation-only
errs, _ := pdf.Validate(ctx)

// Lower-level: render straight to PDF and get the audit report back
report, err := render.RenderToPDFWithAudit(html, "out.pdf", render.DefaultOptions())
Available options
Option Description
WithSourceDir(dir) MDX source directory (default: book)
WithOutputFile(path) Output PDF path (default: out.pdf)
WithTitle(title) Document title
WithSubtitle(sub) Document subtitle
WithAuthor(author) Document author
WithCSS(css) Custom CSS content string
WithTemplate(html) Custom HTML template string
WithTheme(t) Apply a raw theme.Theme (no customization/section toggles)
WithThemeName(name, opts) Resolve a theme by name (builtin, custom, or file path) with color/font/section customization
WithComponent(name, handler) Register custom MDX component
WithValidator(v) Custom validation logic
WithTimeout(d) Chrome render timeout (default: 60s)
WithHeaderTitle(t) PDF header title
WithVerbose(bool) Enable verbose logging
WithVars(map) Variable substitution map
WithRenderMargins(t,b,l,r) PDF margins in inches
WithPaperSize(w,h) Paper size in inches
WithConfig(cfg) Apply source/output/title/subtitle/author from config
WithConfigCSSAndTemplate(cfg) Load CSS/template from config file paths
WithFullConfig(cfg) Apply the entire config struct (source, CSS/template, theme, vars, render settings) in one call
WithNetworkAccess(bool) Allow headless Chrome to make network requests while rendering (default: false, blocked)

Themes

Seventeen built-in themes, each a palette/typography layer over one shared structural stylesheet — clean and professional by default, easy to customize without writing CSS, and extendable with your own custom themes:

default · minimal · modern · classic · corporate · dark · academic · editorial · sepia · terminal · blueprint · ivy · government · resume · legal · latex · gruvbox

# Pick a theme, tweak colors/fonts/density, drop sections you don't want
pretty-converter build --theme corporate \
  --color-primary "#0ea5e9" --font-heading "Georgia, serif" \
  --no-cover --no-page-numbers --density compact

# Scaffold your own reusable theme
pretty-converter theme new my-report --from corporate
pretty-converter theme list
prettyconverter.WithThemeName("corporate", theme.Options{
	Colors:   theme.Colors{Primary: "#0ea5e9"},
	Sections: theme.Sections{Cover: theme.BoolPtr(false)},
})

Custom themes live in <name>.theme.yml files (project-local ./themes/ or a global themes directory) and extends a builtin theme. Full reference, all customization fields, and the pretty-converter theme command family: see docs/cli.md#themes.

CLI reference

pretty-converter build     Build a PDF from MDX source files
pretty-converter epub      Build an EPUB from MDX source files (no Chrome required)
pretty-converter kindle    Build a Kindle-ready MOBI/AZW3 file (needs Calibre's ebook-convert)
pretty-converter check     Validate MDX files without building
pretty-converter format    Convert raw .txt into structured .mdx files (heuristic, no AI)
pretty-converter analyze   Static cross-format rendering-quality analysis (no Chrome/Calibre)
pretty-converter theme     List, inspect, and manage themes
pretty-converter init      Scaffold a new book project (interactive wizard)
pretty-converter watch     Watch for changes and rebuild automatically
pretty-converter serve     Preview MDX as HTML with live reload (no Chrome required)
pretty-converter version   Print the version number

Run pretty-converter <command> --help for the full flag list of any command.

Global flags: --config, --source, --verbose, --no-color, --quiet

License

MIT — see LICENSE.

Documentation

Overview

Package prettyconverter transforms MDX source files into print-ready PDFs via headless Chrome.

It is both a composable Go library and a CLI tool. The library exposes a pipeline with step-by-step methods (ParseDir, ValidateDoc, ComposeHTML, Render) and 18 functional options for full customization.

Quick start (library)

pdf, err := prettyconverter.New(
	prettyconverter.WithSourceDir("./docs"),
	prettyconverter.WithOutputFile("output.pdf"),
	prettyconverter.WithTitle("My Book"),
)
if err != nil {
	log.Fatal(err)
}
if err := pdf.Build(context.Background()); err != nil {
	log.Fatal(err)
}

Quick start (CLI)

go install github.com/sazardev/go-pretty-converter/cmd/pretty-converter@latest
pretty-converter build --source ./docs --out out.pdf

MDX files require frontmatter with an `id` field in [X.Y.Z] format (e.g. `[1.0.0]`). Documents are sorted by ID, not filename. Variables in {{key}} syntax are substituted before parsing.

Built-in custom components: <DeepDive>, <Warning>, <Axiom>. Additional components can be registered via WithComponent().

Trust model

MDX is parsed with raw HTML passthrough enabled, and component transpilation does not escape inner content. Only build PDFs from MDX you trust — see SECURITY.md for details. Network access during rendering is blocked by default (see WithNetworkAccess).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ComposeHTML

func ComposeHTML(docs []*mdx.Document, opts ComposeOptions) (string, error)

Types

type ComposeOptions

type ComposeOptions = compose.Options

func DefaultComposeOptions

func DefaultComposeOptions() ComposeOptions

type Option

type Option func(*PDF)

func WithAuthor

func WithAuthor(author string) Option

func WithCSS

func WithCSS(css string) Option

func WithCalibreExecPath

func WithCalibreExecPath(path string) Option

WithCalibreExecPath pins the FormatKindle pipeline to a specific Calibre ebook-convert executable instead of resolving one from PATH. Leave unset (or pass "") to resolve ebook-convert from PATH at build time — see the kindle package for the full resolution order.

func WithChromeExecPath

func WithChromeExecPath(path string) Option

WithChromeExecPath pins rendering to a specific Chrome/Chromium binary instead of chromedp's default system discovery. Leave unset (or pass "") to keep the default behavior. See the chromemgr package for resolving this automatically, including downloading a browser when none is installed.

func WithComponent

func WithComponent(name string, handler mdx.ComponentHandler) Option

func WithConfig

func WithConfig(cfg *config.Config) Option

func WithConfigCSSAndTemplate

func WithConfigCSSAndTemplate(cfg *config.Config) Option

WithConfigCSSAndTemplate resolves cfg.Theme (with cfg.ThemeOptions customization) and then loads CSS/template content from cfg.CSS/ cfg.Template, which — being explicit file overrides — take priority over the theme and replace its CSS/template outright. Read/resolve failures are recorded as warnings and flushed to stderr by New() once all options have been applied, so ordering relative to WithVerbose does not matter.

func WithCoverImage

func WithCoverImage(imagePath string) Option

WithCoverImage replaces the theme's text cover with a full-bleed page built from imagePath (.png/.jpg/.jpeg). That cover page is sized to the image's own pixel dimensions exactly — a square image gets a square cover page — while every other page keeps its configured paper size. The theme's own text cover (title/subtitle/metadata) is suppressed regardless of theme/section settings and regardless of the order options are applied in — see New(), which enforces this once after every Option has run.

func WithEpubLanguage

func WithEpubLanguage(lang string) Option

func WithFormats

func WithFormats(formats ...OutputFormat) Option

func WithFullConfig

func WithFullConfig(cfg *config.Config) Option

WithFullConfig applies every field of cfg: source/output/title/subtitle /author (via WithConfig), CSS/template/theme (via WithConfigCSSAndTemplate), variable substitution (cfg.Vars), and render settings (cfg.Render: timeout, paper size, margins, header title). Unlike WithConfig and WithConfigCSSAndTemplate, which only cover a subset of Config, this is the single option needed to fully apply a loaded go-pretty-converter.yml.

func WithGenerateDocumentOutline

func WithGenerateDocumentOutline(enabled bool) Option

WithGenerateDocumentOutline toggles whether PDF bookmarks/outline are built from the document's headings. Measured cost is near-zero even on very large documents (~50ms on a 3,000-doc/3,533-page book — see BENCHMARKS.md) — disabling it barely moves render time; it exists mainly for the (rare) case where in-PDF navigation bookmarks aren't wanted at all, not as a speed lever. For an actual speedup on large documents, see WithGenerateTaggedPDF or the CLI's --fast flag.

func WithGenerateTaggedPDF

func WithGenerateTaggedPDF(enabled bool) Option

WithGenerateTaggedPDF toggles PDF accessibility tagging (PDF/UA). Tagging is real post-print work (~15-18% of render time on a 3,000-doc book — see BENCHMARKS.md), though page numbers/header cost even more; disable for a real speedup when accessibility metadata isn't needed.

func WithHeaderTitle

func WithHeaderTitle(title string) Option

WithHeaderTitle sets the PDF page header text. If never called, New() defaults it to the document title (WithTitle/composeOpts.Title).

func WithNetworkAccess

func WithNetworkAccess(enabled bool) Option

WithNetworkAccess controls whether headless Chrome may make outbound network requests while rendering. It defaults to false: the composed HTML is a self-contained data URI, so network access is blocked to prevent SSRF/exfiltration from untrusted MDX content (e.g. a malicious <img> or <script> tag). Enable it only if your documents intentionally reference remote images, fonts, or other resources by URL.

func WithOutputFile

func WithOutputFile(path string) Option

func WithPaperSize

func WithPaperSize(width, height float64) Option

func WithRenderMargins

func WithRenderMargins(top, bottom, left, right float64) Option

func WithSharedBrowser

func WithSharedBrowser(browserCtx context.Context) Option

WithSharedBrowser makes subsequent Build calls render PDFs on the given Chrome allocator instead of booting a fresh Chrome process each time. The allocator must come from render.NewBrowser, stay alive across all Build calls, and be canceled by the caller once done. Each Build opens its own tab on the allocator, so concurrent Build calls share one Chrome process safely. This amortizes the per-launch Chrome startup cost across many renders — most useful when a process renders many documents (docsgen's per-theme PDFs, a batch job, a test harness).

func WithSourceDir

func WithSourceDir(dir string) Option

func WithSubtitle

func WithSubtitle(subtitle string) Option

func WithTemplate

func WithTemplate(html string) Option

func WithTheme

func WithTheme(t theme.Theme) Option

WithTheme applies a raw builtin/synthetic Theme's CSS as-is, with no customization (colors/fonts/sections/density) and no section toggles applied. It shares composeOpts.CSS with WithCSS and WithThemeName — whichever of these options is applied last wins, since New() applies options in the order they're passed. Most callers should prefer WithThemeName, which resolves section toggles (cover/TOC/page numbers/header) into composeOpts/renderOpts too.

func WithThemeName

func WithThemeName(name string, opts theme.Options) Option

WithThemeName resolves a theme by name — a builtin ("default", "corporate", ...), a custom theme discovered in ./themes/ or the global themes directory, or a direct path to a .theme.yml/.css file — applies opts customization (colors, fonts, density, network fonts), and wires the resulting section toggles (cover, TOC, page numbers, header) into composeOpts/renderOpts. The theme is resolved through both the PDF and the EPUB pipelines, so a later EPUB build (Build with FormatEPUB or RenderEpub) carries the same theme, not the default stylesheet.

func WithTimeout

func WithTimeout(d time.Duration) Option

func WithTitle

func WithTitle(title string) Option

func WithValidator

func WithValidator(v mdx.Validator) Option

func WithVars

func WithVars(vars map[string]string) Option

func WithVerbose

func WithVerbose(v bool) Option

type OutputFormat

type OutputFormat string
const (
	FormatPDF    OutputFormat = "pdf"
	FormatEPUB   OutputFormat = "epub"
	FormatKindle OutputFormat = "kindle"
)

func ParseFormats

func ParseFormats(s string) ([]OutputFormat, error)

type PDF

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

func New

func New(opts ...Option) (*PDF, error)

func (*PDF) Build

func (p *PDF) Build(ctx context.Context) error

func (*PDF) ComposeHTML

func (p *PDF) ComposeHTML(docs []*mdx.Document) (string, error)

func (*PDF) Formats

func (p *PDF) Formats() []OutputFormat

func (*PDF) LastAudit

func (p *PDF) LastAudit() *render.AuditReport

LastAudit returns the visual/structural audit report from the most recent Build or Render call, or nil if neither has run yet. See render.AuditReport and render/audit.go for what it checks.

func (*PDF) NeedsCalibre

func (p *PDF) NeedsCalibre() bool

NeedsCalibre reports whether FormatKindle is among the configured formats, meaning Build will need Calibre's ebook-convert on PATH (or pinned via WithCalibreExecPath).

func (*PDF) NeedsChrome

func (p *PDF) NeedsChrome() bool

func (*PDF) ParseDir

func (p *PDF) ParseDir() ([]*mdx.Document, error)

func (*PDF) Render

func (p *PDF) Render(html string) error

func (*PDF) RenderEpub

func (p *PDF) RenderEpub(docs []*mdx.Document, outputPath string) error

func (*PDF) RenderKindle

func (p *PDF) RenderKindle(ctx context.Context, docs []*mdx.Document, outputPath string) error

RenderKindle converts docs into a Kindle-ready file at outputPath, reusing the same EPUB options (theme, metadata, cover) as RenderEpub but piping the result through Calibre's ebook-convert. See the kindle package for the conversion pipeline and Calibre resolution order.

func (*PDF) RenderWithContext

func (p *PDF) RenderWithContext(ctx context.Context, html string) error

RenderWithContext is Render with the browser rooted in ctx instead of context.Background(): canceling ctx (client disconnect, SIGINT wired to context cancellation) tears down the in-flight Chrome render rather than running to completion or to opts.Timeout regardless. opts.Timeout still applies as an upper bound layered on top of ctx.

func (*PDF) Validate

func (p *PDF) Validate(ctx context.Context) ([]mdx.ValidationError, error)

func (*PDF) ValidateAll

func (p *PDF) ValidateAll(docs []*mdx.Document) []mdx.ValidationError

func (*PDF) ValidateDoc

func (p *PDF) ValidateDoc(doc *mdx.Document) []mdx.ValidationError

func (*PDF) Warnings

func (p *PDF) Warnings() []string

Warnings returns the non-fatal configuration warnings recorded by New — e.g. an unresolvable theme name or an unreadable --css/--template file — each of which caused that option to fall back rather than apply. They are also printed to stderr by New itself; this accessor exists for callers that want to detect the condition programmatically instead.

Directories

Path Synopsis
Package analyze statically inspects already-parsed MDX documents for content patterns that render poorly (or break outright) across the project's output formats — PDF, EPUB, and Kindle.
Package analyze statically inspects already-parsed MDX documents for content patterns that render poorly (or break outright) across the project's output formats — PDF, EPUB, and Kindle.
Package chromemgr resolves a working Chrome/Chromium executable for headless rendering without requiring the user to install one by hand.
Package chromemgr resolves a working Chrome/Chromium executable for headless rendering without requiring the user to install one by hand.
cmd
Package epub converts parsed MDX documents into a single EPUB 3 file — no headless Chrome involved, unlike the PDF pipeline in render.
Package epub converts parsed MDX documents into a single EPUB 3 file — no headless Chrome involved, unlike the PDF pipeline in render.
Package format converts raw, unstructured .txt files into clean, structured .mdx files (proper [X.Y.Z] frontmatter, Markdown headings, lists, and fenced code blocks) ready for the existing mdx/compose/build pipeline.
Package format converts raw, unstructured .txt files into clean, structured .mdx files (proper [X.Y.Z] frontmatter, Markdown headings, lists, and fenced code blocks) ready for the existing mdx/compose/build pipeline.
Package kindle converts parsed MDX documents into a Kindle-ready ebook file (MOBI/AZW3).
Package kindle converts parsed MDX documents into a Kindle-ready ebook file (MOBI/AZW3).
scripts
benchmark command
Command benchmark runs an automated performance verification of go-pretty-converter across multiple book sizes.
Command benchmark runs an automated performance verification of go-pretty-converter across multiple book sizes.
bump command
docsgen command
Package theme implements go-pretty-converter's theme engine: a set of professional built-in themes plus a customization layer (colors, fonts, section toggles, density) that composes on top of them via CSS custom properties, and a YAML format for user-defined custom themes.
Package theme implements go-pretty-converter's theme engine: a set of professional built-in themes plus a customization layer (colors, fonts, section toggles, density) that composes on top of them via CSS custom properties, and a YAML format for user-defined custom themes.

Jump to

Keyboard shortcuts

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