Navegación, Image y Metadata

Por: Artiko
vinextnavigationimagemetadataseo

next/link proporciona navegación client-side con prefetch inteligente:

import Link from "next/link";

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Inicio</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/about" prefetch={false}>Acerca de</Link>
    </nav>
  );
}

Prefetch inteligente

Vinext implementa prefetch con IntersectionObserver:

Para App Router, prefetch descarga payloads .rsc y los cachea. Para Pages Router, inyecta <link rel="prefetch">.

<Link href="/dashboard" replace>    {/* Reemplaza en historial */}
<Link href="/blog" scroll={false}>  {/* No hace scroll al top */}
<Link href="/posts" prefetch={false}> {/* Deshabilita prefetch */}

Vinext detecta teclas modificadoras (Cmd/Ctrl + click) y abre en nueva pestaña, igual que Next.js.

next/image — Imágenes optimizadas

Vinext reimplementa next/image usando @unpic/react para imágenes remotas (soporta 28 CDNs) y un endpoint /_vinext/image para imágenes locales.

Imagen remota

import Image from "next/image";

export default function Avatar() {
  return (
    <Image
      src="https://cdn.example.com/avatar.jpg"
      alt="Avatar del usuario"
      width={200}
      height={200}
    />
  );
}

Para imágenes remotas, Vinext detecta automáticamente el CDN y aplica transformaciones nativas (resize, formato).

Imagen local

import Image from "next/image";
import foto from "../public/foto.jpg";

export default function Gallery() {
  return (
    <Image
      src={foto}
      alt="Foto de la galería"
      placeholder="blur"
    />
  );
}

Imagen con fill

<div style={{ position: "relative", width: "100%", height: 300 }}>
  <Image
    src="/hero.jpg"
    alt="Hero image"
    fill
    style={{ objectFit: "cover" }}
  />
</div>

Configuración de dominios remotos

En next.config.js:

/** @type {import('next').NextConfig} */
module.exports = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "cdn.example.com" },
      { protocol: "https", hostname: "**.cloudinary.com" },
    ],
  },
};

Limitaciones de imagen

Metadata API

Metadata estática

// app/layout.tsx
export const metadata = {
  title: "Mi App",
  description: "Aplicación construida con Vinext",
};

Metadata dinámica

// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Title templates

Los templates de título se heredan de layouts padres:

// app/layout.tsx
export const metadata = {
  title: {
    template: "%s | Mi App",
    default: "Mi App",
  },
};

// app/blog/page.tsx
export const metadata = {
  title: "Blog", // Renderiza: "Blog | Mi App"
};

Open Graph y Twitter

export const metadata = {
  openGraph: {
    title: "Mi App",
    description: "Descripción para redes sociales",
    url: "https://miapp.com",
    siteName: "Mi App",
    type: "website",
    images: [
      {
        url: "https://miapp.com/og-image.jpg",
        width: 1200,
        height: 630,
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    title: "Mi App",
    description: "Descripción para Twitter",
  },
};

Metadata de archivos

Vinext soporta metadata basada en archivos:

ArchivoPropósito
app/sitemap.tsGenera sitemap.xml
app/robots.tsGenera robots.txt
app/manifest.tsGenera manifest.json
app/favicon.icoFavicon
app/opengraph-image.tsxImagen OG dinámica
// app/sitemap.ts
export default function sitemap() {
  return [
    { url: "https://miapp.com", lastModified: new Date() },
    { url: "https://miapp.com/blog", lastModified: new Date() },
  ];
}

next/script

import Script from "next/script";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <Script
        src="https://analytics.example.com/script.js"
        strategy="lazyOnload"
      />
    </>
  );
}

Estrategias soportadas: beforeInteractive, afterInteractive, lazyOnload, worker.

Siguiente paso

En el siguiente capítulo aprenderemos sobre middleware, API routes del App Router y las utilidades de next/server.