---
title: 'Build a multilingual site'
description: 'Build an English and Dutch Baseline site with explicit multilingual opt-in, hreflang links emitted by Baseline, per-language sitemaps, and environment-driven URLs.'
slug: 'multilingual-baseline-site'
type: 'article'
date: 2026-01-26T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/tutorial/multilingual-baseline-site/'
---

Add a second language to a Baseline site, with everything you'd expect: per-language URLs, hreflang on every page, a sitemap for each language plus an index.

Multilingual is opt-in. Most projects don't need it; if yours does, this chapter is the full setup.

By the end you'll have an English and Dutch site sharing a single Eleventy build, with Baseline doing the hreflang and sitemap wiring on its own.

---

## What you will build

- Pages in English and Dutch sharing one Eleventy build.
- Baseline configured with multilingual on, plus `defaultLanguage` and `languages` set in `_data/settings.js`.
- Hreflang links (the `<link rel="alternate" hreflang="...">` tags that point search engines at a page's translations) written automatically, and a per-language sitemap for each language.

---

## Prerequisites

- Node 20.15.0 (or >=20) and npm.
- `package.json` with `"type": "module"` and the `dev`/`build` scripts from the [[simple-baseline-site | simple site tutorial]]:
  ```json
  {
  	"name": "simple-baseline-site",
  	"type": "module",
  	"scripts": {
  		"start": "rimraf dist/ && npx @11ty/eleventy --serve",
  		"build": "rimraf dist/ && cross-env ELEVENTY_ENV=production npx @11ty/eleventy"
  	}
  }
  ```

---

## How activation works

Multilingual mode requires three things, all present:

- `options.multilingual: true` passed to `baseline()` in `eleventy.config.js`.
- `defaultLanguage` set in `_data/settings.js`.
- A non-empty `languages` map in `_data/settings.js`.

{% alertBlock "warning" %}

Setting `defaultLanguage` and `languages` is the site's identity. Turning `multilingual` on is the runtime decision. Both halves are needed; Baseline won't infer one from the other.

{% endalertBlock %}

---

## 1) Install required packages

```bash
npm install @11ty/eleventy @11ty/eleventy-img # Eleventy and 11ty Image
npm install rimraf cross-env # Helper packages
npm install @apleasantview/eleventy-plugin-baseline # Finally, install Baseline
```

---

## 2) Configure Eleventy with multilingual on

Create `eleventy.config.js`:

```js
import baseline, { config as baselineConfig } from '@apleasantview/eleventy-plugin-baseline';
import settings from './src/_data/settings.js';

/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
export default async function (eleventyConfig) {
	await eleventyConfig.addPlugin(
		baseline(settings, {
			multilingual: true,
			head: {
				titleSeparator: ' | ',
				showGenerator: true
			}
		})
	);
}

export const config = baselineConfig;
```

Create a local `.env` so canonical URLs, hreflang, and sitemaps use a real origin during development. In production, set `URL` in your hosting environment so the same outputs reach for the live domain instead.

```txt
ELEVENTY_ENV="development"
URL="http://localhost:8080/"
```

{% alertBlock "info" %}

Baseline's multilingual support is a thin layer over Eleventy's built-in i18n plugin. The [Eleventy i18n docs](https://www.11ty.dev/docs/plugins/i18n/) cover the mechanics underneath.

{% endalertBlock %}

---

## 3) Site data and languages

Open `src/_data/settings.js` from the previous tutorials and add `defaultLanguage` and the `languages` map. Everything else (title, url, head extras) carries forward; the merged file looks like the below.

Each language's `contentDir` points at its per-language source folder under `src/`. Baseline reads the map to know which languages exist, and uses `defaultLanguage` to mark the canonical one.

```js
export default {
	title: 'Multilingual Baseline Site',
	tagline: 'Hello, Eleventy + Baseline (i18n)',
	url: process.env.URL || 'http://localhost:8080/',
	noindex: false,

	defaultLanguage: 'en',
	languages: {
		en: {
			contentDir: 'content/en/',
			locale: 'en',
			languageName: 'English',
			title: 'Baseline (EN)',
			tagline: 'Hello'
		},
		nl: {
			contentDir: 'content/nl/',
			locale: 'nl',
			languageName: 'Nederlands',
			title: 'Baseline (NL)',
			tagline: 'Hallo'
		}
	},

	head: {
		link: [{ rel: 'stylesheet', href: '/assets/css/index.css' }],
		script: [{ src: '/assets/js/index.js', defer: true }],
		meta: [{ name: 'color-scheme', content: 'light dark' }]
	}
};
```

---

## 4) Per-language directory data

Tell Eleventy which language each content folder belongs to. Baseline reads `lang` on each page to group translations and emit hreflang.

`src/content/en/en.11tydata.js`:

```js
export default { lang: 'en' };
```

`src/content/nl/nl.11tydata.js`:

```js
export default { lang: 'nl' };
```

---

## 5) Minimal layout

Create `src/_includes/layouts/base.njk`. The `<baseline-head>` placeholder is what Baseline replaces with the rendered `<head>` at build time.

With multilingual on and a `translationKey` on each page (next step), the hreflang links go in automatically. No `for` loop in your layout.

Refer to the minimal layout step in the [[simple-baseline-site | simple site]] tutorial.

---

## 6) Add localized pages

A `translationKey` is the string that ties translations of the same page together. Pages with the same key across languages are recognised as translations of one another, and that's what lets Baseline emit the right hreflang.

Delete the permalink field from `src/content/pages/index.md`.

`src/content/en/pages/index.md`:

```md
---
title: 'Hello Baseline (EN)'
slug: 'multilingual-site-en'
description: 'English home'
permalink: '/'
layout: 'layouts/base.njk'
translationKey: 'homepage'
---

Welcome to the English home page.
```

`src/content/nl/pages/index.md`:

```md
---
title: 'Hallo Baseline (NL)'
slug: 'multilingual-site-nl'
description: 'Nederlandse home'
permalink: '/nl/'
layout: 'layouts/base.njk'
translationKey: 'homepage'
---

Welkom op de Nederlandse homepagina.
```

---

## 7) Minimal assets (reuse from prior tutorial)

The asset side is unchanged, so reuse what you already have from the [[simple-baseline-site | simple site]] tutorial:

- `src/assets/css/index.css` with the same minimal styles as before.
- `src/assets/js/index.js`, optionally a small script (a `DOMContentLoaded` log is plenty).

## 8) Hreflang

Hreflang is the set of `<link rel="alternate" hreflang="...">` tags that point search engines at a page's translations. Baseline emits them automatically once two things are in place:

1. Multilingual mode is on (`options.multilingual: true` plus `defaultLanguage` and `languages` in settings).
2. The page has a `translationKey` in its front matter, matched by its translations.

With both pages in place from the previous step and multilingual on, the rendered `<head>` for the homepage will contain:

```html
<link rel="alternate" hreflang="en" href="http://localhost:8080/" />
<link rel="alternate" hreflang="nl" href="http://localhost:8080/nl/" />
<link rel="alternate" hreflang="x-default" href="http://localhost:8080/" />
```

## 9) Run the site locally

```bash
npm start
```

- Visit `/` for English (the default-language permalinks are unprefixed) and `/nl/` for Dutch.
- View page source on either side. You should see `<link rel="alternate" hreflang="en" href="...">`, `<link rel="alternate" hreflang="nl" href="...">`, and the `x-default` entry. Baseline emits all three because both pages share `translationKey: 'homepage'` and multilingual is on.
- Dev writes to `dist/` while it runs, so the generated sitemap index at `/sitemap.xml` plus per-language sitemaps (`dist/en/sitemap.xml`, `dist/nl/sitemap.xml`) are inspectable while the server is up.

---

## 10) Production build and inspect output

Change `URL` in `.env` to an absolute URL. On a deployed site that's your real production URL; for this exercise, any absolute URL (e.g. `https://www.example.com/`) works to verify the output. Then run:

```bash
npm run build
```

- Check `dist/` for per-language output.
- `dist/sitemap.xml` is now the sitemap index, with the per-language sitemaps (`dist/en/sitemap.xml`, `dist/nl/sitemap.xml`) listed inside.
- Reset `URL` in `.env` to "http://localhost:8080/".

---

## Next steps

- Add more pages under `src/content/en/` and `src/content/nl/`, matching `translationKey` values so the translations stay linked.
- Localise labels (nav text and similar) through data files keyed by `lang`.
- Set `pathPrefix` if you deploy under a subpath, and keep `settings.url` pointing at the production origin so canonicals and sitemap links match.
- The [[multilang | multilang module]] reference has the full activation rules and helpers; the [[filters | filters]] reference covers the translation-aware filters.
