Using header-based links as a way to mark relationship to the linked note

Marking relationship with a linked note is a powerful feature request made by many people.
As in the screenshot below, note A can expand upon note B, while C disproves the same note B. These are different relationships to the same note, but Obsidian treats them equally and by design does not allow user to enter this information anywhere.


However, there is a somewhat “sacrificial” way to actually add this feature. However, you need to be willing to rethink your use of linking to specific headers, that is [[note B#specific-header]].

What if, instead, we told Obsidian not the name of the header, but the relationship between the linked notes?

My script leverages the information stored in the header-link and treats it as a relationship marker instead.

Let’s say you have a note [[Germany]] that links to another like this: [[Berlin#is its capital]].
The string after # doesn’t have to be a valid header.

After running this script:

var list = dv.pages('[[]]').sort(p => p.file.path, 'desc').map(p => + " _" + p.file.outlinks.filter(
	outlink => outlink.path == dv.current().file.path
).map(link => dv.func.default(link.subpath, 'no annotation')) + "_")

dv.header(2, "Related")

You get the following results:

Berlin [is its capital]
another note [no annotation]

1 Like

I kind of like your thinking, but I don’t like that you sacrifice the header links altogether to get these links. So just thinking out load, but would it be better if you said that only subpath’s starting with say _ were translated into relationship links?

And what would happen if you had multiple links from a file to your current file? Wouldn’t that break the formatting as you’ve written it with an underscore on either side of the filtering of the outlinks?

So accommodating for both of these thought I think I’d rather use something like:

const list = dv.pages('[[]]')
  .sort(p => p.file.path, 'desc')
  .map(p => + " " + 
       .filter( outlink => outlink.path == dv.current().file.path &&
                outlink.subpath && 
       .map(link => "_"  + dv.func.default(link.subpath.substring[1], 'no annotation') + "_")
       .join(", ")

if (list.length > 0) {
  dv.header(2, "Related")

Or some variation thereof depending on whether I would only like to include the annotated links, or all links, and so on.

But to repeat myself, it’s an interesting concept, and I can scenarios where this would be useful!

1 Like

Thanks for the reply! I actually tried picking a special character and only treating strings starting with it as “relationship markers”. However, Obsidian does two weird things when it comes to using them:

  • some characters visually break the link CSS, at least on Minimal and AnuPpucin theme (_ is a notable mention)
  • Obsidian API discards the character for some reason in the Link object. When you look the object up in the inspector, the obj.subpath property says foo instead of :foo or _foo.

Believe me, I’d love to be able to accommodate both workflows (using header-links as designed, as well as using them as relationship markers), but as things stand, Obsidian doesn’t really play well with special characters in the # part.

I tested special characters in subpath again and can confirm only hyphen is actually preserved:

Ironically the display doesn’t mind them:

So maybe filtering by hyphen, then?

1 Like

I believe your code has a small error, or maybe I misunderstand your intentions here. Why substring[1]? Your script doesn’t work, showing _no annotation_ where there clearly is an annotation. If I get rid of it, it works.

So I believe this is the correct line instead:
.map(link => "_" + dv.func.default(link.subpath, 'no annotation') + "_")

Btw, for anyone that wants to put this code in a hundred notes going forward - spare your sanity a moment and put this script into a file inside your Vault, for example, code/related.json then run it with this code instead:

await dv.view('code/related')

Thanks to this, whenever you want to make some changes to the code, you’ll only make it in that file, and it will take affect in every note referencing it.

I combined and misused some stuff related to which kind of brackets to use here and there, so it was indeed in error. Here is an updated example better showcasing the point I was trying to showcase:


[[A#-First letter|Some alias]]

[[A#-As a grade]]

[[A|Just A]]

  .map(o => (o.subpath && o.subpath[0] == "-")
     ? dv.fileLink(o.path, false, o.display.includes(">") ? "" : o.display) + " _" + o.subpath.substring(1) + "_"
     : o))

Here we’ve got four links to the same document, one with a “proper heading” links, two variants with annotations (with and without an alias), and a normal variant with an alias.

This coding (tries to) ensure that we only present proper links possibly with an alias, and add the alias if present. This document should display something like:


Now if only we could make that into a function so that it could be simpler to write…

const dvg = dv.evaluationContext.globals

dvg.annotatedLink = (link) => {
  return (link.subpath && link.subpath[0] == "-")
    ? dv.fileLink(link.path, false, link.display.includes(">") ? "" : link.display) +
      " _" + link.subpath.substring(1) + "_"
     : link

  .map(o => dvg.annotatedLink(o) ))

By the way, using dv.view() for something like this when fully built out is a very good idea, which more people should consider instead of repeating some query through insertion into templates. This allows for a lot easier customisation and changes during the lifespan of any given query.

1 Like

Adding these cases so that they’re properly handled now in your code is great! Love these additions. Also, the move to a function is also smart.

What is thr evaluationContext.globals object you’re referencing in your code? Even though it is part of the dv namespace, the Dataview documentation does not have any mention of it.

I also believe you should be referencing inlinks here, not outlinks. We’re looking for notes that link TO the dv.current(), not notes linked FROM dv.current().

EDIT: For some reason, inlinks forgets subpath every time and shows undefined even though there is a header specified. Maybe it never gets copied to it. Hm… only outlinks seems to preserve the heading information.

It’s not documented, for sure, but it’s the context used for each new query used to hold the query related variables and so on. Not entirely sure on all uses of it, but if I’ve understood it correctly (and tests indicate that) then it’s only “alive” during any given query, which allows for us to use it as kind of a temporary storage for stuff like this function.

In theory inlinks and outlinks should hold the same, but as you’ve discovered there are differences, and it seem like it’s only the outlinks which holds the full information with alias/subpath/heading/… I’ve had to rewrite a few queries to go the other way just because of this.

For test purposes I processed the links from the current file, and so it was natural to use dv.current().file.outlinks. But it shouldn’t be that hard to change it up, so that the query limits the source, using FROM, and then uses file.outlinks matching current file to get the same effect of all links to the current file with full details.