Реалізація вбудовування контенту (Embed) з сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація вбудовування контенту (Embed) з сайту
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація вбудовування контенту (Embed)

Embed — це не просто <iframe src="...">. Правильна реалізація включає безпеку (sandbox, CSP), продуктивність (lazy-loading, aspect-ratio), протокол oEmbed для автоматичної генерації та підтримку користувацьких провайдерів.

oEmbed: стандарт автоматичного вбудовування

oEmbed — це JSON-протокол, який дозволяє отримати HTML-код для вбудовування за URL. Більшість платформ його підтримують.

interface OEmbedResponse {
  type: 'rich' | 'video' | 'photo' | 'link'
  html?: string
  width?: number
  height?: number
  thumbnail_url?: string
  title?: string
  author_name?: string
}

async function fetchOEmbed(url: string, maxWidth = 800): Promise<OEmbedResponse> {
  // Більшість провайдерів приймають запит напрямку, але через CORS — через проксі
  const proxyUrl = `/api/oembed?url=${encodeURIComponent(url)}&maxwidth=${maxWidth}`
  const res = await fetch(proxyUrl)
  if (!res.ok) throw new Error(`oEmbed failed: ${res.status}`)
  return res.json()
}
// Laravel: проксі для запитів oEmbed
// app/Http/Controllers/OEmbedController.php
class OEmbedController extends Controller
{
    private array $providers = [
        'youtube.com' => 'https://www.youtube.com/oembed',
        'youtu.be'    => 'https://www.youtube.com/oembed',
        'vimeo.com'   => 'https://vimeo.com/api/oembed.json',
        'twitter.com' => 'https://publish.twitter.com/oembed',
        'x.com'       => 'https://publish.twitter.com/oembed',
        'instagram.com' => 'https://graph.facebook.com/v18.0/instagram_oembed',
        'soundcloud.com' => 'https://soundcloud.com/oembed',
        'spotify.com'   => 'https://open.spotify.com/oembed',
        'tiktok.com'    => 'https://www.tiktok.com/oembed',
        'codepen.io'    => 'https://codepen.io/api/oembed',
    ];

    public function fetch(Request $request): JsonResponse
    {
        $url = $request->validate(['url' => 'required|url'])['url'];
        $host = parse_url($url, PHP_URL_HOST);
        $host = preg_replace('/^www\./', '', $host);

        $endpoint = collect($this->providers)
            ->first(fn($v, $k) => str_contains($host, $k));

        if (!$endpoint) {
            return response()->json(['error' => 'Provider not supported'], 422);
        }

        $response = Http::timeout(5)->get($endpoint, [
            'url'      => $url,
            'maxwidth' => $request->integer('maxwidth', 800),
            'format'   => 'json',
        ]);

        return response()->json($response->json(), $response->status());
    }
}

Безпечне відтворення HTML з oEmbed

HTML із зовнішніх джерел не можна вставляти прямо через innerHTML. Використовуйте DOMPurify:

npm install dompurify @types/dompurify
import DOMPurify from 'dompurify'

// Конфіг: дозволяємо iframe тільки від довірених доменів
const ALLOWED_IFRAME_ORIGINS = [
  'https://www.youtube.com',
  'https://player.vimeo.com',
  'https://open.spotify.com',
  'https://w.soundcloud.com',
  'https://www.tiktok.com',
  'https://codepen.io',
]

DOMPurify.addHook('uponSanitizeElement', (node, data) => {
  if (data.tagName === 'iframe') {
    const src = node.getAttribute('src') || ''
    const allowed = ALLOWED_IFRAME_ORIGINS.some(origin => src.startsWith(origin))
    if (!allowed) {
      node.remove()
    }
  }
})

const config = {
  ADD_TAGS: ['iframe'],
  ADD_ATTR: ['allowfullscreen', 'frameborder', 'scrolling', 'allow', 'referrerpolicy'],
}

function SafeEmbed({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, config)
  return <div dangerouslySetInnerHTML={{ __html: clean }} className="embed-wrapper" />
}

Адаптивне вбудовування: видалення фіксованих розмірів

oEmbed часто повертає width="560" height="315" прямо в HTML. Це зламує адаптивність. Виправляємо через CSS або постобробку:

function makeResponsive(html: string): string {
  const parser = new DOMParser()
  const doc = parser.parseFromString(html, 'text/html')

  doc.querySelectorAll('iframe').forEach(iframe => {
    const w = parseInt(iframe.getAttribute('width') || '0')
    const h = parseInt(iframe.getAttribute('height') || '0')

    if (w && h) {
      const ratio = (h / w * 100).toFixed(4)
      iframe.setAttribute('width', '100%')
      iframe.removeAttribute('height')
      iframe.style.aspectRatio = `${w}/${h}`
    }
  })

  return doc.body.innerHTML
}
.embed-wrapper {
  width: 100%;
}

.embed-wrapper iframe {
  width: 100%;
  border: none;
  aspect-ratio: 16/9;
}

Ледача завантаження через Intersection Observer

import { useEffect, useRef, useState } from 'react'

function LazyEmbed({ url, label }: { url: string; label: string }) {
  const [loaded, setLoaded] = useState(false)
  const [embedHtml, setEmbedHtml] = useState('')
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setLoaded(true)
          observer.disconnect()
        }
      },
      { rootMargin: '300px' }
    )
    if (containerRef.current) observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  useEffect(() => {
    if (!loaded) return
    fetchOEmbed(url).then(data => {
      if (data.html) setEmbedHtml(makeResponsive(data.html))
    })
  }, [loaded, url])

  return (
    <div ref={containerRef} className="embed-container" aria-label={label}>
      {embedHtml
        ? <SafeEmbed html={embedHtml} />
        : <div className="embed-placeholder">Завантаження...</div>
      }
    </div>
  )
}

Користувацький провайдер: вбудовування Figma

// Figma не підтримує oEmbed — будуємо вручну
function buildFigmaEmbed(url: string, options = { width: 800, height: 450 }): string {
  const embedUrl = `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`
  return `<iframe
    src="${embedUrl}"
    width="${options.width}"
    height="${options.height}"
    allowfullscreen
    style="border: 1px solid rgba(0, 0, 0, 0.1);"
  ></iframe>`
}

// Визначаємо тип за URL і вибираємо стратегію
function resolveEmbed(url: string): Promise<string> {
  if (url.includes('figma.com')) {
    return Promise.resolve(buildFigmaEmbed(url))
  }
  if (url.includes('github.com') && url.includes('/blob/')) {
    return buildGistEmbed(url)
  }
  return fetchOEmbed(url).then(d => makeResponsive(d.html ?? ''))
}

Content Security Policy та iframe sandbox

// Laravel middleware для CSP заголовків
class ContentSecurityPolicy
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $frameAncestors = [
            "'self'",
            'https://www.youtube.com',
            'https://player.vimeo.com',
        ];

        $response->headers->set(
            'Content-Security-Policy',
            implode('; ', [
                "default-src 'self'",
                "frame-src " . implode(' ', $frameAncestors),
                "script-src 'self' 'nonce-" . $request->attributes->get('csp_nonce') . "'",
            ])
        );

        return $response;
    }
}
<!-- sandbox обмежує можливості iframe -->
<iframe
  src="https://www.youtube.com/embed/..."
  sandbox="allow-scripts allow-same-origin allow-presentation allow-fullscreen"
  loading="lazy"
  referrerpolicy="no-referrer-when-downgrade"
></iframe>

Атрибут sandbox без allow-same-origin повністю ізолює iframe, але зламує більшість програвачів. Мінімальний безпечний набір — allow-scripts allow-same-origin.

Кешування oEmbed-відповідей

// Кешуємо відповідь oEmbed на 24 години — платформи рідко змінюють embed-код
public function fetch(Request $request): JsonResponse
{
    $url = $request->input('url');
    $cacheKey = 'oembed:' . md5($url);

    $data = Cache::remember($cacheKey, now()->addDay(), function () use ($url) {
        // ... запит до провайдера
    });

    return response()->json($data);
}

Терміни

Базовий oEmbed-проксі з 10 провайдерами — 1 день. З безпечним відтворенням, адаптивом, lazy-завантаженням та кешуванням — 2–3 дні. Повноцінний редактор з вбудовуванням WYSIWYG за URL (як у Notion) — 1 тиждень.