PrimLagicPrimLagic

Building a bilingual static blog with Eleventy: structure, SEO and my mistakes

By Ali Abdollahi · · 2 min read

I wanted this site's blog to be simple and fast: every article is one Markdown file, the output is fully static, two languages (Persian and English), and correct SEO. Eleventy (11ty) exists exactly for this.

Structure

src/
  fa/posts/*.md      ← Persian articles
  en/posts/*.md      ← English articles
  fa/index.njk       ← Persian list (with pagination)
  en/index.njk
  _includes/         ← templates (base, post)
  _data/site.js      ← shared per-language names and strings

The build writes into the site folder, so a static host needs no build step.

One file per article

Each Markdown file starts with front matter:

---
title: "Article title"
description: "A unique description for search results"
date: 2026-10-24
slug: english-short-slug
altSlug: english-short-slug
tags: [post, tag]
---

A shared per-language data file builds the final URL from slug (e.g. /blog/<slug>/) and the other language's URL from altSlug. Those two values feed the canonical and hreflang tags.

Per-article SEO

  • Unique title and description, a single h1.
  • link rel="canonical" to itself.
  • hreflang for the Persian, English and x-default versions.
  • Open Graph with a WebP image.
  • Structured data: BlogPosting and BreadcrumbList as JSON-LD.
  • Short English slug.
  • sitemap and RSS generated automatically.

Mistakes I made myself

1. A colon in the description. In YAML, any value containing : must be quoted, or the build fails. Always quote titles and descriptions.

2. Persian comma in tags. [absence, sms] written with a Persian «،» becomes one tag, not two; YAML needs the ASCII comma.

3. The template engine eats braces. Eleventy processes Markdown with a template engine (Nunjucks) by default, so double curly braces around a name (like a template variable in a chat message example) lose their value and render empty. Wrap that section in the engine's raw tags.

4. The Jalali calendar. Instead of hand-rolling date conversion, I used a battle-tested library (jalaali-js) and compared its output against known dates (Persian website tips).

5. Broken links. After every build, a small script checks every internal href and src against the files on disk.

Pagination

Eleventy has built-in pagination: give it a collection and a size, and it generates /blog/, /blog/page/2/ and so on. Take the next/previous links from pagination.href.

When is it a good choice?

When the content is article-shaped, changes are not heavily dynamic, and you want high speed and SEO at near-zero cost. If you need comments, accounts or personalized content, you need a backend or a separate service.

Summary

One template, one Markdown file per article and a few data files are enough for a bilingual blog. For the speed side, see this guide.

EleventySEOStatic sites