Is it possible to change an already created link from a previous note with js?

What I’m trying to do

I’m trying to tweak my Daily Note template (using Templater) and I’ve written a user script for getting the last created file for a Previous link, but the Next link defaults to tomorrow and I want to be able to update that when a new daily note is generated and tries to update its own Previous link. The need is because I don’t use the daily note every single day and I want the daily note links for Previous/Next to target the equivalent previous/next file in the folder, going by creation date.

Things I have tried

const last_created_metadata = app.metadataCache.getFileCache(last_created);
        const last_created_links = last_created_metadata?.links;
        
        if(last_created_links && last_created_links.length > 0)
        {            
            last_created_next_link = last_created_links.find(x=> x.displayText.includes("Next"));
            if(last_created_next_link){
                console.log("Found last_created Next link: "+last_created_next_link.displayText);

                const link = app.fileManager.generateMarkdownLink(tp.config.target_file, tp.file.folder(true), ...[, ], last_created_next_link.displayText);

                last_created_next_link.link = link;
                last_created_next_link.original = "[["+link+"|Next >]]";
            }
        }

Currently this is what I have. In the function that basically finds the last_created TFile in the Daily Notes folder (excluding the one we’re currently generating), gets its links through its cached metadata and tries to update the underlying link.

The template file’s links are defined like so:

[[<%tp.user.find_last_daily_note(tp)%>|< Previous ]] - [[<%tp.date.tomorrow("YYYY-MM-DD")%>|Next >]]

Not sure I’m even doing this correctly. Any ideas?

We’ve touched in on this subject lately, and two of my responses with almost the same answer is given below. The trick is to defer showing the previous/next links until you show the note. This way you don’t need to change a link in an existing note, but just let the note itself discover which is the previous and next links.

As far as I can see, you’re leaning on dataview reevaluating itself every time, not deferring per se.
DV is gonna run forever and I’m already using it for a TaskList coming from a single file, showing only the currently not completed tasks grouping per section (heading), but it’s refreshing all the time and it annoys me a bit. That said, it auto updates when you complete a task, so I guess it’s to be expected.

Also tried using dv on a link and it’s surrounded by a code block (quite expectedly) which I’m also not too keen about.

What I want to know is how Obsidian’s main API works and how I can change the underlying pre-generated link, knowing the TFile itself, short of editing its content like text (which I’ve already seen used, although not sure I want to go that way if I can avoid it).

I ended up using straight up text replacement to do this. Not sure if it’s possible to use the Obsidian js API to replace the links but this seems to work easily.

This is what I ended up with:

function ctimeComparer(a, b) 
{
    if (a.stat.ctime < b.stat.ctime) {
        return 1;
    } else if (a.stat.ctime > b.stat.ctime) {
        return -1;
    }
    // a must be equal to b
    return 0;
}

async function find_last_daily_note (tp) {
    today = null;
    last_created = null;

    const daily_folder = "Daily Notes/";

    const dailies = app.vault.getMarkdownFiles().filter(file=>
        {
            return file.path.startsWith(daily_folder)
            && file.basename != tp.file.title;
        });

    console.log(dailies.length);
    if(dailies.length > 0)
    {
        //Sorts with newest in 0 index
        dailies.sort(ctimeComparer);

        last_created = dailies[0];
        const last_created_metadata = app.metadataCache.getFileCache(last_created);
        const last_created_links = last_created_metadata?.links;
        
        if(last_created_links && last_created_links.length > 0)
        {            
            last_created_next_link = last_created_links.find(x=> x.displayText.includes("Next"));

            if(last_created_next_link){
                const new_link = app.fileManager.generateMarkdownLink(tp.config.target_file, tp.config.target_file.path, null, "|Next>");

                try {
                    app.vault.process(last_created, (data) => {
                        console.log("Replacing " + last_created_next_link.original + " with " + new_link);
                        return data.replace(last_created_next_link.original, new_link);
                    })
                } catch (e) {
                    console.error(e);
                    throw e;
                }
            }
        }
        return last_created.basename;
    }
    return tp.date.yesterday("YYYY-MM-DD");
}
module.exports = find_last_daily_note;

Since I’ve found the previous note, I look at its internal links through its metadata and then generate a markdownlink to the newly created daily note and do a string replace through vault.process which is recommended for this sort of modification.

How and when will you call this replacement function?

You’re correct that my approach is using dynamic creation of the links, but how do you track when a change is needed in your solution?

It’s not just creating dynamic links…my approach is also dealing with dynamic links since they are not fixed (since there’s need for a replacement). The issue is that dataview will run forever, every 5s (default interval), needlessly, unless your use case actually is that volatile that you create and delete dailys for fun. Just seems too wasteful, albeit much simpler.

The change is only relevant when creating a new daily. Dailys are created on a single date. So no reason to store any kind of extra date info(like in a metadata header) since TFiles already store creation time (file.stats.ctime). When you create a new daily using templater, you can call js code from tp.user.<function_name> to construct those links kinda like so:

[[<%tp.user.find_previous_note(tp)%>|Previous]] - [[<%tp.date.tomorrow(..)%>|Next]]

Since this is the template for the new daily, there’s no tomorrow yet, but I still generate a potential link.

find_previous_note is the code you see in the previous reply.

This is all done during daily creation, so runs once and I’m basically creating current->previous and updating previous->next at the same time.

Realistically, there’s no reason for me to delete a non-current daily, so no reason re-evaluating it all the time. I can do a manual update if there’s ever any good reason. The main case is when there’s a gap between the new and the previous one, where a fixed link won’t cut it.

It’s run when you view that particular note in live preview or reading mode. Not when it’s hidden, nor every 5 seconds.

Or when you add a daily interfering with the previous order. And that happens in my case as I’m rewriting some of my entries from an analog source every now and then.

If you do any sync or backup/restore or just some copy operations the ctime is not to be trusted. This happens far more frequently than you think if you have your vault on multiple devices and different operating systems.

So I always add the date into either the file title or as a property. This is also very useful when rewriting entries, or when doing an entry a few days after it happened.

This is I do partially agree with, as the next/previous doesn’t change very often, but it do happen, and it’s a rather cheap operation to do it dynamically. Not to mention it doesn’t require me to change other files, which might cause race conditions.

But your mileage and preferences may vary, so I’m happy you found a solution that works for you.

That’s what I meant. That this code, contrary to template-triggered code, will run “forever” because it’s reevaluated every time you view that file, be it now or in 1 year or 10.

I see. I don’t use analog means anymore. I opted for Sync because I use Daily as a log for decisions I make that day or tasks I’ve completed etc and every so often I work from other devices (laptops). If I forget to write one, I will go back, but I don’t do it that often.

That said, not sure if you mean sth else by “rewriting”. I understand it as transferring from paper into a new note. Say I create a note in between of previous ones. I can still apply a template to it which can trigger code as with daily. My current template will take care to update the current->previous and previous-> next links, but I could switch

[[<%tp.user.find_previous_note(tp)%>|Previous]] - [[<%tp.date.tomorrow(..)%>|Next]]

to

[[<%tp.user.find_previous_note(tp)%>|Previous]] - [[<%tp.user.find_next_note(tp)%>|Next]]

with similar code as before which will take care of current->next and next->previous and will default to tomorrow if there’s no note beyond it. It would still run once, only on template creation. So even if I add a new note in between, it will fix both links in all 3 files (previous, current, next).

I think I came across another post where they were talking about this and I do use sync from multiple devices, but usually work is done on one device and depending on where I decide to do work from another(less frequent), I switch. So far I haven’t come across any issues in a few months I’ve been using Sync, but again my use case is fairly simple and less volatile.

That said, one consideration for this is whether dataview constantly remodifies the note it’s in, which might overburden Sync with constant modification updates. My solution above will run once, so it’s far simpler.

On the other hand, I’ve only now added dataview in my daily template and it constantly reevaluates to show non completed tasks. I wonder if I’ll now see the issues you’re talking about.
I will think about what you said about ctime corruption though. If I see any issues due to Sync, I will have to account for it in the solution.

Btw I seem to remember some part of the Obsidian API which allows you to set ctime and mtime, so perhaps when creating a note in between, for the Daily Note template alone, you can also set the file’s ctime and mtime to be a fixed time within the date the daily is from.

So if you create 10/12/2023 between 9/12/2023 and 15/12/2023, even if you’re creating this on 20/12/2023, you can set its ctime to be e.g. 9am of 10/12/2023. Then sorting by ctime won’t be affected, at least by this specific issue.

The files being modified apart from the currently generated note, are not used at the moment and I’m using the recommended modification way for them(vault.process), and I think it’s also how Obsidian treats the update of existing links when a file is renamed (going by a github repo fixing some issue in that aspect). And since it happens only once, the probability that a race condition happens is significantly lower.

All vault file operations (create, read, modify etc) have an optional options parameter which contains optional ctime and mtime. If you provide them yourself, you override the default behavior.

I take this back, it’s dependent on whether I have multiple files open already in the editor. I guess it will require some testing to see how updates to files that are already open behave.

There might be some more edge cases due to Sync there. I see other forum posts about difficulty to preserve creation date due to other filesystems like iCloud. At the moment I almost never use Obsidian on non-Windows environments, so I don’t see this becoming an issue, but I will add the ctime as metadata in the template likely and use that for sorting instead. Thanks for letting me know about that.

A dataview query will not keep modifying the file (unless you program it to do so), so it’s not causing any modification updates. It works by adding a step to the conversion process from markdown to html.

But you’re right it’ll run each time the file is viewed, and not be static like your solution. Static in the meaning that once the template completes it’s not changed when viewed or processed by other plugins. So in combination with something like an export plugin, your solution is better as the query is persisted and static.

You corrected yourself on the other part on your script modifying the previous file in addition to the file you’re creating. And it’s good that you’re using the Obsidian API functions to do so.

My experience with trying to set ctime and mtime is not ideal. It often fails at some OS’es, and I don’t rely on such operations. So I’d rather use metadata for this.

Your interpretation of my (non-native English) writing was correct and adding a function to also handle the next file case would counter it.

On a side note; my header query also handles previous/next headers on other groups of files, where it sometimes uses alphanumerical sorting and sometimes it adds prefixes like month/year links for journal entries, as well as calculating some values for the given day (like kcal eaten, or average a day/event rating score).

This could probably to some extent also be done within a template, but my setup also allows for all files using the template to get a new header by changing the query script file with the changes being displayed the next time any for using the header is viewed

Right, that makes sense…the file itself only contains, the js code. You’re absolutely right about that.

In that case, I’ll take your word for it and go the metadata way.

Again, I opted for this on-demand approach due to the simplicity of my use case.
And you’re right that my previous daily notes are vulnerable to changes on the template itself, but I’m not really bothered by it since the reason for their changes usually mean difference in importance to certain subjects (e.g. a log for a project is added to the template in December when it wasn’t before because other projects had to progress first). It’s also a kind of log in its own right.

But anyhow, thanks for all the insight. I will modify the template to handle both next since there’s no reason to avoid it and use metadata instead of ctime.

1 Like

Thanks to you as well, for challenging a common solution with a fresh look on stuff! :slight_smile:

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.