Each language of a website searches and describes itself¶
- Status: accepted
- Date: 2026-10-18
Context¶
The websites in English and German, such as those of the blueprint, ha-autodarts and paperless-sync, had one search index for both languages. An English search for "checkout" on ha-autodarts' website found a German page first, a German search found the English version of a page that has a German one, and 18 German sections of ha-autodarts' roadmap led to …/undefined, because the i18n plugin had taken their page out of the index: its title and empty text looked like those of the English page. 67 entries of the blueprint's index were the Mermaid sources of the live diagrams, so a search for "token" showed flowchart LR admin([… as its text.
A German page without a translation, such as de/roadmap.html, is the English page under a German menu, but it said it was German, named itself as its canonical address and was a German version for search engines: duplicate content in the wrong language for search engines and screen readers. The links to the versions of a page were relative, which search engines ignore, and had no x-default. Every page had the description of the website, the switch between light and dark spoke English on the German pages, the changelog showed the date of its own file of one line, not that of CHANGELOG.md, which it includes, the sitemap the date of the build for every page, and the badges of the README showed the release and the checks of the day of the build. The template of the decision records was in the search and the sitemap.
Options¶
- A second build for each language, each with its own search: the language switch of Material between the two builds needs addresses that the i18n plugin doesn't give, and every repository would build its website twice.
- One index, and a script that hides the results of the other language: Material renders the results ten at a time as the reader scrolls, and counts them, so hidden results leave a short list that never loads the rest and a count that is wrong.
- An index for each language beside that of every language, which the hook writes after the build, and a script in the head of each page that gives Material the index of its language in place of that of every language.
Decision¶
Option 3, in the hook of every website, .github/mkdocs_blueprint.py.
---
config:
flowchart:
wrappingWidth: 400
---
flowchart TB
accTitle: How each language searches its pages
accDescr: You write guide.md and guide.de.md, and roadmap.md without a German version. MkDocs builds the English and the German website with the hook of the blueprint. The German roadmap is a copy of the English page, with its text marked as English, the English page as its canonical address and noindex, and stays out of the search and the sitemap. After the build, the hook writes beside the index of every language one index for each: the English pages, and the German pages with the English ones that have no German version, at their address in de/. A German page asks for the German index and an English one for the English index.
you(["You"])
pages["`**docs/**<br>guide.md · guide.de.md<br>roadmap.md`"]
build["MkDocs with the hook<br>of the blueprint<br>English, then German"]
copy["`**de/roadmap.html**<br>a copy: lang en · canonical<br>to roadmap.html · noindex`"]
index["`**search/search_index.json**<br>every language`"]
english(["`**search_index.en.json**<br>guide · roadmap`"])
german(["`**search_index.de.json**<br>de/guide · de/roadmap`"])
reader(["A reader of a German page"])
you -- "writes" --> pages --> build
build --> copy
build --> index
copy -. "not in the search" .-> index
index --> english
index --> german
reader -- "searches" --> german
classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
class build,copy,index bot
class you,pages,reader person
class english,german done
- An index for each language:
search/search_index.en.jsonholds the English pages,search/search_index.de.jsonthe German ones and the English pages without a German version, at their address inde/, so that a German reader stays in the German menu. A page of a project website in the search of its owner's website, such asha-autodarts/de/games.html, counts as German too.search/search_index.jsonkeeps every language, for the search of the owner's website, which adds the index of each project. The i18n plugin no longer takes entries out of it (reconfigure_search: false), and a section whose page an index lacks gets one. - Untranslated pages are copies: a page that the German website shows in English has its text marked as English, the English page as its canonical address and
noindex, and stays out of the search and the sitemap; its date speaks German. The English page names no German version. - Full addresses: every other page names its versions with their full addresses and
x-default, the version of the default language. - A description of each page, the start of its first paragraph that has words outside its links, at most 160 characters, unless its front matter has one; the home page of each language keeps the description of the website, and its preview card the name of the website as its title. The picture of a preview card may still show the description of the website: the social plugin starts drawing it before the hook knows the description of the page.
- The date of the newest file that a page includes with
--8<--, in the page and in the sitemap. - No status badges on the website, while buttons such as those of My Home Assistant stay, and German names for the switch between light and dark.
- The template of the decision records, and any page with
robots: noindexin its front matter, stay out of the search and the sitemap.
Consequences¶
An English page loads 420 KB of index instead of 513 KB on the blueprint's website, a German one 432 KB, and neither finds a page of the other language. The hook depends on two more parts of the plugins that are not documented as their interface: the versions of a page that the i18n plugin lists, and two methods of git-revision-date-localized that format a date. A version of a plugin that changes them stops the strict build, and the update of Renovate shows it.
The script in the head of each page redirects one request of Material, the one for search/search_index.json; a Material whose scripts no longer ask for it with an XMLHttpRequest would search every language again, so the hook then warns, which stops the strict build of the update of Material until the script follows it.