Ways to write markdown links without language prefix?

Based on Joe’s reply, omitting the slash works for the front matter, but not for markdown links. I still have the links with the language prefix for the translated pages since they always point to the base URL, if the prefix is ommitted.

What I would do would be

  • Drop language prefix in internal links in Markdown
  • Create a link render hook that resolves these using .Page.GetPage
  • The above should handle relative links ala ../foo etc. nicely.

You would probably need a way to signal that you want to link to another language (a query parameter? see the result of urls.Parse, but in general, I like this approach.

A simplified hook that I wrote some moons ago can be found here:

This did not work either. Still points to the base URL

Does this match what you are currently doing?

content/en/posts/
├── post-1.md
└── post-2.md

content/en/posts/post-1.md

+++
title = 'Post 1'
date = 2022-12-16T07:28:20-08:00
draft = false
slug = 'post-1-en'
+++

content/en/posts/post-2.md

+++
title = 'Post 2'
date = 2022-12-16T07:28:21-08:00
draft = false
slug = 'post-2-en'
+++

[Post 1](/en/posts/post-1-en)

Structure

content/posts/subsec/
├── post-1.md
└── post-1.[lang].md

content/posts/subsec/post-1.md

+++
title = 'Post 1'
date = 2022-12-16T07:28:20-08:00
draft = false
url = 'post-1-en'
+++

content/posts/subsec/post-1.[lang].md

+++
title = 'Post 1 translated'
date = 2022-12-16T07:28:20-08:00
draft = false
url = 'post-1-translated'
+++

So, your markdown links point to the slug/url.

How many of these internal cross-references do you have on your site?

For translated pages, not a lot. For other pages, thousands! (I have one section that uses slug, but that one does not have internal links. I edited the structure).

But some may be levels deep, e,g, [lorem](/lang-prefix/lorem/ipsum/dola/sit/amet) for translated pages or without the language prefix for main language.

You probably don’t want to hear this, but…

Due to all the ways that URL’s can be modified via configuration (permalinks, url, slug, baseURL, uglyURLs, languages), hardcoding link destinations is risky.

If you were starting a new site, I would recommend creating a link render hook that uses site.GetPage as bep described. Then you write your links pointing to the file path within the content folder, not pointing to the ultimate URL (which can easily change via any of the configuration options above). This also has the benefit of easily detecting broken links.

For your existing site, you need to determine if it’s worth the effort to make the changes.

Render hooks tend to be quite opinionated. Here’s mine:

layouts/_default/_markup/render-link.html
{{- $attrs := dict }}
{{- $u := urls.Parse .Destination }}

{{- if $u.IsAbs }}
  {{- /* Remote. */}}
  {{- $attrs = dict "href" $u.String "rel" "external" }}
{{- else }}
  {{- with $u.Path }}
    {{- with site.GetPage . }}
      {{- /* Page. */}}
      {{- $href := .RelPermalink }}
      {{- with $u.RawQuery }}
        {{- $href = printf "%s?%s" $href $u.RawQuery }}
      {{- end }}
      {{- with $u.Fragment }}
        {{- $href = printf "%s#%s" $href $u.Fragment }}
      {{- end }}
      {{- $attrs = dict "href" $href }}
    {{- else }}
      {{- with $.Page.Resources.GetMatch $u.Path }}
        {{- /* Page resource; drop query and fragment. */}}
        {{- $attrs = dict "href" .RelPermalink }}
      {{- else }}
        {{- with resources.Get $u.Path }}
          {{- /* Global resource; drop query and fragment. */}}
          {{- $attrs = dict "href" .RelPermalink }}
        {{- else }}
          {{- errorf "Unable to resolve reference to %s from %s" $u.Path $.Page.File.Path }}
        {{- end }}
      {{- end }}
    {{- end }}
  {{- else }}
    {{- with $u.Fragment }}
      {{- /* Fragment only; prepend page's relative permalink. */}}
      {{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }}
    {{- else }}
      {{- errorf "Unable to resolve reference to %s from %s" $u.Path $.Page.File.Path }}
    {{- end }}
  {{- end }}
{{- end }}

{{- with .Title }}
  {{- $attrs = merge $attrs (dict "title" .) }}
{{- end -}}

<a
{{- range $k, $v := $attrs }}
  {{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end -}}
>{{ .Text | safe.HTML }}</a>
{{- /**/ -}}


1 Like

I do wanna hear it. It is a learning process.

I couldn’t do anything about this anyway since that was how the WP way did it and converting to markdown generated paths like that. So, If I am to understand you, the links should instead point to the file path like content/posts/subsec/post-1.md?

I always do a search/replace in case I change links, and add the redirect in my headers file. So, that is not a big issue.

Sometimes I forget to hard code the language prefix in the markdown link only to realise days later that the links are broken. It also used to happen with the front matter until you pointed out the solution in the comment I linked, which I did a search/replace. I thought setting a different language baseURL in the docs e.g. https://example.com/lang/ would help, until I read the docs that the domain should be completely different. So, I am now stuck with hard coding the language prefix in the URL.

Now my question is, how hard is it? I have translated a lot of pages, but I was willing to do a search/replace to remove the language part, or even do it manually, but if it is hard to implement, I will stick with hard coding the language prefix.

Another reason I may be hesitant to use the file path (if question 1 is correct), is for transferability. Let’s say, for one site I write with a team, they decide they want to move from Hugo? Which though unlikely (they like it a lot), we would be stuck with thousands of links that do not work, since they point to file paths, not permalink.

Perhaps a follow-up question is, how hard would it be to implement a feature for markdown like relLangURL? Where Hugo detects the translated language pages and adds the language prefix despite the leading slash being there? urls.RelLangURL | Hugo

(My markdown render hook is not complicated. But it does use some conditionals to add some content for external and PDF links, such as different aria-label values).

Your markdown would look like:

[Post 1](/posts/subsec/post-1)

As you can see from the markdown example above, the path is quite portable. You include neither the content portion of the file path, nor the extension (this is optional).

That’s what the render hook does.

Follow up per bep’s response above…

If you want to link across languages (e.g., link from en/posts/post-1.md to de/posts/post-1.md), which should be rare, you’ll need to add a provision for this to the hook… maybe via a query string.

I will have to test this with my previous Jekyll and/or WP installation to confirm. Because I am not sure if those platforms would translate the file path to the permalink.

That’s not what I am doing actually. I am linking articles of the same type in the same language. In this case, in content/posts/subsec/post-2.[lang].md I link to post-1.[lang].md using its URL, but it requires adding the language prefix to the markdown URL because otherwise it retuns a 404 error.

I suggest you create a small test site, two languages, two pages of content, add url or slug to front matter, add permalinks in site config, etc.

Then test the hook I provided above.

It’s a lot easier to understand what’s happening when working with an isolated data set.

These are the errors I got with the test site

Template changed WRITE         "C:\\hugo\\test-site\\themes\\test-theme\\layouts\\_default\\_markup\\render-link.html"
ERROR 2022/12/16 22:14:40 Unable to resolve reference to /ipsum/ from posts\test\lorem\index.md
ERROR 2022/12/16 22:14:40 Unable to resolve reference to /lorem/ from posts\test\ipsum\index.md
ERROR 2022/12/16 22:14:40 Unable to resolve reference to /ipsum-fr/ from posts\test\lorem\index.fr.md
ERROR 2022/12/16 22:14:40 Unable to resolve reference to /lorem-fr/ from posts\test\ipsum\index.fr.md
ERROR 2022/12/16 22:14:40 Rebuild failed: logged 4 error(s)
Total in 26 ms

Can you share the test site?

Here you go.

Your links are incorrect. Here’s a diff file:

diff --git a/content/posts/test/ipsum/index.fr.md b/content/posts/test/ipsum/index.fr.md
index 34fea14..8dd8576 100644
--- a/content/posts/test/ipsum/index.fr.md
+++ b/content/posts/test/ipsum/index.fr.md
@@ -8,6 +8,6 @@ Lorem markdownum crebri. Sunt risere diu blandis neque; more nobis *doctas*!
 Corpore creavit licent nec quaecumque fama habebam liquitur fumo terrore bella
 est extemplo gramen pedibus Lenaee Credulitas magis revolet. Pestiferos mutat,
 aut properas, sic te **relictus** regno **dignoque Cromyona Victoria** erat est
-olivis feliciter. A iners [clamato capillos](/lorem-fr/),
+olivis feliciter. A iners [clamato capillos](/posts/test/lorem),
 lunae aliud ditia adveniet Circen, deusque at animal et obstitit Colchis ignavo
-media.
\ No newline at end of file
+media.
diff --git a/content/posts/test/ipsum/index.md b/content/posts/test/ipsum/index.md
index db8ee26..2251e75 100644
--- a/content/posts/test/ipsum/index.md
+++ b/content/posts/test/ipsum/index.md
@@ -3,4 +3,4 @@ title: Ipsum
 url: "ipsum"
 date: 04 Dec 2022
 ---
-Cratem responsa evolat moresque Iunonis tabuerant scylla? Deducitur oneratos pretioque [habendum nube residant](/lorem/); in nil accedat sacra, meque plebe et suos! Insignis iusserat magnanimo visa ad illos colubris nihil puppim? Me ab et levis captavit, perosus trunca!
+Cratem responsa evolat moresque Iunonis tabuerant scylla? Deducitur oneratos pretioque [habendum nube residant](/posts/test/lorem); in nil accedat sacra, meque plebe et suos! Insignis iusserat magnanimo visa ad illos colubris nihil puppim? Me ab et levis captavit, perosus trunca!
diff --git a/content/posts/test/lorem/index.fr.md b/content/posts/test/lorem/index.fr.md
index 973c58e..f69d667 100644
--- a/content/posts/test/lorem/index.fr.md
+++ b/content/posts/test/lorem/index.fr.md
@@ -8,6 +8,6 @@ Lorem markdownum crebri. Sunt risere diu blandis neque; more nobis *doctas*!
 Corpore creavit licent nec quaecumque fama habebam liquitur fumo terrore bella
 est extemplo gramen pedibus Lenaee Credulitas magis revolet. Pestiferos mutat,
 aut properas, sic te **relictus** regno **dignoque Cromyona Victoria** erat est
-olivis feliciter. A iners [clamato capillos](/ipsum-fr/),
+olivis feliciter. A iners [clamato capillos](/posts/test/ipsum),
 lunae aliud ditia adveniet Circen, deusque at animal et obstitit Colchis ignavo
-media.
\ No newline at end of file
+media.
diff --git a/content/posts/test/lorem/index.md b/content/posts/test/lorem/index.md
index 83904ea..74bdcee 100644
--- a/content/posts/test/lorem/index.md
+++ b/content/posts/test/lorem/index.md
@@ -3,4 +3,4 @@ title: Lorem
 url: "lorem"
 date: 05 Dec 2022
 ---
-Cratem responsa evolat moresque Iunonis tabuerant scylla? Deducitur oneratos pretioque [habendum nube residant](/ipsum/); in nil accedat sacra, meque plebe et suos! Insignis iusserat magnanimo visa ad illos colubris nihil puppim? Me ab et levis captavit, perosus trunca!
+Cratem responsa evolat moresque Iunonis tabuerant scylla? Deducitur oneratos pretioque [habendum nube residant](/posts/test/ipsum); in nil accedat sacra, meque plebe et suos! Insignis iusserat magnanimo visa ad illos colubris nihil puppim? Me ab et levis captavit, perosus trunca!

That’s the structure of my main site. Or what did you want me to test?

Look at the links. Your using stuff like this:

[clamato capillos](/lorem-fr/),

Instead of this:

[clamato capillos](/posts/test/lorem)

Remember, you provide the path to the content file.

Previously, I had asked if it was possible to get a solution with the structure that I shared (see below) which is the way my site is structured. I thought you were asking me to create a test site to demonstrate the issue I am facing…