Linking to a post with a URL different from its filename

I am setting-up the URL config for my blog and I’ve run into a usability gotcha, which I’m wondering if there’s a way around.

When I first set up my blog I had

[permalinks]
posts = "/posts/:filename/"

and all of my posts were page-bundles (I believe the term is?) under one folder, so I could link to them very easily in markdown by filename

[See here](./my_cool_article)

This was great because my editor (VS Code) would auto-suggest paths based on the file-system, allowing me to tab complete & be sure I had the correct path.

Now the number of articles has grown and I have split into folders per year. But I do not want the year to appear in the URL, and I want to use the slug, rather than the filename. I’ve got this all set up and working fine.

This issue arises when linking, I need to remember what the slug is which, of course, VS code knows nothing about.

So I was wondering is it possible to get Hugo to render markdown links to files by looking at the front matter and deriving the URL from that? (Perhaps a custom render hook would work but I don’t know enough about Hugo to do that)

Perhaps this is also an X/Y problem and I should not be linking to articles like this at all, but I couldn’t easily find any good documentation about this.

Thanks,
Dan

Yes. Enable Hugo’s embedded link and image render hooks in your site configuration.

[markup.goldmark.renderHooks.image]
enableDefault = true

[markup.goldmark.renderHooks.link]
enableDefault = true

Then use the page’s logical path as the link destination.

The above won’t work if your theme already has a link render hook. Let me know if that’s the case and I can provide additional instructions.

Thanks for the help.

I do indeed have a custom render hook. It adds an icon for external links and target="_blank".

What version are you running?

$ hugo version
hugo v0.146.6-1e0b058efe8d6e236bc7c8d6981d9bfb1443178e+extended linux/amd64 BuildDate=2025-04-20T10:58:40Z VendorInfo=snap:0.146.6

Did you create the hook, or did it come with the theme?

I am not using a “theme” as such, I started with a completely empty Hugo setup and have built-up everything from scratch.
The code for the render hook I found from searching for how to achieve what I wanted wrt the external links.
Here’s the hook

<a href="{{ .Destination | safeURL }}" {{ with .Title}} title="{{ . }}" {{ end }}{{ if
    strings.HasPrefix.Destination "http" }} target="_blank" rel="noreferrer noopener" {{ end }}>
    {{ .Text | safeHTML }}
    {{if strings.HasPrefix .Destination "http" }}
    <span style="white-space: nowrap;">
        &nbsp;
        <!-- snip inline <svg> node for icon -->
    </span>
    {{ end }}
</a>

For v0.146.0 and later the directory structure is:

layouts/
├── _markup/
│   └── render-link.html/
├── _partials/
├── _shortcodes/
├── baseof.html
├── home.html
├── page.html
├── section.html
├── taxonomy.html
└── term.html

Replace the contents of render-link.html with this code.

You can modify the new hook to add target="_blank" for external links, but I wouldn’t: most people find that behavior annoying:

https://www.digital.ink/blog/website-links-new-tab/

1 Like

Or just delete your hook and use the embedded hook:

[markup.goldmark.renderHooks.link]
enableDefault = true

And if you’re going to do that, and you don’t have an image render hook, then do this too:

[markup.goldmark.renderHooks.link]
enableDefault = true

Excuse my ignorance, but what is the default hook and why do I need to enable it if not using a custom hook?
Why do images enter in the picture, aren’t they always referred to by file path?

I’ve written about this many times. See this article, specifically the overview and problems sections.

Also, in my view, every new site should have this in its site configuration:

[markup.goldmark.renderHooks.image]
enableDefault = true

[markup.goldmark.renderHooks.link]
enableDefault = true

The above is automatically enabled for multilingual single-host sites.

And if theme authors provide their own hooks, they should model them after the default hooks.

That’s a super useful write-up, thanks!
I’ll digest it and try to update my set-up accordingly.

1 Like

If your content structure looks something like this:

content/
├── posts/
│   ├── 2025/
│   │   ├── post-1/
│   │   │   └── index.md
│   │   └── post-2/
│   │       └── index.md
│   └── _index.md
└── _index.md

And you want to link from post-1 to post-2…

1) These make the most sense (direct match to logical path):

[Post 2](/posts/post-2)
[Post 2](../post-2)

2) These also work (the Page.GetPage method has some additional lookup logic):

[Post 2](./post-2)
[Post 2](post-2)

Using the render-hook code you provided I have manged to get this it working so I can link to the file path in markdown and it correctly deduces the url. Thanks!

One thing to note is I found it necessary to link to the index.md, and using an absolute path to /posts/ did not work, so for example

[Post 2](/posts/post-2) This did not work
[Post 2](/posts/post-2/index.md) Neither did this
[Post 2](../post-2) This did not work
[Post 2](../post-2/index.md) But this did

But actually linking to the index.md is better because that way my editor actually understands the link.

I could not seem to get the broken link highlighting working and searching on GitHub I see nothing in the Hugo code which would add the broken CSS class?

The results of your tests don’t make sense to me, but it’s also unclear to me which render hook you are using? Can you share your project repository?

The render hook provided in the article that I linked to is different that Hugo’s embedded render hook. The embedded link render hook does not provide link validation or highlighting of broken links. Again, having access to your project repository would be very helpful.

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