I built an SEO-first internet radio site — and learned why `curl` can’t validate audio streams

від

у

Я створив сайт на основі SEO для інтернет-радіо — і дізнався, чому curl не може валідати аудіо-потоки

https://ift.tt/5s6OT1I

Нещодавно я зібрав Radio Balkan — односторінковий веб-плеєр, який об’єднує понад 750 балканських радіостанцій в одному місці: без реклами, без реєстрації, без банерів cookie. Це мій перший «серйозний» проект, і шлях навчив мене більше, ніж будь-який туторіал. Нижче — ті частини, які, на мою думку, стануть корисними іншим розробникам.

Стек (навмисно прозаїчно)

  • Один статичний HTML-файл для програми — vanilla HTML/CSS/JS, без фреймворку.
  • Supabase (Postgres + REST) як база даних станцій.
  • Netlify для хостингу + безкоштовний SSL.
  • Маленький Node-скрипт, який генерує SEO-сторінки.

Жодного процесу збірки, жодного SPA-фреймворку. Усе завантажується швидко, розгортається просто (перетягніть папку на Netlify).

SEO-спочатку: те, що конкуренти роблять неправильно

Більшість існуючих радіо-сайтів — це JavaScript-SPA. Відкрити джерело сторінки — і там… нічого — контент рендериться на клієнті. Google може виконати JS, але це повільніше та менш надійно, і головне: відсутні доступні для індексу per-станцій сторінки.

Тож я обрав інший шлях. Node-скрипт витягує кожну станцію з Supabase та генерує понад 500 статичних HTML-сторінок — одну для станції, країни та жанру — плюс sitemap.xml. Кожна сторінка — реальний HTML із контентом просто там.

// для кожної станції -> написати статичну, crawLable-сторінку
fs.writeFileSync(`radio/${slug}/index.html`, stationPage(station));

За кілька днів сайт індексувався і почав отримувати перші органічні переходи. Статичні сторінки переважали більший, але невидимий каталог.

Supabase + RLS: публічно, але безпечно

Браузер звертається до Supabase напряму з anon-ключем, що нормально якщо для ролі Row Level Security налаштовано правильно:

  • stations → публічний тільки для читання.
  • submissions (станції, запропоновані користувачами) → лише вставка, без політики читання, щоб ніхто не міг прочитати чужі пропозиції.
  • Ключ service_role ніколи не відправляється клієнту — використовується лише локально для ініціалізації.

Публічний anon-ключ + коректний RLS = безсерверна бекенд-система без бекенд-коду.

Урок, який обійшов мене найдорожче: curl бреше про аудіо

Я імпортував пакет станцій та валідатив кожен потік так:

curl -s -o /dev/null -w "%{http_code} %{content_type}" -r 0-1 "$URL"
# 200 audio/mpeg ... так?

200 audio/mpeg виглядав як пропуск. Так не було. Багато з цих потоків повертали чистий 200 curl-у, але відмовлялися грати в браузері. curl перевіряє, що байти повертаються; він не перевіряє, чи зможе елемент браузера <audio> дійсно декодувати та відтворити їх.

Тож я перевіряв їх найбільш правдивим способом — реальне відтворення у безголовному браузері:

function canPlay(url, ms = 14000) {
  return new Promise(resolve => {
    const a = new Audio();
    a.preload = 'auto';
    const done = r => { a.src = ''; resolve(r); };
    a.addEventListener('canplay', () => done('OK'));
    a.addEventListener('loadeddata', () => done('OK'));
    a.addEventListener('error', () => done('ERR' + (a.error && a.error.code)));
    a.src = url;
    a.load();
    setTimeout(() => done('TIMEOUT'), ms);
  });
}

Три речі, які це зловило, але curl ніколи не зможе:

  1. Корені Icecast часто потребують завершуального символу ‘;’. https://host:9152/ видавав MediaError code 4 (джерело не підтримується), але https://host:9152/; відтворювався чудово. Це старий трюк SHOUTcast/Icecast для примусового використання аудіо-монту замість сторінки зі статусом.
  2. Деякі кодеки просто не грають у <audio>. Цінний CDN з HE-AAC / Opus-потоками повертав 200 curl-у, але зупинявся у браузері — вони ніколи не доходили до canplay.
  3. Використовуйте щедрий таймаут. Мій перший варіант мав 8 секунд і дав ~12 хибно-негативних — робочі потоки, просто повільно буферяться. При 14 секус вони всі проходили. Не вимикайте станцію через одну коротку затримку.

Висновок: якщо основна дія вашого продукту — «медіа відтворюється у браузері», перевіряйте це у браузері. HTTP-статус не є відтворенням.

Візуалізатор без порушення відтворення (CORS)

Я хотів аудіо-віуалізатор (Web Audio AnalyserNode), але для цього потрібне crossOrigin = "anonymous", що не працює приблизно у 30% потоків без заголовків CORS. Вирішення: два аудіо-елементи.

  • fxAudiocrossOrigin = "anonymous", маршрутизований через граф Web Audio (віуалізатор працює).
  • plainAudio — без CORS, резервний варіант, який завжди відтворює.

Спробуйте спочатку fxAudio; у разі невдачі запам’ятайте, що у станції немає CORS, і повторно відтворюйте її на plainAudio. Ви отримуєте візуалізатор там, де можливо, і ніколи не жертвуєте відтворенням.

Чому без AdSense

Було б привабливо, але рекламні мережі змушують банер з підтвердженням cookie і збільшують час завантаження. Для зручності, яку люди відкривають з «натиснили Плей і залишили працювати», це невдала угода. Сайт залишають без cookie та швидким; якщо зростатиме, монетизуватиму безпосередньо (преміум-розміщення станцій), а не через рекламні мережі.

Підсумок

Скучний стек, статичні сторінки, безкомпромісна валідація. Якщо хочете побачити результат, він живий на radiobalkan.net — і якщо знаєте балканську станцію, якої не вистачає, скажіть — додам її.

Готовий відповісти на запитання щодо генерації SEO або налаштування тестування потоків у коментарях.

HI-FI News

через DEV Community: javascript https://ift.tt/qIS5wH2

червень 13, 2026 о 23:28 українською. Надано лише текст, що підлягає перекладу.

June 13, 2026 at 11:28PM


Коментарі

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *