AI schema generators often output authors and publishers as bare strings rather than linked Person/Organization entities. "author": "Jane Baker" is just a name — AI engines can't connect Jane to other articles she wrote, her LinkedIn, or her credentials. "author": {"@type": "Person", "@id": "..."} is an entity that author trust signals compound across. This guide covers the closed-graph pattern.
{
"@type": "Article",
"headline": "How to choose a CRM",
"author": "Jane Baker", ❌ string
"publisher": "Acme Corp" ❌ string
}
{
"@type": "Article",
"headline": "How to choose a CRM",
"author": { ⚠️ inline (works, doesn't link)
"@type": "Person",
"name": "Jane Baker"
},
"publisher": {
"@type": "Organization",
"name": "Acme Corp"
}
}
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://example.com/authors/jane-baker#person",
"name": "Jane Baker",
"jobTitle": "Sales Operations Lead",
"url": "https://example.com/authors/jane-baker",
"sameAs": [
"https://linkedin.com/in/janebaker",
"https://twitter.com/janebaker"
]
},
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Acme Corp",
"url": "https://example.com",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
{
"@type": "Article",
"headline": "How to choose a CRM",
"author": { "@id": "https://example.com/authors/jane-baker#person" },
"publisher": { "@id": "https://example.com/#organization" }
}
]
}
author → Person or Organizationpublisher → Organizationbrand → Brand or Organizationmanufacturer → Organizationcontributor → Person or Organizationcreator → Person or Organizationeditor → PersoncopyrightHolder → Person or Organizationlocation → PlaceitemReviewed → Thing (any specific type)If your generator can't be configured to use @id references, transform after generation:
function entityifyAuthors(schema, knownPeople, knownOrgs) {
function walk(obj) {
if (!obj || typeof obj !== 'object') return obj;
// Fields that should be entities
for (const field of ['author', 'publisher', 'brand', 'creator', 'editor']) {
if (typeof obj[field] === 'string') {
const name = obj[field];
// Look up known entity by name
const person = knownPeople[name];
const org = knownOrgs[name];
if (person) {
obj[field] = { '@id': person.id };
} else if (org) {
obj[field] = { '@id': org.id };
} else {
// No match — at least make it an inline entity
obj[field] = {
'@type': field === 'publisher' ? 'Organization' : 'Person',
'name': name
};
}
}
}
for (const v of Object.values(obj)) {
if (typeof v === 'object') walk(v);
}
return obj;
}
return walk(schema);
}
// Usage
const knownPeople = {
'Jane Baker': { id: 'https://example.com/authors/jane-baker#person' },
// ... load from CMS
};
const knownOrgs = {
'Acme Corp': { id: 'https://example.com/#organization' }
};
const fixed = entityifyAuthors(generatedSchema, knownPeople, knownOrgs);
Define each entity once, in one place, used everywhere:
// /lib/schema-entities.js
export const ORG = {
'@type': 'Organization',
'@id': 'https://example.com/#organization',
'name': 'Acme Corp',
'url': 'https://example.com',
'logo': {
'@type': 'ImageObject',
'url': 'https://example.com/logo.png',
'width': 600,
'height': 60
},
'sameAs': [
'https://linkedin.com/company/acme',
'https://twitter.com/acmecorp'
]
};
export const WEBSITE = {
'@type': 'WebSite',
'@id': 'https://example.com/#website',
'url': 'https://example.com',
'name': 'Acme',
'publisher': { '@id': 'https://example.com/#organization' }
};
// Per-author records, generated from CMS
export function personEntity(author) {
return {
'@type': 'Person',
'@id': `https://example.com/authors/${author.slug}#person`,
'name': author.name,
'url': `https://example.com/authors/${author.slug}`,
'jobTitle': author.jobTitle,
'image': author.imageUrl,
'sameAs': author.profiles // [linkedin, twitter, github]
};
}
// In your article render path
import { ORG, WEBSITE, personEntity } from '@/lib/schema-entities';
function articleGraph(article) {
const author = personEntity(article.author);
return {
'@context': 'https://schema.org',
'@graph': [
ORG,
WEBSITE,
author,
{
'@type': 'Article',
'@id': `${article.url}#article`,
'headline': article.title,
'datePublished': article.publishedAt,
'dateModified': article.modifiedAt,
'author': { '@id': author['@id'] },
'publisher': { '@id': ORG['@id'] },
'isPartOf': { '@id': WEBSITE['@id'] },
'mainEntityOfPage': article.url
}
]
};
}
// Output as one JSON-LD block per page
<script type="application/ld+json">
{JSON.stringify(articleGraph(article))}
</script>
Reference well-known entities by their canonical URL — Wikidata, official sites:
{
"@type": "Article",
"about": {
"@type": "Thing",
"@id": "https://en.wikipedia.org/wiki/Customer_relationship_management"
},
"mentions": [
{ "@id": "https://www.salesforce.com/#organization" },
{ "@id": "https://www.hubspot.com/#organization" }
]
}
// AI engines and Google often verify external @id targets
// Where they resolve to actual schema or recognised entities, signal strengthens
function validateGraphReferences(graph) {
const defined = new Set();
const referenced = new Set();
function walk(obj) {
if (!obj || typeof obj !== 'object') return;
if (obj['@id'] && Object.keys(obj).length > 1) {
// Has @id and other properties = definition
defined.add(obj['@id']);
} else if (obj['@id']) {
// Only @id = reference
referenced.add(obj['@id']);
}
for (const v of Object.values(obj)) {
if (typeof v === 'object') walk(v);
}
}
walk(graph);
const unresolved = [...referenced].filter(id => !defined.has(id));
return { defined, referenced, unresolved };
}