8.8 KiB
Fitur Embed Konten di Materi Markdown & Slide
Tanggal: 2026-07-19
Status: Implementasi selesai
Lokasi kode: Backend services/lesson_service.py + Frontend (CSS only)
1. Latar Belakang
Elemes memerlukan fitur agar author materi dapat menyisipkan konten embedded (iframe dari platform luar) langsung dari markdown — baik di tubuh materi maupun di dalam slide presentasi. Contoh penggunaan: video YouTube, desain Canva, Google Docs, Figma, widget Scratch, dll.
Infrastruktur yang Sudah Ada
Elemes sudah punya pipeline markdown→embed untuk circuit dan flowchart:
- Backend (
services/lesson_service.py): regex fencecircuit→<div class="*-embed" data-*>, lalumd.markdown()render jadi HTML. - Frontend (
src/lib/actions/render*Embeds.ts):IntersectionObserverganti div →<iframe>lazy load. - Slide sudah diparse di
lesson_service.py, dan di dalam loop slide embed circuit/flowchart sudah diproses — jadi embed otomatis berlaku di materi dan slide.
Kesimpulan feasibility: ✅ Sangat feasible — infrastruktur sudah ada, tinggal diperluas polanya.
2. Evolusi Pendekatan
Opsi Awal (Ditolak): URL-only fence + whitelist domain
Pendekatan pertama: user tulis URL di fence embed, backend bikin div, frontend pasang iframe.
```embed,100%,400px
https://www.youtube.com/embed/VIDEO_ID
```
Masalah ditemukan saat testing:
- Canva menolak di-iframe — "canva.com refused to connect". Canva set
X-Frame-Options: DENYuntuk URL design biasa; butuh URL khusus?embeduntuk mengizinkan iframe. - Embed di slide stuck "Memuat..." — karena iframe ditolak,
onloadtidak fire, loading text tidak dihapus. - Perlu transform per-platform — Canva butuh
?embed, Google Docs butuh/preview, Figma butuh format khusus. Hardcode per-platform tidak fleksibel.
Pendekatan Final (Dipilih): Raw HTML embed code + bleach sanitizer
Alih-alih URL, user paste embed HTML code siap pakai dari platform (Share → Embed):
```embed
<div style="position: relative; width: 100%; padding-top: 56.25%;">
<iframe loading="lazy" src="https://www.canva.com/design/.../view?embed" allowfullscreen></iframe>
</div>
<a href="https://www.canva.com/..." target="_blank" rel="noopener">Judul</a> by Author
```
Kelebihan:
- User kontrol penuh (aspect ratio, style, link credit) — embed code dari platform resmi sudah optimize.
- Support Canva, YouTube, Google Docs, Figma, Scratch, dll sekaligus — tanpa hardcode transform per-platform.
- Lebih fleksibel: author bisa kustomisasi wrapper, caption, dll.
Konsekuensi keamanan: Raw HTML = potensi XSS. Wajib sanitize sebelum render. Tanpa sanitize, author bisa sisipkan <script>, onclick, onerror, dll.
3. Desain Teknis
Keamanan — Dual Layer
-
bleachlibrary (Python standar industri):- Whitelist tag:
div,iframe,a,span,p,br,img. - Whitelist attribute per tag:
iframe[src|style|loading|allowfullscreen|allow|title],a[href|target|rel|style], dll. - Whitelist CSS property:
position,width,height,padding,margin,border-radius,box-shadow, dll (viaCSSSanitizer+tinycss2). - Strip:
<script>,onclick,onerror,javascript:URL, dan semua tag/attr/style berbahaya.
- Whitelist tag:
-
Domain blacklist (
EMBED_BLOCKED_HOSTS):- Cek
iframe[src]setelah sanitasi: wajibhttps://, hostname tidak boleh di blacklist. - Default blokir:
localhost,127.0.0.1,0.0.0.0,metadata.google.internal,169.254.169.254(cegah SSRF / metadata leak). - Subdomain ikut diblokir (mis.
sub.localhost).
- Cek
Resiliensi — Graceful Degradation
Import CSSSanitizer di-bungkus try/except ImportError. Kalau tinycss2 tidak terinstall di environment, aplikasi tidak crash — fallback ke bleach.clean() tanpa CSS sanitizer (tags/attrs tetap di-sanitize, hanya style CSS tidak difilter). Di production, tinycss2 wajib ada di requirements.txt untuk keamanan penuh.
Alur Pipeline
Markdown (```embed\nRAW HTML\n```)
↓
_process_embed_embeds() — regex match fence
↓
_sanitize_embed_html() — bleach.clean() + iframe src blacklist check
↓
HTML bersih (iframe jadi) → md.markdown() → lesson_content / slides_html
↓
Frontend: langsung render via {@html} — tidak perlu action khusus
Backend memanggil _process_embed_embeds() di 4 titik agar berlaku di semua konten:
- Loop slide (slide carousel)
lesson_content(tubuh materi)exercise_content(latihan)lesson_info(info pelajaran)
Frontend tidak butuh action baru — HTML sudah berisi iframe jadi dari backend. Action renderEmbedEmbeds.ts dari pendekatan URL-only lama sudah dihapus.
4. Implementasi
File yang Dimodifikasi/Dibuat
| File | Aksi | Detail |
|---|---|---|
services/lesson_service.py |
EDIT | Tambah import bleach, EMBED_ALLOWED_TAGS/ATTRS/STYLES, fungsi _sanitize_embed_html() + _process_embed_embeds() (ganti URL-only lama). 4 call sites tetap dipanggil. |
services/tests/test_lesson_service_embed.py |
CREATE | 9 pytest: Canva HTML, YouTube HTML, strips <script>, strips onclick, blocked domain, non-https iframe, empty, unchanged, dangerous style. |
services/requirements.txt |
EDIT | Tambah bleach>=6.0.0, tinycss2>=1.2.0. |
frontend/src/app.css |
EDIT | Hapus .generic-embed* (tidak dipakai lagi), simpan .embed-error. |
frontend/src/routes/lesson/[slug]/+page.svelte |
EDIT | Hapus import + call renderEmbedEmbeds (tidak perlu lagi). |
frontend/src/lib/actions/renderEmbedEmbeds.ts |
DELETE | Pendekatan URL-only dihapus. |
examples/content/dasar/test_slides.md |
EDIT | Contoh Canva (raw HTML, di dalam slide) + YouTube (raw HTML, di body materi). |
Verifikasi
- ✅
pytest tests/ -v— 9/9 passed. - ✅
npm run build— sukses tanpa error. - ✅ Manual test: Canva embed (dengan
?embedURL) load di slide; YouTube embed load di body.
5. Cara Pakai
Untuk Author Materi
- Buka platform (Canva, YouTube, Google Docs, dll) → klik Share / Bagikan → Embed.
- Copy kode HTML yang diberikan (biasanya berisi
<iframe>+ optional wrapper<div>+<a>credit). - Paste di markdown materi di dalam fence
embed:
```embed
<div style="position: relative; width: 100%; padding-top: 56.25%;">
<iframe loading="lazy" src="https://www.canva.com/design/.../view?embed" allowfullscreen></iframe>
</div>
```
- Embed otomatis muncul di materi. Kalau diletakkan di dalam blok
---slide-start---/---slide-end---, embed muncul di slide carousel.
Catatan Platform
| Platform | Cara dapat embed code |
|---|---|
| Canva | Share → Embed → Copy. URL sudah include ?embed. |
| YouTube | Share → Embed → Copy. URL pakai youtube.com/embed/VIDEO_ID. |
| Google Docs | File → Share → Publish to web → Embed → Copy. URL pakai /preview. |
| Figma | Share → Get embed code → Copy. URL pakai figma.com/embed?.... |
Pesan Error
- "Konten embed ditolak: iframe harus https." — URL iframe pakai
http://, ganti kehttps://. - "Konten embed ditolak: domain iframe diblokir." — Domain iframe ada di blacklist (internal/metadata endpoint).
- "Konten embed kosong." — Fence
embedtidak berisi apa-apa.
6. Pertanyaan Umum
Kenapa pakai raw HTML, bukan URL saja?
Karena setiap platform punya format embed berbeda (Canva butuh ?embed, Google Docs butuh /preview, Figma butuh embed_host). Dengan raw HTML, author paste kode siap pakai dari platform — lebih fleksibel dan tidak perlu hardcode transform per-platform di backend.
Apakah aman?
Ya. HTML di-sanitize pakai bleach (whitelist tag/attr/style) + cek domain iframe di blacklist. <script>, event handler (onclick), javascript: URL, dan domain berbahaya semua ditolak.
Bisa dipakai di slide?
Ya. Embed di dalam ---slide-start--- / ---slide-end--- otomatis diproses — backend panggil _process_embed_embeds() di loop slide.
Kenapa .generic-embed* CSS dihapus?
Itu CSS dari pendekatan URL-only lama (frontend bikin div + lazy iframe). Sekarang iframe sudah jadi dari backend, tidak butuh wrapper CSS khusus. .embed-error tetap dipertahankan untuk pesan error.
7. Riwayat Dokumen
Dokumen ini mengonsolidasi 4 file plan awal yang sudah superseded:
possibility-study-embed.md— studi feasibility awal (opsi A/B/C).plan-embed-implementation.md— plan implementasi Opsi A (URL + whitelist).plan-fix-embed-stuck.md— diagnosis "stuck loading" (sempat dikira race condition, ternyata Canva block).plan-embed-rawhtml.md— plan final pendekatan raw HTML + bleach.
Konsolidasi dilakukan agar pembaca masa depan tidak perlu membaca 4 file perjalanan; cukup 1 dokumen koheren yang menceritakan konteks, keputusan, dan hasil akhir.