Add structured data to any website
The AtlasSchema SDK builds correct schema.org structured data (JSON-LD) from plain fields and checks it with Google’s rich result rules. It runs in your own code, needs no account and sends nothing anywhere. Connect AtlasSchema when you want every page checked and watched. Free and open source (MIT).
On this page
- How it works · WordPress · Install · Build and print
- All types and fields · How values are written · One graph per page
- Check and fix · Mistakes and strict mode · Connect AtlasSchema
- Crawler files · What AI sees · AI fact check · Same business everywhere · Moving an existing website · Framework recipes · Questions
How it works
- Build. Give a builder plain fields (a product’s name, price and currency) and get a correct schema.org node back. Required fields and value formats are checked as you build.
- Print. Put the page’s nodes in one graph and print it as a single JSON-LD script. The Organization, WebSite and WebPage get stable ids and point at each other, so search engines and AI assistants see one entity.
- Check. Run the same checks the AtlasSchema service runs (Google’s required and recommended properties, schema.org basics, price and stock against the page), offline, as often as you like.
- Monitor (optional). Send your pages to AtlasSchema with an API key. It checks the whole website, keeps history and emails you when something breaks.
There is one SDK in two languages with the same builders, the same checks and the same output: PHP for WordPress, Laravel, Symfony, Joomla, Drupal, CodeIgniter and plain PHP, and JavaScript for Node, Next.js, Nuxt, Astro, React and the browser. Any other language can use the API.
WordPress: nothing to install
The free Atlas Schema plugin has the PHP SDK built in. It prints one connected graph on every page (Organization, WebSite, WebPage, breadcrumbs, articles), with forms for FAQ, products, courses, events, local businesses and custom JSON-LD, and it works with Yoast SEO, Rank Math and All in One SEO.
- In WordPress, open Plugins → Add New, search for Atlas Schema, install and activate it.
- Open Schema and follow the setup steps (your organization, logo and social profiles).
- Edit a post or page: the Structured data panel shows what is printed and lets you add FAQ, product details or a guided type.
- To check every page: in the AtlasSchema dashboard open your website’s Connect tab, create a key, paste it in Schema → Validate and press Send all pages now.
For theme and plugin developers
Inside WordPress, use the plugin’s SDK through Atlas_Schema_Sdk and add your nodes with the atlas_schema_graph filter. They join the same graph, so there is still one Organization and one WebPage.
add_filter( 'atlas_schema_graph', function ( $nodes, $post ) { if ( 'event' !== $post->post_type ) { return $nodes; } $built = Atlas_Schema_Sdk::build( 'event', array( 'name' => get_the_title( $post ), 'startDate' => get_post_meta( $post->ID, 'starts', true ), // 2026-11-05T19:00:00+01:00 'attendance' => 'offline', 'locationName' => 'City Hall', 'address' => array( 'street' => '1 Main St', 'city' => 'Dublin', 'country' => 'IE' ), ) ); $nodes[] = $built['node']; return $nodes; }, 10, 2 );
Other filters: atlas_schema_organization (the site’s publisher), atlas_schema_home_graph (the blog home page) and atlas_schema_guided_node (a node from a guided form). If you ship your own plugin without Atlas Schema, install the PHP SDK with Composer and prefix it with Strauss, so another plugin’s copy can never clash with yours.
Install
composer require atlasschema/sdk
npm install @atlasschema/sdk
PHP 7.4 or newer, no dependencies (Laravel finds it on its own). JavaScript: Node 18 or newer, modern browsers, React 17 or newer for <JsonLd>; no dependencies.
Build and print
Build each node from plain fields, put the page’s nodes in one graph, and print it in the page’s <head> (or anywhere in the body).
use AtlasSchema\Schema; $page = Schema::graph([ Schema::organization(['name' => 'Acme Ltd', 'url' => 'https://acme.com/', 'logo' => 'https://acme.com/logo.png']), Schema::product([ 'name' => 'Blue mug', 'image' => 'https://acme.com/mug.jpg', 'offer' => ['price' => 12.5, 'currency' => 'EUR', 'inStock' => true], 'rating' => ['value' => 4.6, 'count' => 38], ]), ]); echo $page->toScript(); // <script type="application/ld+json">…</script>
// In a controller use AtlasSchema\Schema; return view('product', ['schema' => Schema::graph([ Schema::product(['name' => $product->name, 'offer' => ['price' => $product->price, 'currency' => 'EUR']]), ])]); {{-- In the Blade view (the @jsonld directive is registered on its own) --}} @jsonld($schema)
const { schema, graph } = require('@atlasschema/sdk'); const page = graph([ schema.organization({ name: 'Acme Ltd', url: 'https://acme.com/', logo: 'https://acme.com/logo.png' }), schema.product({ name: 'Blue mug', image: 'https://acme.com/mug.jpg', offer: { price: 12.5, currency: 'EUR', inStock: true }, rating: { value: 4.6, count: 38 }, }), ]); page.toScript(); // '<script type="application/ld+json">…</script>'
import { JsonLd, schema } from '@atlasschema/sdk/react'; export default function ProductPage({ product }) { return ( <> <JsonLd items={[schema.product({ name: product.name, offer: { price: product.price, currency: 'EUR' } })]} /> <h1>{product.name}</h1> </> ); } // Rendered on the server with the page (App Router or Pages Router), so crawlers read it without JavaScript.
A value can never break out of the script tag: < is always escaped. For a type the SDK has no builder for, use Schema::custom([...]) / schema.custom({...}) with any schema.org node.
All types and fields
One builder per type (Schema::product() in PHP, schema.product() in JavaScript). Pass type to choose a more specific kind where one is listed. This table is made from the SDK’s own type list, so it always matches the SDK.
| Builder | schema.org type | Required | Other fields | |
|---|---|---|---|---|
organizationOrganization | Organization | Rich result | name url | logo description email telephone address sameAs |
localBusinessLocal business | LocalBusinesstype: Restaurant, CafeOrCoffeeShop, Bakery, BarOrPub, Store, and 22 more | Rich result | name address | telephone email url image logo priceRange geo hours sameAs parentOrganization |
personPerson | Person | — | name | url image jobTitle worksFor description email sameAs |
serviceService | Service | — | name | description serviceType provider areaServed url offer |
websiteWebsite | WebSite | Rich result | name url | searchUrl inLanguage publisher |
webPageWeb page | WebPagetype: AboutPage, ContactPage, ProfilePage, CollectionPage, ItemPage, and 2 more | — | name url | description inLanguage datePublished dateModified image primaryImage speakable isPartOf breadcrumb mainEntity publisher |
breadcrumbsBreadcrumbs | BreadcrumbList | Rich result | items | |
itemListList or carousel | ItemList | Partly | items | name |
articleArticle | Articletype: BlogPosting, NewsArticle, TechArticle | Rich result | headline image datePublished author | url dateModified publisher description inLanguage articleSection keywords wordCount mainEntityOfPage isPartOf speakable |
blogBlog | Blog | — | name url | description publisher inLanguage posts |
faqFAQ | FAQPage | Retired 2026 | questions | |
qaQuestion and answer | QAPage | Rich result | question answer | author |
howToHow-to | HowTo | Retired 2023 | name steps | description image totalTime |
videoVideo | VideoObject | Rich result | name description thumbnailUrl uploadDate | contentUrl embedUrl duration |
recipeRecipe | Recipe | Rich result | name image | description author prepTime cookTime totalTime recipeYield recipeCategory recipeCuisine ingredients steps rating |
productProduct | Product | Rich result | name | url image description sku gtin brand offer rating review subjectOf isVariantOf |
productGroupProduct with variants | ProductGroup | Rich result | name variants | url image description productGroupID brand variesBy |
model3d3D model | 3DModel | — | name files | description url image width height depth license free creator keywords dateModified |
reviewReview | Review | Rich result | itemName author ratingValue | itemType bestRating reviewBody datePublished |
aggregateRatingRating summary | AggregateRating | Rich result | itemName ratingValue ratingCount | itemType bestRating |
softwareApplicationSoftware application | SoftwareApplicationtype: WebApplication, MobileApplication, VideoGame | Rich result | name applicationCategory offer | operatingSystem url image description downloadUrl featureList publisher rating |
bookBook | Book | Retired 2025 | name author | isbn url image datePublished publisher description |
eventEvent | Event | Rich result | name startDate | endDate url attendance locationName address onlineUrl status image description organizer performer offer |
jobPostingJob posting | JobPosting | Rich result | title description datePosted hiringOrganization | validThrough employmentType address remote salary |
courseCourse | Course | Partly (since 2025) | name description provider | url image courseCode inLanguage courseMode courseWorkload startDate endDate offer |
learningResourceLearning resource | LearningResource | Retired 2025 | name | description url image learningResourceType educationalLevel teaches timeRequired inLanguage free publisher |
26 types. "Google" says whether Google Search still shows a rich result for the type; retired types are still valid structured data that other search engines and AI assistants read. Open a type for every field:
organization() · Organization · Organization
Google: Logo and details in knowledge panels and attribution.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url required | Address (URL) | web address |
logo | Logo address | image address |
description | Description | text |
email | text | |
telephone | Telephone | text |
address | Address | address |
sameAs | Social profiles | web addresses (list) One address per line: X, LinkedIn, Facebook, Wikipedia… |
localBusiness() · Local business · LocalBusiness
Kinds (type): LocalBusiness, Restaurant, CafeOrCoffeeShop, Bakery, BarOrPub, Store, ClothingStore, Dentist, MedicalClinic, Physician, HairSalon, BeautySalon, AutoRepair, Hotel, RealEstateAgent, LegalService, AccountingService, Plumber, Electrician, HomeAndConstructionBusiness, ProfessionalService, FinancialService, MedicalBusiness, HealthAndBeautyBusiness, AutomotiveBusiness, LodgingBusiness, SportsActivityLocation, EducationalOrganization.
Google: Business details in the knowledge panel: hours, ratings, directions.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
address required | Address | address |
telephone | Telephone | text |
email | text | |
url | Address (URL) | web address |
image | Image address | image address |
logo | Logo address | image address |
priceRange | Price range | text Such as €€ or 10-30 EUR |
geo | Map position | lat, lng lat, lng |
hours | Opening hours | opening hours days, opens, closes |
sameAs | Social profiles | web addresses (list) One address per line: X, LinkedIn, Facebook, Wikipedia… |
parentOrganization | Part of (organization) | organization The company or chain this location belongs to |
person() · Person · Person
Google: No rich result of its own; names authors and profile pages.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url | Address (URL) | web address |
image | Image address | image address |
jobTitle | Job title | text |
worksFor | Works for | organization |
description | Description | text |
email | text | |
sameAs | Social profiles | web addresses (list) One address per line: X, LinkedIn, Facebook, Wikipedia… |
service() · Service · Service
Google: No rich result of its own.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description | Description | text |
serviceType | Kind of service | text |
provider | Provider | organization |
areaServed | Area served | text |
url | Address (URL) | web address |
offer | Offer | offer price, currency, inStock or availability, url, validFrom, category, returns { days, country, free }, shipping { cost, country, handlingDays, transitDays } |
website() · Website · WebSite
Google: The site name shown in search results.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url required | Address (URL) | web address |
searchUrl | Site search address | search address Such as https://example.com/?s={search_term_string} |
inLanguage | Language | language or list Such as en-US; a list for a site in several languages |
publisher | Publisher | organization |
webPage() · Web page · WebPage
Kinds (type): WebPage, AboutPage, ContactPage, ProfilePage, CollectionPage, ItemPage, FAQPage, CheckoutPage.
Google: No rich result of its own; ProfilePage has one.
| Field | What it is | Takes |
|---|---|---|
name required | Title | text |
url required | Address (URL) | web address |
description | Description | text |
inLanguage | Language | text |
datePublished | Published | date |
dateModified | Last changed | date |
image | Image address | image address |
primaryImage | Main image | image address |
speakable | Parts to read aloud | CSS selectors CSS selectors, one per line, such as h1 |
isPartOf | Part of (website) | ref The @id of a node in the same graph (such as https://example.com/#organization), or the node itself |
breadcrumb | Breadcrumbs | ref The @id of a node in the same graph (such as https://example.com/#organization), or the node itself |
mainEntity | Main subject | ref What the page is about: the @id of a product, software, FAQ… or the node itself |
publisher | Publisher | organization |
breadcrumbs() · Breadcrumbs · BreadcrumbList
Google: The page path shown in search results.
| Field | What it is | Takes |
|---|---|---|
items required | Path | path name, url — from the home page to this page |
itemList() · List or carousel · ItemList
Google: A carousel only together with Recipe, Course, Restaurant or Movie items.
| Field | What it is | Takes |
|---|---|---|
name | Name | text |
items required | Page addresses | addresses (list) One address per line, in order (or url, name) |
article() · Article · Article
Kinds (type): Article, BlogPosting, NewsArticle, TechArticle.
Google: Title and larger images in article results.
| Field | What it is | Takes |
|---|---|---|
headline required | Headline | text Up to 110 characters |
url | Address (URL) | web address |
image required | Image address | image address |
datePublished required | Published | date and time |
dateModified | Last changed | date and time |
author required | Author | person |
publisher | Publisher | organization |
description | Description | text |
inLanguage | Language | text |
articleSection | Section | text Such as the category |
keywords | Keywords | text Comma separated |
wordCount | Words | number |
mainEntityOfPage | Page it is the main subject of | ref The @id of a node in the same graph (such as https://example.com/#organization), or the node itself |
isPartOf | Part of (blog) | ref The @id of a node in the same graph (such as https://example.com/#organization), or the node itself |
speakable | Parts to read aloud | CSS selectors CSS selectors, one per line |
blog() · Blog · Blog
Google: No rich result of its own; its posts are articles.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url required | Address (URL) | web address |
description | Description | text |
publisher | Publisher | organization |
inLanguage | Language | text |
posts | Posts | refs The @ids of the posts, or the posts themselves |
faq() · FAQ · FAQPage
Google: Google stopped showing FAQ rich results on 7 May 2026.
| Field | What it is | Takes |
|---|---|---|
questions required | Questions and answers | questions q, a |
qa() · Question and answer · QAPage
Google: Q&A result: one question and its answers.
| Field | What it is | Takes |
|---|---|---|
question required | Question | text |
answer required | Accepted answer | text |
author | Asked by | person |
howTo() · How-to · HowTo
Google: Google stopped showing How-to rich results on 13 September 2023.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description | Description | text |
image | Image address | image address |
totalTime | Total time | duration Such as PT30M |
steps required | Steps | steps One step per line |
video() · Video · VideoObject
Google: Video results, key moments and live badges.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description required | Description | text |
thumbnailUrl required | Thumbnail address | image address |
uploadDate required | Uploaded | date and time |
contentUrl | Video file address | web address |
embedUrl | Player address | web address |
duration | Length | duration Such as PT2M30S |
recipe() · Recipe · Recipe
Google: Recipe results and carousels.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
image required | Image address | image address |
description | Description | text |
author | Author | person |
prepTime | Preparation | duration |
cookTime | Cooking | duration |
totalTime | Total time | duration |
recipeYield | Serves | text |
recipeCategory | Category | text |
recipeCuisine | Cuisine | text |
ingredients | Ingredients | list One per line |
steps | Instructions | steps One step per line |
rating | Rating | rating value, count, best (only ratings shown on the page) |
product() · Product · Product
Google: Price, availability and ratings; merchant listings.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url | Address (URL) | web address |
image | Image address | image address |
description | Description | text |
sku | SKU | text |
gtin | GTIN (barcode) | text |
brand | Brand | brand |
offer | Offer | offer price, currency, inStock or availability, url, validFrom, category, returns { days, country, free }, shipping { cost, country, handlingDays, transitDays } |
rating | Rating | rating value, count, best (only ratings shown on the page) |
review | A review | review author, rating, body |
subjectOf | 3D model or video of it | ref A 3D model (3DModel) or video about the product: its @id or the node itself |
isVariantOf | Variant of (product group) | ref The @id of a node in the same graph (such as https://example.com/#organization), or the node itself |
productGroup() · Product with variants · ProductGroup
Google: Product variants in merchant listings.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
url | Address (URL) | web address |
image | Image address | image address |
description | Description | text |
productGroupID | Group ID | text Such as the parent SKU |
brand | Brand | brand |
variesBy | Varies by | enums |
variants required | Variants | refs The variant products: their @ids or the products themselves |
model3d() · 3D model · 3DModel
Google: No rich result of its own in Search.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description | Description | text |
url | Address (URL) | web address |
image | Image address | image address |
files required | Model files | mediaFiles url, format (model/gltf-binary for GLB, model/vnd.usdz+zip for USDZ), size |
width | Width (cm) | measure |
height | Height (cm) | measure |
depth | Depth (cm) | measure |
license | License address | web address Such as https://creativecommons.org/publicdomain/zero/1.0/ |
free | Free to use | yes/no |
creator | Made by | organization |
keywords | Keywords | text Comma separated |
dateModified | Last changed | date |
review() · Review · Review
Google: Review snippets.
| Field | What it is | Takes |
|---|---|---|
itemName required | What is reviewed | text |
itemType | Kind of thing | one of Product, LocalBusiness, SoftwareApplication, Book, Course, Event, Recipe, Organization |
author required | Reviewer | person |
ratingValue required | Rating | number |
bestRating | Best possible rating | number Default 5 |
reviewBody | Review text | text |
datePublished | Written | date |
aggregateRating() · Rating summary · AggregateRating
Google: Rating stars in review snippets.
| Field | What it is | Takes |
|---|---|---|
itemName required | What is rated | text |
itemType | Kind of thing | one of Product, LocalBusiness, SoftwareApplication, Book, Course, Recipe, Organization |
ratingValue required | Average rating | number |
ratingCount required | Number of ratings | number |
bestRating | Best possible rating | number |
softwareApplication() · Software application · SoftwareApplication
Kinds (type): SoftwareApplication, WebApplication, MobileApplication, VideoGame.
Google: Software app results with rating and price.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
applicationCategory required | Category | one of BusinessApplication, DeveloperApplication, DesignApplication, EducationalApplication, FinanceApplication, GameApplication, HealthApplication, LifestyleApplication, MultimediaApplication, SecurityApplication, SocialNetworkingApplication, TravelApplication, UtilitiesApplication |
operatingSystem | Operating system | text Such as Windows, macOS, Android, Web |
url | Address (URL) | web address |
image | Image address | image address |
description | Description | text |
downloadUrl | Download address | web address |
featureList | Features | list One per line |
publisher | Publisher | organization |
offer required | Offer | offer price, currency, inStock or availability, url, validFrom, category, returns { days, country, free }, shipping { cost, country, handlingDays, transitDays } |
rating | Rating | rating value, count, best (only ratings shown on the page) |
book() · Book · Book
Google: Google retired Book actions in June 2025; a book's reviews can still show as a review snippet.
| Field | What it is | Takes |
|---|---|---|
name required | Title | text |
author required | Author | person |
isbn | ISBN | text |
url | Address (URL) | web address |
image | Image address | image address |
datePublished | Published | date |
publisher | Publisher | organization |
description | Description | text |
event() · Event · Event
Google: Event listings with date and place.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
startDate required | Starts | date and time With a time zone, such as 2026-11-05T19:00:00+01:00 |
endDate | Ends | date and time |
url | Address (URL) | web address |
attendance | Where | one of offline, online, mixed |
locationName | Venue | text |
address | Venue address | address |
onlineUrl | Online address | web address With a venue and Where = mixed, both are listed |
status | Status | one of EventScheduled, EventPostponed, EventRescheduled, EventMovedOnline, EventCancelled Default EventScheduled |
image | Image address | image address |
description | Description | text |
organizer | Organizer | organization |
performer | Performer or speaker | name |
offer | Offer | offer price, currency, inStock or availability, url, validFrom, category, returns { days, country, free }, shipping { cost, country, handlingDays, transitDays } |
jobPosting() · Job posting · JobPosting
Google: Google job search.
| Field | What it is | Takes |
|---|---|---|
title required | Job title | text |
description required | Description | text |
datePosted required | Posted | date |
validThrough | Open until | date and time |
employmentType | Employment type | one of FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM, OTHER |
hiringOrganization required | Employer | organization |
address | Workplace address | address |
remote | Remote job | yes/no |
salary | Salary | salary min, max or value, currency, unit (HOUR, MONTH, YEAR) |
course() · Course · Course
Google: Google retired the course info result in June 2025; the course list carousel still shows name, description and provider.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description required | Description | text |
provider required | Provider | organization |
url | Address (URL) | web address |
image | Image address | image address |
courseCode | Course code | text |
inLanguage | Language | text Such as en |
courseMode | How it is taught | one of online, onsite, blended |
courseWorkload | Workload | duration Such as PT10H |
startDate | Starts | date |
endDate | Ends | date |
offer | Offer | offer price, currency, inStock or availability, url, validFrom, category, returns { days, country, free }, shipping { cost, country, handlingDays, transitDays } |
learningResource() · Learning resource · LearningResource
Google: Google retired the learning video result in June 2025.
| Field | What it is | Takes |
|---|---|---|
name required | Name | text |
description | Description | text |
url | Address (URL) | web address |
image | Image address | image address |
learningResourceType | Kind | text Such as Lesson, Tutorial, Quiz, Worksheet |
educationalLevel | Level | text Such as Beginner |
teaches | What it teaches | text |
timeRequired | Time it takes | duration Such as PT15M |
inLanguage | Language | text Such as en |
free | Free to use | yes/no |
publisher | Publisher | organization |
How values are written
Plain values are enough; the SDK writes the schema.org shapes.
- Person or organization (author, publisher, provider…): a name (
'Jane Doe'), the parts (['name' => …, 'url' => …, 'sameAs' => …]), or a reference to a node elsewhere on the page (['@id' => 'https://acme.com/#organization']). - Offer:
price(0 for free),currency(EUR, USD…), and optionallyinStock(true/false) oravailability(InStock, OutOfStock, SoldOut, PreOrder…),url,validFrom,validUntil,category. - Address:
street,city,postalCode,region,country(or one line of text). - Rating:
value,count,best(5 when left out). Review:author,rating,body,date. - FAQ: a list of
['q' => …, 'a' => …]. Breadcrumbs: a list of['name' => …, 'url' => …]from the home page (only the last may leave out the address). Steps and ingredients: a list, or text with one per line. - Opening hours: a list of
['days' => ['Mon', 'Tue'], 'opens' => '09:00', 'closes' => '17:30']. Salary:min,maxorvalue,currency,unit(HOUR, MONTH, YEAR). - Dates: ISO 8601,
2026-10-14or2026-11-05T19:00:00+01:00(give events a time zone). Durations:PT30M,PT1H15M. Web addresses (url, image, logo…): full, starting withhttps://.
One graph per page
Schema::graph() / graph() links what you give it: the Organization gets https://your-site/#organization, the WebSite #website, the WebPage its-address#webpage; the WebSite names the Organization as its publisher, the WebPage is part of the WebSite, and articles name the Organization as publisher. Nodes that already have an @id keep it, so you can use your own ids (and must, when your site lives in a folder such as https://example.com/shop/).
Print the same Organization and WebSite (with the same ids) on every page, and one WebPage per page. Two FAQ blocks or two breadcrumbs on a page confuse Google; the checks below warn you.
Check and fix
$result = $page->check(); // ['valid' => true, 'status' => 'warning', 'errors' => [], 'warnings' => [...], 'types' => [...]] $fix = $page->fixed(); // ['nodes' => [...], 'changes' => [...]]: the graph with fixable errors repaired $report = Schema::checkHtml($html); // a whole page: every JSON-LD block, its language, price and stock
const { checkHtml } = require('@atlasschema/sdk'); page.check(); // { valid, status, errors, warnings, types } page.fixed(); // { nodes, changes }: the graph with fixable errors repaired checkHtml(html); // a whole page: every JSON-LD block, its language, price and stock
Errors stop a rich result (a missing required property, a FAQ question without an answer, a price that differs from the page); warnings are recommended properties; info explains Google’s rules. A good place for check() is a test: build the structured data of a few typical pages and fail the build when valid is false.
Mistakes and strict mode
A missing required field, a field the type does not have or a value in the wrong format is a problem. The node is still built, so a live page never breaks over structured data.
- PHP: a PHP warning by default (frameworks such as Laravel turn it into an exception while you develop).
Schema::configure(['strict' => true])throwsAtlasSchema\SchemaException(getProblems()lists them by field);['onProblem' => fn ($e) => …]reports them your way;Schema::build($type, $fields)returns the node and its problems without reporting anything. - JavaScript: throws a
SchemaErrorin development and is a console warning whenNODE_ENV=production. Change it withconfigure({ strict: true })orconfigure({ strict: false }).
Connect AtlasSchema (optional)
Building and checking never need an account. To have AtlasSchema check the whole website, keep history and email you when something breaks:
- Create a free account at the dashboard and add your website.
- Open the website’s Connect tab and create a key (it starts with
as_live_). Keep it in an environment variable, never in the page. - Send pages after you publish or change them. Each page sent counts as one page of your plan.
use AtlasSchema\Client; $client = new Client(getenv('ATLASSCHEMA_API_KEY')); $client->sendPages([ ['url' => 'https://acme.com/mug/', 'title' => 'Blue mug', 'graph' => $page], ]); // up to 50 pages per call $client->site(); // score, page counts, this month's usage $client->check('https://acme.com/'); // fetch and check one live page now
const { AtlasSchema } = require('@atlasschema/sdk'); const client = new AtlasSchema({ apiKey: process.env.ATLASSCHEMA_API_KEY }); await client.sendPages([{ url: 'https://acme.com/mug/', title: 'Blue mug', graph: page }]); await client.site(); // score, page counts, this month's usage await client.check('https://acme.com/'); // fetch and check one live page now
A refused request throws an error with the API’s error code (quota_exceeded, invalid_api_key…; PHP: AtlasSchema\ApiException with getErrorCode()). In PHP you can pass your framework’s HTTP client as ['transport' => fn ($method, $url, $headers, $body) => ['status' => …, 'body' => …]]. Every endpoint is described in the API reference.
Every website check also gives an AI readiness score: whether AI crawlers can reach the site, whether pages show their text without JavaScript, whether the sitemap lists the right pages, the structured data score, and whether 3D model files are served correctly. Each item says what to fix and on which pages.
Crawler files: robots.txt, llms.txt and sitemaps
The SDK also writes the files crawlers read first. They are plain text from plain options, so you can serve them from any route or write them at build time. PHP and JavaScript write exactly the same text.
use AtlasSchema\Seo; // robots.txt: one switch for AI answer engines, one for AI training crawlers echo Seo::robotsTxt([ 'disallow' => ['/cart/', '/account/'], 'answerEngines' => true, // ChatGPT search, Claude, Perplexity can read and cite the site 'training' => false, // GPTBot, ClaudeBot, Google-Extended… may not 'sitemaps' => ['https://acme.com/sitemap.xml'], ]); // llms.txt: a short map of the site for AI tools echo Seo::llmsTxt([ 'title' => 'Acme', 'intro' => 'Handmade mugs, shipped worldwide.', 'sections' => [[ 'title' => 'Shop', 'links' => [['title' => 'Blue mug', 'url' => 'https://acme.com/mug/']], ]], ]); // An XML sitemap echo Seo::sitemap([['loc' => 'https://acme.com/', 'lastmod' => '2026-10-22']]);
const { seo } = require('@atlasschema/sdk'); // robots.txt: one switch for AI answer engines, one for AI training crawlers seo.robotsTxt({ disallow: ['/cart/', '/account/'], answerEngines: true, // ChatGPT search, Claude, Perplexity can read and cite the site training: false, // GPTBot, ClaudeBot, Google-Extended… may not sitemaps: ['https://acme.com/sitemap.xml'], }); // llms.txt: a short map of the site for AI tools seo.llmsTxt({ title: 'Acme', intro: 'Handmade mugs, shipped worldwide.', sections: [{ title: 'Shop', links: [{ title: 'Blue mug', url: 'https://acme.com/mug/' }], }], }); // An XML sitemap seo.sitemap([{ loc: 'https://acme.com/', lastmod: '2026-10-22' }]);
seo.indexNow({ key, keyLocation, urls }) (PHP Seo::indexNow) writes the IndexNow request that tells Bing, Yandex and other IndexNow engines which pages changed; send it with any HTTP client and serve the key at keyLocation. facts.diff(before, after) tells which facts an edit changed.
Blocking AI answer engines means AI assistants cannot cite your pages. Blocking training crawlers does not affect that. llms.txt is optional: there is no proof yet that it changes AI answers, so put the effort into the sitemap first. List every public page in it, including pages your app makes itself, such as product or model pages.
What AI sees, and whether AI crawlers can read the site
Most AI crawlers do not run JavaScript, and a firewall or CDN can refuse them even when robots.txt allows them. The SDK shows both. Reading a page needs no network; the access check fetches the site you give it, through your own HTTP client in PHP.
use AtlasSchema\AiView; use AtlasSchema\AiAccess; // The page as a crawler without JavaScript reads it, and what of your content it misses $view = AiView::view($html); // text, words, headings, links, images, jsRisk… $gap = AiView::compare($view, $postContent); // coverage, missing sentences, missingPrices // Can the AI crawlers read the site? Pass any HTTP client as the transport $result = AiAccess::check('https://acme.com/', [ 'transport' => fn ($url, $userAgent, $accept) => myGet($url, $userAgent, $accept), // return ['status' => 200, 'url' => $finalUrl, 'headers' => [...], 'body' => $html] or ['error' => $message] ]); $result['summary']['answer']; // ['total' => 6, 'open' => 6, 'blockedByServer' => 0] AiAccess::fixSteps($result); // what to do, step by step, for the AI search crawlers that are blocked
const { aiView, compareViews, aiAccess } = require('@atlasschema/sdk'); // The page as a crawler without JavaScript reads it, and what of your content it misses const view = aiView(html); // text, words, headings, links, images, jsRisk… const gap = compareViews(view, postContent); // coverage, missing sentences, missingPrices // Can the AI crawlers read the site? Node's fetch is used unless you pass a transport const result = await aiAccess.check('https://acme.com/'); result.summary.answer; // { total: 6, open: 6, blockedByServer: 0 } aiAccess.fixSteps(result); // what to do, step by step, for the AI search crawlers that are blocked
Each crawler gets a verdict: open, blocked_robots (robots.txt), blocked_server (refused or a bot challenge page), different (a different or much shorter page) or unknown. The fixes name the CDN when it can be told from the answers (Cloudflare, Vercel, Sucuri, Akamai, Fastly, Amazon CloudFront) and include a message you can send to your host. The check asks from your server, so a firewall that only blocks outside traffic can still refuse the real crawlers; the AtlasSchema service checks from outside.
Do AI assistants get your facts right?
People ask ChatGPT and Perplexity for prices, opening hours and addresses. The SDK takes the facts from your own structured data, asks the AI engines neutral questions (they never contain the answer), and judges every answer as right, wrong, outdated (an earlier value of yours) or not_mentioned. The judging is plain rules, not AI, so the same answers always give the same verdicts. You use your own API keys; failed requests are reported, never judged.
use AtlasSchema\Facts; $facts = Facts::fromGraph($nodes); // prices, stock, opening hours, address, phone, event dates $questions = Facts::questions($facts); // "How much does Blue mug from Acme cost?" $answers = Facts::ask($questions, [ 'engines' => ['openai', 'perplexity'], 'keys' => ['openai' => getenv('OPENAI_API_KEY'), 'perplexity' => getenv('PERPLEXITY_API_KEY')], 'runs' => 2, // answers vary: ask more than once 'transport' => fn (array $r) => myRequest($r), // $r: method, url, headers, body; return ['status', 'body'] ]); $result = Facts::compare($facts, $answers); $result['summary']; // right, wrong, outdated, notMentioned, answers, errors
const { facts } = require('@atlasschema/sdk'); const list = facts.fromGraph(nodes); // prices, stock, opening hours, address, phone, event dates const questions = facts.questions(list); // "How much does Blue mug from Acme cost?" const answers = await facts.ask(questions, { engines: ['openai', 'perplexity'], keys: { openai: process.env.OPENAI_API_KEY, perplexity: process.env.PERPLEXITY_API_KEY }, runs: 2, // answers vary: ask more than once }); // Node's fetch, unless you pass a transport const result = facts.compare(list, answers); result.summary; // right, wrong, outdated, notMentioned, answers, errors
Engines: openai (the Responses API with web search) and perplexity (Sonar). Their API answers are close to, but not the same as, what people see in the apps. Pass previous (your earlier values, by fact key) to compare to tell an outdated answer from a wrong one. The AtlasSchema service runs this every week for you, on its own keys, within your plan.
Is your business the same everywhere?
AI assistants put together what every place says about a business. entity.check(nodes) (PHP Entity::check($nodes, ['transport' => …])) compares the organization in your structured data with each profile it lists in sameAs: a Wikidata item through Wikidata's public data, other pages through their public meta tags and JSON-LD. Social networks and Wikipedia are only checked to answer, because their terms do not allow reading them. Pass pages ([{ url, nodes }]) to also check that every page states the same name, phone and postal code. Names are compared without case, accents, spaces or a legal form ("Blue Mug Café, Inc" and "BlueMug Cafe Ltd" match).
Moving an existing website
Most sites already print some structured data. Follow these steps so search engines see one clean graph and nothing you rely on gets lost:
- Take stock. Save each main page’s title, description, canonical, robots tag and JSON-LD, plus robots.txt, the sitemaps and llms.txt. This is your “before” copy.
- Choose one source of structured data. Find everything that prints JSON-LD: the theme, an SEO plugin, review or shop apps. Either the SDK prints the whole graph, or it adds to the existing graph through the same
@ids. Two separate graphs for the same organization or page confuse search engines. - Give every kind of page a type. Home, docs, product, catalogue and article pages each get a page type (
WebPage,CollectionPage,ItemPage…), breadcrumbs, and the thing the page is about as its main entity (the product, the software, the 3D model). - Keep your
@ids. Search engines already know the ids your pages print. Keep them, and pass your site’s address tograph()so the Organization and WebSite stayhttps://your-site/#organizationand#website. - Write the crawler files with the SDK (see above).
- Compare before and after. Every difference in the JSON-LD and the head should be one you meant to make.
- Connect and check. Add the website in AtlasSchema, create a key on its Connect tab, send your pages and run the first check.
- Turn on monitoring. Scheduled checks and alerts email you when something breaks. Read the AI readiness tab too: a firewall or CDN can block AI crawlers that robots.txt allows.
Framework recipes
Symfony (Twig)
Build the graph in the controller and pass $page->toScript() to the template, then print it with {{ schema|raw }} in the <head> block. The script is already escaped safely.
Joomla
In a template override or a system plugin: Factory::getApplication()->getDocument()->addCustomTag($page->toScript());
Drupal, CodeIgniter, plain PHP
Anywhere you render HTML: <?php echo $page->toScript(); ?> in the page’s head.
Node and Express
Add page.toScript() to the HTML you send, or pass it to your template engine as unescaped HTML (<%- schema %> in EJS, {{{schema}}} in Handlebars).
Next.js, Nuxt, Astro and static sites
Next.js: <JsonLd> in the page component. Nuxt: useHead({ script: [{ type: 'application/ld+json', innerHTML: JSON.stringify(page.toJSON()) }] }). Astro: <script type="application/ld+json" set:html={JSON.stringify(page.toJSON())} />. Build the graph from the same data as the page, so the two always agree.
Questions
Does the SDK send anything to AtlasSchema?
No. Building and checking run in your code with no network access. Only the optional client calls the API, with the key and pages you give it.
Do the PHP and JavaScript SDKs give the same result?
Yes. They read the same type definitions and pass the same test cases, so the same fields give the same JSON-LD, the same checks and the same fixes.
Can I use it with Yoast SEO or Rank Math?
On WordPress, the Atlas Schema plugin joins their graph instead of printing a second one. In your own code, give your nodes the same @ids as the SEO plugin’s Organization and WebSite, or print only what it does not.
Which versions are supported?
PHP 7.4 to the newest PHP 8, and Node 18 or newer. Both follow semantic versioning: a minor version adds types and fields, never removes them.
Where do I report a problem?
Write to contact@atlasaidev.com or use the dashboard’s Help page.