Technical SEO
Author Schema: How to Mark Up Authors With Person, and What Google Actually Uses
Author schema is the author property on an Article, BlogPosting or NewsArticle, filled with a Person (or, for a company byline, an Organization) that names who wrote the page and links to a page that identifies them. There is no separate “Author” type in schema.org: author markup is a Person node attached to your content, and ideally the same Person node on every post that person writes.
This is the markup how-to. It covers what Google’s documentation actually says it uses, how to write the author property correctly (one author, several authors, organisation authors), how to build one Person node and reference it by @id across a whole site, how to mark up the author page with ProfilePage, and how to ship it in Astro, Next.js and WordPress. Every JSON-LD block on this page parses, and the WordPress snippet was run under PHP 8.4 against stubbed WordPress functions to check its output.
Key takeaways
- Google’s Article documentation supports three author properties:
author,author.nameandauthor.url. Everything else on a Person is valid schema.org, but it is not on Google’s supported list for Article. author.nameholds the name and nothing else. Google lists four things to keep out of it: the publisher name, job title, honorifics and introductory words such as “posted by”.- Each author gets their own entry in an
authorarray. Merging two names into one string is the example Google shows of what not to do. - Define each author once as a Person with a stable
@id, then reference that@idfrom every article. One record, no drift. - The author page should carry
ProfilePagemarkup, whose only required property ismainEntity, the Person the page is about.
What Google actually uses from author markup
Author schema guides often list every Person property schema.org has, as if Google read all of them. Google’s own documentation is narrower, and it is worth knowing exactly where the line sits before you decide how much to build.
On Google’s Article structured data page, there are no required properties at all. The supported, recommended set is seven properties, and three of them are about the author:
| Property | Type Google documents | What Google says it is for |
|---|---|---|
author | Person or Organization | The author of the article |
author.name | Text | The name of the author |
author.url | URL | A page that uniquely identifies the author, such as a social media page, an “about me” page or a bio page |
Google’s author best practices then add two more fields it “strongly” recommends: the @type, and url or sameAs, both of which must be valid URLs. The same section names jobTitle, honorificPrefix and honorificSuffix as the places those facts belong if you want to state them.
The ProfilePage documentation covers the Person on the author page, and there the list is longer: name is required, and alternateName, description, identifier, image, sameAs, interactionStatistic and agentInteractionStatistic are recommended.
So a practical reading of Google’s docs is:
- On the article:
@type,name,url, and optionallysameAs,jobTitleand honorifics. - On the author page: a ProfilePage whose
mainEntityis the Person, with the recommended profile properties. - Everything else (
knowsAbout,worksFor,alumniOf,affiliation): valid schema.org Person properties that other consumers may read, but not properties Google lists for either feature.
That last group is not wasted, just not documented as used by Google. Why answer engines care about a well-described author entity is a separate argument, and it is made in E-E-A-T for AI search: author entity signals. This page stays with the markup.
The author property on Article, done correctly
The full Article implementation (dates, images, headline, the canonical guideline) is in the Article schema guide. What follows is the author part only, in more depth.
A single person
The minimum that follows every Google best practice is a typed Person with a name and a URL:
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How we moved 40 locales to one hreflang sitemap",
"datePublished": "2026-09-01T09:00:00+01:00",
"author": {
"@type": "Person",
"name": "Willow Lane",
"url": "https://example.com/authors/willow-lane/"
}
}
The url should be the page on your site that describes this person, if you have one. Google gives a social profile, an “about me” page or a bio page as examples, and adds that if the URL is an internal profile page, you should mark that page up with ProfilePage. An internal author page is the better target because you control it and can mark it up.
Several authors
If the byline shows two people, the markup names two people. Google’s first author best practice is to include every author presented on the page, and its second is to give each one their own entry:
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How we moved 40 locales to one hreflang sitemap",
"datePublished": "2026-09-01T09:00:00+01:00",
"dateModified": "2026-09-14T16:20:00+01:00",
"author": [
{
"@type": "Person",
"name": "Willow Lane",
"jobTitle": "Head of SEO",
"url": "https://example.com/authors/willow-lane/"
},
{
"@type": "Person",
"name": "Regula Felix",
"url": "https://example.com/authors/regula-felix/"
}
],
"publisher": {
"@type": "Organization",
"name": "Example Engineering",
"url": "https://example.com/"
}
}
The version Google shows as wrong is a single object with "name": "Willow Lane, Regula Felix", which is what a CMS that stores the byline as one text field produces. The reverse also applies: editors and reviewers the page does not credit as authors do not belong in author.
When the author is an organisation
Google accepts Organization as an author and shows the pattern: type it as an Organization and link to the organisation’s home page.
{
"author": [
{
"@type": "Organization",
"name": "Example Engineering",
"url": "https://example.com/"
}
]
}
Use it when the page genuinely has no individual byline: a press release, a changelog, a policy page, a team-written report. Do not use it to avoid naming a person who is named on the page. Google’s last best practice is to use Person for people and Organization for organisations, and to avoid Thing or the wrong type, with “using the Organization type for a person” given as the example.
If an organisation author is also your publisher, reference the same Organization node by @id rather than writing it out twice. How to build that node is covered in the Organization schema guide.
The name-only mistakes Google lists
Google’s fourth best practice is that author.name contains only the author’s name. It then lists four things that people put there anyway, and where each one should go instead:
Mistake in author.name | Example | Where it belongs |
|---|---|---|
| Publisher name | "Willow Lane, Example Engineering" | publisher |
| Job title | "Willow Lane, Head of SEO" | jobTitle |
| Honorific | "Dr Willow Lane" | honorificPrefix / honorificSuffix |
| Introductory words | "Posted by Willow Lane" | Nowhere. Delete them. |
These usually come from templates that build the name from the visible byline string, so the fix belongs in the template.
Two related mistakes are not on Google’s list but come from the same root. A CMS default account name such as “admin” or “editor” is not a person, and Yoast’s own schema specification says a Person named “admin” or similar should be invalidated rather than output. And a byline in the markup that does not match the visible byline (a ghostwriter in one, a founder in the other) is markup that describes something the page does not show.
One Person node, reused by @id across every post
Writing the full Person object inside every article creates as many copies as you have posts, and copies drift. The cleaner pattern is the one used for organisations: define the Person once with a stable @id, and have every article refer to it. On a site that emits a @graph, that looks like this:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://example.com/#/schema/person/willow-lane",
"name": "Willow Lane",
"jobTitle": "Head of SEO",
"url": "https://example.com/authors/willow-lane/",
"image": "https://example.com/authors/willow-lane.jpg",
"description": "Technical SEO lead for Example's documentation and blog.",
"worksFor": { "@id": "https://example.com/#organization" },
"knowsAbout": ["Technical SEO", "Internationalisation", "JSON-LD"],
"sameAs": [
"https://www.linkedin.com/in/willow-lane-example",
"https://github.com/willowlane-example"
]
},
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Example Engineering",
"url": "https://example.com/"
},
{
"@type": "BlogPosting",
"@id": "https://example.com/blog/hreflang-sitemap/#article",
"headline": "How we moved 40 locales to one hreflang sitemap",
"datePublished": "2026-09-01T09:00:00+01:00",
"dateModified": "2026-09-14T16:20:00+01:00",
"author": { "@id": "https://example.com/#/schema/person/willow-lane" },
"publisher": { "@id": "https://example.com/#organization" },
"mainEntityOfPage": "https://example.com/blog/hreflang-sitemap/"
}
]
}
Three details make this work.
The Person node ships on the article page too. An @id reference only resolves to a name if a node with that @id and a name is present in the data the parser reads. Referencing an @id that is defined only on the author page leaves the article with an author that has no name in its own markup. Emit the Person (or at least a stub with @id, @type, name and url) in the graph of every page that references it. The @id is what tells a parser that the Person on post 1 and the Person on post 200 are the same entity.
The @id is stable and says nothing private. The value is an identifier, not a page, so any URL-shaped string on your domain works, as long as it never changes. Yoast’s specification uses the site’s home URL followed by #/schema/Person/ and a unique ID, and warns that the ID should not reveal personally identifiable or sensitive information such as a username or email address. A slug or an internal numeric ID is fine.
url and @id do different jobs. url points to a page a person can visit (the author page). @id is the node’s name in the graph. They can share a base, but do not set @id to the author page URL and then give the ProfilePage the same @id: that makes the page and the person the same node.
What to put on the Person
Split the properties by who documents using them, and add the second group only where it is true and maintained:
| Group | Properties | Why |
|---|---|---|
| Google-documented for authors | name, url, sameAs, jobTitle, honorificPrefix | Named in Google’s Article author best practices |
| Google-documented for profile pages | alternateName, description, identifier, image | Recommended on the ProfilePage Person |
| schema.org only | worksFor, knowsAbout, alumniOf, affiliation | Valid Person properties; not listed by Google for either feature |
sameAs is the one to take care over. schema.org defines it as the URL of a page that unambiguously indicates the item’s identity, such as a Wikipedia page, a Wikidata entry or an official website. Point it at profiles that are clearly this person and that you expect to stay live: LinkedIn, GitHub, a Google Scholar or ORCID page, a staff page at an employer. A dead or reassigned profile in sameAs is worse than a short list.
For the image, Google’s ProfilePage guidance says that if there is no photo you should not include a default image, icon or placeholder. Omit the property rather than pointing it at a silhouette.
ProfilePage for the author page
The author page is where the Person is described in full, and Google has a dedicated type for it. ProfilePage markup is for pages where creators share first-hand perspectives, and Google notes that Article and other types with authors can link to pages carrying it.
What qualifies
Google’s content guideline is that the page’s primary focus must be a single person or organisation affiliated with the website. Its valid examples include an author page on a news site, an “About Me” page on a blog and an employee page on a company website. Its invalid examples are a store’s home page and a review site’s page about an organisation the site is not associated with.
In practice, a per-author page with a real bio qualifies; a team page describing twelve people equally does not.
Required and recommended properties
| Level | Property | Notes from Google’s docs |
|---|---|---|
| Required | mainEntity | The Person or Organization the page is about. If you do not know which, default to Person |
| Required (on the Person) | name | Real name here; handles go in alternateName |
| Recommended | dateCreated, dateModified | dateModified should ideally reflect only human-edited changes to the profile |
| Recommended (on the Person) | alternateName, description, identifier, image, sameAs | description is the byline or credential |
| Recommended (on the Person) | interactionStatistic, agentInteractionStatistic | Only statistics about your own platform |
The dateModified note matters for author pages because many of them list the author’s latest posts. Google says that adding outlinks to places the profile is referenced would not count as a modification, so do not wire dateModified to “last post published”. Wire it to the last edit of the bio.
The interaction statistics are for forums and social platforms. Google says to include only statistics about the platform hosting the profile, so on a publisher’s author page they usually stay out.
A complete author page
This uses the same Person @id as the article example, so the author page and every post resolve to one entity. The hasPart block follows Google’s technical guideline for profiles that list recent activity: reference each item by URL and point its author back to the page’s main entity.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "ProfilePage",
"@id": "https://example.com/authors/willow-lane/#profilepage",
"url": "https://example.com/authors/willow-lane/",
"name": "Willow Lane, Head of SEO at Example Engineering",
"dateCreated": "2025-03-10T10:00:00+00:00",
"dateModified": "2026-08-02T11:30:00+01:00",
"mainEntity": { "@id": "https://example.com/#/schema/person/willow-lane" },
"hasPart": [
{
"@type": "BlogPosting",
"headline": "How we moved 40 locales to one hreflang sitemap",
"url": "https://example.com/blog/hreflang-sitemap/",
"datePublished": "2026-09-01T09:00:00+01:00",
"author": { "@id": "https://example.com/#/schema/person/willow-lane" }
}
]
},
{
"@type": "Person",
"@id": "https://example.com/#/schema/person/willow-lane",
"name": "Willow Lane",
"alternateName": "willowlane",
"identifier": "author-0142",
"jobTitle": "Head of SEO",
"description": "Head of SEO, Example Engineering",
"url": "https://example.com/authors/willow-lane/",
"image": "https://example.com/authors/willow-lane.jpg",
"sameAs": [
"https://www.linkedin.com/in/willow-lane-example",
"https://github.com/willowlane-example"
]
}
]
}
identifier is optional. Google describes it as a unique ID used within your site, such as an internal database ID that survives a handle change. If your CMS has an author ID, it is a reasonable value.
This site uses the same structure. The about page carries a ProfilePage whose mainEntity references the sitewide Person @id, and every article’s author references that same @id.
Implementing it in Astro, Next.js and WordPress
The markup is the same everywhere. What changes is where the author data lives and how the JSON reaches the HTML. In every framework, render the JSON-LD on the server or at build time, so it is in the HTML that crawlers fetch rather than injected later by client-side JavaScript.
Astro
Keep authors in one module (or a content collection) keyed by slug, build the graph in the page, and serialise it once in the layout.
// src/lib/authors.ts
const SITE = 'https://example.com';
export const AUTHORS = {
'willow-lane': {
name: 'Willow Lane',
jobTitle: 'Head of SEO',
sameAs: ['https://www.linkedin.com/in/willow-lane-example'],
},
} as const;
export type AuthorSlug = keyof typeof AUTHORS;
export const personId = (slug: AuthorSlug) => `${SITE}/#/schema/person/${slug}`;
export function personNode(slug: AuthorSlug) {
const a = AUTHORS[slug];
return {
'@type': 'Person',
'@id': personId(slug),
name: a.name,
jobTitle: a.jobTitle,
url: `${SITE}/authors/${slug}/`,
sameAs: a.sameAs,
};
}
---
// in the article layout
import { personNode, personId } from '../lib/authors';
const { post } = Astro.props;
const slugs = post.data.authors; // e.g. ['willow-lane']
const graph = {
'@context': 'https://schema.org',
'@graph': [
...slugs.map(personNode),
{
'@type': 'BlogPosting',
headline: post.data.title,
datePublished: post.data.date.toISOString(),
author: slugs.map((s) => ({ '@id': personId(s) })),
},
],
};
const json = JSON.stringify(graph).replace(/</g, '\\u003c');
---
<script type="application/ld+json" is:inline set:html={json} />
Astro’s directives reference covers both halves: is:inline tells Astro to leave the tag as-is in the output HTML instead of processing and bundling it, and set:html is the documented way to put JSON.stringify() output into a JSON-LD script tag. The same page warns that set:html values are not escaped by Astro, which is why the < replacement is there (it is the protection Next.js recommends too, covered below). The Astro SEO guide covers the rest of the head.
Next.js (App Router)
The Next.js JSON-LD guide (last updated 2 March 2026 when read in September 2026) recommends rendering structured data as a <script> tag in layout.js or page.js. It also warns that JSON.stringify does not sanitise strings used in XSS injection, and suggests replacing < with its unicode escape.
// app/blog/[slug]/page.tsx
import { getPost, getAuthor } from '@/lib/content';
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
const authors = await Promise.all(post.authorSlugs.map(getAuthor));
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
...authors.map((a) => ({
'@type': 'Person',
'@id': `https://example.com/#/schema/person/${a.slug}`,
name: a.name,
url: `https://example.com/authors/${a.slug}/`,
sameAs: a.sameAs,
})),
{
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: authors.map((a) => ({ '@id': `https://example.com/#/schema/person/${a.slug}` })),
},
],
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
/>
{/* ... */}
</article>
);
}
The escape matters more for author data than for most fields, because display names and bios are often user-editable. Run in Node 22, a name containing </script><script>alert(1)</script> serialises to </script><script>alert(1)</script>, which cannot close the script tag and still parses back to the original string. Next.js also notes that next/script is not the right component here, because JSON-LD is data rather than executable code. More on rendering and metadata is in the Next.js SEO guide.
WordPress
On WordPress, if an SEO plugin such as Yoast builds the schema graph, the post author’s Person is probably already there, and the job is to correct it rather than add a second one. Yoast documents its Person output as a top-level graph node added when an Article has an author, with a wpseo_schema_person filter to change it and a wpseo_schema_person_social_profiles filter to control which profiles feed sameAs. Its own example adds GitHub, personal site and WordPress.org profiles:
add_filter( 'wpseo_schema_person_social_profiles', 'yoast_add_social_profiles' );
function yoast_add_social_profiles( $profiles ) {
array_push( $profiles, 'github', 'personal', 'wordpress' );
return $profiles;
}
If the site has no SEO plugin producing a graph, a theme can output the pattern directly. This emits one Person and a BlogPosting that references it, using the author archive as url:
add_action( 'wp_head', function () {
if ( ! is_singular( 'post' ) ) {
return;
}
$author_id = (int) get_post_field( 'post_author', get_queried_object_id() );
$person = array(
'@type' => 'Person',
'@id' => home_url( '/#/schema/person/' . $author_id ),
'name' => get_the_author_meta( 'display_name', $author_id ),
'url' => get_author_posts_url( $author_id ),
);
$same_as = array_values( array_filter( array(
get_the_author_meta( 'linkedin_url', $author_id ),
get_the_author_meta( 'user_url', $author_id ),
) ) );
if ( $same_as ) {
$person['sameAs'] = $same_as;
}
$graph = array(
'@context' => 'https://schema.org',
'@graph' => array(
$person,
array(
'@type' => 'BlogPosting',
'headline' => get_the_title(),
'datePublished' => get_the_date( 'c' ),
'dateModified' => get_the_modified_date( 'c' ),
'author' => array( '@id' => $person['@id'] ),
),
),
);
echo '<script type="application/ld+json">' . wp_json_encode( $graph, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . '</script>' . "\n";
} );
JSON_HEX_TAG does the same job as the < replacement in the other two frameworks. linkedin_url is a custom user meta key here; use whatever field your profile form actually saves. Run against stubbed WordPress functions, the snippet printed a valid graph and encoded a </script> in the title as </script>. Do not run it alongside a plugin that already outputs Article markup, or you will ship two competing descriptions of the same page.
Add ProfilePage to the author archive only once it has a real bio. A default archive that only lists posts is not focused on the person.
Validating what you shipped
- Parse it. Paste the rendered JSON-LD into the Schema Markup Validator, or run
python3 -m json.toolon it. Syntax errors hide every node on the page. - Test the live URL in the Rich Results Test. Google’s Article page asks you to validate with the Rich Results Test and then check deployed pages with the URL Inspection tool. URL mode tests what Google fetches, which catches markup that exists only after client-side rendering.
- Check the graph, not just the page. On an article, confirm that
authorpoints to an@idthat exists in the same graph with aname. On the author page, confirmmainEntityuses the same@id. - Grep
author.nameacross a crawl. Look for commas, “by”, titles and your brand name. This is where the name-only mistakes show up. - Open every
sameAsURL. A profile that now returns 404 or belongs to someone else should come out.
Two expectations to set with whoever asked for this work. Google says it does not guarantee that features consuming structured data will show up. And if a page receives a structured data manual action, its structured data is ignored, although the page can still appear in Search. Accurate author markup makes a page eligible and unambiguous. It does not buy a visible feature. The general construction and validation workflow for other types is in how to create rich snippets with JSON-LD.
If you would rather have the author graph, templates and validation built into your stack than maintained by hand, that is part of a technical SEO and GEO engagement.
Frequently asked questions
Is there an “Author” schema type?
No. schema.org has an author property, and its value is a Person or an Organization. “Author schema” is the common name for that property plus the Person node it points to, placed on an Article, BlogPosting or NewsArticle.
What is the difference between Person schema and author schema?
Person schema describes a human being: name, job title, profile URL, sameAs profiles. Author schema is the relationship that connects an article to that Person through the author property. You build the Person once and reference it from the author field of every article that person wrote.
Which author properties does Google use?
Google’s Article documentation lists author, author.name and author.url as supported, and strongly recommends adding @type and url or sameAs. It names jobTitle, honorificPrefix and honorificSuffix as the right places for titles and honorifics. On author pages, the ProfilePage documentation adds alternateName, description, identifier, image and sameAs for the Person.
Should author.url point to LinkedIn or to my own author page?
Google accepts either: a social profile, an “about me” page or a bio page. Your own author page is usually the better choice because you control it and can mark it up with ProfilePage, which Google recommends when the URL is an internal profile. Put LinkedIn and other external profiles in sameAs instead.
Can an Organization be the author of an article?
Yes. Google accepts Organization as an author type and shows linking it to the organisation’s home page. Use it only when no individual is credited on the page, such as a press release or a team-written report. Using Organization for a named person is one of the wrong-type mistakes Google calls out.
Do I need ProfilePage markup on my author pages?
It is not required for article author markup to work, but Google recommends it when author.url points to a profile page on your own site. The page must focus on a single person or organisation affiliated with the site, and its only required property is mainEntity. Marking up the author page with the same Person @id your articles use ties the whole graph together.