Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

A two-language static site that search engines understand: hreflang, JSON-LD, llms.txt and IndexNow

We run a small web studio in Tashkent. Our own site is plain HTML on Firebase Hosting, in Uzbek (the root) and Russian (/ru/). No framework, no CMS. Here is the checklist we use so that Google, Yandex and AI assistants r

We run a small web studio in Tashkent. Our own site is plain HTML on Firebase Hosting, in Uzbek (the root) and Russian (/ru/). No framework, no CMS. Here is the checklist we use so that Google, Yandex and AI assistants read both languages correctly. After the last pass, Google Search Console showed 78 pages indexed and 2 not indexed — and both of those are redirect URLs, which is expected.

1. One URL per language, linked both ways

Every page carries the same three alternates, and each language version points to the other:

<link rel="canonical" href="https://example.uz/ru/ceny.html">
<link rel="alternate" hreflang="uz" href="https://example.uz/narxlar.html">
<link rel="alternate" hreflang="ru" href="https://example.uz/ru/ceny.html">
<link rel="alternate" hreflang="x-default" href="https://example.uz/narxlar.html">

The most common bug we saw: the Uzbek page links to the Russian one, but the Russian page has no link back. Search engines ignore one-way hreflang.

2. Kill duplicate home pages with 301s

A static host serves both / and /index.html. We redirect /index.html → / and /ru/index.html → /ru in firebase.json and point every internal link at the short form. Those two redirect URLs are exactly the "not indexed" pages in Search Console, which is what you want.

3. Structured data per page type

  • Every page: ProfessionalService (name, phone, address, opening hours).
  • Service pages: plus FAQPage.
  • Blog posts: Article, FAQPage, BreadcrumbList and ImageObject.

We build posts with a small Node script from a frozen HTML template, so the JSON-LD is generated, not hand-typed. Our crawl script (step 7) parses every JSON-LD block on the live site and reports invalid JSON.

4. Titles and descriptions with limits

One H1 per page, a meta description of 120–160 characters, a short title. Our crawl script flags anything outside those limits on every sitemap URL.

5. llms.txt for AI assistants

We keep a plain-text /llms.txt with the facts we want AI answers to get right: services, starting prices, timelines, contacts, and a list of key pages in both languages. It is also where we write what we are not (other businesses share our name).

6. Tell search engines about changes

After every deploy a 20-line script posts all sitemap URLs to IndexNow (Yandex and api.indexnow.org):

const urlList = [...sitemap.matchAll(/<loc>(.*?)<\/loc>/g)].map(m => m[1]);
for (const endpoint of ['https://yandex.com/indexnow', 'https://api.indexnow.org/indexnow']) {
  await fetch(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=utf-8' },
    body: JSON.stringify({ host: HOST, key: KEY, keyLocation: `https://${HOST}/${KEY}.txt`, urlList }),
  });
}

Google does not use IndexNow, so for Google we resubmit the sitemap in Search Console.

7. Crawl your own site before saying "done"

Our last crawl covered 82 sitemap pages and 144 linked URLs: status codes, one H1, description length, canonical, hreflang, JSON-LD parsing and broken links. Zero issues. It takes a minute and catches the mistakes that a manual check misses.

Takeaway

None of this needs a framework. A two-language static site can be fully understood by search engines with consistent hreflang, real 301s, generated JSON-LD, an llms.txt and an automated crawl. You can see the result on our site.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.