Skip to content
← All writing
Engineering4 min read20 July 2026

Server-rendering the portfolio

The homepage fetched its own API from a useEffect, so crawlers saw nothing. Moving to a shared server data layer fixed it in about forty lines.

Most portfolio sites fetch their projects in a useEffect. Mine did too, and it meant a crawler saw an empty page.

The problem with client fetching

The homepage rendered a spinner server-side, then asked /api/projects for the data once React hydrated. Fine for a logged-in dashboard. Bad for a page whose entire job is to be found.

Reading through a shared data layer

export const getProjects = cache(async (): Promise<ProjectRecord[]> => {
  const rows = await prisma.project.findMany({
    where: { visible: true },
    include: FULL_INCLUDE,
    orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
  });
  return rows.map(shape);
});

Wrapping it in React's cache() means generateMetadata and the component body share one query instead of issuing two.

The API route still exists — the admin panel needs it. It just stopped being how the public site reads.

That's the whole change. The markup ships in the first response now.

  • Next.js
  • Prisma
  • SEO

Share this