How to add Person, ProfilePage, WebSite and FAQPage JSON-LD to a static site
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:
<!-- 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:
// 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:
// 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:
// 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:
// 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@graphto describe several linked things at once. - Give each node a stable
@idand cross-link with references (about,isPartOf,publisher) instead of copying data. Person+sameAsunifies your identity across GitHub/LinkedIn;FAQPagecan 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.