En Next.js 13+ con App Router, cada página es un componente React ubicado en una carpeta específica. Aquí aprenderás a crear páginas comunes como Home y Dashboard de forma eficiente.
app/
├── page.tsx # Home (/)
├── dashboard/
│ ├── layout.tsx # Layout del dashboard
│ ├── page.tsx # Dashboard principal (/dashboard)
│ ├── users/
│ │ └── page.tsx # Usuarios (/dashboard/users)
│ └── settings/
│ └── page.tsx # Configuración (/dashboard/settings)
├── about/
│ └── page.tsx # Acerca de (/about)
└── layout.tsx # Layout global // app/page.tsx
export default function HomePage() {
return (
<main className="min-h-screen bg-gray-50">
<section className="container mx-auto px-4 py-16">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-6">
Bienvenido a Mi App
</h1>
<p className="text-xl text-gray-600 mb-8">
La mejor solución para tu negocio
</p>
<button className="bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700">
Comenzar
</button>
</div>
</section>
</main>
)
} // app/page.tsx
import Hero from '@/components/Hero'
import Features from '@/components/Features'
import CTA from '@/components/CTA'
export default function HomePage() {
return (
<main>
<Hero />
<Features />
<CTA />
</main>
)
}
// components/Hero.tsx
export default function Hero() {
return (
<section className="bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
<div className="container mx-auto px-4 text-center">
<h1 className="text-5xl font-bold mb-6">
Transforma tu Negocio
</h1>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Herramientas modernas para empresas que quieren crecer
</p>
<div className="space-x-4">
<button className="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold">
Prueba Gratis
</button>
<button className="border border-white px-6 py-3 rounded-lg">
Ver Demo
</button>
</div>
</div>
</section>
)
} Usa Server Components por defecto para mejor performance. Solo convierte a Client Component cuando necesites interactividad.
// app/dashboard/layout.tsx
import Sidebar from '@/components/Sidebar'
import Header from '@/components/Header'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex h-screen bg-gray-100">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-6">
{children}
</main>
</div>
</div>
)
} // app/dashboard/page.tsx
import StatsCards from '@/components/StatsCards'
import RecentActivity from '@/components/RecentActivity'
import Chart from '@/components/Chart'
export default function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Resumen de tu actividad</p>
</div>
<StatsCards />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Chart />
<RecentActivity />
</div>
</div>
)
}
// components/StatsCards.tsx
export default function StatsCards() {
const stats = [
{ name: 'Usuarios', value: '2,651', change: '+12%' },
{ name: 'Ventas', value: '$45,231', change: '+8%' },
{ name: 'Pedidos', value: '1,423', change: '+23%' },
{ name: 'Conversión', value: '3.24%', change: '+5%' },
]
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat) => (
<div key={stat.name} className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">{stat.name}</p>
<p className="text-2xl font-semibold text-gray-900">{stat.value}</p>
</div>
<span className="text-green-600 text-sm font-medium">{stat.change}</span>
</div>
</div>
))}
</div>
)
} // components/Sidebar.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
HomeIcon,
UsersIcon,
ChartBarIcon,
CogIcon
} from '@heroicons/react/24/outline'
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
{ name: 'Usuarios', href: '/dashboard/users', icon: UsersIcon },
{ name: 'Reportes', href: '/dashboard/reports', icon: ChartBarIcon },
{ name: 'Configuración', href: '/dashboard/settings', icon: CogIcon },
]
export default function Sidebar() {
const pathname = usePathname()
return (
<div className="bg-gray-900 text-white w-64 space-y-6 py-7 px-2">
<div className="text-center">
<h1 className="text-2xl font-semibold">Mi App</h1>
</div>
<nav className="space-y-2">
{navigation.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-2 py-2 px-4 rounded transition-colors ${
isActive
? 'bg-gray-700 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
}`}
>
<item.icon className="h-5 w-5" />
<span>{item.name}</span>
</Link>
)
})}
</nav>
</div>
)
} Usa ‘use client’ solo en componentes que necesiten hooks como usePathname. El resto mantén como Server Components.
// app/dashboard/users/page.tsx
import UserTable from '@/components/UserTable'
import AddUserButton from '@/components/AddUserButton'
async function getUsers() {
// Simulación de fetch de datos
return [
{ id: 1, name: 'Juan Pérez', email: 'juan@email.com', role: 'Admin' },
{ id: 2, name: 'María García', email: 'maria@email.com', role: 'User' },
{ id: 3, name: 'Carlos López', email: 'carlos@email.com', role: 'User' },
]
}
export default async function UsersPage() {
const users = await getUsers()
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-semibold text-gray-900">Usuarios</h1>
<p className="text-gray-600">Gestiona los usuarios de tu aplicación</p>
</div>
<AddUserButton />
</div>
<UserTable users={users} />
</div>
)
} // app/dashboard/settings/page.tsx
import SettingsForm from '@/components/SettingsForm'
export default function SettingsPage() {
return (
<div className="max-w-2xl space-y-6">
<div>
<h1 className="text-2xl font-semibold text-gray-900">Configuración</h1>
<p className="text-gray-600">Personaliza tu aplicación</p>
</div>
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900">Configuración General</h2>
</div>
<div className="p-6">
<SettingsForm />
</div>
</div>
</div>
)
} // app/dashboard/users/[id]/page.tsx
import { notFound } from 'next/navigation'
import UserProfile from '@/components/UserProfile'
async function getUser(id: string) {
// Simulación de fetch
const users = [
{ id: '1', name: 'Juan Pérez', email: 'juan@email.com', role: 'Admin' },
{ id: '2', name: 'María García', email: 'maria@email.com', role: 'User' },
]
return users.find(user => user.id === id)
}
export default async function UserPage({
params
}: {
params: { id: string }
}) {
const user = await getUser(params.id)
if (!user) {
notFound()
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold text-gray-900">
Perfil de {user.name}
</h1>
<p className="text-gray-600">Información del usuario</p>
</div>
<UserProfile user={user} />
</div>
)
} // app/dashboard/users/[id]/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
Usuario no encontrado
</h2>
<p className="text-gray-600 mb-6">
El usuario que buscas no existe o ha sido eliminado.
</p>
<Link
href="/dashboard/users"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Volver a Usuarios
</Link>
</div>
</div>
)
} // app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="space-y-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/3"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-white p-6 rounded-lg shadow animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-8 bg-gray-200 rounded w-3/4"></div>
</div>
))}
</div>
</div>
)
} // app/dashboard/users/loading.tsx
export default function UsersLoading() {
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-32 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-48"></div>
</div>
<div className="h-10 bg-gray-200 rounded w-32 animate-pulse"></div>
</div>
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="animate-pulse">
{[...Array(5)].map((_, i) => (
<div key={i} className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-4">
<div className="h-10 w-10 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-1/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/3"></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
} Los archivos loading.tsx se muestran instantáneamente mientras se cargan los datos. Úsalos para mejorar la experiencia del usuario.
// app/dashboard/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Dashboard',
default: 'Dashboard',
},
description: 'Panel de administración de la aplicación',
}
// app/dashboard/users/page.tsx
export const metadata: Metadata = {
title: 'Usuarios',
description: 'Gestión de usuarios del sistema',
} // components/Header.tsx
'use client'
import { useState } from 'react'
import { BellIcon, UserCircleIcon } from '@heroicons/react/24/outline'
export default function Header() {
const [showNotifications, setShowNotifications] = useState(false)
return (
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="flex justify-between items-center px-6 py-4">
<div className="flex items-center space-x-4">
<h2 className="text-xl font-semibold text-gray-800">
Panel de Control
</h2>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => setShowNotifications(!showNotifications)}
className="p-2 text-gray-400 hover:text-gray-600"
>
<BellIcon className="h-6 w-6" />
</button>
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
<UserCircleIcon className="h-8 w-8" />
<span>Mi Cuenta</span>
</button>
</div>
</div>
</header>
)
} Cada página debe tener metadatos únicos y descriptivos. Usa el template en layout para mantener consistencia en los títulos.
Incluye navegación por teclado y etiquetas ARIA en componentes interactivos como el sidebar y header del dashboard.
Organiza las páginas por funcionalidad en carpetas. Usa layouts anidados para compartir UI común entre páginas relacionadas.