SiteBrief: white-label зсередини — кастомні домени, PDF з логотипом клієнта і клієнтський портал

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

(Перша частина — тут, друга — тут)

У минулому пості обіцяв розкрити white-label. Тримаю слово — і одразу зізнаюсь у прогалині, яку знайшов поки писав цей текст.

Кастомні домени для status page

Агенція хоче, щоб клієнт бачив status.їхнєагентство.com, а не sitebrief.net/status/xyz. Реалізував — але мінімалістично.

Логіка проста: на профілі є три текстові поля — dashboard_domain, brand_domain, status_page_custom_domain. Коли запит приходить, дивлюсь на заголовок Host і шукаю збіг:

const { data: profile } = await supabase
  .from('profiles')
  .select('brand_name, company_name, full_name, brand_color, logo_url')
  .or(`dashboard_domain.eq.${host},brand_domain.eq.${host},status_page_custom_domain.eq.${host}`)
  .single()

Працює. Але є дірка, яку я зараз чесно визнаю: немає верифікації володіння доменом. Жодного TXT-запису, жодного CNAME-чек. Якщо хтось випадково (або не випадково) вкаже в своєму профілі домен, який вже зайнятий іншим — система просто довіряє рядку в базі. SSL теж не автоматизований через якийсь provisioning — покладаюсь на те, що Netlify сам обробляє сертифікат, якщо домен прив’язаний на рівні DNS.

Для MVP це працювало, бо клієнтська база маленька і ризик низький. Але це той тип технічного боргу, який треба закрити до того, як з’явиться перший серйозний агенційний клієнт з власною інфраструктурою. Додам перевірку власності через TXT-запис — це наступне в черзі.

White-label PDF звіти

Тут менше драми, більше інженерії. Генерація через @react-pdf/renderer.

Перша проблема: react-pdf погано рендерить SVG і віддалені картинки на льоту. Рішення — підвантажувати лого на сервері і вбудовувати як base64, явно відсікаючи SVG:

const res = await fetch(profile.logo_url)
const buf = await res.arrayBuffer()
const mime = res.headers.get('content-type') ?? 'image/png'
if (mime.startsWith('image/') && !mime.includes('svg')) {
  logoSrc = `data:${mime};base64,${Buffer.from(buf).toString('base64')}`
}

White-label сам по собі — просто умовний рендер у футері:

const isWhiteLabel = !!profile?.brand_name || !!profile?.is_admin
...
<Text>{isWhiteLabel ? '' : ' · Powered by SiteBrief'}</Text>

AI-частина звіту (executive summary) генерується через Claude Haiku — короткий текстовий висновок по даним моніторингу за період.

Окрема технічна заковика: Supabase PostgREST обмежує відповідь до 1000 рядків. Коли звіт за 90 днів витягує історію перевірок кожні 2 хвилини — це легко мільйони записів. Довелось писати ручну пагінацію через .range() в циклі, бо штатний клієнт цього не робить сам:

async function fetchAllChecks(from = 0, acc = []) {
  const { data } = await query.range(from, from + PAGE - 1)
  if (!data?.length) return acc
  return fetchAllChecks(from + PAGE, [...acc, ...data])
}

Клієнтський портал

Це окрема фіча для агенцій: створюєш портал на email клієнта, клієнт логіниться через magic link, бачить read-only вигляд — uptime, SLA, інциденти по своїх сайтах, в брендингу агенції.

Найцікавіше тут — не сама фіча, а одна деталь безпеки, яку легко проґавити. URL порталу містить токен. Якщо токен сам по собі давав доступ — будь-яка людина, що побачила лінк в логах сервера або в referrer заголовку іншого сайту, отримала б постійний доступ до даних клієнта.

Тому токен з URL — це тільки ідентифікатор порталу, не ключ доступу. Реальний доступ видається тільки після magic-link логіну і перевіряється сесія:

// Без цього URL-токен (який ловиться через логи/referrer)
// дав би постійний доступ. Сесія видається тільки після логіну.
const session = verifySession(cookieStore.get('client_session')?.value)
if (!session || session.portal_token !== token) {
  redirect('/client-login?error=login_required')
}

Проста різниця — токен-як-ідентифікатор vs токен-як-секрет — але саме вона визначає, чи можна спокійно ділитись лінком портала з клієнтом по email.

Спільна нитка

Всі три фічі — кастомний домен, PDF, портал — читають з одних і тих самих полів профілю: brand_name, brand_color, logo_url. Один джерело правди для всього брендингу. Коли додаю нову white-label поверхню, не треба думати про нову систему — досить підключитись до тих же трьох колонок.

Наступного разу — як я ходив на Product Hunt та інші платформи для лончу, і що з цього вийшло.

Питання, критика — в коментарях.

sitebrief.net

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі