Skip to content

Distinguish link types (e.g. External vs Internal) #1861

Closed

Description

@ClementEXWiki

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 💖

Activity

  1. YousefED commented on Jul 21, 2025

    @YousefED
    Collaborator

    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

  2. ClementEXWiki commented on Jul 22, 2025

    @ClementEXWiki
    Author

    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

    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.

  3. nperez0111 commented on Jul 22, 2025

    @nperez0111
    Contributor

    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.

  4. YousefED commented on Jul 22, 2025

    @YousefED
    Collaborator

    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?

  5. nperez0111 commented on Jul 22, 2025

    @nperez0111
    Contributor

    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.

  6. ClementEXWiki commented on Jul 22, 2025

    @ClementEXWiki
    Author

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

  7. nperez0111 commented on Jul 24, 2025

    @nperez0111
    Contributor

    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

  8. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    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.

    public createLink(url: string, text?: string) {
      if (url === "") {
        return;
      }
      const mark = 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,
          );
        }
      });
    }
  9. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    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.

    Uncaught TypeError: can't access property "create", type is undefined

    Image

    Note that the InlineContent custom spec has been registered correctly in the BlockNote schema.

  10. nperez0111 commented on Aug 4, 2025

    @nperez0111
    Contributor

    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:

    editor.insertInlineContent({ type: "InternalLink", props: {"your-props-here": true } })
  11. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    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?

  12. nperez0111 commented on Aug 4, 2025

    @nperez0111
    Contributor

    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:

    editor.insertInlineContent({ type: "InternalLink", props: {"your-props-here": true }, content: editor.getSelectedText() })
  13. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    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.

  14. nperez0111 commented on Aug 4, 2025

    @nperez0111
    Contributor

    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.

    We don't pin to specific version for exactly this reason:

    "@tiptap/pm": "^2.12.0",
    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
  15. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    We don't pin to specific version for exactly this reason:

    Oh, would the main package's version and BlockNote's dependency version automatically align? I wasn't aware of such a feature.

    set the selection to be the bounds of the link, and replace the content at that selection with the new updates

    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?

  16. nperez0111 commented on Aug 4, 2025

    @nperez0111
    Contributor

    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 restoration
    const { from, to } = tr.selection
    // set to beginning
    tr.setSelection(TextSelection.create(0, 1))
    // insert based on the selection
    tr.insertText("1234")
    // put back the selection
    tr.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

  17. ClementEXWiki commented on Aug 4, 2025

    @ClementEXWiki
    Author

    I'll try that, thank you :)

  18. ClementEXWiki commented on Aug 5, 2025

    @ClementEXWiki
    Author

    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:

    • 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?

  19. nperez0111 commented on Aug 5, 2025

    @nperez0111
    Contributor

    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.

  20. ClementEXWiki commented on Aug 5, 2025

    @ClementEXWiki
    Author

    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?

  21. nperez0111 commented on Aug 5, 2025

    @nperez0111
    Contributor

    I think I already addressed that:

    Our inline content API could definitely be better.

    we haven't gotten far enough with the inline content API to offer a simpler alternative than this

    Yes we intend to improve this in the future, given the time & funding to complete it

  22. ClementEXWiki commented on Aug 6, 2025

    @ClementEXWiki
    Author

    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!

  23. codewitchio commented on Sep 8, 2025

    @codewitchio

    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.

  24. changed the title [-]Distinguishing link types[/-] [+]Distinguish link types (e.g. External vs Internal)[/+] on Feb 19, 2026
  25. locked and limited conversation to collaborators on Feb 19, 2026
  26. converted this issue into a discussion #2479 on Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions