List unresolved links as unlinked mentions

Use case or problem

In my notes, I often put square brackets around important concepts so that once I create a note on that concept, those notes will already be linked to that new note. But sometimes that note ends up getting a different name and the term previously used in square brackets will be an alias.

The problem is that the previous mentions of that that alias will not show up under “unlinked mentions” because they have square brackets around them, i.e. they appear to be linked.

A simple example would be names of people: Let’s say I mention [[John Doe]] in many notes but when I eventually create a note with his name, it is actually called “John Tucker Doe”. So I put “John Doe” as an alias in the notes frontmatter. As a result, all occurrences of “John Doe” will be shown under “unlinked mentions” and I can turn them into links.

The problem is that occurrences of [[John Doe]] will not show up as unlinked mentions, making it very difficult to get those mentions directed to the note entitled John Tucker Doe.

Proposed solution

Make it so that Unlinked mentions also include not just occurences of the note’s alias(es) but also the notes’s alias(es) in square brackets.

It is probably advisable to limit this behaviour to aliases for which no synonymous file names exist so that links to existing files are not listed (and potentially broken). In other words: only unresolved links should be listed as unlinked mentions.

Current workaround (optional)

Sometimes I can work around this by renaming the new note to the alias and then restoring the original name. But most of the time, this is not an option because it will change the text in the linked notes to the name of the new note instead of creating an alias link.

Related feature requests (optional)

Given links to to a nonexistent note named “John Doe” that you want to point to existing note “John Tucker Doe”:

  1. Make an empty note named “John Doe”.
  2. In the note menu (3 dots in upper right) of the “John Doe” note, select “Merge entire file with…”
    • (if you don’t see this, turn on Settings > Core Plugins > Note Composer and try again).
  3. Select “John Tucker Doe”.

Your “John Doe” links should now be “John Tucker Doe” links.

Does this do anything different than

i.e. to rename the note John Tucker Doe to John Doe and then back to John Tucker Doe?

(I guess I’ll try it…)

Edit: As far as I can see, this does the same as my existing workaround, i.e. it is of limited use because it is destructive in the sense that

Ah, right, sorry.

There’s a feature request to use aliases when merging, if you’d like to upvote it.

I don’t know of a community plugin that addresses that case, but I’ll look when I have a chance. One that lets you choose whether or not to alias each link would be very handy (tho you wouldn’t need that option here).

I don’t see anything relevant in Community Plugins when searching “alias” or “merge”.

No, neither did I. Thanks for checking!

I think this (together with this) really is a design flaw/shortcoming in obsidian core. Not sure it makes sense to try and work around that via plugins. Unless I fail to grasp some design principle that requires the current way of handling aliases, I’d say that Obsidian’s implementation of aliases is incompmlete and I would almost assume that fixing this already is on some roadmap.

The challenge may well be that there are probably varied ways of using aliases at the same time as aliases are widely used so that changing the core behaviour to fix the above problem may break something in some other use case.

But so far, I have failed to imagine such a use case. Can you come up with a scenario in which the propsed solution might lead to problems?

I see the use case, but I’d guess it’s not an urgent problem for most people. If it’s not on the roadmap and you urgently want the feature, I’d say working around makes sense.

The main problem I see is that including links in “unlinked mentions” is bound to confuse people. I see value in having a separate feature or plugin to list unresolved links that match aliases.

A complication is the case where multiple notes have the same alias — for example, notes about 3 people who are all nicknamed “Bob”.

A possible workaround for now: List linked-but-not-linked aliases by searching "[[John Doe]]", then use an external app to do a vault-wide search-replace.

First I thought: hmm, you have a point. Didn’t think of that. But then: those are not really links (since they are not linking to anything) at least if you

If only “unlinked links” are shown as unlinked mentions, I don’t see how people would be confused.

This complication is not specific to my suggestion. It exists already with unlinked mentions as we have them today.

I can have three notes with the alias “Bob” and in each of them I get to see all mentions of Bob under unlinked mentions and in each note I can choose which of those Bobs to convert into a linked mention. This works fine and nothing would change if also all mentions of [[Bob]] were listed under unlinked mentions.

There is no problem.

I’m not sure what I’m suppose to search and replace? Replace all “[[John Doe]]” with “John Doe”?

What would be the advantage over

A link is a link. Regardless of whether you reach anything at the other end, the text has been marked as a link. It’s not unlinked (if it doesn’t go anywhere yet, it’s “unresolved”).

I thought you wanted to replace [[John Doe]] with [[John Tucker Doe|John Doe]] in some cases.

I could be reading this wrong, but if you change any link name, that change will change all occurrences of that link.

You might find clarity in this from the help vault:

Fine. Thanks for clarifying the language. I have added the clarification to the OP and title.

So the question is whether people would be confused if unresolved links would be listed under unlinked mentions and I don’t think so. It makes intuitive sense.

Yes. So how do I do that with some external tool? Even if I wanted to convert all occurrences of [[John Doe]], using an external tool seems rather complicated to me.

I suppose you mean if you change the name of a note, then all links pointing to that note will be changed to point to the note’s new name? If so, that would be the workaround suggested in the OP, which comes with the downside that it is destructive in the sense that it changes the text in the notes containing the link (rather than modifying the link to become an alias link).

  • A linked mention in a note is where text is referenced by link to another note or a header or block in the current note.|

  • An unlinked mention is unadorned text in the current note that actually has note with that name.

  • As @CawlinTeffid indicated, a link is a link; it is a defined connection. If you want to give it a different name where it is used, enter a pipe symbol ( | ) as he indicated [[John Tucker Doe|John Doe]]. This is not an alias, but called DisplayText. It only displays for the individual link it is used in.

  • An unlinked mention is text (but there is a note with that name). It is not formally linked. They are potential links just waiting to be formally created:)

I’m sorry, I don’t think the developers will include links in the list of unlinked text. A separate list seems much more likely.

It’s like normal search and replace except you need an editor that can do it across multiple files, like Virtual Studio Code (free, cross-platform), BBEdit (Mac; I think the free version includes this feature), or Geany (free, Linux).

That would work just fine.

1 Like

Unresolved links which are aliased somewhere

So here is a script which lists unresolved links which are aliased in an existing files:

## Unresolved links which are aliased in an existing file
```dataviewjs

// Build the list of all existing aliases in the vault,
// with references to every file it's defined in
const aliasToPaths = {}  // alias to file.path(s)
await dv.pages()
  .where(p => p.file.aliases.length > 0)
  .forEach(p => {
    p.file.aliases.forEach( a => {
      if ( !(a in aliasToPaths ) ) 
        aliasToPaths[a] = []
      aliasToPaths[a].push(p.file.path)
    })
  })

// Build the complete list of unresolved notes,
// with where they're defined as the origin
const unresolved = {}
Object.entries( dv.app.metadataCache.unresolvedLinks )
  .filter( ([origin, unresolvedList]) => 
           Object.keys(unresolvedList).length )
  .forEach( ([origin, unresolvedList]) => {
    Object.entries( unresolvedList )
      .forEach( ([newNote, count]) => {
        if ( !unresolved[newNote] )
          unresolved[newNote] = []

        unresolved[newNote].push( dv.fileLink(origin) )
      })
  })   

// Combine the two and list all unresolved notes,
// with a column listing where they're defined,
// and a column with where their alias is defined
dv.table(["Unresolved", "Origin(s)", "Aliased in"],
  Object.entries(unresolved)
    .sort( (a, b) => (a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1) )
    .map( item => [dv.fileLink( item[0] ), 
                   item[1], 
                   ( aliasToPaths[ item[0] ] ? 
                       aliasToPaths[ item[0] ].map( p => dv.fileLink(p) ) :
                       "" ) ] )
    .filter( row => row[2] )
  )
```

In my test vault this lists the following:

How to read this table:

  • Neither “Johnny Boy” nor “SameSame” exists as separate notes in my test vault
  • Someone™ linked to “Johnny Boy” from “f52147 Nested tables” and “Sandbox”
  • Someone™ linked to “SameSame” from “SameName”
  • “Johnny Boy” is listed as an alias within the note “John Tucker Doe”
  • “SameSame” is listed as an alias in both “SameName” and “Jane Smith”
  • Someone™ is a little confused when creating notes and aliases… :smiley:

Adaption to list origin of all unresolved files

To list the origin of all unresolved files, one can use the script below, where I’ve removed the alias references, and removed the column from the output table. If one would want to combine both of these result one could remove the .filter(row => row[2]) close to the bottom.

But if one just want the full list of unresolved files with their origin, one could use this script:

```dataviewjs
// Build the complete list of unresolved notes,
// with where they're defined as the origin
const unresolved = {}
Object.entries( dv.app.metadataCache.unresolvedLinks )
  .filter( ([origin, unresolvedList]) => 
           Object.keys(unresolvedList).length )
  .forEach( ([origin, unresolvedList]) => {
    Object.entries( unresolvedList )
      .forEach( ([newNote, count]) => {
        if ( !unresolved[newNote] )
          unresolved[newNote] = []

        unresolved[newNote].push( dv.fileLink(origin) )
      })
  })   

// Combine the two and list all unresolved notes,
// with a column listing where they're defined
dv.table(["Unresolved", "Origin(s)"],
  Object.entries(unresolved)
    .sort( (a, b) => (a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1) )
    .map( item => [dv.fileLink( item[0] ), item[1] ] )
  )
```

In my test vault the top of the output from this is:

Here one can see that I’ve got some random links to dates which are not resolved (aka non-existing), referred to from various workout notes.


I hope this is helpful to someone passing by this thread, and even though this is a feature request, I think this also helps to showcase one part of what’s asked for in this request.

It should be easy to see that given the unresolved link to “John Doe”, it would show up in this list with an aliased in “John Tucker Doe” (in the last column), and from there one could rather easily change the reference to the existing link.

1 Like

Do you have an automated procedure in mind?

Sorry, but no automated procedure. Since you’re coming from a list stating what the alias and the link should be, it shouldn’t that hard to do the manual edit. In most cases, there wouldn’t be a lot of rows in the table anyways.

So my preferred method when cleaning up stuff like this, would be to have the query in a pane on the left, and open the files to be edited in a separate tab, and then edit and close as I make my way through the list.

Love this, it will allow me to get rid of an plugin! One problem for me, how do I exclude a folder? It’s finding links in my ‘templates’ folder that I don’t want/need listed. I was thinking something like the following would do it (don’t show anything with a link that includes ‘_Meta’), but can’t get it to work:

  Object.entries(unresolved)
    .filter( (a, b) => !b.fileLink.includes("_Meta"))
    .sort( (a, b) => (a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1) )
    .map( item => [dv.fileLink( item[0] ), item[1] ] )
  )

I’m guessing it could be filtered even earlier in the process, but my copy/paste scripting abilities aren’t measuring up to this challenge…

Where do the origin contain _Meta, is that at the very start of the origin file path?

The reason your code doesn’t work is that you can’t use includes() directly on a list of links. It would be better to do this higher up, and not let meta files get pushed to the unresolved list at all.

‘_Meta’ is a folder at the root of the vault that contains my templates, scripts, etc. I figured earlier would be better but couldn’t get anything to work. For instance, I tried the following, but ‘includes’ doesn’t seem to work there either:

Object.entries( dv.app.metadataCache.unresolvedLinks )
  .filter( ([origin, unresolvedList]) => 
           Object.keys(unresolvedList).length )
  .forEach( ([origin, unresolvedList]) => {
    Object.entries( unresolvedList )
      .forEach( ([newNote, count]) => {
        if ( !unresolved[newNote] )
          unresolved[newNote] = []
		if ( !dv.fileLink(origin).includes("_Meta"))
          unresolved[newNote].push( dv.fileLink(origin) )
      })
  })