ehsan.blog
~/blog/how-to-add-person-and-faqpage-jsonld-to-a-static-site — zsh
cat how-to-add-person-and-faqpage-jsonld-to-a-static-site.md

How to add Person, ProfilePage, WebSite and FAQPage JSON-LD to a static site

·5 min read

Search engines read your page’s text, but they guess a lot about what it means. Is “Ehsan Shahid” a person, a company, a product? Structured data removes the guessing. On my portfolio I add a block of JSON-LD that spells out, in a machine-readable way, who the page is about and what questions it answers. This is what can earn you rich results — the little enhanced listings in Google, like an FAQ dropdown right under your link.

This post walks through the exact JSON-LD I ship, node by node, so you can reproduce it on your own static site.

What is JSON-LD?

JSON-LD (JSON for Linked Data) is just a <script> tag full of JSON that describes your page using the vocabulary from schema.org. “Linked Data” means each thing you describe (a person, a page, a website) gets an @id, and the things can reference each other by that id. Search engines parse it and build a little knowledge graph about your site.

You drop it straight into the <head> of your HTML:

html
<!-- index.html -->
<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@graph": [ /* ...nodes... */ ]
  }
</script>

Two keys matter here. @context tells parsers “the words I’m about to use come from schema.org.” @graph is an array where each item is one node — one thing — that you’re describing.

Why an @graph instead of one object?

You could write a single object, but a real page describes several related things at once: a person, the page itself, the website it belongs to, and a list of FAQs. @graph lets you list them all and cross-link them with @id. Here is the shape:

graph LR
  P[Person #person] --- WP[ProfilePage #webpage]
  WP -- isPartOf --> WS[WebSite #website]
  WP -- about --> P
  WS -- publisher --> P
  FAQ[FAQPage #faq]

Notice how ProfilePage points at the Person with about, and the WebSite points back at the same Person as its publisher. They all reference one canonical #person node instead of repeating the data.

Node 1: Person

The Person node is the heart of a portfolio. It says who you are, where you work, what you know, and — importantly — links to your other profiles with sameAs so Google can connect them into one identity:

json
// index.html — trimmed
{
  "@type": "Person",
  "@id": "https://developerehsan.com/#person",
  "name": "Ehsan Shahid",
  "givenName": "Ehsan",
  "familyName": "Shahid",
  "alternateName": "developerEhsan",
  "url": "https://developerehsan.com/",
  "image": "https://developerehsan.com/ehsan-profile.webp",
  "jobTitle": "Full Stack Developer",
  "worksFor": {
    "@type": "Organization",
    "name": "Platas",
    "url": "https://platas.io"
  },
  "knowsAbout": ["React", "Next.js", "Node.js", "TypeScript", "Electron"],
  "sameAs": [
    "https://github.com/developerehsan",
    "https://linkedin.com/in/developerehsan",
    "https://blog.developerehsan.com/"
  ]
}

The @id (#person) is the label other nodes use to point back here. sameAs is the field that tells search engines “these GitHub/LinkedIn accounts are the same person as this page.”

Node 2: ProfilePage

ProfilePage describes the page you’re on — as opposed to the person. It links the two together:

json
// index.html — trimmed
{
  "@type": "ProfilePage",
  "@id": "https://developerehsan.com/#webpage",
  "url": "https://developerehsan.com/",
  "name": "Ehsan Shahid — Full Stack Developer",
  "isPartOf": { "@id": "https://developerehsan.com/#website" },
  "about": { "@id": "https://developerehsan.com/#person" },
  "inLanguage": "en"
}

about says “this page is about the #person node,” and isPartOf says “this page belongs to the #website node.” No data is duplicated — it’s all references.

Node 3: WebSite

WebSite describes the whole site, and names the Person as its publisher:

json
// index.html — trimmed
{
  "@type": "WebSite",
  "@id": "https://developerehsan.com/#website",
  "url": "https://developerehsan.com/",
  "name": "Ehsan Shahid",
  "publisher": { "@id": "https://developerehsan.com/#person" },
  "inLanguage": "en"
}

Node 4: FAQPage

This is the one that can visibly change your search listing. FAQPage holds a list of question-and-answer pairs. When Google trusts it, those Q&As can appear as an expandable FAQ block directly in the results:

json
// index.html — trimmed
{
  "@type": "FAQPage",
  "@id": "https://developerehsan.com/#faq",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Is Ehsan Shahid a full stack developer?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Ehsan Shahid is a full stack developer based in Lahore, Pakistan..."
      }
    }
  ]
}

Each entry is a Question with a name (the question text) and an acceptedAnswer of type Answer with a text. Google’s rule is that this content should actually be visible or truthful on the page — don’t stuff it with keywords that aren’t real.

Why a static site is the perfect place for this

My portfolio is prerendered with vite-react-ssg — the HTML is fully baked at build time. Because the JSON-LD lives in index.html and never depends on JavaScript, crawlers see it instantly on the first request. No hydration, no client-side rendering, no waiting. That reliability is exactly what structured data wants.

If you also run a blog, the same structured-data thinking applies per article — see adding BlogPosting and BreadcrumbList JSON-LD to Astro posts, and unifying authorship across two sites by sharing one Person @id so the person node here and your blog’s author line up.

What to remember

  • JSON-LD goes in a <script type="application/ld+json"> in your <head>; use @graph to describe several linked things at once.
  • Give each node a stable @id and cross-link with references (about, isPartOf, publisher) instead of copying data.
  • Person + sameAs unifies your identity across GitHub/LinkedIn; FAQPage can earn a rich FAQ result.
  • Keep FAQ answers honest and reflected on the page — structured data describes real content, it doesn’t fake it.
  • Validate your markup with Google’s Rich Results Test before shipping.
ls ./related
cat ./comments

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