Перейти к содержимому

Данные о бэклинках в формате JSON: $100 бесплатных кредитов, без карты, вход через Google.

SEOMasters.dev
SEOMasters.devЗарегистрироваться

Проверщик обратных ссылок на Next.js с хранением ключа на сервере

CORS открыт, поэтому браузер может обращаться к API напрямую. Использование платного ключа в этом случае — ошибка. На этой странице показан тонкий серверный слой, который это исправляет.

Бронированные кабели соединяют стеклянный дом с подземным серверным помещем, защищённым тяжёлыми замками

Миф против реальности

МифРеальность
"Открытый CORS означает, что я могу передать ключ в бандл React."Открытый CORS означает, что запрос работает. Любой всё ещё может прочитать ключ во вкладке Network и потратить ваш баланс.
"Таблица ссылок должна рендериться на клиенте, потому что это панель управления."Запрос данных на сервере и отправка HTML. Пользователь видит строки при первой отрисовке, а ваш ключ никогда не покидает сервер.
"Бесплатные действия тоже нуждаются в защите."domain, overview, stats и suggest не требуют ключа, поэтому браузер может обращаться к ним напрямую.
"Я должен перезапрашивать данные при каждом просмотре страницы."Индекс перестраивается примерно раз в месяц. Кэша на 24 часа достаточно, чтобы ничего не упустить.
"Прокси должен перенаправлять всё, что отправляет клиент."Разрешите только те действия, которые вы используете. Перенаправленный action=search стоит 3 кредита.

Обработчик маршрута

App Router, файл app/api/links/route.ts. Переменная окружения никогда не попадёт в клиентский бандл, так как у неё нет префикса NEXT_PUBLIC_.

ts
import { NextRequest, NextResponse } from "next/server";

const ALLOWED = new Set(["refdomains", "anchors", "toppages"]);

export async function GET(req: NextRequest) {
  const p = req.nextUrl.searchParams;
  const action = p.get("action") ?? "";
  const q = (p.get("q") ?? "").toLowerCase();
  if (!ALLOWED.has(action) || !/^[a-z0-9.-]+$/.test(q)) {
    return NextResponse.json({ ok: false, error: "bad request" }, { status: 400 });
  }
  const url = `https://seomasters.dev/api.php?action=${action}&q=${q}&per=100`;
  const upstream = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.SEO_API_KEY}` },
    next: { revalidate: 86400 },
  });
  if (upstream.status === 402) {
    return NextResponse.json({ ok: false, error: "data temporarily unavailable" }, { status: 503 });
  }
  return new NextResponse(upstream.body, { status: upstream.status, headers: { "content-type": "application/json" } });
}

Почему обработчик выглядит именно так

Список разрешённых действий

Ошибка в вашем интерфейсе не должна случайно запускать платный запрос search на 3 кредита.

Регулярное выражение для q

Пройдёт только имя хоста. Никакой схемы, пути или строки запроса не просочится наверх.

revalidate: 86400

Один запрос к источнику на домен в день. При ежемесячных данных можно использовать и более длительные интервалы.

402 отображается как 503

Вашим посетителям не нужно знать, что баланс пуст. Вам — нужно, поэтому ведите лог.

Турист идёт по каменному мосту, пока дрон парит над туманной долиной

Рендеринг: что видят и поисковый робот, и клиент

Если эта панель управления доступна только после входа, выбор между серверным и клиентским рендерингом — вопрос UX. Если часть страниц публична (например, бесплатная страница поиска, которую нужно проиндексировать), рендерите первую таблицу на сервере.

Страница, которая приходит пустой оболочкой и заполняется после гидратации, представляет риск как для индексации, так и для медленных устройств. Google может выполнять JavaScript, но ставит этот шаг в очередь и не всегда успевает обработать его быстро.

Оставьте бесплатный обзор на сервере

Поместите дешёвый или бесплатный запрос overview в Server Component, а платные и более глубокие запросы пусть запускает посетитель по кнопке. Такое разделение также делает расходы на кредиты предсказуемыми.

Бесплатные запросы ничего не стоят, поэтому серверный рендеринг обзора не увеличивает расходы на кредиты. Только платные запросы требуют кнопки и сессии.

Сначала продумайте пустое состояние. Посетитель, увидевший пустую таблицу, решит, что инструмент сломан, и быстрая вторая отрисовка не исправит первое впечатление.

Выбирайте бесплатные действия для первого экрана, а платные — для детализации. Посетитель, открывший страницу и увидевший бесплатный обзор, не нажмёт на платную кнопку случайно.

Связывайте объяснение, а не только число

Привязка чисел — простая часть. Объяснить их сложнее, поэтому добавьте ссылку на формулу DR из всплывающей подсказки и упомяните, что индекс обновляется ежемесячно.

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

Мастер держит латунный ключ рядом с закрытым ящиком за деревянной перегородкой

Серверная и клиентская части одной панели управления

Большинству панелей управления нужны оба типа кода. Разделение определяется владением данными. Ключ, данные и правила доступа имеют своё место. Публичная страница проверки обратных ссылок требует такого же разделения, как и страница за логином.

Запрос на сервере, сортировка на клиенте

Таблица обратных ссылок — разумный клиентский компонент, если пользователи сортируют, фильтруют и переходят по страницам. Строки должны приходить из Server Component, который уже выполнил запрос к вашему маршруту, чтобы браузер никогда не хранил ключ или URL источника.

Сортировка десяти строк на клиенте происходит мгновенно. Сортировка пяти тысяч строк при каждом новом запросе медленная, поэтому храните полный набор данных в компоненте, который вы уже загрузили.

Держите всё просто. Таблица с мгновенной сортировкой лучше той, что перезапрашивает данные при каждом клике.

Держите границу клиента низкой

Сдвигайте границу как можно глубже в дереве компонентов. Динамически импортируемый виджет, работающий только на клиенте, рендерится в браузере, а компонент, получивший строки, остаётся на сервере. Передавайте строки через props, а запрос оставляйте выше границы.

Если вы переместите запрос ниже границы, браузер будет выполнять его самостоятельно. Это та самая ошибка, о которой говорится в разделе о утечке ключа, поэтому проверяйте вкладку Network после каждого изменения этой границы.

Где именно проходит линия? Сначала нарисуйте её на бумаге: один блок для запроса и по одному для каждого интерактивного элемента, и посчитайте стрелки, пересекающие границу.

Проверка оболочки и сессии остаются на сервере

Макет и проверка сессии определяют, что вообще может видеть посетитель, поэтому они должны быть на сервере. Интерактивные таблицы, формы и графики могут гидратироваться позже, не меняя этого решения.

Клиентская проверка может скрыть кнопку, но не может остановить запрос. Маршрут и серверный макет — это места, где доступ фактически контролируется.

Затем отправьте тот же запрос без cookie и убедитесь, что сервер отказывает в доступе, а не только страница.

То же правило касается самого прокси. Проверка сессии, которая происходит только в браузере, не защитит маршрут, поэтому маршрут также проверяет сессию перед тем, как потратить кредит.

Кэширование на домен, а не на посетителя

Маршрут формирует URL источника на основе домена, поэтому ключ кэша также зависит от домена. Один посетитель, проверивший сайт, «подогревает» кэш для следующего посетителя, запросившего тот же сайт.

В этом суть ежедневной ревалидации в обработчике. Два посетителя на одном домене стоят один запрос к источнику, а тысяча посетителей на тысяче доменов стоят тысячу запросов, поэтому кэш — это ваш бюджет.

Следите и за противоположным случаем. Страница, которая меняет строку запроса с помощью временной метки, полностью ломает кэш, и счёт это показывает раньше логов.

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

Укрепление системы в порядке выполнения

Обработчик — это лёгкая часть. Укрепление — это то место, где происходит утечка ключа или опустошение баланса, поэтому работайте через эти шаги по порядку.

Отсканируйте свою вкладку Network

Откройте Chrome DevTools на своей панели управления и внимательно прочитайте вкладку Network. Если запрос там содержит sk_live_ где-либо, смените ключ сегодня. Эта проверка занимает минуту и находит самую распространённую утечку.

Ищите ключ также в телах ответов, а не только в заголовках запросов. Ключ, продублированный в сообщении об ошибке, — это тоже утечка.

Ограничьте количество запросов на пользователя

Далее добавьте лимит на пользователя. Разместите его перед маршрутом. Обработчик выше тратит ваш кредит для любого, кто до него доберётся, поэтому привяжите его к сессии и ограничьте количество запросов в час. Выберите лимит на основе расчётов бюджета на странице кредитов.

Начните с щедрых лимитов и ужесточайте их позже. Лимит, который блокирует реальных пользователей с первого дня, заставит их найти другой инструмент.

Укажите, кто пополняет баланс

Также стоит добавить в руководство (runbook) строку о том, кого вызывают, когда баланс падает ниже установленного уровня, и кто его пополняет. Укажите конкретное имя.

Запишите шаг пополнения в виде чек-листа со ссылкой на страницу аккаунта и ожидаемым временем выполнения. Руководство, которое существует только в голове одного человека, бесполезно, когда этот человек в отпуске. Сам шаг занимает минуты, поэтому написание инструкции — более важная задача.

Ведите лог баланса до его исчерпания

Затем логируйте _credits из каждого ответа источника в свои метрики. График оставшегося баланса, обновляемый ежедневно, предупредит вас за неделю до появления HTTP 402.

Выберите уровень оповещения на основе вашего еженедельного использования. Число, выбранное из шаблона, редко соответствует вашим реальным запросам.

Храните последнее виденное значение как в памяти, так и в логе, чтобы маршрут мог отказывать в платных запросах, когда число падает ниже уровня оповещения. Эта проверка занимает одну строку кода и предотвращает расходование остатка при всплеске трафика.

Храните список разрешённых действий в одном месте

Ещё одна привычка окупится. Храните список разрешённых действий в общей константе, импортируемой обработчиком и тестами, чтобы новое действие не попало в продакшн без редактирования видимого списка и замечания ревьюера.

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

Тестируйте среду выполнения, в которой разворачиваете

В последнюю очередь запустите обработчик в Node 20 или в edge-среде по вашему выбору и протестируйте оба варианта. Потоковая передача тела источника, как в коде, работает в обоих, но обработка заголовков различается, поэтому её стоит протестировать.

Добавьте один тест, который вызывает маршрут с недопустимым действием, и один с допустимым действием, и проверяйте коды состояния. Эти два теста ловят большинство регрессий в списке разрешённых действий и обработке заголовков.

Подключение прокси по порядку

  1. 1

    Добавьте ключ на сервере

    Сохраните ключ в переменных окружения сервера под именем без публичного префикса, затем перезапустите dev-сервер, чтобы он загрузился.

  2. 2

    Сначала напишите список разрешённых действий

    Перечислите действия, которые действительно вызывают ваши экраны. Всё остальное получит 400 от вашего маршрута, прежде чем достигнет API источника.

  3. 3

    Запросите маршрут из Server Component

    Рендерите строки там, где приходят данные. Браузер будет обращаться только к вашему домену, поэтому адрес источника останется за пределами страницы.

  4. 4

    Следите за балансом

    Логгируйте _credits в каждом ответе источника и установите уровень оповещения, по которому можно принять меры, до того как 402 достигнет посетителя как 503.