Khalid / Docs

SEO settings

Every SEO setting lives in site.toml, under [seo]. Each one has a default, so a site works with none of them set. The admin's SEO settings screen edits the same values. This page explains each setting added for the Yoast comparison of 2 November 2026; the Rust docs of khalid_core::SeoSettings cover the rest.

The admin screen

The admin's SEO settings screen edits them through the settings API (GET and PUT /api/admin/v1/settings/), which reads the table as written and changes it one part at a time. A part the screen saves replaces that part whole; any other table and comment in the file stays as it was. Every change is checked as khalid serve checks the file at start, and a change the site could not start with saves nothing. The running server reads site.toml at start, so a saved change applies after a restart.

Part What it sets Default Admin section
[seo.titles] separator, tagline, and a title and description template per page type: home, post, page, author, tag, category, collection, date, search, glossary, glossary_term, not_found separator |, {title} {sep} {site} with {excerpt}; the home page {site} {sep} {tagline} Search appearance
[seo.index] index or noindex for tags, categories, authors, dates, search and the whole site; thin_archives_below tags, categories and authors indexed; dates and search not; thin below 3 Show in search
[seo.publisher] type (organization or person), name, logo, same_as (social profiles), email, telephone, founded an organisation named after the site Site representation
[seo.social] twitter (the X handle) and image, the default share image none Social sharing
[seo.verification] google, bing, yandex, baidu, pinterest codes, and files none Webmaster tools
[seo.crawlers] disallow paths, allow and block crawler names every crawler reads every page Crawlers and AI bots
[seo.feeds] content (full or excerpt), before_item, after_item, source_line, limit full posts, 20 items Feeds
[seo.indexnow] key and endpoint off IndexNow
[seo.ai_page] off, contact, covers, cite on AI info page
[seo.links] affiliate_domains and the other outbound link rules none Not on the screen yet
[[seo.locations]] the site's shops or offices, for local search none Not on the screen yet
[seo.breadcrumbs], [seo.llms_txt], [seo.search_filter], [seo.local], [seo.schema_map] see their sections below see below Not on the screen yet
[[seo.schema]] schema nodes per category or collection none Schema templates screen

The AI bot toggles on the screen write the crawler's name into [seo.crawlers] block. The known AI crawlers are GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Google-Extended, Applebot-Extended, CCBot, Bytespider, meta-externalagent and Amazonbot.

Titles and descriptions

[seo.titles] holds a title and a description template per page type, such as [seo.titles.tag]. A template is text with variables in braces. A variable with no value on a page writes nothing, and a separator left alone at either end is dropped.

Variable What it writes
{title} The page's title.
{site} The site's name.
{tagline} The tagline, from [seo.titles] tagline.
{sep} The separator, | unless [seo.titles] separator says otherwise.
{excerpt} The page's summary: a post's description.
{category} A post's category name.
{author} A post's author name.
{date} The day a post was published, such as 14 September 2026.
{modified} The day a post was last really updated, or published when it never was.
{page} On page 2 and after of a long list, Page 2 of 5. Nothing on page 1.
{term} On a tag or category page, the tag or category name.
{term_description} On a tag or category page, its description from categories.toml or tags.toml.
{searchphrase} On a search results page, what the reader searched for.
{currentyear} The year the page was made, such as 2026. Handy for "best tools of 2026".
{currentmonth} The month the page was made, such as November.
{tag} A post's first tag.
{focuskw} A post's focus_keyword.

Dates, month names and the page line are written in the page's language, with the same words the templates use.

[seo.titles.post]
title = "{title} {sep} {site}"
description = "{excerpt}"

[seo.titles.tag]
title = "{term} {page} {sep} {site}"
description = "{term_description}"

Show in search results

[seo.index] says which pages search engines may list. Each value is "index" or "noindex". A post's own robots field always wins.

Key Default What it covers
posts index Every post.
pages index Every standalone page, such as About.
collections.<name> as posts The posts of one collection, such as news.
tags, categories index Tag and category pages.
authors index Author pages. "off" builds none.
dates noindex Year and month pages. "off" builds none.
search noindex Search results.
thin_archives_below 3 A tag or author page with fewer posts is noindex.
site index "noindex" keeps the whole site out, for a staging copy.

An archive set to "off" has no pages and no sitemap entries, and each of its addresses sends readers to the home page with a permanent redirect. Use it on a blog with one author, where the author page only repeats the home page.

[seo.index]
pages = "noindex"
authors = "off"
dates = "off"

[seo.index.collections]
news = "noindex"

Feeds

Each language has feeds next to its home page, such as /blog/feed.xml and /blog/de/feed.xml. Each category with posts in a language, and each channel, has its own feeds in its folder, such as /blog/category/technical-seo/feed.xml and /blog/featured/feed.xml. A feed holds the newest posts of its page, up to limit. Every one of these pages links its feeds in its head, so a feed reader finds them from the page's address.

[seo.feeds] off lists the feed formats Khalid does not write: "rss" (feed.xml), "atom" (atom.xml) and "json" (feed.json). Every format is written by default.

[seo.feeds]
off = ["atom", "json"]

[seo.breadcrumbs] shapes the trail above each page, when the template shows it, and the BreadcrumbList search engines read.

Key Default What it does
separator / What stands between two steps. Screen readers skip it.
home the site name The first step's name, such as Home.
archive_prefix none Words before the last step on a tag, category, author or date page, such as Archives for.
search_prefix none Words before the last step on the search page, such as You searched for.
bold_last false Shows the last step, the page itself, in bold.
taxonomy category Which term a post's trail passes through: category, tag (its first tag) or none.

A post may set breadcrumb_title: in its front matter to name itself more briefly in the trail, such as Sitemaps for a long headline.

A post's preview on X and Slack shows two short lines under its title: who wrote it and how long it takes to read, in the page's language. This is Yoast's "enhanced Slack sharing". A site that hides reading time ([reader] reading_time = false) hides it here too. To turn both lines off:

[seo.social]
share_labels = false

robots.txt

[seo.crawlers] shapes robots.txt.

Key Default What it does
disallow none Paths no crawler may read, such as /preview/.
allow, block none Crawlers allowed or blocked by name, such as GPTBot.
disallow_search true Keeps crawlers off the search page, so they do not spend their visits on endless result pages.
extra_lines none Lines added as written, after the rules and before the sitemap, such as Crawl-delay: 5. Each must be one line.

Search filters

Bots fill a site's search page with long or spam queries, so the page shows their words and links. [seo.search_filter] refuses such searches before anything is looked up, as Yoast's crawl settings do. A refused search answers 400 (a bad request) with the empty search page, and the query is not shown back. Every filter is on.

Key Default What it does
max_length 50 The most characters a search may have. 0 sets no limit.
symbols true Refuses a search with an emoji or another symbol that is not a letter, a digit or punctuation.
spam true Refuses a search with a link, markup, a spam domain such as .xyz, or one character six times in a row.

llms.txt

llms.txt is a short guide to the site for language models. [seo.llms_txt] shapes it.

Key Default What it does
off false true stops writing the file.
intro none Text after the tagline, such as who writes the site and how to cite it.
pages none Paths listed first, under "Key pages", in the order given, such as ["/about/"].
exclude none Paths left out, with every page below them, such as ["/tag/"].

Whole-site schema

Each language's pages that search engines may list have one JSON-LD graph together, at schema.json in the language's folder (/blog/schema.json, /blog/de/schema.json). Each node with an @id appears once. An AI agent or a search engine reads all of the site's structured data in one request instead of one page at a time. This is Yoast's schema aggregation. Each language's home page points to its file with <link rel="alternate" type="application/ld+json" href=".../schema.json"> in its head.

Yoast also names each file in robots.txt on a Schemamap: line. Khalid leaves that line out by default, because Lighthouse marks a robots.txt with a line it does not know as invalid, and that costs the SEO score on every page. To add the line anyway, as Yoast does:

[seo.schema_map]
robots_txt = true
Key Default What it does
off false true writes no schema.json files and no home page link.
robots_txt false true also names each file in robots.txt on a Schemamap: line. Lighthouse then marks robots.txt invalid.

To turn the files off:

[seo.schema_map]
off = true

Local business

A shop, office or clinic lists each of its locations in a [[seo.locations]] table. Each one becomes a schema.org node on the home page, so search engines can show its address and hours.

[[seo.locations]]
type = "Dentist"
name = "Mill Lane Dental"
street = "12 Mill Lane"
locality = "Leeds"
postal_code = "LS1 4AB"
country = "GB"
telephone = "+44 113 496 0000"
map = "https://maps.example.com/mill-lane"
geo = { latitude = 53.7997, longitude = -1.5492 }

[[seo.locations.hours]]
days = ["mon", "tue", "wed", "thu", "fri"]
opens = "09:00"
closes = "17:30"
Key What it does
type The schema.org type, such as Dentist. LocalBusiness when left out.
name, street, locality, postal_code, country The name and address. The country is two capital letters, such as GB.
region, telephone, price_range, map Optional: the county or state, the phone number, how expensive it is, a link to a map.
geo Optional: the point on the map. It puts the location in locations.kml.
hours When it is open, one table per group of days, on a 24-hour clock.

A post or page shows one location's card with the location block, by its name as written in site.toml:

::location{name="Mill Lane Dental"}

The card shows the address, the phone number as a link, the opening hours day by day and a link to map, or to OpenStreetMap at the geo point. With scripts on, it also says "Open now" or "Closed now" by the reader's clock. A name site.toml does not list shows nothing, and khalid build warns about it.

A site with locations also gets a locations page at /locations/ in each language. It lists every location's card in the order of site.toml, so it works with scripts off. When two or more locations have a geo point and the browser can tell where the reader is, a button sorts the list nearest first. The page is in the sitemap. A page of your own at /locations/ takes its place.

[seo.local] switches what a site with locations gets. Both are on by default.

Key Default What it does
kml true Writes locations.kml, a geo sitemap with a pin for each location that has a geo point, and lists it in the sitemap index, as Yoast Local does.
page true Builds the locations page at /locations/.

Category and tag pages

Each category or tag page can have its own search settings, one table per language under seo. Categories live in categories.toml and tags in tags.toml, both in the site folder. A tag needs an entry in tags.toml only when it has settings: its name still comes from the posts. A name written all in lowercase, such as internal links, titles its page as "Internal Links"; write any capital, such as AI search, and the page keeps your spelling.

[[tags]]
slug = "crawl-budget"

[tags.seo.en]
title = "Crawl budget guides {sep} {site}"
description = "How search engines decide how much of a site to read."
intro = "Every guide on **crawl budget**, newest first."
robots = "noindex"

[tags.seo.en.image]
url = "https://www.example.com/media/crawl.png"
alt = "A spider on a sitemap"
Key What it does
title The page's title, with the same variables as [seo.titles].
description The meta description. Titles can use it as {term_description}.
robots The page's own robots directives, such as noindex.
intro Markdown shown above the posts on the first page of the list.
image The picture link previews show, with url, alt and optional width and height.

A term page with no intro shows search engines only a list of links. khalid build names such pages in one note, and still builds.

These fields go in a post's front matter, between the --- lines. page_type also works on a page in pages/.

Field What it does
breadcrumb_title A shorter name for the post in its breadcrumb trail.
page_type The kind of web page in structured data: WebPage (the default), FAQPage, ContactPage, AboutPage, QAPage, ItemPage or CollectionPage.
article_type The kind of article: BlogPosting (the default), Article, NewsArticle, TechArticle or Report. It wins over the collection's default, so a report in a news collection stays a report.
news false leaves a post of a news collection out of the news sitemap.
stock_tickers Up to five stock tickers a news post is about, such as ["NASDAQ:GOOG"], for Google News.
---
title: Our 2026 survey of 500 SEO teams
article_type: Report
stock_tickers: ["NASDAQ:GOOG"]
---

The video block takes an optional thumbnail, the picture search results show for the video when it should not be the poster:

::video{src="/media/tour.mp4" title="A tour" poster="/media/tour.jpg" thumbnail="/media/tour-thumb.jpg"}

Written in the Khalid repository. Edit this page on GitHub.