heddle check --only surface
Surface Check
The surface check reads each built page's share card, structured data, dates, FAQ markup, alt text and sitemap entry, and fails what a search engine or a share would show wrong. Without it, a post can share as a bare link, show no date in results, or carry markup Google ignores, and nothing looks wrong in a browser. The full search, answer-engine and share surface of every built page, by kind. txt and the Markdown twins.
When Heddle is set up on your site, this check runs on every build alongside 25 others, so a page that fails it is fixed before a buyer or a search engine sees it.
Why it exists
The full search, answer-engine and share surface of every built page, by kind. playbook/seo-aeo-geo.md is the list and its sources; meta and agent-ready already hold titles, descriptions, canonicals, the og:image existing, BreadcrumbList being present, llms.txt and the Markdown twins. This check holds the rest:
- Open Graph complete. Og:title, og:description, og:type, og:url, og:image with og:image:width, og:image:height and og:image:alt, plus a Twitter card (summary_large_image) with its title and image. A share card that lacks any of them renders as a bare link on at least one network.
- The card image is real. The file is in the build and its pixels, read from the PNG, JPEG, GIF or WebP header, are 1200x630, matching what the tags declare. A tag that says 1200x630 over a 400x400 logo is the commonest way a card ships wrong, and nothing else catches it. An image on another host cannot be measured, so it fails until it is served from the build.
- JSON-LD that parses, every node typed, and the types a kind requires. The home page an Organization (or a reference to one) and a WebSite; every indexable page a page node (WebPage or a subtype, or an Article); every post a BlogPosting or Article with headline, author (a Person with a name and a url, or a reference to one), ISO datePublished and dateModified, image and publisher. Every page but the home page a BreadcrumbList whose items carry a position, a name and a URL. Every page node a dateModified (or datePublished), so every page says when it last changed. An author url on the site's own host is a page the build contains, and no URL in the markup has a second scheme glued into it (a site URL prefixed to an absolute one).
- Posts: og:type article, article:published_time in ISO 8601, a share image of their own (not the home page's), and a visible date in a
- An FAQ on the page (a section headed Questions or FAQ with
<details>) has FAQPage markup, and every Question has an accepted answer. - No AggregateRating or Review unless site.json surface.allowRatings says the ratings are real and on the page: Google treats self-serving or invented review markup as spam.
- Every
<img>has an alt attribute (empty is allowed, for decoration). - A page that plays a video carries a VideoObject eligible for a video result. Name, description, thumbnailUrl, uploadDate with a time and timezone, an ISO duration, and contentUrl or embedUrl (lib/video/schema.mjs). A missing field makes it ineligible, not partly so.
- Every indexable page is in sitemap.xml with a
<lastmod>, and the lastmod is the day the content changed. The same date as the page's own dateModified, never after the build, and not the build date on every URL. On the site this came from every URL carried the build time, so six deploys in one day told Google 294 pages had changed six times, and a sitemap whose dates do not survive a crawl stops being trusted (21 Sep 2026). A launch-day build may date everything today. Surface.launched names that day. - Dates with a time carry the site's offset. A UTC timestamp moved an evening event to the next day and an AI Overview hedged its time (12 Sep 2026). A time with no offset is ambiguous, a time in another offset than surface.timezone is wrong for half the year's readers, and an Event's startDate needs its time.
- One publisher. Every publisher reference names the same @id, and the home page's Organization has that @id, a url and a logo. Two Organization entities for one company split what Google and the assistants know about it.
- The feed is announced. When the build has an RSS or Atom feed, every indexable page's head links it, and each item names its author (dc:creator) and, as a warning, its categories. A metadata helper that replaced the layout's
alternatesdropped the link from every post. - llms.txt is in the build, and every indexable page has its Markdown copy (route + ".md", "/index.md" for the home page). agent-ready holds the finer rules (llms.txt naming each page, Link headers); this check holds that the files exist, so the surface is one gate.
Kinds come from site.json surface.posts (route patterns, default ^/blog/[^/]+$); pages that say noindex, and meta.exempt routes, are skipped like the meta check skips them.
From lib/checks/surface.mjs in the Heddle repository at ddb1b16.
Questions
What does the surface check look for?
The full share card, typed JSON-LD per page kind, post dates and images, FAQ markup, alt text and sitemap dates on the built pages.
Does the surface check need an AI model?
No. It reads the built pages or the sources directly, so it runs offline and costs nothing per run.
How do I run it?
Build the site, then run node bin/heddle.mjs check <site> --only surface from the Heddle repository.
Work with us
Let the agents bring you the leads.
Tell us about your business and who buys from you. On a short call we'll show you the searches your buyers make where you don't show up yet, and what the agents would do about them in their first month.
On the call, for your site
- The searches your buyers makeMeasured, with how many people make each one
- Where you show up, and where you don'tOn Google and in ChatGPT's answers
- The agents' first monthThe pages, fixes and links they would start with