Rebuilding My Portfolio with Next.js 16: Fonts, OG Images & Entity SEO
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.tsandrobots.tsas code — they stay in sync with routes instead of rotting inpublic/.MotionConfig reducedMotion="user"so Framer Motion respects the OS accessibility setting.- A real
favicon.icobuilt by wrapping the generated PNG in an ICO container — legacy requesters get a file, modern browsers get the generated icon.
Takeaways
- Fonts belong on your own domain.
next/fontmakes this a five-minute change with measurable paint improvements. - Generate your OG images from code and review them rendered — they're the front door of every link you share.
- Structured data plus
sameAslinks is how a personal site wins its own name. The code is trivial; most people just never write it. - 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.