You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Distinguish link types (e.g. External vs Internal) #1861
Is your feature request related to a problem? Please describe.
We currently need a very important feature that is not present in BlockNote.
Our documents can contain two types of links: they can either refer to external resources (URLs), or internal resources (documents, wikis, etc.). Inside the editor, these internal links are encoded with a complete URL for ease of use.
We need to make internal and external links visually distinct. The current design would be to add an icon after the label of external links, but this could change in the future.
It's pretty high priority for us, I'm not sure how hard it could be to implement, but if this is doable on your side, it would be very appreciated :)
Describe the solution you'd like
There are multiple ways this could be implemented, the easiest being attaching an internal: true property to links in BlockNote's AST, that would then be reflected in the DOM (e.g. a data attribute such as data-blocknote-internal-link). We could then use some CSS based on that and add the icon using the ::after marker.
Describe alternatives you've considered
Enabling attaching raw DOM attributes to elements
Enabling attaching DOM attributes prefixed with e.g. data-blocknote-*
Enabling prefixing and suffixing the DOM of the links with additional React elements
All these have quite a bit of drawbacks, so I think the first solution I described above would be a better trade-off.
Additional context
N/A
Bonus
[ ] I'm a sponsor and would appreciate if you could look into this sooner than later 💖
Otherwise, we'd need to look deeper to add support for this
Yes, that is the goal. We can't match on href as there isn't any common pattern in the URL distinguishing internal and external links, but if there is any attribute that can be attached to a links to distinguish them, that'll work for us.
I would probably implement this as being a different kind of inline content (custom inline content) so that you can separate the links meaningfully, as I doubt that all that you would need is to visually distinguish them, you'll also likely need different onclick behaviors at a minimum, and could likely need hover-overs.
I would probably implement this as being a different kind of inline content (custom inline content) so that you can separate the links meaningfully, as I doubt that all that you would need is to visually distinguish them, you'll also likely need different onclick behaviors at a minimum, and could likely need hover-overs.
if we make customizing default blocks / ic easier, do you think the better approach would be to have a different type of custom inline content - or would it be better to extend the default link with an additional prop?
It depends on how divergent the link ends up becoming.
Like if you could list resources & then insert them into the document, and clicking on the resource takes you to a different part of the application. I would say this is different than just a link - I would say that this is more of a "reference" since it has more context about what the link is pointing to (e.g. like a mention).
As for configuration for a link, I would expect only to expose:
how it is rendered
props (e.g. target="_blank")
behaviors of how it interacts with the editor (e.g. renders as inline content, is draggable, etc.)
Even just for understanding that these links are somehow "different" (e.g. iterating the document). I'd make them their own custom inline content.
I would probably implement this as being a different kind of inline content (custom inline content) so that you can separate the links meaningfully, as I doubt that all that you would need is to visually distinguish them, you'll also likely need different onclick behaviors at a minimum, and could likely need hover-overs.
For our specific use case all links are gonna behave the same way in terms of redirection, we only need to open actual pages that have a well-defined URL.
But it's true that may need popovers or things like that in the future though.
Would using a new type of inline content allow for a consistent user experience? E.g. same accessibility features, etc.?
Yep, it should, there shouldn't be anything special about your link type over ours. It can even still render to an <a> tag, it'll have a an attribute on it that says what type it is to distinguish them
I'm having an additional issue with this approach: when inserting a custom link, I obviously can't use the editor.createLink method. So I tried reproducing this method by checking its inner code, but it makes use of the TextSelection class from @tiptap/pm/state which is not re-exported by BlockNote. Could it be possible to fix that?
I need the ability to create and edit internal links in-place, just like normal links.
publiccreateLink(url: string,text?: string){if(url===""){return;}constmark=this.pmSchema.mark("link",{href: url});this.transact((tr)=>{const{ from, to }=tr.selection;if(text){tr.insertText(text,from,to).addMark(from,from+text.length,mark);}else{tr.setSelection(TextSelection.create(tr.doc,to)).addMark(from,to,mark,);}});}
So I tried reproducing this method by checking its inner code, but it makes use of the TextSelection class from @tiptap/pm/state which is not re-exported by BlockNote. Could it be possible to fix that?
You can just import it from that package, it is already in your app bundle since we depend on it
Actually, there is another issue as well: calling the mark method on editor.pmSchema causes an error as the inline content type is not recognized.
inline content is represented as a node in prosemirror (as opposed to a mark), so the createLink method will not be 1:1, try this instead:
You can just import it from that package, it is already in your app bundle since we depend on it
I can't, as it's not stated as an explicit dependency. Technically, it is in the bundle, but the compiler won't allow me to import it (for good reasons).
Adding TipTap as a dependency would also be a no-go, as I would have a duplicate dependency, and need to ensure the version is updated every time BlockNote changes its dependency on TipTap.
inline content is represented as a node in prosemirror (as opposed to a mark), so the createLink method will not be 1:1, try this instead:
This doesn't work as the insertInlineContent doesn't allow wrapping an existing text. For instance, if I type a word in the editor, select it, and then trigger this method, the content will be deleted.
EDIT: Actually it seems like the method does replace the selected content, so this would actually work.
But how can I update an existing link without replacing its content?
I can't, as it's not stated as an explicit dependency. Technically, it is in the bundle, but the compiler won't allow me to import it (for good reasons).
Adding TipTap as a dependency would also be a no-go, as I would have a duplicate dependency, and need to ensure the version is updated every time BlockNote changes its dependency on TipTap.
That's not how packaging/bundling works, if your explicit dependency on @tiptap/pm is compatible with the semver range that we declare, then it will not duplicate the package, there would only be a single version in your project, since they are compatible. This is kind of the point of packaging as deps, for de-duplication.
insertInlineContent removes the word because it would be expected to be supplied in a content like this:
That's not how packaging/bundling works, if your explicit dependency on @tiptap/pm is compatible with the semver range that we declare, then it will not duplicate the package, there would only be a single version in your project, since they are compatible. This is kind of the point of packaging as deps, for de-duplication.
You're right, I'm mixing things up. The need to pinpoint the exact same version as BlockNote and manually update it every time still stands though, it's not a robust solution.
insertInlineContent removes the word because it would be expected to be supplied in a content like this:
Yes that's what's I'm doing ; but that I want to know is the following:
Let's say we have a custom link in the editor. The user puts their cursor somewhere in the middle of it, we show the toolbar, and they edit the link's target. How would that work? The user did NOT select anything, we just happen to have the cursor in the middle of the inline content. And given we can't update an existing inline content, I'm not sure what the right approach would be.
Anything major version 2 and that minor and up is fine
The user puts their cursor somewhere in the middle of it, we show the toolbar, and they edit the link's target.
Our inline content API could definitely be better. But, the idea would be to update the link by replacing the node content. You can do this in a couple of ways:
set the selection to be the bounds of the link, and replace the content at that selection with the new updates
drop to prosemirror, and update the attributes on the inline content node once you can find it from the selection
Oh, would the main package's version and BlockNote's dependency version automatically align? I wasn't aware of such a feature.
Yea, they would, that is what ^ in ^2.12.0 means, this effectively says any minor version of major version 2, that is above or equal to 12.
Wouldn't this have an impact on UI? Like, displaying the text as selected even though the user did nothing to actually select the whole link's label?
you can move a selection for an operation, and then put it back. Like:
editor.transact(tr=>{// cache the selection for restorationconst{ from, to }=tr.selection// set to beginningtr.setSelection(TextSelection.create(0,1))// insert based on the selectiontr.insertText("1234")// put back the selectiontr.setSelection(TextSelection.create(from,to))})
It depends on the UX you are going for of course whether you want to restore the selection or not. It can be somewhat surprising that modifying an attribute changes a whole range in some cases
Let's say the user's cursor is inside a link (internal or external). I have to consider many use cases:
If the cursor is not in a link, I create it using editor.createLink() (for external URLs) or using editor.insertInlineContent() (for internal references)
If it is inside an internal link, and the user wants to update it, I need to find the node and update it using a transaction
If it is inside an internal link, and the user wants to put an external link instead, I need to find the node, extract its text content, and then call editor.createLink()
If it is inside an external link, and the user wants to put an internal link instead, I need to find the parent node of the selection and use editor.insertInlineContent with said content
If it is inside an external link, and the user wants to update it, I can use editor.createLink()
In addition, finding the node is not that simple, as I need to call multiple methods in ProseMirror to find the relevant nodes, especially since the current node may not be the one I want - e.g. a link containing unstyled text, then bold text, then unstyled text ; the cursor in the middle of the bold text, I need to find the first ascending link node from the cursor position, which is more complex.
So overall it's a lot of logic for such a simple thing as distinguishing internal/external links. Wouldn't there be a more simple approach than that one?
So overall it's a lot of logic for such a simple thing as distinguishing internal/external links. Wouldn't there be a more simple approach than that one?
Welcome to our world! Yes, it is complex. And BlockNote tries to shield you from this sort of complexity, but we haven't gotten far enough with the inline content API to offer a simpler alternative than this. This is essentially the sort of logic that we'd be doing under the hood on our side to offer an API as simple as createLink & insertInlineContent.
Yes, I'm very aware of the complexities BlockNote is facing, and am very glad it exists :)
My question was more on the line of: is there a much more simple alternative for my use case, and do you have any intent to support this kind of things natively in the future?
Hey! Just chiming in as a +1 to this, we have need for this type of functionality as well. All we really want is to change the render method or props of links, without having to re-implement all the surrounding UX.
The need comes from our (admittedly hacky) mobile apps that use web views to render our React app. In order for the apps to properly handle internal links without accidentally opening them in a new tab/subview/external browser, we would need to remove the target="_blank" prop from internal links.
Activity
YousefED commentedon Jul 21, 2025
Thanks. First thing that comes to mind is doing this via CSS. Could you use CSS attribute matchers for this? https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
Otherwise, we'd need to look deeper to add support for this
ClementEXWiki commentedon Jul 22, 2025
Yes, that is the goal. We can't match on
hrefas there isn't any common pattern in the URL distinguishing internal and external links, but if there is any attribute that can be attached toalinks to distinguish them, that'll work for us.nperez0111 commentedon Jul 22, 2025
I would probably implement this as being a different kind of inline content (custom inline content) so that you can separate the links meaningfully, as I doubt that all that you would need is to visually distinguish them, you'll also likely need different onclick behaviors at a minimum, and could likely need hover-overs.
YousefED commentedon Jul 22, 2025
if we make customizing default blocks / ic easier, do you think the better approach would be to have a different type of custom inline content - or would it be better to extend the default link with an additional prop?
nperez0111 commentedon Jul 22, 2025
It depends on how divergent the link ends up becoming.
Like if you could list resources & then insert them into the document, and clicking on the resource takes you to a different part of the application. I would say this is different than just a link - I would say that this is more of a "reference" since it has more context about what the link is pointing to (e.g. like a mention).
As for configuration for a link, I would expect only to expose:
Even just for understanding that these links are somehow "different" (e.g. iterating the document). I'd make them their own custom inline content.
ClementEXWiki commentedon Jul 22, 2025
For our specific use case all links are gonna behave the same way in terms of redirection, we only need to open actual pages that have a well-defined URL.
But it's true that may need popovers or things like that in the future though.
Would using a new type of inline content allow for a consistent user experience? E.g. same accessibility features, etc.?
nperez0111 commentedon Jul 24, 2025
Yep, it should, there shouldn't be anything special about your link type over ours. It can even still render to an
<a>tag, it'll have a an attribute on it that says what type it is to distinguish themClementEXWiki commentedon Aug 4, 2025
I'm having an additional issue with this approach: when inserting a custom link, I obviously can't use the
editor.createLinkmethod. So I tried reproducing this method by checking its inner code, but it makes use of theTextSelectionclass from@tiptap/pm/statewhich is not re-exported by BlockNote. Could it be possible to fix that?I need the ability to create and edit internal links in-place, just like normal links.
ClementEXWiki commentedon Aug 4, 2025
Actually, there is another issue as well: calling the
markmethod oneditor.pmSchemacauses an error as the inline content type is not recognized.Note that the
InlineContentcustom spec has been registered correctly in the BlockNote schema.nperez0111 commentedon Aug 4, 2025
You can just import it from that package, it is already in your app bundle since we depend on it
inline content is represented as a node in prosemirror (as opposed to a mark), so the
createLinkmethod will not be 1:1, try this instead:ClementEXWiki commentedon Aug 4, 2025
I can't, as it's not stated as an explicit dependency. Technically, it is in the bundle, but the compiler won't allow me to import it (for good reasons).
Adding TipTap as a dependency would also be a no-go, as I would have a duplicate dependency, and need to ensure the version is updated every time BlockNote changes its dependency on TipTap.
This doesn't work as the
insertInlineContentdoesn't allow wrapping an existing text. For instance, if I type a word in the editor, select it, and then trigger this method, the content will be deleted.EDIT: Actually it seems like the method does replace the selected content, so this would actually work.
But how can I update an existing link without replacing its content?
nperez0111 commentedon Aug 4, 2025
That's not how packaging/bundling works, if your explicit dependency on @tiptap/pm is compatible with the semver range that we declare, then it will not duplicate the package, there would only be a single version in your project, since they are compatible. This is kind of the point of packaging as deps, for de-duplication.
insertInlineContent removes the word because it would be expected to be supplied in a
contentlike this:ClementEXWiki commentedon Aug 4, 2025
You're right, I'm mixing things up. The need to pinpoint the exact same version as BlockNote and manually update it every time still stands though, it's not a robust solution.
Yes that's what's I'm doing ; but that I want to know is the following:
Let's say we have a custom link in the editor. The user puts their cursor somewhere in the middle of it, we show the toolbar, and they edit the link's target. How would that work? The user did NOT select anything, we just happen to have the cursor in the middle of the inline content. And given we can't update an existing inline content, I'm not sure what the right approach would be.
nperez0111 commentedon Aug 4, 2025
We don't pin to specific version for exactly this reason:
BlockNote/packages/core/package.json
Line 93 in c114ff0
Our inline content API could definitely be better. But, the idea would be to update the link by replacing the node content. You can do this in a couple of ways:
ClementEXWiki commentedon Aug 4, 2025
Oh, would the main package's version and BlockNote's dependency version automatically align? I wasn't aware of such a feature.
Wouldn't this have an impact on UI? Like, displaying the text as selected even though the user did nothing to actually select the whole link's label?
nperez0111 commentedon Aug 4, 2025
Yea, they would, that is what
^in^2.12.0means, this effectively says any minor version of major version 2, that is above or equal to 12.you can move a selection for an operation, and then put it back. Like:
It depends on the UX you are going for of course whether you want to restore the selection or not. It can be somewhat surprising that modifying an attribute changes a whole range in some cases
ClementEXWiki commentedon Aug 4, 2025
I'll try that, thank you :)
ClementEXWiki commentedon Aug 5, 2025
This approach is actually very complex.
Let's say the user's cursor is inside a link (internal or external). I have to consider many use cases:
editor.createLink()(for external URLs) or usingeditor.insertInlineContent()(for internal references)editor.createLink()editor.insertInlineContentwith said contenteditor.createLink()In addition, finding the node is not that simple, as I need to call multiple methods in ProseMirror to find the relevant nodes, especially since the current node may not be the one I want - e.g. a link containing unstyled text, then bold text, then unstyled text ; the cursor in the middle of the bold text, I need to find the first ascending link node from the cursor position, which is more complex.
So overall it's a lot of logic for such a simple thing as distinguishing internal/external links. Wouldn't there be a more simple approach than that one?
nperez0111 commentedon Aug 5, 2025
Welcome to our world! Yes, it is complex. And BlockNote tries to shield you from this sort of complexity, but we haven't gotten far enough with the inline content API to offer a simpler alternative than this. This is essentially the sort of logic that we'd be doing under the hood on our side to offer an API as simple as createLink & insertInlineContent.
ClementEXWiki commentedon Aug 5, 2025
Yes, I'm very aware of the complexities BlockNote is facing, and am very glad it exists :)
My question was more on the line of: is there a much more simple alternative for my use case, and do you have any intent to support this kind of things natively in the future?
nperez0111 commentedon Aug 5, 2025
I think I already addressed that:
Yes we intend to improve this in the future, given the time & funding to complete it
ClementEXWiki commentedon Aug 6, 2025
I wasn't sure whether you meant you didn't had the time or you didn't have the intention to go that far.
Thanks for clarifying :)
I'll keep this issue open as a reference, when the time comes and the API evolves, we can then close it.
Thanks again for your help!
codewitchio commentedon Sep 8, 2025
Hey! Just chiming in as a +1 to this, we have need for this type of functionality as well. All we really want is to change the render method or props of links, without having to re-implement all the surrounding UX.
The need comes from our (admittedly hacky) mobile apps that use web views to render our React app. In order for the apps to properly handle internal links without accidentally opening them in a new tab/subview/external browser, we would need to remove the
target="_blank"prop from internal links.[-]Distinguishing link types[/-][+]Distinguish link types (e.g. External vs Internal)[/+]