ehsan.blog
~/blog/how-to-unify-authorship-across-two-sites-with-jsonld — zsh
cat how-to-unify-authorship-across-two-sites-with-jsonld.md

How to unify authorship across two sites by sharing one Person @id in JSON-LD

·4 min read

I run two sites: a portfolio at developerehsan.com and this blog at blog.developerehsan.com. To a search engine, those are two different domains that both happen to mention someone named Ehsan Shahid. Are they the same person? It has no way to know — unless I tell it. This post is about the one line of structured data that does exactly that, and the linked-data idea that makes it work.

The problem: duplicated identities

Both sites describe an author. If each site invents its own author node, a crawler sees two unrelated Person entities that share a name. That is a missed opportunity: the reputation, the projects, and the articles never accumulate onto one identity. Worse, ambiguous authorship can weaken the trust signals that feed rich results.

The fix is not to copy author details into the blog. It is to reference the author that already exists on the portfolio.

The key idea: @id is a stable identity, not a location

In JSON-LD, @id is a globally unique name for a thing. It usually looks like a URL, but it is not a link you fetch — it is an identifier. When two JSON-LD nodes anywhere on the web use the same @id, they are asserting they describe the same entity. This is the core of linked data: identity by shared name, not by copied data.

My portfolio publishes a Person node (along with ProfilePage, WebSite, and FAQPage) with a fixed id: https://developerehsan.com/#person. The #person fragment makes it a distinct node on the homepage. I store that id once, in the blog’s constants, right next to a comment reminding me to keep it in sync:

ts
// blog/src/consts.ts
// Keep authorship values in sync with the root portfolio's structured data
// (index.html Person @id).
export const SITE = {
  title: "Ehsan Shahid — Blog",
  url: "https://blog.developerehsan.com",
  portfolioUrl: "https://developerehsan.com",
  author: "Ehsan Shahid",
  authorId: "https://developerehsan.com/#person",
  ogImage: "/og-image.png",
} as const

The important field is authorId. Its value is the portfolio’s Person id — a URL on a different domain than the blog. That cross-domain pointer is the whole trick.

Referencing it from the blog’s posts

In PostLayout.astro, when I build the BlogPosting JSON-LD, the author and publisher do not spell out a person. They reference one by @id:

astro
---
// blog/src/layouts/PostLayout.astro
import { SITE } from "@/consts"
// ...
author: { "@type": "Person", "@id": SITE.authorId, name: SITE.author },
publisher: { "@type": "Person", "@id": SITE.authorId, name: SITE.author },
---

Because SITE.authorId is https://developerehsan.com/#person, every blog post now says: “the author and publisher of this article is the exact same Person the portfolio already describes.” I include name as a friendly label, but the @id is what does the unifying. The rich profile — job title, social links, skills — lives once on the portfolio; the blog just points at it.

flowchart LR
  subgraph Portfolio
    P["Person\n@id: developerehsan.com/#person"]
  end
  subgraph Blog
    A1["Post A author"] --> P
    A2["Post B author"] --> P
    A3["Post C publisher"] --> P
  end

Every arrow is the same @id. To a crawler consuming both sites, there is one Person, referenced from many places, rather than a fresh stranger per page.

Why reference instead of copy?

You could paste the full author object into every post. Referencing by @id is better for three reasons:

  • Single source of truth. Update your title or add a social profile once on the portfolio; the blog never goes stale because it holds no author details to drift.
  • Explicit sameness. A shared @id is an unambiguous claim that these are one entity. Matching names across domains is only a guess.
  • Consolidated signals. Articles, projects, and profile all attach to one identity, so authority accrues in one place.

The one discipline it requires: the portfolio must actually emit a Person node whose @id is https://developerehsan.com/#person. If that id ever changes, the blog’s authorId must change with it — which is why consts.ts carries that sync comment. Keep them equal and the two sites read as one author.

Verifying it

Build the blog, open a post’s source, and find the application/ld+json script. Confirm both author and publisher show "@id": "https://developerehsan.com/#person". Then run the portfolio and confirm its Person node uses the identical id. Google’s Rich Results Test will parse each page independently; the shared id is what ties them together across the crawl.

What to remember

  • In JSON-LD, @id is a stable identifier, not a URL you fetch — same @id means same entity.
  • Store the portfolio’s Person id once as SITE.authorId and reference it from every post’s author and publisher.
  • Reference, don’t copy: one source of truth, an explicit sameness claim, and consolidated authority.
  • The portfolio must publish a Person node with that exact @id; keep the two values in sync.

Related reading: this builds on the per-post BlogPosting JSON-LD and complements the RSS feed, sitemap, and SEO meta setup.

ls ./related
cat ./comments

Comments are not configured yet. Enable GitHub Discussions and paste the giscus repo-id / category-id into src/consts.ts.