Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use a API do Pexels para buscar fotos e vídeos gratuitos de alta qualidade. O Pexels usa um cabeçalho Authorization para autenticação e fornece coleções selecionadas, além de busca.
Passo 1: Obtenha uma chave de API
.env.local:PEXELS_API_KEY=your_api_key_herePasso 2: Crie um cliente de API tipado
// lib/pexels.ts
interface PexelsPhotoSrc {
original: string;
large2x: string;
large: string;
medium: string;
small: string;
portrait: string;
landscape: string;
tiny: string;
}
interface PexelsPhoto {
id: number;
width: number;
height: number;
url: string;
photographer: string;
photographer_url: string;
photographer_id: number;
avg_color: string;
src: PexelsPhotoSrc;
alt: string;
}
interface PexelsSearchResponse {
total_results: number;
page: number;
per_page: number;
photos: PexelsPhoto[];
next_page?: string;
prev_page?: string;
}
const PEXELS_BASE = "https://api.pexels.com/v1";
async function pexelsFetch<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${PEXELS_BASE}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
const res = await fetch(url.toString(), {
headers: {
Authorization: process.env.PEXELS_API_KEY!,
},
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Pexels API error: ${res.status} ${res.statusText}`);
}
return res.json();
}
export async function searchPhotos(query: string, page = 1, perPage = 15) {
return pexelsFetch<PexelsSearchResponse>("/search", {
query,
page: String(page),
per_page: String(perPage),
});
}
export async function getCuratedPhotos(page = 1, perPage = 15) {
return pexelsFetch<PexelsSearchResponse>("/curated", {
page: String(page),
per_page: String(perPage),
});
}
export async function getPhoto(id: number) {
return pexelsFetch<PexelsPhoto>(`/photos/${id}`);
}
export type { PexelsPhoto, PexelsPhotoSrc, PexelsSearchResponse };Um componente seletor de imagem de destaque que permite aos editores pesquisar e selecionar uma imagem de destaque:
// app/hero-picker/page.tsx
import { getCuratedPhotos, searchPhotos } from "@/lib/pexels";
import { HeroImagePicker } from "./hero-image-picker";
interface PageProps {
searchParams: Promise<{ q?: string }>;
}
export default async function HeroPickerPage({ searchParams }: PageProps) {
const params = await searchParams;
const query = params.q;
const results = query
? await searchPhotos(query, 1, 12)
: await getCuratedPhotos(1, 12);
return (
<main className="mx-auto max-w-5xl px-4 py-8">
<h1 className="mb-2 text-3xl font-bold">Hero Image Picker</h1>
<p className="mb-6 text-gray-500">
{query ? `Results for "${query}"` : "Curated photos"}
</p>
<HeroImagePicker photos={results.photos} initialQuery={query || ""} />
</main>
);
}// app/hero-picker/hero-image-picker.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import type { PexelsPhoto } from "@/lib/pexels";
interface HeroImagePickerProps {
photos: PexelsPhoto[];
initialQuery: string;
}
export function HeroImagePicker({ photos, initialQuery }: HeroImagePickerProps) {
const [query, setQuery] = useState(initialQuery);
const [selected, setSelected] = useState<PexelsPhoto | null>(null);
const router = useRouter();
function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/hero-picker?q=${encodeURIComponent(query.trim())}`);
}
}
return (
<div>
<form onSubmit={handleSearch} className="mb-6 flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for hero images..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2"
/>
<button
type="submit"
className="rounded-lg bg-emerald-600 px-6 py-2 text-white hover:bg-emerald-700"
>
Search
</button>
</form>
{selected && (
<div className="mb-8 overflow-hidden rounded-2xl border-2 border-emerald-500">
<p className="bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-700">
Selected Hero Image
</p>
<div className="relative aspect-[21/9]">
<Image
src={selected.src.large2x}
alt={selected.alt}
fill
sizes="100vw"
priority
className="object-cover"
/>
</div>
<div className="flex items-center justify-between bg-gray-50 px-4 py-3 text-sm">
<span>
Photo by{" "}
<a
href={selected.photographer_url}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-emerald-600 hover:underline"
>
{selected.photographer}
</a>
{" on "}
<a
href="https://www.pexels.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-emerald-600 hover:underline"
>
Pexels
</a>
</span>
<span className="text-gray-500">
{selected.width} x {selected.height}
</span>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{photos.map((photo) => (
<button
key={photo.id}
onClick={() => setSelected(photo)}
className={`group relative overflow-hidden rounded-lg transition-all ${
selected?.id === photo.id
? "ring-3 ring-emerald-500 ring-offset-2"
: "hover:ring-2 hover:ring-gray-300"
}`}
>
<div className="relative aspect-[3/2]" style={{ backgroundColor: photo.avg_color }}>
<Image
src={photo.src.medium}
alt={photo.alt}
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover"
/>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/50 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100">
{photo.photographer}
</div>
</button>
))}
</div>
</div>
);
}Authorization contendo apenas a chave de API (sem prefixo "Bearer")./v1/search aceita query, orientation (landscape, portrait, square), size (large, medium, small), color, locale, page e per_page (máximo 80)./v1/curated retorna fotos selecionadas editorialmente, úteis para conteúdo padrão ou em destaque.src com tamanhos pré-gerados: original, large2x (1880px), large (940px), medium (350px), small (130px), portrait (800x1200), landscape (1200x627) e tiny (280x200).avg_color fornece a cor dominante como uma string hexadecimal, útil como um plano de fundo de placeholder enquanto a imagem carrega.next_page e prev_page no corpo da resposta em vez de contagens totais de páginas.Usando avg_color como um placeholder de carregamento:
<div
className="relative aspect-[3/2]"
style={{ backgroundColor: photo.avg_color }}
>
<Image src={photo.src.medium} alt={photo.alt} fill className="object-cover" />
</div>Buscando por orientação:
const landscapePhotos = await pexelsFetch<PexelsSearchResponse>("/search", {
query: "mountains",
orientation: "landscape",
per_page: "10",
});PexelsPhotoSrc mapeia diretamente para o objeto src da API. Cada chave de tamanho está sempre presente na resposta.next_page e prev_page são strings opcionais (URLs) que podem estar ausentes na primeira ou última página.Authorization é a chave de API bruta, não Bearer <key>. Usar o prefixo Bearer retornará um 401.per_page é 80. Valores acima de 80 serão silenciosamente definidos como 80.images.pexels.com como nome do host. Adicione isso a remotePatterns em next.config.ts.alt da API pode estar vazio ou genérico. Considere adicionar seu próprio texto alternativo para acessibilidade.| Abordagem | Prós | Contras |
|---|---|---|
| API Pexels | Boa qualidade, endpoint selecionado, campo avg_color | Biblioteca menor que Unsplash |
| API Unsplash | Maior biblioteca de fotos gratuitas | Atribuição estritamente necessária |
| API Pixabay | Nenhuma atribuição necessária, inclui vídeos | Qualidade média inferior |
| API Shutterstock | Biblioteca massiva, conteúdo editorial | Pago, licenciamento complexo |
Authorization com a chave de API bruta como valor.Bearer; fazer isso retorna um erro 401..env.local como PEXELS_API_KEY.page e per_page.original, large2x (1880px), large (940px), medium (350px), small (130px).portrait (800x1200), landscape (1200x627), tiny (280x200).<div style={{ backgroundColor: photo.avg_color }}>
<Image src={photo.src.medium} alt={photo.alt} fill />
</div>Ele fornece a cor dominante como uma string hexadecimal, útil como um plano de fundo de placeholder enquanto a imagem carrega.
Authorization: abc123xyz.Bearer abc123xyz retorna um erro 401.next_page e prev_page como strings de URL opcionais.prev_page) ou na última página (sem next_page).export type { PexelsPhoto, PexelsPhotoSrc, PexelsSearchResponse };Exporte tipos junto com as funções de API para que os componentes consumidores possam referenciá-los sem redeclarar.
alt pode estar vazio ou genérico.images.pexels.com a remotePatterns em next.config.ts.next/image não carregará as fotos do Pexels.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥