I’m using ProseMirror via Tiptap inside a Flutter app (through a WebView). One issue I’m facing is that empty lines are not preserved — when users press Enter multiple times, only one paragraph is stored and the extra blank lines are lost.
I understand that both Notion and Linear are also built on top of ProseMirror.
How do they handle this? Do they store actual empty paragraph nodes in the document to preserve the visual gaps, or is there another recommended approach?
This is not something that is expected to happen. Firstly, ProseMirror doesn’t work with ‘lines’, but block nodes of some kind. So you’d typically get a bunch of empty paragraphs in this case. If your styles allow empty paragraphs to be collapsed, maybe the issue is that you’re not seeing them because of that.
(But also, controlling document whitespace by putting in empty paragraphs is a somewhat messy way to do that. ProseMirror is kind of designed with the idea that you use semantic header nodes and such, and they are styled to show the appropriate amount of margin around them.)
Thanks so much for your insight in the earlier reply — that really helped clarify things around how block-level nodes and whitespace work in ProseMirror.
I’d like to follow up with one more question related to markdown serialization:
I’m using Tiptap (with tiptap-markdown) inside a Flutter app via WebView. Visually, everything works fine: when a user presses Enter multiple times, multiple empty paragraph nodes are rendered in the HTML output as expected (
etc.).
However, when I call editor.storage.markdown.getMarkdown(), all empty paragraphs are completely stripped out of the result — regardless of how many empty lines the user inserted. This results in a user typing:
first
second
…being serialized as:
first
second
This leads to visual mismatch or data loss if I save and reload content via markdown.
I attempted to solve this by using appendTransaction to mark empty paragraphs with an isEmpty attribute, then trying to hook into the serializer. Unfortunately, that approach didn’t work in tiptap-markdown.
So, my question is:
Is creating a custom markdown serializer the only viable path for preserving empty paragraphs in serialized markdown output?
If so, could you recommend best practices or known patterns for customizing the ProseMirror (or tiptap-markdown) serialization logic to ensure empty paragraph nodes are preserved?
Alternatively, is there a built-in ProseMirror-native way of ensuring those nodes are kept in serialization, perhaps via schema config?
I don’t think it’s possible to express empty paragraphs in CommonMark. In general, empty paragraphs don’t really serve a purpose in semantic text—you might use them during editing when starting a new paragraph, but they don’t mean anything.