← All posts

Bilingual site: what the <head> of every page really needs

In French-speaking Switzerland, a serious website usually exists in two languages. French for local clients, English for everyone else. This site is no exception: every page has a twin, /fr/… and /en/….

What most people don't see is that half of the "bilingual" work doesn't happen in the text. It happens in the page's <head>, twenty-odd lines nobody reads, but that Google, LinkedIn and RSS readers read very carefully.

Here, tag by tag, is what every page on stackshaka.com emits, and why. Nothing theoretical: you can open the source of this article and check all of it.

The canonical: one official address

<link rel="canonical" href="https://stackshaka.com/en/blog/site-bilingue-balises-head/">

The same page can be reached in several ways: with or without a trailing /, over http, with a tracking parameter glued on by a social network. To a search engine, those are separate URLs splitting the same visibility between them.

The canonical says: "this one is the original". I always use the absolute URL, in https, with the trailing /, in exactly the form the server serves it. If the .htaccess redirect enforces the trailing slash, the canonical must include it too. A canonical pointing at a URL that redirects is a contradictory signal.

On a bilingual site, the classic trap: setting the FR version as the canonical of the EN version "because it's the original". No. Each language is its own original page. The canonical of /en/… points to /en/…. The relationship between the two languages is the next tag's job.

Hreflang: linking the two languages without confusing them

<link rel="alternate" hreflang="fr" href="https://stackshaka.com/fr/blog/site-bilingue-balises-head/">
<link rel="alternate" hreflang="en" href="https://stackshaka.com/en/blog/site-bilingue-balises-head/">
<link rel="alternate" hreflang="x-default" href="https://stackshaka.com/fr/blog/site-bilingue-balises-head/">

Without hreflang, Google sees two pages about the same thing and may show the English one to someone searching in French from Lausanne. With it, it knows which version to serve to whom.

Three rules I follow every time, because each has a well-known failure mode:

  • Reciprocity. The FR page declares the EN page, and the EN page declares the FR page. If only one side does it, Google ignores the annotation. This is the number one error in Search Console reports.
  • Self-reference. Each page lists itself. The EN page also declares hreflang="en" pointing to its own URL. It looks redundant; it's required.
  • x-default. It names the version to serve when no language matches. Here that's French, because it's the main language of the site and its clientele. An internationally oriented site would pick English instead.

In this blog's generator, the two Markdown files of an article share a translationKey. That's what produces the three lines above. Since everything is generated, there's no line to copy by hand, so no typo is possible in a URL. In practice, every article is published in both languages on the same day, which avoids a page declaring a twin that doesn't exist.

Open Graph: what people see when they share

<meta property="og:type" content="article">
<meta property="og:title" content="… — Stackshaka">
<meta property="og:description" content="…">
<meta property="og:url" content="https://stackshaka.com/en/blog/site-bilingue-balises-head/">
<meta property="og:image" content="https://stackshaka.com/og-image.jpg">
<meta name="twitter:card" content="summary_large_image">

Paste a link into LinkedIn, WhatsApp or Slack: the preview card comes from these lines. Without them, you get a bare URL or a random image grabbed from the page. With them, you control the title, the summary and the visual.

Two details that matter. The image must be an absolute URL, not /og-image.jpg, because LinkedIn's bot doesn't resolve relative paths the way a browser does. And og:url must be identical to the canonical, otherwise share counts and previews get scattered across several addresses.

The og:description reuses the page's meta description. One source of truth: the description field in the article's frontmatter. I don't write three different summaries for three tags.

JSON-LD: telling machines who wrote what

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "…",
  "datePublished": "2026-09-16",
  "inLanguage": "en",
  "author": { "@type": "Person", "name": "Sylvain", "url": "https://stackshaka.com/en/" },
  "mainEntityOfPage": "https://stackshaka.com/en/blog/site-bilingue-balises-head/"
}
</script>

JSON-LD changes nothing on screen. It describes the page in a vocabulary that Google and, increasingly, AI answer engines understand without ambiguity: this is a blog post, published on this date, in this language, by this person.

The field I care about most is author. On a freelancer's site, the author is the brand. An article signed by an identifiable person, linked to an "about" page, carries more weight than an anonymous one. That's exactly what Google's E-E-A-T guidelines describe, and it's free.

inLanguage matches the page's language. Again, the FR version says "fr", not "en". Consistency between lang on <html>, the hreflang and inLanguage is what keeps a robot from filing a page under the wrong language.

What I leave out

No meta keywords: search engines have ignored it for over fifteen years. No generator tag announcing which tool built the page: useless to the reader, useful to someone hunting for known vulnerabilities. No analytics script in the <head>: this site has none, and the CSP would block it anyway.

And no tags generated "just in case". Every line in the <head> has a reason I can explain. If I can't, it goes.

How to check your own site

Three steps, five minutes:

  1. Open a FR page, view the source, search for hreflang. Do the same on the EN page. Both lists must be identical.
  2. Paste the URL into LinkedIn's Post Inspector or an Open Graph validator. If the title or image is wrong, that's where you'll see it.
  3. Run the page through Google's Rich Results Test. The BlogPosting should show up with no errors.

If any of the three fails, it's never dramatic, but it's almost always the reason one language version "doesn't show up" in results.

In short

A well-built bilingual site isn't just two good texts. It's two pages that declare each other, each with its own official address, a clean preview when shared, and an ID card machines can read. On this site, all of that comes out of one script and an eight-field frontmatter. Nothing is touched by hand, and that's the only way it stays correct over time.

If you have a two-language site and a doubt about what Google makes of it, get in touch. A <head> audit is quick, and it's often where the problem hides.

Got a project in mind? Let’s talk.

Get in touch