fix: video not rendering — add TipTap Video extension, allow HTTP src

TipTap didn't recognize <video> tags without a custom node extension,
serializing them as escaped text. Also revert video src to allow HTTP
since request.base_url returns http:// behind reverse proxy.
This commit is contained in:
Fringg
2026-03-23 12:11:51 +03:00
parent 74080004e8
commit 25f3602aea
3 changed files with 39 additions and 6 deletions

30
src/lib/tiptap-video.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Node, mergeAttributes } from '@tiptap/core';
/**
* TipTap extension for inline <video> elements.
*
* Without this extension, TipTap does not recognize <video> tags and
* serializes them as escaped text when calling editor.getHTML().
*/
export const VideoExtension = Node.create({
name: 'video',
group: 'block',
atom: true,
addAttributes() {
return {
src: { default: null },
controls: { default: true },
class: { default: null },
preload: { default: 'metadata' },
};
},
parseHTML() {
return [{ tag: 'video' }];
},
renderHTML({ HTMLAttributes }) {
return ['video', mergeAttributes(HTMLAttributes, { controls: '' })];
},
});

View File

@@ -10,6 +10,7 @@ import PlaceholderExtension from '@tiptap/extension-placeholder';
import TextAlignExtension from '@tiptap/extension-text-align';
import UnderlineExtension from '@tiptap/extension-underline';
import HighlightExtension from '@tiptap/extension-highlight';
import { VideoExtension } from '../lib/tiptap-video';
import { newsApi } from '../api/news';
import { AdminBackButton } from '../components/admin';
import { Toggle } from '../components/admin/Toggle';
@@ -246,6 +247,7 @@ export default function AdminNewsCreate() {
types: ['heading', 'paragraph'],
}),
HighlightExtension,
VideoExtension,
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@@ -319,13 +321,13 @@ export default function AdminNewsCreate() {
if (result.media_type === 'image') {
editor.chain().focus().setImage({ src: result.url, alt: file.name }).run();
} else {
const safeUrl = result.url.replace(/"/g, '&quot;');
editor
.chain()
.focus()
.insertContent(
`<video src="${safeUrl}" controls class="w-full rounded-xl max-h-96"></video>`,
)
.insertContent({
type: 'video',
attrs: { src: result.url, class: 'w-full rounded-xl max-h-96' },
})
.run();
}
haptic.success();

View File

@@ -189,13 +189,14 @@ function sanitizeHtml(html: string): string {
}
});
// Hook: validate <video> src — HTTPS only
// Hook: validate <video> src — only allow http/https (block javascript:, data:, etc.)
// HTTP is permitted because request.base_url behind a reverse proxy returns http://
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'VIDEO') {
const src = node.getAttribute('src') ?? '';
try {
const url = new URL(src);
if (url.protocol !== 'https:') {
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
node.remove();
return;
}