
Я створив сайт на основі 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 ніколи не зможе:
- Корені Icecast часто потребують завершуального символу ‘;’.
https://host:9152/видававMediaError code 4(джерело не підтримується), алеhttps://host:9152/;відтворювався чудово. Це старий трюк SHOUTcast/Icecast для примусового використання аудіо-монту замість сторінки зі статусом. - Деякі кодеки просто не грають у
<audio>. Цінний CDN з HE-AAC / Opus-потоками повертав200curl-у, але зупинявся у браузері — вони ніколи не доходили доcanplay. - Використовуйте щедрий таймаут. Мій перший варіант мав 8 секунд і дав ~12 хибно-негативних — робочі потоки, просто повільно буферяться. При 14 секус вони всі проходили. Не вимикайте станцію через одну коротку затримку.
Висновок: якщо основна дія вашого продукту — «медіа відтворюється у браузері», перевіряйте це у браузері. HTTP-статус не є відтворенням.
Візуалізатор без порушення відтворення (CORS)
Я хотів аудіо-віуалізатор (Web Audio AnalyserNode), але для цього потрібне crossOrigin = "anonymous", що не працює приблизно у 30% потоків без заголовків CORS. Вирішення: два аудіо-елементи.
fxAudio—crossOrigin = "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

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