← Blog

Rebuilding My Portfolio with Next.js 16: Fonts, OG Images & Entity SEO

Ron Tabachnik4 min read

My old portfolio was a WordPress page. It worked, but it said very little about how I actually build software. So I rebuilt it from scratch with Next.js 16, and treated the rebuild itself as an engineering project: performance, social sharing, and — the part most portfolios skip — teaching Google who I am.

Here's what went into it, with the parts I'd reuse on any project.

Self-hosted fonts with next/font

The first version loaded Plus Jakarta Sans and Syne through a Google Fonts CSS @import. That's a render-blocking request to an external host before the browser can paint text.

next/font fixes this by downloading the fonts at build time and serving them from your own domain, with size-adjust fallbacks that eliminate layout shift:

import { Plus_Jakarta_Sans, Syne } from "next/font/google";

const jakarta = Plus_Jakarta_Sans({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-jakarta",
});

const syne = Syne({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-syne",
});

The variable option matters more than it looks: next/font generates hashed font-family names, so any hardcoded font-family: 'Syne' in your styles silently breaks. Exposing CSS variables and referencing var(--font-syne) everywhere keeps one source of truth.

OG images generated from code

When someone shares your site on LinkedIn or WhatsApp, the preview card is the first impression. Instead of designing a static image, I generate it at build time with ImageResponse — JSX and flexbox that Satori renders to a PNG:

// app/opengraph-image.tsx
export default async function Image() {
  const photo = await readFile(join(process.cwd(), "assets/ron.jpg"), "base64");

  return new ImageResponse(
    <div style={{ display: "flex", background: "#060608" /* ... */ }}>
      {/* name, title, portrait */}
    </div>,
    { ...size, fonts: [{ name: "Syne", data: syneFont, weight: 800 }] }
  );
}

Two gotchas that cost me time:

  • Satori doesn't decode WebP. My portrait was a .webp; the fix was converting to JPEG and embedding it as a base64 data URI.
  • Wide display fonts overflow fast. Syne ExtraBold at 84px clipped my own last name off the canvas. Render the image and look at it — don't trust the layout in your head.

The same technique generates the favicon (app/icon.tsx) and Apple touch icon, so every icon stays consistent with the brand without opening a design tool.

Entity SEO: telling Google who you are

For a personal site, the most valuable query is your own name. The goal is for Google to treat your domain — not LinkedIn — as the canonical answer. Three things move that needle:

1. ProfilePage + Person structured data. JSON-LD that connects your name to your role, your companies, and your profiles:

{
  "@type": "ProfilePage",
  "mainEntity": {
    "@type": "Person",
    "name": "Ron Tabachnik",
    "jobTitle": "Software Engineer",
    "affiliation": { "@type": "Organization", "name": "Digitab" },
    "sameAs": [
      "https://www.linkedin.com/in/rontabachnik/",
      "https://github.com/ronTabachnik"
    ]
  }
}

The sameAs links are the entity glue — they tell Google that this page, that LinkedIn profile, and that GitHub account are the same person.

2. Headings that carry meaning. My section headings were designed for humans: "Skills & Stack", "Builder & Founder". Visually great, semantically empty. Rather than redesign, I added visually-hidden spans inside the same headings — screen readers and crawlers get "Builder & Founder of Digitab, ClinicHub & iZicards", sighted visitors see the clean design. Accessibility and SEO from one change.

3. Inbound links from your own profiles. The highest-leverage SEO action for a name query isn't on the page at all: it's setting your website URL on LinkedIn, GitHub, and your App Store listings. Google decides the canonical "you" partly from who links where.

The small stuff that rounds it out

  • sitemap.ts and robots.ts as code — they stay in sync with routes instead of rotting in public/.
  • MotionConfig reducedMotion="user" so Framer Motion respects the OS accessibility setting.
  • A real favicon.ico built by wrapping the generated PNG in an ICO container — legacy requesters get a file, modern browsers get the generated icon.

Takeaways

  1. Fonts belong on your own domain. next/font makes this a five-minute change with measurable paint improvements.
  2. Generate your OG images from code and review them rendered — they're the front door of every link you share.
  3. Structured data plus sameAs links is how a personal site wins its own name. The code is trivial; most people just never write it.
  4. The best SEO for a portfolio takes ten minutes and isn't code: put your domain on every profile you own.

The full site — including this blog — is built with Next.js 16, Tailwind 4, and Framer Motion, deployed on Vercel.