nextjs-optimizacion-seo-imagenes

SEO, Images, and Fonts in Next.JS

  • 5 min

Making an application work is one part of the job. Making it load quickly and ensuring Google understands what’s inside is the next.

In traditional SPAs (Vite), SEO was a nightmare. We had to use libraries like react-helmet to try and inject tags into the <head> dynamically, and even then, many crawlers (search robots) saw a blank page before the JavaScript executed.

Next.js solves this from the ground up. By rendering on the server, the HTML that travels over the network already carries all the semantic information.

Additionally, it offers native tools to optimize the two heaviest resources on the web: Images and Fonts.

Metadata API

Next.js provides a typed API to define the title, description, Open Graph images (Twitter/Facebook), and more.

In any layout.tsx or page.tsx, we can export a constant object called metadata.

// src/app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    template: '%s | My Company',
    default: 'My Company - Tech Solutions', // Default title
  },
  description: 'The best application built with Next.js',
  openGraph: {
    title: 'My Company',
    description: 'The best application...',
    images: ['/og-image.jpg'],
  },
};
Copied!

The template property is great: if you define title: 'Contact' in a child page, the final title will be “Contact | My Company”.

What about a product page /products/[id]? We can’t set a fixed title. We need the product name coming from the database.

For this, we use the generateMetadata function.

// src/app/productos/[id]/page.tsx
import { Metadata } from 'next';
import db from '@/lib/db';

type Props = {
  params: Promise<{ id: string }>
};

// This function runs BEFORE the page component
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;

  // 1. Fetch the data
  const product = await db.product.findUnique({ where: { id } });

  // 2. Return the custom metadata
  return {
    title: product?.name,
    description: product?.description,
    openGraph: {
      images: [product?.imageUrl || ''],
    },
  };
}

export default function Page({ params }: Props) {
  // ... normal rendering ...
}
Copied!

Next.js automatically deduplicates fetch requests. If you call the product API in generateMetadata and then again in the Page component, Next.js only makes one real request to the database/API.

The <Image /> Component

Images are the main culprits for slow web performance. If you use the standard <img src="giant-photo.jpg" /> tag, you are forcing the user to download the original file (which could be 5MB) even if they are viewing it on a small mobile device.

Furthermore, images cause Cumulative Layout Shift (CLS): the text suddenly jumps when the image finishes loading and pushes the content down. Google penalizes this heavily.

Next.js provides the <Image /> component to automatically solve this.

import Image from 'next/image';
import profilePhoto from '../../public/perfil.jpg'; // Local image

export default function Profile() {
  return (
    <div>
      {/* Option 1: Local Image (Imported) */}
      {/* Next.js calculates width and height automatically */}
      <Image
        src={profilePhoto}
        alt="Photo of Luis"
        placeholder="blur" // Blur effect while loading
      />

      {/* Option 2: Remote Image (URL) */}
      {/* MANDATORY to define width and height to reserve space */}
      <Image
        src="https://myserver.com/image.jpg"
        alt="Product"
        width={500}
        height={300}
        className="object-cover rounded-md"
      />
    </div>
  );
}
Copied!

What does it do for us?

  1. Format Conversion: Automatically converts your JPG/PNG to WebP or AVIF (modern formats that weigh 30% less) if the user’s browser supports it.
  2. Resizing: If the user visits from a mobile device, Next.js generates and serves a small version of the image. It doesn’t serve the 4K version.
  3. Lazy Loading: Images not visible on the screen aren’t downloaded until the user scrolls to them.
  4. CLS Prevention: Forces space reservation, preventing the page from “jumping.”

To use remote images (external URLs), you must authorize the domain in your next.config.mjs for security.

Fonts: next/font

Another major bottleneck is typography. Traditionally, we would add a <link> to Google Fonts. This is bad because:

The browser has to make a DNS request to another server (fonts.googleapis.com).

It produces a “flash” (FOIT/FOUT) where the text is invisible or changes font abruptly.

Next.js introduces next/font. This system downloads the font at build time and hosts it alongside your static files.

// src/app/layout.tsx
import { Roboto, Open_Sans } from 'next/font/google'; // Imported from Google, but hosted locally

// Configure the font
const roboto = Roboto({
  weight: ['400', '700'],
  subsets: ['latin'],
  variable: '--font-roboto', // Optional: to use with Tailwind
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      {/* Apply the class to the body */}
      <body className={roboto.className}>
        {children}
      </body>
    </html>
  );
}
Copied!

By doing this, the font is part of your initial HTML. Zero requests to Google. Total privacy. Instant loading.