Инвесторы в GonkaHub хотели делиться результатами в Telegram — "смотри, моя нода намайнила 150 GONKA". Проблема: вся платформа за авторизацией. NextAuth 5 с Bearer токенами. Каждый эндпоинт требует авторизацию. Расшарить ссылку на транзакцию невозможно — получатель видит форму логина.

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

Публичный эндпоинт

Первое решение — DRF view без авторизации. PublicTransactionView возвращает санитизированные данные: хэш транзакции, сумму, тип, timestamp. Но не привязку к конкретному инвестору. Не email, не ID пользователя.

class PublicTransactionView(APIView):
    permission_classes = [AllowAny]

    def get(self, request, tx_hash):
        tx = Transaction.objects.filter(hash__startswith=tx_hash).first()
        if not tx:
            return Response({'error': 'Not found'}, status=404)

        return Response({
            'hash': tx.hash,
            'amount': str(tx.amount),
            'type': tx.tx_type,
            'timestamp': tx.created_at.isoformat()
        })

Полный хэш транзакции — 64 символа. В URL это некрасиво. Взял первые 8 символов: txHash.slice(0, 8). Получается gonkahub.com/tx/a1b2c3d4. В базе запрос через hash__startswith — если коллизий нет, возвращает единственную транзакцию.

SSR для Telegram превью

Telegram показывает OG-карточку при шеринге ссылки. Но только если meta теги рендерятся на сервере. Client-side рендеринг не работает — Telegram не выполняет JavaScript.

Next.js 14 App Router: страница /tx/[shortHash] как Server Component. Загружаю данные транзакции на сервере, рендерю OG meta теги в <head>. Telegram получает готовый HTML с превью.

// app/tx/[shortHash]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
    const tx = await fetchPublicTransaction(params.shortHash);

    return {
        title: `Транзакция ${params.shortHash}`,
        description: `${tx.amount} GONKA — ${tx.type}`,
        openGraph: {
            title: `Транзакция ${params.shortHash}`,
            description: `${tx.amount} GONKA — ${tx.type}`,
            images: ['/og-image.png']
        }
    };
}

На клиенте добавил интерактивные кнопки шеринга. ShareButtons компонент — Client Component с директивой "use client". Две кнопки: копирование ссылки в буфер и Telegram share.

Копирование и шеринг

navigator.clipboard.writeText() для копирования. После копирования показываю toast-нотификацию — пользователь видит фидбек.

Telegram share через официальный URL: t.me/share/url?url=${url}&text=${text}. Параметр url — encoded ссылка на транзакцию, text — заголовок. Открываю в новой вкладке через window.open().

export function ShareButtons({ txHash }: { txHash: string }) {
    const shortHash = txHash.slice(0, 8);
    const shareUrl = `https://gonkahub.com/tx/${shortHash}`;

    const copyLink = async () => {
        await navigator.clipboard.writeText(shareUrl);
        toast.success('Ссылка скопирована');
    };

    const shareToTelegram = () => {
        const text = encodeURIComponent('Моя транзакция в Gonka');
        const url = encodeURIComponent(shareUrl);
        window.open(
            `https://t.me/share/url?url=${url}&text=${text}`,
            '_blank'
        );
    };

    return (
        <div className="flex gap-2">
            <button onClick={copyLink}>Копировать ссылку</button>
            <button onClick={shareToTelegram}>Telegram</button>
        </div>
    );
}

Кнопки работают только на клиенте — поэтому Client Component. Но OG-метаданные рендерятся на сервере. Гибридный подход: SSR для ботов и поисковиков, CSR для интерактива.

Шеринг становится бесполезным, если получатель не может подключить свой адрес. Добавил параметр add_ в URL. Когда незарегистрированный пользователь открывает ссылку вида gonkahub.com/tx/a1b2c3d4?add_=1, срабатывает AddAddressModal.

Модалка предлагает вставить адрес кошелька. Проверяю адрес через backend, сохраняю в базу, показываю личный дашборд. Расшаренная ссылка становится механизмом привлечения — growth через реферальную механику без явного реферального кода.

useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    if (params.get('add_') && !session) {
        setAddModalOpen(true);
    }
}, [session]);

Серверная пагинация

Публичная страница транзакции показывает список наград. Если наград 500+, клиентская пагинация убьёт браузер. Перешёл на серверную пагинацию с курсорной навигацией.

Backend возвращает limit записей + next_cursor. Frontend запрашивает следующую порцию через ?cursor=abc123&limit=20. DRF имеет встроенный CursorPagination — настроил порядок сортировки по created_at, курсор кодируется в base64.

class RewardPagination(CursorPagination):
    page_size = 20
    ordering = '-created_at'

На клиенте использую React Query для управления состоянием загрузки. При скролле до конца списка срабатывает fetchNextPage(), данные аппендятся к существующему списку.

Результат

19 января 2026 задеплоил публичные страницы транзакций. Инвесторы расшаривают результаты в Telegram — видят красивое превью с суммой и типом транзакции. Клик по ссылке ведёт на страницу с деталями и кнопкой "Добавить свой адрес".

За первую неделю 37 новых адресов пришли через deep-link онбординг. Конверсия из клика по расшаренной ссылке в добавление адреса — 12%. Growth без рекламного бюджета.

SSR решил проблему с превью. AllowAny эндпоинт не раскрывает приватную информацию. Deep-link механика превращает шеринг в канал привлечения. И всё это работает без изменения архитектуры авторизации — NextAuth остался, Bearer токены остались, просто один роут стал публичным.