Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Utiliza la API de Unsplash para buscar y mostrar fotos de alta calidad y libres de derechos en tu aplicación Next.js. Obtén datos en Server Components para mantener tu clave API segura y evitar limitaciones de velocidad en el lado del cliente.
Paso 1: Obtén una clave API
.env.local:UNSPLASH_ACCESS_KEY=your_access_key_herePaso 2: Crea un cliente API tipado
// lib/unsplash.ts
interface UnsplashPhoto {
id: string;
alt_description: string | null;
urls: {
raw: string;
full: string;
regular: string;
small: string;
thumb: string;
};
user: {
name: string;
links: {
html: string;
};
};
links: {
download_location: string;
};
width: number;
height: number;
}
interface UnsplashSearchResponse {
total: number;
total_pages: number;
results: UnsplashPhoto[];
}
const UNSPLASH_BASE = "https://api.unsplash.com";
async function unsplashFetch<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${UNSPLASH_BASE}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
const res = await fetch(url.toString(), {
headers: {
Authorization: `Client-ID ${process.env.UNSPLASH_ACCESS_KEY}`,
},
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Unsplash API error: ${res.status} ${res.statusText}`);
}
return res.json();
}
export async function searchPhotos(query: string, page = 1, perPage = 12) {
return unsplashFetch<UnsplashSearchResponse>("/search/photos", {
query,
page: String(page),
per_page: String(perPage),
orientation: "landscape",
});
}
export async function getRandomPhotos(count = 6, query?: string) {
const params: Record<string, string> = { count: String(count) };
if (query) params.query = query;
return unsplashFetch<UnsplashPhoto[]>("/photos/random", params);
}
export async function trackDownload(downloadLocation: string) {
await unsplashFetch<void>(downloadLocation.replace(UNSPLASH_BASE, ""));
}Un componente de búsqueda de imágenes que obtiene datos de Unsplash con la atribución adecuada:
// app/photos/page.tsx
import { searchPhotos } from "@/lib/unsplash";
import { PhotoGrid } from "./photo-grid";
import { SearchForm } from "./search-form";
interface PageProps {
searchParams: Promise<{ q?: string; page?: string }>;
}
export default async function PhotosPage({ searchParams }: PageProps) {
const params = await searchParams;
const query = params.q || "nature";
const page = Number(params.page) || 1;
const results = await searchPhotos(query, page);
return (
<main className="mx-auto max-w-6xl px-4 py-8">
<h1 className="mb-6 text-3xl font-bold">Búsqueda de Fotos</h1>
<SearchForm initialQuery={query} />
<p className="mb-4 text-sm text-gray-500">
{results.total.toLocaleString()} resultados para "{query}"
</p>
<PhotoGrid photos={results.results} />
<div className="mt-6 flex justify-center gap-4">
{page > 1 && (
<a href={`/photos?q=${query}&page=${page - 1}`} className="rounded bg-gray-200 px-4 py-2">
Anterior
</a>
)}
{page < results.total_pages && (
<a href={`/photos?q=${query}&page=${page + 1}`} className="rounded bg-gray-200 px-4 py-2">
Siguiente
</a>
)}
</div>
</main>
);
}// app/photos/search-form.tsx
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function SearchForm({ initialQuery }: { initialQuery: string }) {
const [query, setQuery] = useState(initialQuery);
const router = useRouter();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/photos?q=${encodeURIComponent(query.trim())}`);
}
}
return (
<form onSubmit={handleSubmit} className="mb-6 flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar fotos..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2"
/>
<button type="submit" className="rounded-lg bg-black px-6 py-2 text-white hover:bg-gray-800">
Buscar
</button>
</form>
);
}// app/photos/photo-grid.tsx
import Image from "next/image";
interface Photo {
id: string;
alt_description: string | null;
urls: { regular: string; small: string };
user: { name: string; links: { html: string } };
width: number;
height: number;
}
export function PhotoGrid({ photos }: { photos: Photo[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{photos.map((photo) => (
<figure key={photo.id} className="group relative overflow-hidden rounded-xl">
<div className="relative aspect-[3/2]">
<Image
src={photo.urls.regular}
alt={photo.alt_description || "Foto de Unsplash"}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<figcaption className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-4 text-white opacity-0 transition-opacity group-hover:opacity-100">
Foto de{" "}
<a
href={`${photo.user.links.html}?utm_source=your_app&utm_medium=referral`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{photo.user.name}
</a>
{" en "}
<a
href="https://unsplash.com/?utm_source=your_app&utm_medium=referral"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Unsplash
</a>
</figcaption>
</figure>
))}
</div>
);
}Authorization: Client-ID <access_key>./search/photos acepta parámetros query, page, per_page, orientation, color y order_by./photos/random devuelve fotos aleatorias, opcionalmente filtradas por query, collections o topics.raw (original), full (JPEG de alta resolución), regular (1080px de ancho), small (400px de ancho) y thumb (200px de ancho).download_location cuando un usuario descargue o utilice una foto.Componente de foto aleatoria para secciones heroicas:
// app/components/random-hero.tsx
import Image from "next/image";
import { getRandomPhotos } from "@/lib/unsplash";
export async function RandomHero() {
const [photo] = await getRandomPhotos(1, "landscape");
return (
<div className="relative h-[60vh] w-full overflow-hidden">
<Image
src={photo.urls.full}
alt={photo.alt_description || "Imagen heroica"}
fill
priority
sizes="100vw"
className="object-cover"
/>
<div className="absolute bottom-4 right-4 text-sm text-white/80">
Foto de{" "}
<a href={photo.user.links.html} className="underline">
{photo.user.name}
</a>
</div>
</div>
);
}unsplash-js proporciona tipos, pero un fetch tipado ligero suele ser suficiente.next: { revalidate } en las opciones de fetch para controlar el almacenamiento en caché del lado del servidor en Next.js.// Opcional: utiliza el SDK oficial
// npm install unsplash-js
import { createApi } from "unsplash-js";
const unsplash = createApi({
accessKey: process.env.UNSPLASH_ACCESS_KEY!,
});utm_source=your_app&utm_medium=referral en los enlaces de atribución.download_location cuando un usuario selecciona o descarga activamente una foto. Este es un requisito de los términos de la API, no opcional.raw devuelve una imagen sin procesar que puede ser muy grande (10MB+). Utiliza regular o small para mostrar y full para descargas.next/image requiere images.remotePatterns para images.unsplash.com en next.config.ts. Sin esto, las imágenes no se cargarán.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| API de Unsplash | Fotos de la más alta calidad, biblioteca grande | Atribución requerida, límites de velocidad |
| API de Pexels | No se requiere atribución (pero se aprecia), límites generosos | Biblioteca más pequeña que Unsplash |
| API de Pixabay | Verdaderamente gratis, sin atribución necesaria | Calidad promedio más baja, anuncios en resultados |
| Lorem Picsum | Marcadores de posición simples basados en URL, sin clave de API | Solo fotos aleatorias, sin búsqueda |
Authorization: Client-ID <access_key> en cada solicitud..env.local como UNSPLASH_ACCESS_KEY.raw -- original, sin procesar (puede ser 10MB+).full -- JPEG de alta resolución.regular -- 1080px de ancho.small -- 400px de ancho.thumb -- 200px de ancho.utm_source=your_app&utm_medium=referral en los enlaces de atribución.download_location cuando un usuario descarga o utiliza una foto.next: { revalidate } para el almacenamiento en caché del lado del servidor.// next.config.ts
images: {
remotePatterns: [
{ protocol: "https", hostname: "images.unsplash.com" },
],
}Sin esto, next/image no podrá cargar fotos de Unsplash.
UnsplashPhoto, UnsplashSearchResponse).unsplashFetch<T> para solicitudes seguras de tipo.unsplash-js también proporciona tipos, pero un fetch tipado ligero suele ser suficiente.interface PageProps {
searchParams: Promise<{ q?: string; page?: string }>;
}
const params = await searchParams;
const query = params.q || "nature";En el App Router de Next.js, searchParams es una Promise que debe ser esperada.
raw devuelve una imagen sin procesar que puede ser muy grande (10MB+).regular o small para mostrar.full solo para descargas.next: { revalidate: 3600 } almacena en caché la respuesta durante 1 hora en el servidor.links.download_location de cada foto.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥