Desarrollo de Páginas Específicas en Next.js

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.

Estructura de Carpetas

bash
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

Creación del Home

Home Básico

typescript
// 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>
)
}

Home con Componentes

typescript
// 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.

Construcción del Dashboard

Layout del Dashboard

typescript
// 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>
)
}

Dashboard Principal

typescript
// 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>
)
}
typescript
// 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.

Rutas Dashboard Específicas

Página de Usuarios

typescript
// 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>
)
}

Página de Configuración

typescript
// 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>
)
}

Páginas Dinámicas

Página de Usuario Individual

typescript
// 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>
)
}

Página 404 Personalizada

typescript
// 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>
)
}

Loading States

Loading del Dashboard

typescript
// 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>
)
}

Loading de Usuarios

typescript
// 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.

Metadatos por Página

Metadatos del Dashboard

typescript
// 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',
}

Componentes Reutilizables

Header del Dashboard

typescript
// 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.