Реализация NFT-галереи на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация NFT-галереи на сайте
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация NFT-галереи на сайте

NFT-галерея — страница или секция сайта, которая показывает токены коллекции: сетка карточек, фильтрация по trait-атрибутам, сортировка по редкости, детальная страница токена с историей. Это не то же самое, что галерея кошелька пользователя — здесь отображается контент коллекции целиком, обычно доступный без подключения кошелька.

Источник данных

Два подхода: читать метаданные напрямую из контракта через tokenURI или использовать NFT API.

Прямое чтение через tokenURI — медленно для коллекций 5000+ токенов. На каждый токен — один RPC-запрос, потом запрос к IPFS за JSON. Для галереи с фильтрацией по trait это неприемлемо.

OpenSea API, Alchemy NFT API и Reservoir API индексируют метаданные и отдают их с фильтрацией. Reservoir — лучший выбор для gallery без паймента: бесплатный тир 60 req/min, поддержка trait-фильтров и редкости.

Загрузка коллекции через Reservoir API

// lib/collection.ts
const RESERVOIR_BASE = 'https://api.reservoir.tools';

export interface CollectionToken {
  tokenId: string;
  name: string;
  image: string;
  rarityScore: number;
  rarityRank: number;
  attributes: Array<{ key: string; value: string; tokenCount: number }>;
  lastSalePrice: string | null;
  floorAskPrice: string | null;
}

export async function getCollectionTokens(
  contractAddress: string,
  opts: {
    limit?: number;
    offset?: number;
    sortBy?: 'floorAskPrice' | 'rarity' | 'tokenId';
    attributes?: Record<string, string>;
  } = {},
): Promise<{ tokens: CollectionToken[]; total: number }> {
  const params = new URLSearchParams({
    collection: contractAddress,
    limit: String(opts.limit ?? 20),
    offset: String(opts.offset ?? 0),
    sortBy: opts.sortBy ?? 'tokenId',
    includeAttributes: 'true',
    includeLastSale: 'true',
  });

  if (opts.attributes) {
    for (const [key, value] of Object.entries(opts.attributes)) {
      params.append('attributes[' + key + ']', value);
    }
  }

  const res = await fetch(`${RESERVOIR_BASE}/tokens/v7?${params}`, {
    headers: { 'x-api-key': process.env.RESERVOIR_API_KEY ?? '' },
    next: { revalidate: 60 },
  });

  const data = await res.json();

  return {
    tokens: data.tokens.map(mapToken),
    total: data.totalTokens ?? 0,
  };
}

Фильтры по атрибутам

// Получить все trait-типы и значения коллекции
export async function getCollectionAttributes(contractAddress: string) {
  const res = await fetch(
    `${RESERVOIR_BASE}/collections/${contractAddress}/attributes/all/v4`,
    { headers: { 'x-api-key': process.env.RESERVOIR_API_KEY ?? '' } },
  );
  const data = await res.json();

  // Структура: { attributes: [{ key, kind, values: [{ value, count }] }] }
  return data.attributes as Array<{
    key: string;
    kind: 'string' | 'number' | 'range';
    values: Array<{ value: string; count: number }>;
  }>;
}

Компонент галереи с URL-фильтрами

Фильтры хранятся в URL — пользователь может поделиться ссылкой на отфильтрованный вид:

// app/gallery/page.tsx (Next.js App Router)
import { useSearchParams, useRouter } from 'next/navigation';
import { getCollectionTokens, getCollectionAttributes } from '@/lib/collection';

const CONTRACT = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!;

export default async function GalleryPage({
  searchParams,
}: {
  searchParams: Record<string, string>;
}) {
  const page = parseInt(searchParams.page ?? '1');
  const sortBy = (searchParams.sort ?? 'tokenId') as 'floorAskPrice' | 'rarity' | 'tokenId';

  // Собираем attribute-фильтры из search params
  const attributes: Record<string, string> = {};
  for (const [key, value] of Object.entries(searchParams)) {
    if (!['page', 'sort'].includes(key)) {
      attributes[key] = value;
    }
  }

  const [{ tokens, total }, attrs] = await Promise.all([
    getCollectionTokens(CONTRACT, {
      limit: 24,
      offset: (page - 1) * 24,
      sortBy,
      attributes: Object.keys(attributes).length ? attributes : undefined,
    }),
    getCollectionAttributes(CONTRACT),
  ]);

  return (
    <div className="flex gap-8">
      <TraitFilters attributes={attrs} activeFilters={attributes} />
      <div className="flex-1">
        <SortControl currentSort={sortBy} />
        <TokenGrid tokens={tokens} />
        <Pagination total={total} page={page} perPage={24} />
      </div>
    </div>
  );
}

Карточка токена с редкостью

// components/TokenCard.tsx
import Link from 'next/link';
import { CollectionToken } from '@/lib/collection';

function RarityBadge({ rank, total }: { rank: number; total: number }) {
  const percentile = (rank / total) * 100;
  const tier =
    percentile <= 1 ? { label: 'Legendary', color: 'text-yellow-400 bg-yellow-400/10' } :
    percentile <= 5 ? { label: 'Epic', color: 'text-purple-400 bg-purple-400/10' } :
    percentile <= 15 ? { label: 'Rare', color: 'text-blue-400 bg-blue-400/10' } :
    { label: 'Common', color: 'text-neutral-400 bg-neutral-400/10' };

  return (
    <span className={`rounded-md px-2 py-0.5 text-xs font-medium ${tier.color}`}>
      #{rank} · {tier.label}
    </span>
  );
}

export function TokenCard({ token, totalSupply }: { token: CollectionToken; totalSupply: number }) {
  return (
    <Link href={`/gallery/${token.tokenId}`} className="group block">
      <div className="overflow-hidden rounded-xl border border-white/5 bg-neutral-900 transition hover:border-white/20">
        <div className="relative aspect-square overflow-hidden bg-neutral-800">
          <img
            src={token.image}
            alt={token.name}
            loading="lazy"
            className="h-full w-full object-cover transition-transform group-hover:scale-105"
          />
        </div>
        <div className="p-3 space-y-2">
          <div className="flex items-start justify-between gap-2">
            <span className="font-medium truncate">{token.name}</span>
            <RarityBadge rank={token.rarityRank} total={totalSupply} />
          </div>
          {token.floorAskPrice && (
            <p className="text-sm text-neutral-400">
              Floor: <span className="text-white">{token.floorAskPrice} ETH</span>
            </p>
          )}
        </div>
      </div>
    </Link>
  );
}

Детальная страница токена

// app/gallery/[tokenId]/page.tsx
export default async function TokenPage({ params }: { params: { tokenId: string } }) {
  const token = await getToken(CONTRACT, params.tokenId);

  return (
    <div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
      <TokenImage src={token.image} name={token.name} />
      <div className="space-y-6">
        <TokenHeader token={token} />
        <AttributeGrid attributes={token.attributes} />
        <TradeActions token={token} />
        <SaleHistory contractAddress={CONTRACT} tokenId={params.tokenId} />
      </div>
    </div>
  );
}

SEO и статическая генерация

Для коллекций до 10000 токенов — статическая генерация generateStaticParams в Next.js. Для больших коллекций — ISR с revalidate: 3600.

Сроки: сетка с базовыми фильтрами и детальной страницей — 2–3 дня. Полная галерея с сортировкой по редкости, многоуровневыми фильтрами, историей продаж и SEO-оптимизацией — 5–7 дней.