# How do apps like Notion and Linear preserve empty lines using ProseMirror?

**URL:** <https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386>\
**Category:** Uncategorized\
**Created:** [April 30, 2025, 6:52am UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386 "2025-04-30T06:52:40Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![kevin\_giantree](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/kevin_giantree/32/4706_2.png) [@kevin\_giantree](https://discuss.prosemirror.net/u/kevin_giantree)\
**Post date:** [April 30, 2025, 6:52am UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386/1 "2025-04-30T06:52:40Z")

</div>

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?

Thanks in advance for your insights.

---

<div class="post-metadata">

**Author:** ![marijn](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/marijn/32/15_2.png) [@marijn](https://discuss.prosemirror.net/u/marijn)\
**Post date:** [April 30, 2025, 6:55am UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386/2 "2025-04-30T06:55:06Z")

</div>

> [@kevin\_giantree](#):
>
> when users press Enter multiple times, only one paragraph is stored and the extra blank lines are lost.

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.)

---

<div class="post-metadata">

**Author:** ![kevin\_giantree](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/kevin_giantree/32/4706_2.png) [@kevin\_giantree](https://discuss.prosemirror.net/u/kevin_giantree)\
**Post date:** [April 30, 2025, 8:22am UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386/3 "2025-04-30T08:22:39Z")

</div>

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:

```auto
first

second

```

…being serialized as:

```auto
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?

Thank you again!

---

<div class="post-metadata">

**Author:** ![marijn](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/marijn/32/15_2.png) [@marijn](https://discuss.prosemirror.net/u/marijn)\
**Post date:** [April 30, 2025, 12:34pm UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386/4 "2025-04-30T12:34:08Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![marmoure](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/marmoure/32/3829_2.png) [@marmoure](https://discuss.prosemirror.net/u/marmoure)\
**Post date:** [May 1, 2025, 9:28pm UTC](https://discuss.prosemirror.net/t/how-do-apps-like-notion-and-linear-preserve-empty-lines-using-prosemirror/8386/5 "2025-05-01T21:28:21Z")

</div>

One way we did this in our project was adding hard breaks in our schema.

```ts
      hard_break: {
        inline: true,
        group: 'common_inline all_inline',
        selectable: false,
        parseDOM: [{tag: "br"}],
        toDOM() { return ["br"] }
      } as NodeSpec

```

And when we are done with editing we save the content as XML.

PR:

> <https://github.com/evolvedbinary/prosemirror-lwdita/pull/325>
>
> This PR enables inserting line breaks in the middle of text when pressing enter.
