Navegación, Image y Metadata
next/link — Navegación optimizada
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:
- Links visibles en el viewport se prefetchean automáticamente
- Usa
rootMarginde 250px para anticipar el scroll - Deduplicación de requests — no prefetchea la misma ruta dos veces
- Usa
requestIdleCallbackpara no bloquear interacciones
Para App Router, prefetch descarga payloads .rsc y los cachea. Para Pages Router, inyecta <link rel="prefetch">.
Opciones de Link
<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
- Sin optimización en build time — las imágenes se transforman en runtime
- En Cloudflare Workers, puedes usar el binding de Images para resize nativo
- Breakpoints responsivos: 640, 750, 828, 1080, 1200, 1920, 2048, 3840
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:
| Archivo | Propósito |
|---|---|
app/sitemap.ts | Genera sitemap.xml |
app/robots.ts | Genera robots.txt |
app/manifest.ts | Genera manifest.json |
app/favicon.ico | Favicon |
app/opengraph-image.tsx | Imagen 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.