Skip to content

Add HTML5 article rendering with embedded content viewers#14548

Draft
rtibbles wants to merge 5 commits into
learningequality:developfrom
rtibbles:html_viewer_injection
Draft

Add HTML5 article rendering with embedded content viewers#14548
rtibbles wants to merge 5 commits into
learningequality:developfrom
rtibbles:html_viewer_injection

Conversation

@rtibbles
Copy link
Copy Markdown
Member

@rtibbles rtibbles commented Apr 7, 2026

Summary

Refactors the content viewer architecture to support rendering HTML content directly in the DOM (rather than exclusively in sandboxed iframes), and migrates the MediaPlayer from Vuex to composables.

Key changes:

  • Content viewer composable API refactored so viewer components receive data via composables instead of props
  • New DOM element-based rendering path alongside existing iframe rendering
  • MediaPlayer Vuex module replaced with useMediaPlayer composable
  • VideoPlayer extracted from MediaPlayerIndex; new AudioPlayer with custom controls and inline transcript
  • Shared ViewerToolbar component replaces per-viewer toolbar implementations (HTML5, PDF, EPUB)
  • SafeHtml5Renderer updated to render HTML content with embedded media support and progress tracking
  • Design refresh across the media players (audio + video) and embedded viewers (PDF, EPUB)

References

Reviewer guidance

To test: load any HTML5 content, PDF, EPUB, video, or audio content and verify it renders and plays correctly. For HTML5 content with embedded media, use the Kolibri QA Channel under HTML5 > HTML5 Article.

HTML5 Articles with embedded content use a blended progress model: 50% from scroll progress, 50% from embedded content completion. The 50/50 split is arbitrary and may need tuning — worth scrutinizing whether this feels right in practice.

Implementation involved judgment calls that may deviate from the design specs — worth comparing against the original designs during review.

Risky areas:

  • packages/kolibri/internal/pluginMediator.js — viewer registration now supports DOM element viewers alongside preset-based viewers; this is the core extension point
  • packages/kolibri/components/internal/ContentViewer/index.js — major rewrite from a simple wrapper to a render-function component with provide/inject context, viewer ID tracking, and DOM element file extraction
  • kolibri/core/content/hooks.py — new css_selectors and allow_object_tag on ContentRendererHook; the all_css_selectors classmethod uses module-level caching

Standalone players (desktop)

State Screenshot
Video player Standalone video
Audio player Standalone audio

Embedded in HTML5 article (desktop, 1280px)

State Screenshot
Article text rendering Article top
Embedded audio players Audio players
Embedded video player Video player
Embedded PDF viewer PDF viewer
Embedded EPUB viewer EPUB viewer

Embedded video player

Screencast.From.2026-04-07.19-26-04.mp4

Sticky audio player

Screencast.From.2026-04-07.19-24-22.mp4

Embedded in HTML5 article (mobile, 412px)

State Screenshot
Article text Mobile article
Embedded audio Mobile audio
Embedded video Mobile video
Embedded PDF Mobile PDF
Embedded EPUB Mobile EPUB

AI usage

Implementation was done collaboratively with Claude Code (Opus). I directed the architecture and made design decisions; Claude helped with implementation, test writing, and iterating on the embedded content rendering approach. All code was reviewed and verified against a running dev server.

@github-actions github-actions Bot added DEV: renderers HTML5 apps, videos, exercises, etc. DEV: backend Python, databases, networking, filesystem... APP: Learn Re: Learn App (content, quizzes, lessons, etc.) APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend SIZE: very large labels Apr 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

@rtibbles rtibbles force-pushed the html_viewer_injection branch 8 times, most recently from 4735d6d to f00e02e Compare April 14, 2026 00:01
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

npm Package Versions

Warning

The following packages have changed files but no version bump:

Package Version Changed files
kolibri 0.18.0 15

If these changes affect published code, consider bumping the version.

@rtibbles rtibbles force-pushed the html_viewer_injection branch 3 times, most recently from d597e47 to 5da5ce9 Compare April 21, 2026 15:09
Copy link
Copy Markdown
Member Author

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some things I need to update before undrafting.

import useContentViewer, { contentViewerProps } from 'kolibri/composables/useContentViewer';
import useContentViewer from 'kolibri/composables/useContentViewer';

In order to log data about users viewing content, the component should emit ``startTracking``, ``updateProgress``, and ``stopTracking`` events, using the Vue ``$emit`` method. ``startTracking`` and ``stopTracking`` are emitted without any arguments, whereas ``updateProgress`` should be emitted with a single value between 0 and 1 representing the current proportion of progress on the content.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for reviewers - should I move the entire public API surface into the composable? Just make methods for startTracking, stopTracking and updateProgress and skip the emit altogether?

allow_object_tag = False

@classmethod
def all_css_selectors(cls):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using css_selectors allows for specific content viewers to only render certain DOM nodes if they have particular attributes present, for example.

from le_utils.constants.file_formats import BLOOMPUB
from le_utils.constants.file_formats import H5P
from le_utils.constants.file_formats import HTML5
from le_utils.constants.file_formats import HTML5_ARTICLE
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add this to allow larger media files to be loaded via the zip content backend from kpub files.

:style="{ width: iframeWidth }"
@changeFullscreen="isInFullscreen = $event"
>
<div
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had copied this across HTML5, EPUB, PDF, and Bloompub - rule of three more than applied!

class DocumentEPUBRenderAsset(content_hooks.ContentRendererHook):
bundle_id = "main"
presets = (format_presets.EPUB,)
allow_object_tag = True
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use this as a shorthand to let the <object> tag be used for these kinds of files (but we use the preset logic to work out which files in the object tag they should render and generate CSS selectors accordingly).

Comment thread kolibri/plugins/media_player/frontend/composables/useScrollContainer.js Outdated
Comment thread kolibri/plugins/media_player/frontend/composables/useMediaProgress.js Outdated
Comment thread kolibri/plugins/media_player/frontend/views/__tests__/AudioPlayer.spec.js Outdated
Comment thread kolibri/plugins/media_player/frontend/views/__tests__/AudioPlayer.spec.js Outdated
Comment thread kolibri/plugins/pdf_viewer/frontend/views/__tests__/PdfRendererIndex.spec.js Outdated
@rtibbles rtibbles force-pushed the html_viewer_injection branch from 5da5ce9 to fdb1287 Compare April 22, 2026 03:00
@rtibbles rtibbles force-pushed the html_viewer_injection branch from fdb1287 to e307c3c Compare May 1, 2026 02:24
rtibbles and others added 5 commits May 13, 2026 15:29
…nents

to use the viewer props, and instead direct all data via the composable api.
…ction

Replace the Vuex store module with composables for instance-specific
state management. Extract VideoPlayer and implement AudioPlayer with
custom transport controls, sticky player, and inline transcript.
Add useMediaProgress composable for shared progress tracking logic.
ContentViewer now generates unique viewer IDs and appends them to all
interaction events, allowing parent components to track which embedded
viewer emitted each event.

SafeHtml5RendererIndex uses this to aggregate progress from multiple
embedded viewers (e.g., videos in HTML5 content), combining scroll-based
progress with embedded viewer progress using dynamic weighting.
Replace v-bind="$attrs" with explicit class="safe-html" and remove
inheritAttrs: false, reducing the component API surface area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rtibbles rtibbles force-pushed the html_viewer_injection branch from e307c3c to 4eb097a Compare May 14, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) APP: Learn Re: Learn App (content, quizzes, lessons, etc.) DEV: backend Python, databases, networking, filesystem... DEV: frontend DEV: renderers HTML5 apps, videos, exercises, etc. SIZE: very large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate MediaPlayer from Vuex module to composable Allow HTML5 articles and general inclusion of rich text in HTML in content rendering

3 participants