← Back to Blog

ProfilePage.mainEntity as a bare @id reference — the Search Console error that hides in valid schema

ProfilePage.mainEntity as a bare @id reference — the Search Console error that hides in valid schema

Google Search Console flagged a real estate site I work on yesterday with this error on the /about page:

Invalid object type for field 'mainEntity'

The page validated clean on schema.org's validator. It validated clean on Google's Rich Results Test. The JSON-LD parsed without complaint in every browser console. Search Console flagged it anyway, and the rich-result on the page got marked ineligible.

The pattern is common. If you put your Person or Organization entity on the homepage and reference it from a separate /about page, you probably have this bug, and Search Console will tell you about it eventually.

What the bug looks like

The homepage carries the canonical Person definition:

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://example.com/#seth-watte",
  "name": "Seth Watte",
  "url": "https://example.com/",
  "jobTitle": "Real Estate Agent",
  "worksFor": {"@id": "https://example.com/#organization"}
}

The /about page has the ProfilePage rich-result wrapper, which references that Person by @id:

{
  "@context": "https://schema.org",
  "@type": "ProfilePage",
  "@id": "https://example.com/about/#profilepage",
  "mainEntity": {"@id": "https://example.com/#seth-watte"},
  "dateCreated": "2026-04-01",
  "dateModified": "2026-05-13"
}

That reads correctly to a JSON-LD parser. The Person at https://example.com/#seth-watte is defined on the homepage. The /about page's ProfilePage points back at it. Schema.org's data model explicitly allows cross-document @id references.

Search Console disagrees. The validator emits "Invalid object type for field 'mainEntity'" because, on the /about page in isolation, the reference is just {"@id": "https://example.com/#seth-watte"}. There is no @type hint on the reference itself. There is no inline definition of the Person on this page. Search Console's strict validator cannot determine what mainEntity is supposed to be, so it flags the field as having an invalid object type.

Why the validators give different answers

Schema.org's validator answers the question "is the JSON-LD structurally valid and do the field types match the spec?" It walks the document and the answer is yes. A Person is allowed as mainEntity on a ProfilePage. The reference is structurally a valid @id pointer.

Google's Rich Results Test answers a similar question, scoped to whether the JSON-LD is eligible to produce a rich result. It runs more checks than schema.org but it still does not fail this pattern.

Google Search Console's rich-result report uses a stricter validator. It is the same validator that decides whether your ProfilePage actually appears in search results. It treats every page as a self-contained document for validation purposes. If the type of mainEntity cannot be established from this document alone, it flags the field.

The contradiction is real. Schema.org says cross-document @id references are valid. Search Console's rich-result validator wants the type either inline on the reference or defined inline on the same page. You have to satisfy both.

Fix A: type the reference inline

The cheapest fix is to add the @type alongside the @id on the reference itself:

{
  "@type": "ProfilePage",
  "@id": "https://example.com/about/#profilepage",
  "mainEntity": {
    "@id": "https://example.com/#seth-watte",
    "@type": "Person"
  },
  "dateCreated": "2026-04-01T00:00:00-06:00",
  "dateModified": "2026-05-13T00:00:00-06:00"
}

The reference now carries enough information for Search Console to resolve the type without fetching the homepage. The @id still points at the canonical Person on the homepage, so the Knowledge Graph treats it as one entity. Both validators are satisfied.

Note that I also fixed the date-only issue at the same time. Search Console flags "dateModified": "2026-05-13" with "Invalid datetime value for dateModified" for the same class of strict-validator reason. See the companion post on ISO 8601 datetime fields for the timezone-offset rules.

Fix B: define the entity inline on the same page

The stronger pattern is to define the Person inline on the /about page directly, while keeping the same @id so it stays one entity for the Knowledge Graph:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Person",
      "@id": "https://example.com/#seth-watte",
      "name": "Seth Watte",
      "url": "https://example.com/",
      "jobTitle": "Real Estate Agent",
      "worksFor": {"@id": "https://example.com/#organization"}
    },
    {
      "@type": "ProfilePage",
      "@id": "https://example.com/about/#profilepage",
      "mainEntity": {"@id": "https://example.com/#seth-watte"},
      "dateCreated": "2026-04-01T00:00:00-06:00",
      "dateModified": "2026-05-13T00:00:00-06:00"
    }
  ]
}

JSON-LD merges nodes by @id. The Person defined on the homepage and the Person defined inline on the /about page collapse into one entity in the Knowledge Graph because they share the same @id. Search Console gets a typed Person inline on the document, so the strict validator passes. AI Mode gets a Person inline on the page that the ProfilePage describes, which is the binding direction it actually prefers when it picks a canonical bio page for a name query.

Fix B is more lines of HTML and more bytes per page, but it is the pattern I recommend when the page IS the bio page for the entity. ProfilePage exists specifically to flag a page as the canonical home of a person or organization. Defining that entity inline on its own canonical page is more consistent with how AI Mode walks the graph than relying on a cross-document reference.

Cases where Fix A is right and Fix B is wrong

The author bio page is one case where Fix B fits. Other cases need Fix A.

If a blog post emits an Article with author: {"@id": "https://example.com/#seth-watte"} and the page is not the author's bio page, do not inline the full Person definition on the Article page. Inlining the Person on every Article page means the canonical Person definition exists in fifty different places, with potential drift in alternateName, sameAs, and other fields. Use Fix A on Article pages. Reference the Person by @id with an inline @type: Person hint, and let the canonical definition live on the bio page only.

The shape of the rule. Inline-define an entity on exactly one page (the page that IS the entity's canonical home). Reference it with @id plus inline @type hint everywhere else. Search Console is happy. The Knowledge Graph treats it as one entity. AI Mode follows the canonical-bio binding when name-only queries fire.

How to find every occurrence

The mega analyzer detector for this bug walks every JSON-LD block on the page, collects the set of @id values that are inline-defined with a @type, then walks ProfilePage / AboutPage / WebPage / CollectionPage / Article / BlogPosting / NewsArticle nodes and checks every mainEntity / about / author / publisher field. If the field value is {"@id": "..."} with no @type and the referenced @id is not in the inline-defined set, it surfaces the field as a Search Console risk.

You can run the same check by hand in DevTools:

const blocks = [...document.querySelectorAll('script[type="application/ld+json"]')];

const definedIds = new Set();
const checkRefs = [];
const BIND_FIELDS = ['mainEntity', 'about', 'author', 'publisher'];
const PAGE_TYPES = /^(ProfilePage|AboutPage|WebPage|CollectionPage|Article|BlogPosting|NewsArticle|ItemPage)$/;

function walk(n, fn) {
  if (!n || typeof n !== 'object') return;
  if (Array.isArray(n)) return n.forEach(x => walk(x, fn));
  fn(n);
  for (const k of Object.keys(n)) if (n[k] && typeof n[k] === 'object' && k !== '@type') walk(n[k], fn);
}

blocks.forEach(b => {
  try {
    const data = JSON.parse(b.textContent);
    walk(data, n => {
      const tt = Array.isArray(n['@type']) ? n['@type'] : [n['@type']].filter(Boolean);
      if (n['@id'] && tt.length > 0) definedIds.add(n['@id']);
    });
    walk(data, n => {
      const tt = Array.isArray(n['@type']) ? n['@type'] : [n['@type']].filter(Boolean);
      if (tt.some(t => PAGE_TYPES.test(t))) {
        BIND_FIELDS.forEach(f => {
          const v = n[f];
          const check = vv => {
            if (vv && typeof vv === 'object' && !Array.isArray(vv) && vv['@id'] && !vv['@type']) {
              checkRefs.push({parent: tt[0], field: f, ref: vv['@id'], inline: definedIds.has(vv['@id'])});
            }
          };
          if (Array.isArray(v)) v.forEach(check); else check(v);
        });
      }
    });
  } catch (e) {}
});

const unresolved = checkRefs.filter(r => !r.inline);
console.log({total: checkRefs.length, unresolved});

Anything in the unresolved array is what Search Console flags.

Why this matters for AI search

Schema validators are answering one question. Search Console is answering a stricter version of that same question. AI search retrievers (Google AI Mode, Gemini, Perplexity) are answering a different question entirely.

When AI Mode resolves a name-only query to one URL, it follows a chain of bindings. Person at homepage @id. Person.mainEntityOfPage object pointing at the /about page's ProfilePage @id. ProfilePage.mainEntity pointing back at the Person @id. The bidirectional loop tells AI Mode that the /about page is the canonical bio for this Person.

The bug under discussion breaks the loop one way. ProfilePage cannot establish that mainEntity is a Person without fetching the homepage. Search Console flags it as a rich-result error. AI Mode's confidence drops because the binding is one-sided. Either fix restores the loop. Fix A by inline-typing the reference. Fix B by inline-defining the entity.

If you operate a site where the same Person or Organization is referenced from multiple URLs, the audit habit to build is grep every <script type="application/ld+json"> block for bare {"@id": "..."} references on mainEntity / about / author / publisher fields, then check whether the referenced @id is defined as a typed node on the same page. If not, fix it before Search Console finds it.

When the bug shipped on this site

This bug shipped on the yoseth.com /about page on 2026-05-13 as part of a ProfilePage migration that consolidated the bio pattern across sites. The migration moved Person definitions to the homepage to avoid duplication, then referenced them from /about with bare @id pointers. The validators all passed. Search Console reported the error three days later when the next crawl finished. The detector in the mega analyzer was added the same day Search Console flagged it, so other sites that copied the same pattern will surface this bug before deploy now.

Related reading

Fact-check notes and sources


This post is informational, not legal or SEO-consulting advice. Examples reference my own sites and sites I work on, with no third party named as a negative case study.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026