Eleventy: Shortcode equivalent for Jekyll鈥檚 {% link %} tag?

Created on 27 May 2019  路  8Comments  路  Source: 11ty/eleventy

I鈥檓 tinkering with a move from Jekyll to Eleventy, and am getting a little tripped up trying to create a shortcode equivalent for Jekyll鈥檚 {% link %} tag, which turns this:

{% link _posts/2017-08-31-ampersand.md %}

into something like:

/wrote/ampersand/

I鈥檝e registered a basic shortcode to just pass the path through to the rendered markup, but as near as I can tell, Eleventy鈥檚 complaining about the lack of quotes around the path in my shortcode:

$ npx eleventy
Problem writing Eleventy templates: (more in DEBUG output)
> Having trouble rendering liquid (and markdown) template ./_posts/2018-09-18-revamp.md (TemplateContentRenderError)
> invalid syntax at line 1 col 1:

  _posts/2017-08-31-ampersand.md
  ^, line:7 (RenderError)

If I wrap the path in quotes (e.g., {% link "_posts/2017-08-31-ampersand.md" %}), then the error moves onto the next {% link %} in my post.

Am I reading that correctly? If so, is there a decent way to work around this?

(Also, if there鈥檚 a better forum for intro-level questions like this, please let me know and I鈥檒l redirect. Thank you!)

education

Most helpful comment

Alright I went overboard because I wanted this for my blog and just made it (for Liquid)

eleventyConfig.addLiquidTag("link", function(liquidEngine) {
    return {
        parse: function(tagToken, remainTokens) {
            this.path = tagToken.args;
        },
        render: function(scope, hash) {
            let isQuoted = this.path.charAt(0) === "'" || this.path.charAt(0) === '"';
            let path = isQuoted ? liquidEngine.evalValue(this.path, scope) : this.path;

            // This is cheating a little bit because it鈥榮 using the `collections.all` object
            // Anything not in the `all` collection won鈥檛 resolve
            let results = scope.contexts[0].collections.all.filter(function(tmpl) {
                return tmpl.inputPath === path;
            });
            if( results.length ) {
                return Promise.resolve(results[0].url);
            }
            return Promise.reject(`Template ${path} not found in \`link\` shortcode.`);
        }
    };
});

Sample Usage:

<a href="{% link "./index.liquid" %}">Test</a>
<a href="{% link ./index.liquid %}">Test</a>

Outputs:

<a href="/">Test</a>
<a href="/">Test</a>

All 8 comments

You _can_ do this but it鈥檚 not straightforward, unfortunately.

By default shortcode arguments are treated as code and not raw literal values (like strings). This allows us to use variables as arguments to shortcodes. However, you can work around this limitation by creating your own custom tag. Here鈥檚 how you would do this for (I鈥檓 assuming you鈥檙e using) Liquid:

eleventyConfig.addLiquidTag("link", function(liquidEngine) {
  return {
    parse: function(tagToken, remainTokens) {
      this.path = tagToken.args;
    },
    render: function(scope, hash) {
      let outputPath = doSomethingWithInputPath(this.path);
      return Promise.resolve(outputPath);
    }
  };
});

鈥here you鈥檇 have to implement doSomethingWithInputPath in the above.

Perhaps in the future we could make this an option you could configure on a per-shortcode basis. Not sure!

Docs for LiquidJS custom tags: https://www.11ty.io/docs/custom-tags/#liquidjs-example

@benbrignell might also be interested in this sample code (per his related issue in #496) but his use case was quite a bit more complicated (multi-argument parsing)

Filed #545 which you may want to upvote!

Alright I went overboard because I wanted this for my blog and just made it (for Liquid)

eleventyConfig.addLiquidTag("link", function(liquidEngine) {
    return {
        parse: function(tagToken, remainTokens) {
            this.path = tagToken.args;
        },
        render: function(scope, hash) {
            let isQuoted = this.path.charAt(0) === "'" || this.path.charAt(0) === '"';
            let path = isQuoted ? liquidEngine.evalValue(this.path, scope) : this.path;

            // This is cheating a little bit because it鈥榮 using the `collections.all` object
            // Anything not in the `all` collection won鈥檛 resolve
            let results = scope.contexts[0].collections.all.filter(function(tmpl) {
                return tmpl.inputPath === path;
            });
            if( results.length ) {
                return Promise.resolve(results[0].url);
            }
            return Promise.reject(`Template ${path} not found in \`link\` shortcode.`);
        }
    };
});

Sample Usage:

<a href="{% link "./index.liquid" %}">Test</a>
<a href="{% link ./index.liquid %}">Test</a>

Outputs:

<a href="/">Test</a>
<a href="/">Test</a>

My goodness, Zach鈥攜ou鈥檙e remarkable. Thank you for this, this is officially more help than I was expecting in a single issue!

I鈥檒l give this a shot, but this certainly sounds promising. Thank you again 馃檶

Finally got a chance to try this out, and it worked a _treat_. I did have to prepend a ./ to path, since Jekyll allows (assumes?) a root-relative path ({% link _posts/blah.md %}, rather than {% link ./_posts/blah.md %}), but otherwise I think I鈥檓 set.

Thanks so much, Zach! I really appreciate it.

Is it possible to add a custom folder path while parsing?

I am trying to convert markdown links like that [My favorite link](my-favorite-link.md) to <a href="/posts/my-favorite-link">My favorite link</a>. So I need the parser to add a custom /posts/ string, depending on which folder my markdown files are coming from in the output. Is there a system variable for that?

Was this page helpful?
0 / 5 - 0 ratings