Skip to content

Conversation

@Ovgodd
Copy link
Collaborator

@Ovgodd Ovgodd commented Dec 1, 2025

Purpose

Enable users to export an accessible HTML version of content, replacing the "Copy as HTML" option with a full export feature from the modal.

issue : 599

image

Proposal

  • Move "Copy as HTML" to the export modal
  • Add a new accessible HTML export format (.html in ZIP)
  • Embed media files (images, SVG, PDF, video/audio) in the ZIP with clean filenames
  • Update export modal UI and labels
  • Add E2E test to validate HTML export ZIP structure and content

@Ovgodd Ovgodd requested a review from AntoLC December 1, 2025 14:29
@Ovgodd Ovgodd self-assigned this Dec 1, 2025
@Ovgodd Ovgodd added the enhancement improve an existing feature label Dec 1, 2025
@Ovgodd Ovgodd changed the title Enhance/html copy to download ⚡️Enhance/html copy to download Dec 1, 2025
@Ovgodd Ovgodd force-pushed the enhance/html-copy-to-download branch from 98ffee7 to aac1cca Compare December 1, 2025 14:31
@Ovgodd Ovgodd marked this pull request as ready for review December 1, 2025 14:34
@github-actions
Copy link

github-actions bot commented Dec 1, 2025

Size Change: +26.8 kB (+0.65%)

Total Size: 4.14 MB

Filename Size Change
apps/impress/out/_next/static/c42e3881/_buildManifest.js 0 B -906 B (removed) 🏆
apps/impress/out/_next/static/chunks/1927.js 0 B -703 kB (removed) 🏆
apps/impress/out/_next/static/chunks/8323.js 730 kB +730 kB (new file) 🆕
apps/impress/out/_next/static/e5ee7ee4/_buildManifest.js 907 B +907 B (new file) 🆕

compressed-size-action

@Ovgodd Ovgodd force-pushed the enhance/html-copy-to-download branch 4 times, most recently from c1b12d1 to 999c814 Compare December 1, 2025 15:19
Copy link
Collaborator

@AntoLC AntoLC left a comment

Choose a reason for hiding this comment

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

I have some cases where the mp3 and videos are not exported, I used this doc model:
https://www.blocknotejs.org/docs/features/blocks


expect(Export.default).toBeUndefined();
}, 10000);
}, 60000);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same, is 1mn really necessary, seems big ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The test was a bit flaky, I saw it fail occasionally and one run even took ~9s, so I put timeout to 1min a bit aggressively 😅
We could probably reduce it to 15–20s now ? Wdyt

});

test('It checks the copy as HTML button', async ({ page, browserName }) => {
test('It no longer shows the copy as HTML button', async ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can remove totally this test, we don't assert things that does not exist anymore.


blobExport = await exporter.toODTDocument(exportDocument);
} else if (format === DocDownloadFormat.HTML) {
const editorHtml = await editor.blocksToHTMLLossy();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const editorHtml = await editor.blocksToHTMLLossy();
const editorHtml = editor.blocksToHTMLLossy();

Have you seen that you have editor.blocksToFullHTML available as well ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, indeed, I’ve just switched to editor.blocksToFullHTML(), and it preserves the original document structure much better than blocksToHTMLLossy.
This is the best option BlockNote provides, I think.

Comment on lines +172 to +178
const fetched = await exportCorsResolveFileUrl(doc.id, src);

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can see you use the CORS proxy, but I think it works only with images:

if not content_type.startswith("image/"):
return drf.response.Response(
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
)

Copy link
Collaborator Author

@Ovgodd Ovgodd Dec 3, 2025

Choose a reason for hiding this comment

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

Oh yes, I see, the current CORS proxy only supports images.
So that means external audio / video (when they’re not uploaded to our backend) are rejected and can’t be added to the HTML ZIP? ( as you said sometimes mp3 and vidéos are not exported )
In that case, should we update the backend proxy to also allow audio and video so these files can be exported too?

// Ensure the filename has an extension consistent with the blob MIME type.
const mimeType = fetched.type;
if (mimeType && !baseName.includes('.')) {
const subtype = mimeType.split('/')[1] || '';
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have the feeling it is error prone, what if the mimeType does not have a "/" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, good catch,
I’ve updated it so we first check for the / is present otherwise we skip the extension logic and keep the base filename

Comment on lines 240 to 245
const zipBuffer = await cs.toBuffer(await download.createReadStream());

// ZIP files start with "PK\x03\x04"
expect(zipBuffer.length).toBeGreaterThan(4);
expect(zipBuffer[0]).toBe(0x50);
expect(zipBuffer[1]).toBe(0x4b);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we improve this part to check what is inside the zip file ?

The idea:

import JSZip from 'jszip';

// In your test:
const zipBuffer = await cs.toBuffer(await download.createReadStream());

// Unzip and inspect contents
const zip = await JSZip.loadAsync(zipBuffer);

// Check that index.html exists
const indexHtml = zip.file('index.html');
expect(indexHtml).not.toBeNull();

// Read and verify HTML content
const htmlContent = await indexHtml!.async('string');
expect(htmlContent).toContain('Hello HTML ZIP');

// Check for media files
const mediaFiles = zip.file(/^media\//);
expect(mediaFiles.length).toBeGreaterThan(0);

// Verify the SVG image is included
const svgFile = mediaFiles.find(f => f.name.endsWith('.svg'));
expect(svgFile).toBeDefined();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes I agree this is much better

@Ovgodd Ovgodd force-pushed the enhance/html-copy-to-download branch from fbdb37c to 882b7f3 Compare December 3, 2025 11:49
@Ovgodd Ovgodd requested a review from AntoLC December 3, 2025 11:49
@Ovgodd Ovgodd force-pushed the enhance/html-copy-to-download branch from 882b7f3 to 22ff23f Compare December 3, 2025 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement improve an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants