Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/2026-07-29-static-renderer-audio-video.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/static-renderer': patch
---

Render `audio` and `video` elements with a closing tag instead of as self-closing, so browsers no longer nest following content inside them.
76 changes: 76 additions & 0 deletions packages/static-renderer/__tests__/pm-self-closing-tags.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Node } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { renderToHTMLString } from '@tiptap/static-renderer/pm/html-string'
import { describe, expect, it } from 'vitest'

const Audio = Node.create({
name: 'audio',
group: 'block',
atom: true,
addAttributes() {
return { src: { default: null } }
},
renderHTML({ HTMLAttributes }) {
return ['audio', HTMLAttributes]
},
})

const Video = Node.create({
name: 'video',
group: 'block',
atom: true,
addAttributes() {
return { src: { default: null } }
},
renderHTML({ HTMLAttributes }) {
return ['video', HTMLAttributes]
},
})

const extensions = [Document, Paragraph, Text, Audio, Video]

describe('static renderer: non-void elements get a closing tag', () => {
it('renders <audio> with a closing tag, not self-closed', () => {
const html = renderToHTMLString({
content: {
type: 'doc',
content: [{ type: 'audio', attrs: { src: 'https://example.com/a.mp3' } }],
},
extensions,
})

expect(html).toBe('<audio src="https://example.com/a.mp3"></audio>')
})

it('renders <video> with a closing tag, not self-closed', () => {
const html = renderToHTMLString({
content: {
type: 'doc',
content: [{ type: 'video', attrs: { src: 'https://example.com/v.mp4' } }],
},
extensions,
})

expect(html).toBe('<video src="https://example.com/v.mp4"></video>')
})

it('keeps multiple audio players as siblings instead of nesting them', () => {
const html = renderToHTMLString({
content: {
type: 'doc',
content: [
{ type: 'audio', attrs: { src: 'https://example.com/a.mp3' } },
{ type: 'paragraph', content: [{ type: 'text', text: 'between' }] },
{ type: 'audio', attrs: { src: 'https://example.com/b.mp3' } },
],
},
extensions,
})

expect(html).toBe(
'<audio src="https://example.com/a.mp3"></audio><p>between</p><audio src="https://example.com/b.mp3"></audio>',
)
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions packages/static-renderer/src/pm/html-string/html-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const NON_SELF_CLOSING_TAGS = new Set([
'span',
'a',
'button',
'audio',
'video',
])

/**
Expand Down