HERON

Guide 05 · for whoever answers for search

SEO and languages

What HERON builds by itself, what you fill in and how to move from an old site without losing your place in search. For whoever answers for search: a person or a content and SEO agent.

No plugin is needed for anything on this page: it is part of HERON and works the same with any theme.

In plain words

SEO is how search engines and AI assistants find your site, understand it and show it to the right people. Most of that work is boring and easy to forget: a map of all pages, the main address of each page, notes about translations, data about your company. HERON does all of it on every build, so you only fill in what nobody can guess for you.

What you fill in, per page: three lines at the top — title (what people see in Google), description (the two lines under it) and h1 (the heading on the page). What you fill in once: the domain, the languages and the company details in site.yaml.

Example. You write title: "Roof repair in Vienna: prices and timing". Google shows exactly that as the blue link, and the strict build stops if you forgot the description, so an empty snippet never goes online.

You need this page before the first publication and before moving an old site. You can skip the moving part if your site is new.

What is built by itself

None of this is written by hand or kept as a file. It is built on every build from the pages and site.yaml, so it never goes stale:

What From In detail
title, meta description, canonical title + seo.title_suffix, description, the page address page format
Open Graph and Twitter fields of groups 3–4, otherwise title, description, seo.og_default_image page format
sitemap.xml every published indexable page, lastmod from updated build
hreflang in the head and in the sitemap translations by file name, x-default is the default language languages
robots.txt seo.robots_extra + the Sitemap: line build
JSON-LD the type from the theme, the data from the page and the schema block of site.yaml, breadcrumbs, FAQ build
llms.txt, llms-full.txt the page tree, description, seo.llms_note build
301 and 410 redirect_from of pages and gone in site.yaml, straight into the nginx config build
a closed development build without --env prod: noindex everywhere, Disallow: /, no counters distribution

What you fill in

On every page, group 1 of the SEO block:

  • title: up to 60 characters, for search results. The site suffix is added by itself;
  • h1: for whoever has already opened the page. Not a copy of title;
  • description: up to 155 characters, what the reader gets on the page;
  • group 2, «key queries»: HERON does not read it, it is a working note: one intent, one page. Two files with the same main query are a sign they should be merged.

The other groups when you need them: your own picture for social networks (3), a detail for the markup (5), a catalog picture and its alt (6), the publication date and noindex (7), old addresses (8).

Once per site, in site.yaml:

site:
  domain: example.com
  name: Workshop
  name_ru: Мастерская        # the value for Russian pages
seo:
  title_suffix: " — Workshop"
  title_suffix_ru: " — Мастерская"
  og_default_image: media/themes/heron.jpg
  llms_txt: true
  llms_note:                 # facts about the site for AI assistants, per language
    en: |
      Phone for bookings: …
    ru: |
      Телефон для записи: …
contact: { phone: "…", city: "…" }
organization: { name: "…" }
schema:
  entity:                    # the node for the whole site: who stands behind it
    "@type": Organization
    name: "{{ organization.name }}"
    telephone: "{{ contact.phone }}"
analytics:
  ga4: G-XXXXXXX

{{ … }} in schema are substitutions from the same site.yaml: the address and the phone live in one place and never differ between the layout and the markup. In detail: the schema block.

Prices in the markup are forbidden by default (E020): if the site does not show prices on its pages, they must not leak into JSON-LD. To allow them, set seo.allow_prices: true.

The update date. lastmod and dateModified come from updated in the front matter, and if there is none, from the source in build.updated_from: git (the date of the last commit of the file), file (the file time) or manual (only by hand). A hand-written updated beats the source: if you write it into every page during a move, git dates stop working. The report after the build shows how many pages ended up like that. Search engines get the dates in full ISO 8601 with a time zone: from git, the commit time (2026-10-17T11:30:12+02:00); a hand-written day is midnight UTC (2026-10-14T00:00:00+00:00). The page itself still shows a date. A date in the future, or an update before publication, is a warning, and an error with --strict.

schema.org in plain words

schema.org is a shared vocabulary that tells search engines what a page is about: a clinic, a person, an article, a list of questions. HERON writes it as JSON-LD, a small block of data inside the page.

  • One node for the whole site. The schema.entity block in site.yaml describes who stands behind the site, and it goes onto every indexed page.
  • Substitutions, not copies. {{ contact.phone }} reads the phone from the same site.yaml the theme uses, so the markup and the page never disagree.
  • Language by itself. On a Russian page an address field takes its Russian value.
  • Empty means absent. A field without data is left out of the markup, never written empty.
  • Checked on build. The markup of each page must be valid JSON, and the type the theme promised must be there.

The configurator asks what kind of organization or person the site is about and fills this block for you.

llms.txt for AI assistants

llms.txt is a plain text file at the root of a site: a short summary and a list of the important pages with one line each. AI assistants read it to understand the site quickly. HERON builds it from your pages on every build, together with llms-full.txt, which carries the full texts.

  • On a site with several languages each language gets its own llms-en.txt, llms-ru.txt, and llms.txt links to them.
  • Facts, not instructions. The note in llms_note is for facts about the site. Lines that try to steer someone else's AI look like an attack, and the whole site loses trust.

What llms.txt is and how to make one

Languages

  • A translation is a file with the same name in the folder of the language; hreflang and the language switch appear by themselves.
  • A translation can have its own address (slug): the link between versions rests on the file name.
  • One language means no hreflang, no language maps and no llms-LANG.txt: nothing extra is generated.
  • No translation means a warning in the report and a greyed-out language in the switch; links to such a page from other languages lead to the version in the default language.
  • An empty default language stops the production build (E021).

Moving from an old site

The goal: not a single indexed address may answer 404.

  1. Export the old addresses from the old sitemap, Search Console, server logs.

  2. The address moved? Write it into the SEO block of the new page:

    redirect_from:
      - /старая-страница/
      - /%D1%81%D1%82%D0%B0%D1%80%D0%B0%D1%8F/
    

    Cyrillic and percent-encoding are normalised: both forms above are one address, and the 301 works for either, with or without the trailing slash.

  3. The address is gone? Put it into gone in site.yaml. The server answers 410, and a search engine drops the address faster than after a 404:

    gone:
      - /sample-page/        # exactly this address
      - /wp/*                # everything under /wp/
      - /?p=*                # the home page with the p parameter
    
  4. Build. HERON checks that an old address does not match a live page, does not lead to two places and does not form a chain (E009), and that a 410 does not overlap pages and redirects (E022).

  5. Check it on the image: scripts/site.sh SITE smoke prod starts nginx and checks the answers 200, 404, every 301 and every 410. See Build and deploy.

In detail: old addresses in the specification and moving an existing site.

Before going live

heron build my-site --env prod --strict

--strict turns warnings into errors: a broken link, a picture without alt, an empty required link between pages do not go live. The build report counts pages by markup type, so you see that JSON-LD reached all of them.

Questions

Does HERON make a sitemap?

Yes, on every build: one file for a site in one language, an index with a file per language otherwise. Pages with noindex and the 404 page never get into it.

Are the redirects real?

Yes. HERON writes them into the nginx config of the site, so visitors and robots get a real 301 or 410 from the server, not a page that jumps with a script.

Can I see what the build will tell search engines before publishing?

Yes. A build without --env prod is closed to indexing, and heron check . lists every problem without building. Open the pages and the files in dist/.

Do I need a plugin for SEO?

No. Everything on this page is part of HERON and works the same with any theme.

Source

This page mirrors docs/guide/05-seo.md from the prod branch, with two short additions for this site: schema.org and llms.txt in plain words. The repository is updated first, so the file there is always the latest version.

Next guide: Build and deploy.