Schema, dates and the last check before you publish

Valid structured data and eligible structured data are two different states, and one missing property decides which one you are in. Our own blog post shipped with a publish date seven days in the future, written there by our own product. Three separate claude-seo runs found it, and this lesson walks the fix.

Module 5 · 11 of 13 · 18 min

This course is independent. It has no affiliation with Anthropic or with AgriciDaniel, who writes claude-seo, and nobody on their side reviewed it. Run with claude-seo v2.4.1 on 2026-10-03.

What you will be able to do

  • Tell a valid schema block apart from one that is eligible for a rich result.
  • Check that every surface carrying a date agrees before the page goes public.
  • Deliver an approved draft to your repository and publish it yourself.

Publishing is the step where small errors become public, and the cheapest ones to make are a missing property and a wrong date. On 2026-10-03 the schema agent of our audit scored this site's structured data at 78 out of 100, with a mature graph and two required properties absent. The same day, two other claude-seo runs flagged a blog post of ours for carrying a publish date seven days in the future. That date came out of our own product: the article was planned for 2026-10-10 on the calendar, the delivery wrote that planned day into the file, and the post went live on 2026-09-28. This lesson walks both findings, then the part of publishing a tool never does for you.

A validator and a rich result ask different questions

Valid markup is markup a parser accepts, and eligible markup is markup Google can turn into a feature. A block can clear the first bar and sit well under the second, because a rich result has a list of required properties and your markup either carries them or it does not. Our BlogPosting parsed cleanly, with headline, datePublished, dateModified, author as a Person reference and publisher as an Organization reference, all correctly typed.

It was still ineligible for the Article rich result, because image was missing on every post on the site. One absent property, and the whole blog sat outside a feature it otherwise qualified for. Read the required list for the specific result you want before you grade your own markup, and treat anything else the validator tells you as background.

Our own graph scored 78 out of 100

The audit found structured data on seven page templates, and the shape of it was the good news. Organization, WebSite and SoftwareApplication with an Offer on the home page; WebPage and BreadcrumbList on pricing; WebPage, FAQPage and BreadcrumbList on a feature page; BlogPosting, Organization, Person and FAQPage on the post; a WebApplication with an Offer on a free tool. Every @context pointed at the canonical schema.org URL, every URL was absolute, every date was ISO 8601, and the entities were cross-linked by @id so one graph described one site.

Three things capped the score. BlogPosting had no image and the posts had no BreadcrumbList, while the other four templates did. SoftwareApplication and WebApplication had no aggregateRating or review, which the Software App rich result requires. And FAQPage, present on four templates, no longer earns anything in the SERP.

Some gaps are a line of code and some need data you do not have

Splitting the gap list in two is the first useful move, because the two halves have different costs. image on BlogPosting was free for us: every post already renders an OG card at a stable URL, so filling the property was pointing at an asset that exists. BreadcrumbList on posts was the same kind of work, since four other page templates already emitted one.

aggregateRating is the other kind. The property wants real review data, and a site with no reviews has nothing honest to put there. Our audit wrote the instruction into the finding itself: add it only once genuine review data exists, and never fabricate ratings. An agent asked to "complete the schema" will happily invent a 4.8 out of 237 reviews, and that number is then a fabricated claim on a page whose whole argument is that it reports measured things. Keep the property empty until the data exists.

FAQPage is valid markup with no SERP feature since May 2026

Google retired FAQ rich results for all sites on 2026-05-07, so FAQPage markup wins no search feature today. Our audit records it as information on the four templates that carry it, recommends leaving those alone, and advises against adding it to new pages for SERP reasons. The geo agent in the same run proposed adding FAQPage to more pages, and the orchestrator discarded that proposal with the retirement date attached.

Write the visible FAQ anyway. A reader with a question reads it, and an answer engine looking for a short quotable passage reads it too, which is what lesson 12 is about. If you want the markup for the sake of whatever else parses your JSON-LD, the free FAQ schema generator builds it from the questions you already wrote, and you can decide about the SERP separately.

A calendar date became a publish date

The article was planned for 2026-10-10, and that planned day is the one the delivery wrote into the file's publishedAt field. The post itself went live on 2026-09-28 and narrates a weekend of the 26th and 27th of September. So the page shipped carrying a date in the future on the line a reader sees, in datePublished and dateModified, in article:published_time, and in four lastmod values of the sitemap.

Three runs found it on the same day, from three angles. The audit's orchestrator confirmed it against the live site and tied it to the sitemap's future lastmod values as one root cause. The content command scored it a high-severity trust issue, since the visible date contradicts a body that says the thing went live on September 27. The geo command rated it critical and guessed the mechanism correctly: the product is writing the scheduled date instead of the real one. We corrected the date in the repository to 2026-09-28.

The suggested fix carried the bug with it

The schema agent closed its report with a ready-to-paste JSON-LD block for the missing image, and that block reused 2026-10-10 for both dates. The orchestrator caught it and left a note in the verification section telling the reader to use the real publish date instead.

Copy-pasteable output is where a wrong value travels fastest. A date you would have questioned in a sentence slides through inside a code fence, because a code fence reads as something a machine produced and a machine checked. Read the sample before you paste it, and check the values the sample carried over from the page it was describing.

Delivery puts a draft file in your repository and stops

A delivery target is five answers: repository, branch, path template, body format and the frontmatter mapping. Ours reads GitHub ch0rch/seodraft-app, branch main, content/blog/{locale}/{slug}.mdoc, Markdoc body, Keystatic preset, and that preset maps date to publishedAt, updatedDate to updatedAt, and drops image. delivery_status reads those five back for you, plus whether a token is saved, and it never returns the token itself.

Every delivery writes draft: true into the file whatever the article's own flag says, there is no option to turn that off, and nothing in your repository is ever deleted. Re-delivering overwrites the same path, so repeating it is safe; when a slug moves, the new path is written and the old one is named for you to delete by hand. The CMS delivery feature page describes the targets in more detail.

Approval is one call that checks the rules first

approve_post refuses unless the last run_gate check passed, which makes approval a gate rather than a button. When it passes, the same call moves the article to ready and delivers it to the configured destination. Our own post reached that state with lastGateOk: true and landed at content/blog/en/how-to-distribute-an-mcp-server.mdoc on 2026-09-28 at 03:11 UTC, with its Spanish translation linked to it as the same article in another language.

After that the tool is finished and you are not. The file in your repository waits for a person to open it, check the dates, check the links, upload the images the article's slots asked for, and publish from the CMS. Our date bug lived in that gap for a few days, which is the honest version of the lesson: the gap exists so a human can catch what a pipeline wrote, and it only works when the human looks.

The Spanish tree declared itself English in the raw HTML

The audit's single critical item had nothing to do with the post. Our /es routes served <html lang="en"> in the HTML that comes off the server, with the correct language applied afterwards by a client script. A crawler reading raw HTML, and any client that does not run JavaScript, saw Spanish content labelled as English across the whole Spanish tree.

We fixed it by rendering lang on the server from the route itself, so the attribute is correct in the first byte. The check is one line of curl against your own site, and it is worth running after any change to a root layout. A language declaration is the kind of thing that is either right everywhere or wrong everywhere, and nobody notices in a browser.

What to read on the morning you publish

Four readings make a publishing check, and they take about ten minutes on a normal post. Read the dates on every surface that carries one, including your sitemap, which the free sitemap checker will list for you without installing anything. Read the required properties of the rich result you want, and leave empty anything you cannot back with real data. Open the delivered file and confirm the draft flag is still there, then publish from your CMS yourself.

The fourth reading is the one people skip: open the live URL afterwards and look at what the page actually says. Our post was correct in the draft, correct in the delivery and wrong on the page, and the difference was a field a pipeline filled with the day somebody had planned. Lesson 12 takes the same published page and asks a different question about it: whether an answer engine can quote it at all.

Steps

  1. Step 1

    claude-seo

    Read the schema half of the audit

    The audit you ran in lesson 3 already carries a schema report per page template. Open that file on its own now, because this is the lesson where its findings get acted on. The useful column is the one naming required properties, which is what separates markup a validator accepts from markup Google can show.

    /seo audit https://your-site.com

    What you should see

    A type list per page template and a score. Ours returned 78/100 over a graph with Organization, WebSite, SoftwareApplication, WebPage, BreadcrumbList, BlogPosting, Person and FAQPage, cross-linked by `@id`, with every `@context` on https://schema.org.

  2. Step 2

    claude-seo

    Split the gaps you can fill from the ones you cannot

    Paste this as a prompt against the schema report. Some gaps are a line of code and some need data that does not exist yet, and the second kind is where a careless agent invents a number. A rating with no reviews behind it is fabricated data on a page that claims to be a source.

    For every schema type on this page, list the properties the rich result requires and which of them are missing. Separate the ones I can fill today from the ones that need data I do not have.

    What you should see

    Two short lists. Ours put `image` on BlogPosting in the first, since the OG card already exists, and `aggregateRating` on SoftwareApplication in the second, where the audit wrote the instruction in full: add it only once real reviews exist, never fabricate.

  3. Step 3

    claude-seo

    Make every surface agree on one date

    A publish date lives in four or five places, and they drift apart one deploy at a time. Run this against any post you are about to publish, and against the three you published last month. The sitemap is the surface people forget, and it teaches a crawler how much your `lastmod` is worth.

    Compare the visible date, datePublished, dateModified, article:published_time and the sitemap lastmod for this URL. Tell me which of them disagree with each other or sit in the future.

    What you should see

    One date per surface, side by side. Ours came back with 2026-10-10 on all of them for a post that went live on 2026-09-28, plus four sitemap `lastmod` values carrying the same future day.

  4. Step 4

    seodraft

    Know where your drafts land before you approve one

    `delivery_status` reads back the five answers that make a delivery target: repository, branch, path template, body format and the frontmatter mapping, plus whether a token is saved. It never returns the token itself. Read it once before your first approval, so the first file lands where you expect instead of somewhere you have to go find.

    Show me delivery_status for this workspace.

    What you should see

    The five answers for your workspace. Ours reads GitHub `ch0rch/seodraft-app`, branch `main`, path `content/blog/{locale}/{slug}.mdoc`, Markdoc body and the Keystatic preset, which maps `date` to `publishedAt`.

  5. Step 5

    seodraft

    Approve once, and let the approval deliver

    `approve_post` is one act: it refuses unless the last `run_gate` check passed, moves the article to `ready` and, with a destination configured, delivers it in the same call. Every delivery writes `draft: true` whatever the article says, there is no switch to turn that off, and nothing in your repository is ever deleted.

    Run run_gate on this post and show me every finding. If it passes, wait for me to say yes before calling approve_post.

    What you should see

    A findings list, then a file in your repository. Ours passed with `lastGateOk: true` and landed at `content/blog/en/how-to-distribute-an-mcp-server.mdoc` on 2026-09-28 at 03:11 UTC, with the Spanish translation linked to it.

Checklist

Tick every line and the lesson marks itself as completed.

Checkpoint

A validator accepts your BlogPosting block with no errors. Is the page eligible for an Article rich result?Show the answer

Only if every required property is present, and a validator that reports no errors is answering a narrower question than that. Ours parsed cleanly with `headline`, `datePublished`, `dateModified`, `author` and `publisher` correctly typed, and it was still blocked from the Article rich result because `image` was absent on every post on the site. The audit also flagged the missing BreadcrumbList there, which the other page templates already had. Read the required list for the specific rich result you want, then check your markup against that list.

Your agent approved an article and the file is in your repository. Is the post public?Show the answer

The file carries `draft: true` and your CMS is still the thing that publishes it. seodraft forces that flag on every delivery whatever the article's own field says, and there is no option to turn it off, so a delivered article waits for a human to open it. That gap is where the work of this lesson happens: you check the dates, the schema, the links and the images the article asked for, and then you publish. Our own date bug shows what the gap is for, and the gap only helps somebody who uses it.

What this lesson said

  • A schema block can be valid and still be ineligible: our graph scored 78/100 because BlogPosting had no `image` and the posts had no BreadcrumbList.
  • Google retired FAQ rich results for every site on 2026-05-07, so FAQPage markup is information for whatever reads the JSON-LD and buys no SERP feature.
  • Add `aggregateRating` only once real reviews exist. An audit that tells you to fabricate nothing is giving the most important instruction on the page.
  • Our product wrote the calendar date 2026-10-10 into `publishedAt` of a post that went live on 2026-09-28, and the audit, `/seo content` and `/seo geo` each flagged it.
  • Delivery writes a draft file into your repository with `draft: true` forced and deletes nothing. The human checks it and publishes from their own CMS.

Questions

Which schema types does a blog post actually need?
BlogPosting and BreadcrumbList, each with their required properties filled, plus the Organization and Person your site already declares and links by `@id`. Our audit found the first two short: `image` was missing from BlogPosting across the whole blog, which blocks the Article rich result, and BreadcrumbList existed on the pricing, features, alternatives and tools templates while the posts had none. Adding types a page has not earned buys nothing; filling the required properties of the types it has is the work.
Should I still add FAQPage markup?
Google retired FAQ rich results for all sites on 2026-05-07, so the markup wins no SERP feature today. Our audit records it as information on the pages that already carry it and recommends leaving those alone, and the orchestrator discarded its own geo agent's proposal to add FAQPage to more pages for exactly that reason. The visible FAQ block is still worth writing, because a reader with a question and an answer engine looking for a quotable passage both read it.
Why did a scheduled date end up as the publish date?
The article was planned for 2026-10-10 on the calendar, and the delivery wrote that planned day into the `publishedAt` field of the file. The post itself went live on 2026-09-28 and narrated a weekend in September, so the page shipped claiming a future date on its visible line, in `datePublished` and `dateModified`, in `article:published_time` and in four sitemap `lastmod` values. We corrected it in the repository to the real day. The habit the episode buys is cheap: before you publish, read the date on every surface that carries one.

Next lesson

AI search and GEO

What makes a passage quotable by an answer engine, what claude-seo's geo command scores, and which half of that report is an estimate.

The same lesson, as plain Markdown: /learn/claude-seo/schema-and-publishing.md

Back to the course