Most "AI SEO" advice is about prompts. The unglamorous truth is that an AI crawler has to do five boring things to your site before any of that matters: discover the URL, fetch it, parse the markup, chunk the content, and cite it. Each of those steps has a signal you control. This post is the process I run to make all five work.
It's ordered deliberately. Doing step 4 before step 1 is how you end up with a beautiful schema graph on a page that no crawler can reach.
Step 0 — Inventory before you touch anything
List every URL template on the site (post, page, product, category, tag archive, author archive, search, paginated). For each one write down: is it indexable, is it canonical, does it need to be discovered at all?
Sitemaps get bloated because nobody ever does this. A sitemap listing 4,000 tag archives is not a discovery aid, it's noise that dilutes the crawl budget you wanted to spend on the 200 pages that matter.
Step 1 — Fix status and canonical first
A sitemap entry is a promise: this URL is 200, canonical, and indexable. Break that promise and crawlers learn to trust the file less.
Rules:
- Only
200URLs go in the sitemap. - Only self-canonical URLs go in the sitemap. Never list a URL that canonicals elsewhere.
- Never list a
noindexURL. It's a contradiction — the page says "don't index me," the sitemap says "here's what to index." - Redirects don't go in. The destination does.
On WordPress, core already excludes most of this via wp-sitemap.xml, but check the tag and author archives — those are the usual leaks.
Step 2 — Build the sitemap properly
A minimal, valid file:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/guides/robots-txt/</loc>
<lastmod>2026-10-14</lastmod>
</url>
<url>
<loc>https://example.com/guides/sitemaps/</loc>
<lastmod>2026-09-30</lastmod>
</url>
</urlset>
Mechanics that matter:
- Hard limits are 50,000 URLs and 50 MB uncompressed per file. Past either, split into a sitemap index.
-
lastmodmust reflect a meaningful content change. Bumping it on every deploy trains crawlers to ignore the field entirely. -
priorityandchangefreqare widely ignored. Skip them; they add noise without signal. - Keep the file UTF-8, well-formed, and free of BOM characters.
A sitemap index, for when you split by type:
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-posts.xml</loc>
<lastmod>2026-10-14</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-products.xml</loc>
<lastmod>2026-10-12</lastmod>
</sitemap>
</sitemapindex>
Step 3 — Advertise it in robots.txt and verify
Sitemap: https://example.com/wp-sitemap.xml
That's the whole step — but verify it, because a broken sitemap URL in robots.txt is worse than none:
# 1. Is the sitemap well-formed XML?
curl -s https://example.com/wp-sitemap.xml | xmllint --noout - && echo "XML OK"
# 2. How many URLs, and do they look right?
curl -s https://example.com/wp-sitemap.xml \
| grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \
| wc -l
# 3. Spot-check the first 50 for noindex (should return nothing)
curl -s https://example.com/wp-sitemap.xml \
| grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | head -50 \
| while read -r u; do
curl -sL "$u" | grep -qi 'name="robots" content="noindex"' && echo "NOINDEX: $u"
done
That third check catches the most common sitemap defect in the wild: a plugin change turned a template to noindex and nobody noticed the sitemap still advertises it.
Step 4 — The per-page meta layer
Four tags per page, in this order of importance:
-
<title>— unique, describes the actual page, no template boilerplate. -
<meta name="description">— a real summary, ~150 characters. It gets used as snippet text and as chunk context. -
<link rel="canonical">— self-referencing on every indexable page. -
<meta name="robots">— only where it differs from the default.noindex,followon search results and 404s.
WordPress outputs the canonical for you but not a description. A minimal, runnable addition:
add_action( 'wp_head', function () {
if ( is_singular() ) {
$id = get_queried_object_id();
$desc = wp_strip_all_tags( get_the_excerpt( $id ) );
$desc = trim( preg_replace( '/\s+/', ' ', $desc ) );
$desc = mb_substr( $desc, 0, 155 );
if ( $desc ) {
printf( "<meta name=\"description\" content=\"%s\">\n", esc_attr( $desc ) );
}
}
if ( is_search() || is_404() ) {
echo "<meta name=\"robots\" content=\"noindex,follow\">\n";
}
}, 1 );
Swap get_the_excerpt() for whatever field holds your real summary if you have one — an excerpt built from the first 155 characters of body copy is a fallback, not a description. And if another plugin already emits a description, delete one of the two rather than shipping both.
Step 5 — Structured data, one shape per template
Schema is how you tell a parser what kind of thing the page is, instead of making it guess from prose. Match the type to the template — Article for posts, Product for products, BreadcrumbList site-wide, Organization once on the homepage.
add_action( 'wp_head', function () {
if ( ! is_singular( 'post' ) ) {
return;
}
$id = get_queried_object_id();
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => wp_strip_all_tags( get_the_title( $id ) ),
'description' => wp_strip_all_tags( get_the_excerpt( $id ) ),
'datePublished' => get_the_date( DATE_W3C, $id ),
'dateModified' => get_the_modified_date( DATE_W3C, $id ),
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => get_permalink( $id ),
],
];
echo '<script type="application/ld+json">'
. wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
. "</script>\n";
}, 20 );
Two rules: emit JSON-LD once per type (duplicate graphs from two plugins is a mess to debug), and only mark up what's visibly on the page. Marking up an FAQ that isn't rendered is the fastest route to losing rich results.
Step 6 — Internal links, because crawlers don't browse
Discovery isn't only the sitemap. It's also the link graph. A page with no inbound internal links is an orphan, and orphan pages are effectively undiscoverable no matter how clean the XML is.
- Every indexable page reachable within three clicks of the homepage.
- Descriptive anchor text —
robots.txt patterns for AI crawlers, notclick here. - Link from high-traffic posts down to the pages you actually want cited.
- Check pagination: series and archives that only expose page 1 hide everything after it.
Step 7 — llms.txt, cheaply
llms.txt is a proposed convention, not an adopted standard — no guarantee any crawler reads it. Cost is near zero, so publish one, but don't build a strategy on it.
# Example Studio
> One-person WordPress studio. Templates, custom builds, deployment.
## Guides
- [robots.txt patterns](https://example.com/guides/robots-txt/): allowing search crawlers while blocking training crawlers
- [Sitemap setup](https://example.com/guides/sitemaps/): sitemap structure and per-page meta
## Optional
- [Changelog](https://example.com/changelog/)
Keep it to canonical URLs with one-line descriptions. A stale llms.txt full of 404s is worse than not having one.
Step 8 — The verification loop and its cadence
Per page, before publishing:
curl -sL https://example.com/guides/robots-txt/ \
| grep -Eio '<link rel="canonical"[^>]*>|<meta name="robots"[^>]*>|<meta name="description"[^>]*>|<title>[^<]*</title>'
What you're looking for: one canonical, self-referencing; no stray noindex; a description that isn't just the title repeated.
Then on a schedule:
-
Weekly — new URLs appear in the sitemap,
lastmodis honest. -
Monthly — full sitemap sweep for 404s and
noindex(the script in step 3, without thehead -50). -
On any template change — re-check the schema output and the canonical. Template edits are where
noindexaccidents happen.
Where the manual part stops
The process above is four files and a bash loop — which is fine for one site and miserable for five. This is exactly the boundary I hit, and why the sitemap, llms.txt, schema, meta and internal-link work on my own sites runs through AumViso instead of hand-maintained snippets.
But the tooling is the small part. The part that actually moves the needle is step 1: deciding which URLs deserve to be discovered, and then not contradicting yourself in the markup.
Top comments (0)