Fetch your own pricing page the way a crawler does and read it without the CSS. On a lot of WordPress sites the first screenful of text is a skip link, a forty-item menu, a cookie notice, a shipping announcement, and a footer repeating the menu. The paragraph that answers the question starts somewhere after all that. If a pipeline splits the page before it gets there, the answer never travels.
These are the changes I would make in order, because each one is worthless if the step before it fails: extraction, then structure, then metadata, then access.
Read the rendered page as text before changing anything
curl -sL https://example.com/pricing/ \
| python3 -c '
import re, sys
html = sys.stdin.read()
for tag in ("script", "style", "noscript", "nav", "footer", "header"):
html = re.sub("(?is)<" + tag + ".*?</" + tag + ">", " ", html)
html = re.sub("<[^>]+>", " ", html)
print(" ".join(html.split())[:1200])
'
Run it on the pages you most want quoted: a product page, pricing, the about page, an FAQ, and the post that answers the question your buyers keep emailing about. Different tools strip different elements and this script strips generously, so treat the output as the best case. If even the best case is navigation, the markup is the problem.
Keep navigation outside the element that holds the content
In a block theme, header and footer live in template parts and the post content block sits in the template, so the structure is usually already right. In a classic theme, open the template that wraps the loop and check what is inside <main>. Sidebars, related-post lists, and newsletter boxes end up in there more often than you would expect. Move them out, or mark them <aside>.
A <header> inside <article> is fine, for the title and the date. The site header belongs outside <main>.
If a page builder produces a wall of divs, wrap the content area in <main> from the template rather than fighting the builder's output.
Give sections headings that read as questions
One <h1> per page, naming the subject: the product and what it is, or the question the post answers. <h2> per sub-question, phrased so it means something alone: "Delivery time to Germany" rather than "Details". <h3> for parts of an <h2>. Do not skip levels to get a smaller font, and do not use heading tags for styling.
When a retrieval pipeline splits a page into chunks, headings are the natural boundaries and the heading becomes the context of everything under it. A meaningless heading makes an unusable chunk.
Write each section to stand on its own
Open with the answer to the heading, then the conditions and exceptions. Repeat the subject instead of writing "it" or "as mentioned above", because the previous section may not be in the same chunk. Keep one idea per section: a section that covers price, delivery, and warranty at once either gets split badly or does not get split at all.
Drop phrases like "see the table below". The table may not travel with the sentence.
Move stable facts into tables and lists
Opening hours, dimensions, compatible models, delivery windows, what is in the box, price tiers. A <table> with <th scope> or a <ul> extracts as discrete items. The same facts written as prose extract as one blob and get paraphrased badly.
Two rendering traps catch ecommerce sites. Prices and stock rendered by JavaScript after load, and content inside tabs or accordions that stays hidden until a click. A fetch that does not run scripts sees neither, and themes built around Core Web Vitals use both. Check the raw HTML for the number before assuming it is there.
Metadata that survives the pipeline
-
<title>: the page topic plus the brand. Not "Home | Brand". - Meta description: one accurate sentence about the page, kept stable.
- Canonical: one URL per piece of content. Redirect the
/?p=123,?s=, and trailing-slash variants so the same text is not reachable at four addresses. - JSON-LD for the entities you actually have:
Organizationonce,Articleon posts,Productwith price and availability on product pages,FAQPageonly where the questions and answers are visible in the page body.
add_action('wp_head', function () {
if (!is_singular()) {
return;
}
$post = get_queried_object();
if (!$post instanceof WP_Post) {
return;
}
$data = [
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => wp_strip_all_tags(get_the_title($post)),
'datePublished' => get_post_time('c', true, $post),
'dateModified' => get_post_modified_time('c', true, $post),
'mainEntityOfPage' => get_permalink($post),
'author' => [
'@type' => 'Organization',
'name' => get_bloginfo('name'),
],
];
printf(
'<script type="application/ld+json">%s</script>' . PHP_EOL,
wp_json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
}, 20);
The constraint on structured data is agreement. If the markup states one price and the visible page states another, the page is less trustworthy, not more. dateModified should change when the content changes, not on every save.
Delete the placeholder text that ships with the theme
Sample prices, "Your text here", lorem paragraphs in an unused section, a deactivated plugin's shortcode rendering as [price_table]. These get extracted with everything else, and a model repeating your placeholder price is a support ticket you cannot close. Search the rendered HTML, not the editor.
Confirm the crawler is allowed in
None of the above matters if the request gets a 403. Three places decide that: robots.txt, the bot rules in your CDN or WAF, and any security plugin with a user-agent blocklist. Check the access log for the user agents you care about and the status code they received.
grep -Ei 'gptbot|oai-searchbot|perplexitybot|claudebot' access.log \
| awk '{print $9, $7}' \
| sort | uniq -c | sort -rn | head -20
Field positions assume the common combined log format; adjust if your log differs. Rows of 200s on the pages you want indexed is the answer you need. Rows of 403s is the actual project.
Write an llms.txt if you want the cheap version of a map
A markdown file at the site root listing the pages that matter, one line each. It is a proposal rather than a settled standard, so keep the cost near zero: a static file, updated when the site structure changes. Do not put content in it that is not on the site. The file is an index, not a second copy.
Re-run the checks after theme and plugin updates
The extraction script is the regression test. So is a JSON-LD parse:
curl -sL https://example.com/pricing/ \
| python3 -c '
import json, re, sys
html = sys.stdin.read()
blocks = re.findall("(?is)<script[^>]+application/ld[+]json[^>]*>(.*?)</script>", html)
for block in blocks:
json.loads(block)
print(len(blocks), "JSON-LD blocks parsed")
'
Run both after any template change, any page builder update, and any change to the pricing or checkout widget. The failures are silent: the page looks fine in a browser while the extracted text is empty where the price should be.
If you want llms.txt, schema output, the sitemap, meta tags, and internal links managed from the admin instead of hand-written in the theme, AumViso is a free plugin that does those: https://aumcreate.com/plugins/aumviso?utm_source=devto&utm_medium=content&utm_campaign=devto-ai-readable-wordpress-html-metadata
Top comments (0)