---
url: /site/seo-and-head.md
---
# SEO and the document head

What Evolve puts into `<head>`, which of it an editor controls, and the three endpoints search engines read: the sitemap, `robots.txt` and `security.txt`.

## The head of a page

Every frontend page starts from the head defaults: the title, charset and viewport, the CSRF token, a canonical link to the current URL, and on staging and development a `robots` tag with `noindex, nofollow`. The theme adds `author` and `generator` tags, the site verification tags and, once the company identity is set, the site-wide `Organization` block (see [Structured data](#structured-data)). For a routed page, the route adds:

| Tag | Source |
|---|---|
| `<title>`, `og:title`, `twitter:title` | the route's page title |
| `description`, `og:description`, `twitter:description` | the route's meta description. **No fallback.** An empty description renders no tag at all, because a boilerplate description on hundreds of pages is worse than letting Google write its own snippet. |
| `og:type`, `og:url`, `og:site_name` | `website`, the current URL, the site name (Settings > General, fallback `APP_NAME`) |
| canonical | the route's canonical when set, otherwise the current URL |
| `og:image` (1200 by 630), `twitter:image` (1200 by 628), `twitter:image:alt` | the page's share image: its image field, cropped. Alt text from the image description, then the meta description, then the file name. |
| `twitter:card` | `summary_large_image` |
| `robots` `noindex, nofollow` | when the page is not public (unpublished or hidden) |
| `hreflang` alternates and `x-default` | one per active language the page exists in, only when there are at least two. Regions narrow the set to the locales a region allows. |

Blog posts add `article:author`. Pages whose model implements the head contract add their own tags.

### What editors control

Per page: the title, the meta description, the share image, and whether the page is published or hidden. There is no per-page "noindex" switch; a page that must not be indexed stays hidden or unpublished (a curated metadata contract is tracked in evolve-cms issue #175). Site-wide: the site name (Settings > General), the site verifications below and the company identity (Settings > Company).

### Site verification

| Setting | `.env` fallback | Tag |
|---|---|---|
| Google | `GOOGLE_SITE_VERIFICATION` | `google-site-verification` |
| Bing | `BING_SITE_VERIFICATION` | `msvalidate.01` |
| Pinterest | `PINTEREST_SITE_VERIFICATION` | `p:domain_verify` |

Edited in the panel under **Settings > SEO** since 2.18, saved to the [settings store](/panel/settings-store) group `seo`. The variables are the fallback for an empty field. (Before 2.18 the fields sat on the Tracking tab and wrote the `.env`.) The tags render on every environment.

## Environments and indexing

Staging and development always carry `noindex, nofollow` in the HTML. The tag travels inside cached pages too, which an `X-Robots-Tag` header could not, because page-cache hits never reach PHP. Staging additionally sits behind the preview password. See [Environments](/getting-started/environments).

## Sitemap

`/sitemap.xml` lists every public route whose language is active: pages and routeable module records (posts, projects, events, vehicles and so on). Hidden routes are left out, because advertising a page that carries `noindex` sends contradictory signals. Each URL carries `lastmod` from the record's last change and `hreflang` alternates for its siblings. `changefreq` and `priority` are omitted on purpose; search engines ignore them.

The sitemap is cached for a day when `APP_URL` names a real host. Without a canonical host it is rebuilt per request, so that a request on a preview host cannot fix every crawler's URLs to that host for a day. The same guard protects the page cache.

## robots.txt

The starter ships `public/robots.txt` with `User-agent: *` and an empty `Disallow`, which allows everything. Evolve does not generate it. Non-live environments are kept out of the index by the `noindex` tag and the preview password, not by `robots.txt`.

## security.txt

`/.well-known/security.txt` (RFC 9116) tells security researchers where to report an issue. It is served when `evolve.security.contact` in `config/evolve.php` holds an address or URL, with `evolve.security.policy_url` as the optional policy link. Unset, it falls back to the sender address in `MAIL_FROM_ADDRESS` once that differs from the seeded one, and otherwise answers 404 on a live site: a `security.txt` that names somebody who does not run the site is worse than none.

## Structured data

Two kinds of JSON-LD blocks reach the page.

**The site-wide Organization** . Every page carries one `Organization` block (`id="jsonld-organization"`) built from **Settings > Company**: name (brand name, else legal name), legal name, URL, logo, founding date, phone, email, postal address and the `sameAs` profiles. Its `@id` is `https://<site>/#organization`, so other blocks can point at it. The logo is the one picked on the Company tab. **Without a legal name there is no Organization at all**: Evolve does not guess a company from the site name or a theme logo.

**The record's own block.** Records whose model implements the structured data contract add theirs: jobs produce `JobPosting` and members `Person` (both from the [Company](/modules/company) module), blog posts, projects and FAQ entries their own types. Only CreativeWork types (`Article`, `BlogPosting`, `NewsArticle`, `WebPage` and similar) get `author` and `publisher`, and those are the Organization: a compact node with its `@id`, name, URL and logo. `Person`, `JobPosting` and `Product` get neither. Without a company identity the block ships without them.

Empty values are left out of every block: no empty `image`, no `null` address lines. Before 2.18 `author` and `publisher` came from a `company.name` translation that most projects never defined; projects that relied on such a lang file enter the company identity in the Company tab instead.

## Tools

* The AI **page report** on each page's screen reviews content and structure for SEO, see [Editor AI](/panel/editor-ai).
* Over MCP, `get-seo-meta-tool` returns title, description, Open Graph tags, canonical, hreflang alternates and indexing status for a route; it is one of the three tools an anonymous HTTP caller may use, so an external audit needs no token. See [MCP Server](/modules/mcp-server).
* `curl -s https://<domain>/<path> | grep -E '<title>|name="description"|rel="canonical"|hreflang'` shows what a crawler sees, including the `noindex` tag on staging.
