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 oftitle;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.entityblock insite.yamldescribes who stands behind the site, and it goes onto every indexed page. - Substitutions, not copies.
{{ contact.phone }}reads the phone from the samesite.yamlthe 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, andllms.txtlinks to them. - Facts, not instructions. The note in
llms_noteis for facts about the site. Lines that try to steer someone else's AI look like an attack, and the whole site loses trust.
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.
-
Export the old addresses from the old sitemap, Search Console, server logs.
-
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.
-
The address is gone? Put it into
goneinsite.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 -
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). -
Check it on the image:
scripts/site.sh SITE smoke prodstarts 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.