Сьогодні ми запустили третє джерело міграції в межах нашого поетапного впровадження (Tier-3). Вставте API-ключ Short.io, виберіть вихідний домен Short.io (наприклад, example.short.gy), виберіть цільовий домен Elido і натисніть «Почати». Через три-шість хвилин кожне посилання опиниться на вашому домені Elido зі збереженим слагом.
Цей пост — інженерний звіт про те, що є специфічного для Short.io, що нас здивувало в їхньому REST API, і чому ми зрештою надали перевагу завданням по доменах, а не пакетній обробці по всьому акаунту.
По доменах, а не по акаунту#
Модель даних Short.io має одну особливість, яка визначила весь UX запуску: посилання організовані за доменами, а /links ендпоінт здійснює пагінацію по кожному домену окремо. Немає запиту «дай мені всі посилання по всіх доменах у цьому акаунті».
Ми розглянули кілька варіантів дизайну:
- A. Ітерувати кожен домен на стороні сервера, представити користувачеві одне завдання. Швидше з точки зору кількості кліків; складніше відобразити прогрес і вибір стратегії конфліктів для кожного домену.
- B. Одне завдання Elido на вихідний домен. Повільніше з точки зору кліків (користувач запускає N завдань для N доменів), але кожне завдання має чіткий контракт: один вихідний домен → один цільовий домен → одна стратегія вирішення конфліктів.
- C. Перерахувати всі домени, дозволити користувачеві вибрати декілька, поставити N завдань у чергу на стороні сервера.
Ми випустили варіант B, а варіант C залишили для наступної ітерації плану впровадження. Інструмент запуску запитує ім'я хоста вихідного домену у вигляді текстового поля (без випадаючого списку — /domains список Short.io дешевий у виклику, але додає зайвий раунд-тріп, до того ж користувач завжди знає ім'я хоста свого домену). Одне завдання на домен, поставлене в чергу з панелі керування по одному.
Перевага розміру сторінки#
Short.io за замовчуванням виконує пагінацію по 150 посилань за виклик — це найбільш щедрий показник серед п'яти наших джерел міграції. Порівняємо:
- Bitly: 100 per page
- Rebrandly: 25 per page
- TinyURL: 100 per page (Pro/Bulk)
- Dub.co: 100 per page
- Short.io: 150 per page
Домен Short.io на 5000 посилань потребує 34 раунд-тріпи. Акаунт Rebrandly на 5000 посилань — 200. Воркер витрачає більшу частину свого реального часу на очікування HTTP-відповідей, тому це важливо — Short.io емпірично є найшвидшим джерелом міграції, яке ми підтримуємо.
const shortioPageSize = 150
page := 1
for {
resp, err := w.fetchPage(ctx, opts.Token, opts.DomainID, page)
if err != nil { /* mark failed */ return }
if len(resp.Links) == 0 { break }
for _, link := range resp.Links { /* import */ }
if !resp.HasMore { break }
page++
}
HasMore — це булеве значення, яке Short.io повертає явно — ніякого парсингу курсорів або відстеження останнього ідентифікатора. Їхній API — один із найкраще спроєктованих серед п'яти вендорів, яких ми підтримуємо.
Приватні посилання — що ми робимо#
Short.io має прапорець «private» для кожного посилання. Ми імпортуємо приватні посилання як посилання Elido з is_active=false, щоб слаг не резолвився на периферії (edge). Користувач вибірково активує їх з панелі керування після вибіркової перевірки імпорту.
Обґрунтування: якщо посилання в Short.io було приватним у джерелі, намір користувача полягав у тому, щоб воно не резолвилося публічно. Імпорт як is_active=true зробив би доступними URL, які навмисно були заблоковані. Імпорт як is_active=false зберігає слаг зарезервованим, але недоступним, доки користувач не вирішить інакше — це суворо безпечніше, ніж альтернатива.
isActive := !link.Private
linkID, err := w.links.InsertImported(ctx, sqldb.InsertImportedLinkParams{
WorkspaceID: job.WorkspaceID,
DomainID: job.TargetDomainID,
Slug: slug,
DestinationURL: link.OriginalURL,
Title: truncate(link.Title, 250),
Tags: append(link.Tags, "imported:shortio"),
IsActive: isActive,
CreatedByUserID: createdByUserID,
})
Це невелика поверхнева відмінність від Bitly (немає еквівалентного прапорця) та Rebrandly (немає еквівалентного прапорця). Варто висвітлити це в рецепті після імпорту, щоб користувач розумів, чому деякі імпортовані посилання не працюють одразу.
Що ми не мігруємо#
Конфігурації A/B спліт-тестів Short.io не мають чистого експорту — це внутрішній конструктор застосунку, який не надає детерміновану JSON-структуру через REST API. Перебудуйте їх як Elido правила смарт-посилань після імпорту; синтаксис більш виразний, але ментальна модель та сама.
Історія кліків — це універсальне обмеження для кожного джерела міграції. Дані по кожному кліку в Short.io знаходяться в їхньому експорті аналітики, який доступний лише в плані Team (станом на 2026-05-22) і відображається як агреговані лічильники, а не події по кожному кліку. Нові кліки потрапляють в аналітику Elido з моменту перемикання.
Дизайни QR-кодів і пресети UTM для посилань — та ж історія, що й з Bitly та Rebrandly. Позначені тегом imported:shortio, готові до масової обробки через кампанії Elido.
Передача домену#
Цікавий випадок використання Short.io — «Я використовую брендований домен на Short.io і хочу перенести його на Elido, не змінюючи URL». Міграція чисто обробляє частину з посиланнями; зі сторони DNS — це одна зміна CNAME.
Ми задокументували послідовність передачі на сторінці /migrate-from/shortio — тримайте обидві поверхні (сервіси) у робочому стані паралельно, доки не закінчиться ваша підписка на Short.io, а потім спрямуйте DNS на Elido. Немає потреби терміново вимикати Short.io в день завершення імпорту.
Користувацькі домени в Elido використовують on-demand TLS від Caddy з domain-manager як джерелом списку дозволених доменів, тому перемикання — це зміна CNAME плюс API-виклик для верифікації домену. Жодних танців з сертифікатами зі сторони користувача.
Вирішення конфліктів та контракт воркера#
Ідентично до Bitly та Rebrandly — суфікси mylink-2, mylink-3, ... у разі зіткнення; skip залишає існуюче посилання Elido в спокої і логує вихідний рядок; fail зупиняє роботу при першому ж конфлікті. Пошук — це одне індексоване читання на рядок.
Контракт воркера — MaxLinksPerImport=50_000, ImportRunBudget=30*time.Minute, progressEvery=50, errorLogCap=1_000 — є спільним для всіх п'яти вендорів. Ці константи виконують більшу частину роботи, і це не налаштування конфігурації. Це контракт, на який розраховує UI опитування панелі керування.
Обробка токенів#
bgCtx := context.WithoutCancel(r.Context())
go h.shortio.Run(bgCtx, job.ID, imports.ShortioJobOptions{
Token: req.Token,
DomainID: req.DomainID,
})
source_token_id залишається NULL. Та ж семантика одноразового використання, що й у Bitly та Rebrandly — користувач вставляє токен один раз, воркер виконує роботу, токен видаляється з пам'яті після завершення. Ми не зберігаємо його, оскільки цінність збереження (повторного використання) не стосується міграцій.
context.WithoutCancel підтримує життя воркера після того, як HTTP-запит, що його запустив, завершиться. Такий самий шаблон, як і для кожного іншого вендора міграції в цьому впровадженні.
Порівняння зі шляхом CSV-експорту#
Short.io надає CSV-експорт у планах Team. Ми обрали REST замість CSV, тому що:
- REST зберігає теги Short.io структурно. CSV перетворює їх на рядок, розділений комами, який потребує розбиття після парсингу.
- REST надає прапорець
private. CSV не включає його послідовно. - REST надає нам детермінований прогрес (переглянуто посилань / залишилося посилань). CSV — це одноразове завантаження файлу без сигналу про прогрес під час виконання.
- REST є агностичним до плану — кожен план Short.io надає
/links. CSV-експорт доступний лише в Team.
Шлях через CSV залишається в нашому арсеналі для користувачів застарілих акаунтів Short.io, чий API-токен було відкликано, але у яких залишився CSV з останнього експорту.
Можливість відновлення та проблема деплою#
Такий самий компроміс, як і в перших двох міграціях. Воркер працює в процесі; деплой під час імпорту вбиває горутину. Поле import_jobs.last_progress_at плюс 5-хвилинний крон для очищення «завислих» завдань переводить будь-який рядок зі статусом running без прогресу за останні 30 хвилин у статус failed. Повторний запуск є ідемпотентним при використанні suffix та skip.
Для акаунтів з більш ніж 10 000 посилань у кількох доменах Short.io, дизайн завдань по доменах тут допомагає — кожен домен незалежно обмежений 30-хвилинним бюджетом, тому деплой під час роботи над третім доменом не призведе до втрати роботи, виконаної для перших двох.
Що далі#
Ще два вендори на черзі:
- Dub.co —
GET /api/links?projectSlug=…&limit=100. Папки перетворюються на теги. Найчистіший API з усіх п'яти. - TinyURL — Pro/Bulk REST API по 100 посилань на сторінку. Безкоштовний TinyURL не має API і ніколи не мав; це залишається шляхом для ручної міграції.
Після Dub і TinyURL, впровадження Tier-3 буде завершено. П'ять сторінок міграції (/migrate-from/bitly, /migrate-from/rebrandly, /migrate-from/shortio, /migrate-from/dub, /migrate-from/tinyurl) і п'ять технічних блог-постів охоплюють кожен запит «міграції від» (migration-from), на який може потрапити людина, що шукає альтернативу Bitly.
Якщо ви відкладали порівняння Short.io, тому що історія міграції була недокументованою, тепер вона задокументована. Спробуйте — від API-ключа + домену до останнього імпортованого посилання менш ніж за шість хвилин для типових акаунтів.