A Practical SEO Guide, Part 3: Structured Data & Social Cards

So far we've made the content readable (Part 1) and findable (Part 2). This part is about making it understood and shareable. Two mechanisms, both punching above their weight:

  • Structured data (JSON-LD) — tells search engines what a page literally is: a person, an article, a product, an event.
  • Open Graph / Twitter cards — control how a link renders when someone pastes it into Slack, LinkedIn, or X.

JSON-LD: describe the thing, not just the words

Search engines are good at reading text but love being told the facts explicitly. JSON-LD is a <script> block of schema.org data that does exactly that. For a personal or company site, a Person or Organization schema is the single highest-leverage SEO asset you can add — it's what feeds the Google knowledge panel and links your site to a real-world identity.

Here's the Person + WebSite graph from the portfolio site (trimmed):

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Person",
      "@id": "https://cv.lans.cloud/#person",
      "name": "Sonny N. Lans",
      "jobTitle": "Senior Software Engineer & Architect",
      "alumniOf": { "@type": "CollegeOrUniversity", "name": "TU Delft" },
      "knowsAbout": ["Distributed Systems", "TypeScript", "Kubernetes"],
      "sameAs": [
        "https://github.com/snlans",
        "https://www.linkedin.com/in/sonny-lans"
      ]
    },
    {
      "@type": "WebSite",
      "@id": "https://cv.lans.cloud/#website",
      "url": "https://cv.lans.cloud",
      "publisher": { "@id": "https://cv.lans.cloud/#person" }
    }
  ]
}

The field that earns its keep is sameAs. It's how you tell Google "this site, this GitHub, and this LinkedIn are all the same entity." That identity link is the backbone of a knowledge panel and of ranking for your own name. Use your real profile URLs — a placeholder here is worse than nothing.

Emitting it is just a script tag in your document head:

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>

Match the @type to what the page actually is. A few common ones:

  • Article / BlogPosting for posts (add datePublished, author, headline).
  • Person / Organization for about/portfolio/company pages.
  • BreadcrumbList for hierarchical navigation.
  • Product, FAQPage, Event where they genuinely apply.

Don't invent structure that doesn't match the visible page — Google treats mismatched or spammy markup as a quality signal against you.

Open Graph: kill the bare-URL preview

Paste a link with no OG tags into Slack and you get a sad, naked URL. Paste one with them and you get a title, description, and image card. For anything you want people to share, this is not optional. The core set:

<meta property="og:title" content="Sonny N. Lans — Senior Software Engineer & Architect" />
<meta property="og:description" content="10+ years building distributed systems..." />
<meta property="og:image" content="https://cv.lans.cloud/opengraph-image" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />

The image is what makes it pop, and maintaining a hand-drawn 1200×630 PNG for every page is a chore nobody keeps up. The trick I like: generate the card at request time from code. Next.js has ImageResponse (Satori under the hood) that renders JSX to a PNG — no design tool, always on-brand, and it updates itself when your copy changes:

// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default function Image() {
  return new ImageResponse(
    (
      <div style={{ display: 'flex', /* ...brand gradient, name, title... */ }}>
        Sonny N. Lans
      </div>
    ),
    { ...size },
  );
}

One deployment gotcha worth flagging: next/og examples often set export const runtime = 'edge'. If you self-host in a standalone Docker build rather than on an edge platform, drop that line and let it run on the Node runtime — otherwise the route can fail to render in production. Verify the actual bytes come back:

curl -s -o /dev/null -w 'status=%{http_code} type=%{content_type} bytes=%{size_download}\n' \
  https://cv.lans.cloud/opengraph-image
# status=200 type=image/png bytes=98689

Why this pair matters

Structured data changes how you appear in search (rich snippets, knowledge panels); OG tags change how you appear everywhere links are shared. Both are pure additive markup — no content rewrite, no redesign — and both are still missing from the majority of sites, which means they're easy differentiation.

In Part 4 we cover the thing that's now an explicit ranking factor and is where most sites quietly lose: performance.


Next: Part 4: Performance Is SEO →