Инвесторы в 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 для интерактива.
Deep-link онбординг
Шеринг становится бесполезным, если получатель не может подключить свой адрес. Добавил параметр 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 токены остались, просто один роут стал публичным.