Hreflang Tag Generator

List your language versions once and get the complete, reciprocal block for every page in the set, as HTML link tags, HTTP headers or XML sitemap markup. Already have tags and want them checked? Use the hreflang checker.

Everything runs in your browser. The URLs you type are never sent to a server, because there is no server-side code to receive them. No account, no crawling, no saved projects.

Your pages

One row per language version. Each row needs an hreflang value and the absolute URL of that version.

The page to serve when no language version matches the user. Usually a language selector, or your primary market's page. At most one per set.

Your hreflang tags

pages × tags. Every page gets the same complete block, including a tag pointing at itself.

Paste each block into the <head> of the page named above it.

For non-HTML resources such as PDFs, where there is no <head> to hold a link tag. Send the header named below with the response for that URL.

One <url> entry per page, each repeating the whole set. Use this or the link tags, not both.

Paste your hreflang tags

Paste link tags, a whole <head>, an HTTP Link: header, or sitemap <xhtml:link> entries.

To check return links, paste every page in the set, each under a separator line naming its URL:
--- https://example.com/ ---
<link rel="alternate" hreflang="en" href="https://example.com/">
<link rel="alternate" hreflang="de" href="https://example.com/de/">

--- https://example.com/de/ ---
<link rel="alternate" hreflang="en" href="https://example.com/">
<link rel="alternate" hreflang="de" href="https://example.com/de/">

Reciprocity can only be proven by seeing both ends. This tool never fetches a URL, so it asks you for the other end rather than crawling for it.

How to build an hreflang set that holds together

Generating the markup is the easy half. The half that goes wrong is deciding what belongs in the set before you generate anything, because an hreflang cluster is validated as a unit: one bad member and the whole set is discarded.

Decide what the set contains first

A set is a group of URLs that are the same page for different audiences. Not similar pages, not a category and its subcategory: the same page. Your German pricing page belongs in a set with your English pricing page, and in no other set. Every URL you list must be one you control and one that is indexable on its own.

Work page by page, not site-wide. A common failure is generating one set for the homepage and assuming the rest of the site inherits it. It does not. Each page in your site needs its own set, listing its own equivalents.

Every page gets the same complete block

This is the part the generator above exists to get right. If you have three language versions, each of the three pages carries all three tags, including the one pointing at itself. Nine tags in total, not six, and the three blocks are byte-for-byte identical apart from an optional x-default.

People routinely generate "alternates" instead: two tags per page, each pointing at the other versions and omitting itself. That set never forms. The missing self-reference is why, and it is invisible unless you count the tags.

Use the language subtag alone unless the region earns its place

es is a better tag than es-MX when you publish one Spanish version for every Spanish-speaking audience, because it is the accurate description of what you have. Adding a region narrows who the page is offered to, so a region you cannot back with a genuinely different page costs you reach for nothing.

Reach for a region only when the pages actually differ by country: different prices, different stock, different legal text. And remember the region is a country from ISO 3166-1 Alpha-2, which is why en-UK and es-419 both fail. The generator rejects them as you type; the reasoning is on the hreflang rules page.

Add x-default when you have a genuine fallback

x-default names the page to serve a visitor no listed version fits: a language selector, or your primary market's page doing double duty. It is optional, a set may contain at most one, and two of them invalidate the set.

It is not a catch-all for "everyone else", and pointing it at a page that is already in the set under its own language tag is fine and common. What it must not be is a URL that appears nowhere else in the set.

Three ways to serve hreflang, and when each one fits

The generator produces all three. They are equivalent to a search engine, so the choice is an engineering one. Pick one and use it consistently: implementing the same set two ways invites the two copies to drift, and a stale copy is worse than no copy.

HTML link tags, the default for pages

A block of <link rel="alternate"> elements in the <head>. Use this unless you have a reason not to: it is the form every tutorial and every debugging tool assumes, and it is visible in view-source, which matters more than it sounds when someone is trying to work out why a set broke.

The cost is weight. A set of twenty locales puts twenty lines in the head of every one of those twenty pages, on every page of the site. That is when the sitemap form starts to look attractive.

HTTP Link headers, for files with no head

A PDF, an image or any other non-HTML resource has nowhere to put a link tag, so the annotation moves into the response headers instead. Same information, same rules, different transport. If you are localising documentation as PDFs, this is the only option available to you.

It is also the easiest form to get wrong operationally, because the headers live in server config rather than in the file, so they are invisible to anyone reviewing the content and easy to lose in a migration.

XML sitemap markup, for large sets

Moving hreflang into the sitemap changes the syntax, not the requirements. Each <url> entry needs an <xhtml:link> for every page in the set, including itself, which means the same block repeated under every URL. Reciprocity, self-reference, absolute URLs and the one-x-default limit all still apply.

The one extra trap is the namespace: declare xmlns:xhtml on the <urlset> element or the annotations are ignored wholesale, silently. The generator includes it. The appeal of this form is that a large cluster stops bloating every page's head and one file becomes the single place the set is defined. The cost is that the tags are no longer visible in the page a developer is looking at.

Questions worth settling before you ship a set

What does a correct hreflang tag look like?

For a two-language site: <link rel="alternate" hreflang="en" href="https://example.com/" /> alongside <link rel="alternate" hreflang="de" href="https://example.com/de/" />, with both lines present on both pages. The commonest mistake in an otherwise correct example is listing only the other page.

How many hreflang tags does a set need?

The number of pages in the set, squared, plus one x-default per page if you use one. Three languages means three tags on each of three pages: nine. Ten languages means a hundred. That growth is why large sites move the set into an XML sitemap, and why generating the blocks by hand stops being realistic somewhere around four or five locales.

Do I need a self-referencing hreflang tag?

Yes. Every page in a set must include a tag pointing at itself. Omitting the self-reference means the page is not recognised as a member of the set, and the set does not form. This is the single most common reason a set that looks right in view-source does nothing.

Should I use link tags, HTTP headers, or an XML sitemap?

Link tags in the head are the default for HTML pages. HTTP Link: headers exist for non-HTML resources such as PDFs, which have no head to put tags in. The XML sitemap form keeps the markup out of your pages entirely, which suits very large sets. All three are equivalent to a search engine. Pick one: implementing the same set two ways invites the copies to drift apart.

Do I need an x-default tag?

No, it is optional. Add one when you have a genuine fallback for visitors no listed version fits, typically a language selector or your primary market's page. A set may contain at most one; two invalidate it. Pointing x-default at a page already in the set under its own language tag is fine.

Is anything I type into the generator sent to a server?

No. The generator is JavaScript running on this page, and there is no server-side code here that could receive a URL. Nothing is uploaded, logged or fetched. You can disconnect from the network and it still works, which is the simplest way to satisfy yourself that the claim is true.