eleventy icon indicating copy to clipboard operation
eleventy copied to clipboard

Configuring site baseUrl and built-in filter for converting relative URLs to absolute URLs

Open AleksandrHovhannisyan opened this issue 3 years ago • 11 comments

Is your feature request related to a problem? Please describe.

Often, you need to use absolute URLs instead of relative URLs. Examples of when this is needed include:

  • OpenGraph and Twitter preview images and urls.
  • Canonical URLs.
  • Sitemap (I think?).
  • JSON-LD URLs.

Unfortunately, the only way to turn relative URLs into absolute URLs in 11ty is to create a custom filter every time it's needed. See this article for the general approach: https://pieterheyvaert.com/blog/2019/02/25/11ty-full-paths-locally/. On my site, I have a toAbsoluteUrl filter that basically joins my site URL (which is either localhost or my domain name) with any given relative URL.

Describe the solution you'd like

The proposed solution is twofold:

  1. Add a new configuration option for 11ty to set the site's base production URL (e.g., baseUrl).
  2. Ship a built-in toAbsoluteUrl filter that prefixes any URL with localhost:port in dev and baseUrl in prod.

Something like this:

const toAbsoluteUrl = (url) => {
  const absoluteUrl = new URL(url, site.url).href;
  return absoluteUrl;
};

Describe alternatives you've considered

  • Creating a custom filter in every project that needs it (which has been every 11ty project so far).
  • Is there a way to do this with the url filter or is that for something else? I couldn't tell.
  • Publishing a plugin for the filter, but this seems like it may be better suited as part of the core filters.

Additional context

Would be happy to put in a PR if needed.

AleksandrHovhannisyan avatar May 15 '22 13:05 AleksandrHovhannisyan

The RSS plugin has an “absoluteUrl” filter that does that. https://www.11ty.dev/docs/plugins/rss/#supplies-the-following-nunjucks-filters

pdehaan avatar May 15 '22 13:05 pdehaan

Oh, nice, did not know that! I'm curious why it's not part of core 11ty and is shipped as part of an RSS plugin. Wouldn't it make more sense for this to be one of 11ty's default filters? (e.g., Jekyll has a url filter that does this.)

AleksandrHovhannisyan avatar May 15 '22 18:05 AleksandrHovhannisyan

Maybe. 🤷 It's really just a 2-3 line plugin, but you also need to specify the base every time you use the filter, which feels a bit inconvenient. Not sure if there is an easy way around that, apart from making your own custom plugin/filter which specifies some default base value in the plugin config, or if this does move into Eleventy core, if there would be an easier way to specify your a default base in the global config and then still have the ability to override at the filter-level, if needed.

https://github.com/11ty/eleventy-plugin-rss/blob/c1129020c8e0c8e78cdeefecebed421941d0b831/src/absoluteUrl.js#L4-L12

pdehaan avatar May 15 '22 22:05 pdehaan

Here's a rough example of wrapping the addFilter() in a basic plugin in order to specify a global, default base URL.

// .eleventy.js
module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(UrlPlugin, { base: "https://pdehaan.dev/" });

  return {
    dir: {
      input: "src",
      output: "www",
    }
  };
};

function UrlPlugin(eleventyConfig={}, pluginConfig={}) {
  eleventyConfig.addFilter("absoluteUrl", function (url="", base=pluginConfig.base) {
    try {
      return new URL(url, base).href;
    } catch (err) {
      console.error(err);
      return url;
    }
  });
};
---
# src/index.njk
title: Nunjucks Index
---

<h1>{{ title }}</h1>
<a href="{{ '/about' | absoluteUrl }}">About Me</a>
<a href="{{ '/aboot' | absoluteUrl('https://11ty.dev/') }}">About 11ty</a>

OUTPUT

<h1>Nunjucks Index</h1>
<a href="https://pdehaan.dev/about">About Me</a>
<a href="https://11ty.dev/aboot">About 11ty</a>

pdehaan avatar May 15 '22 22:05 pdehaan

LOL, or we can use the .addGlobalData() method (and [ab]use the internal .globalData storage object) and do something like this:

// .eleventy.js
module.exports = function (eleventyConfig) {
  eleventyConfig.addGlobalData("baseUrl", "https://beep.boop");

  eleventyConfig.addFilter("absoluteUrl", function (url, base = eleventyConfig.globalData.baseUrl) {
    try {
      return new URL(url, base).href;
    } catch (err) {
      console.error(err);
      return url;
    }
  });

  return {
    dir: {
      input: "src",
      output: "www",
    }
  };
};

Might be an easier way we can access global data from a custom filter, but I'm not seeing something obvious that works for both Nunjucks and Liquid since their context and ctx objects seem to be pretty different.

pdehaan avatar May 15 '22 22:05 pdehaan

What I currently do on my site is just have a _data/site.js with a base URL that depends on ELEVENTY_ENV:

src/_data/site.js

const isDev = process.env.ELEVENTY_ENV === 'development';
module.exports = {
  baseUrl: isDev ? 'localhost:8080' : 'https://www.site.com';
}

And then just use that in the filter.

The downside of this is that there's no trivial way to get the current 11ty localhost port/URL. Feels like this is something that's better suited as part of the core config since 11ty knows the localhost URL internally (based on the port used—8080 or a custom one), and then it would know your site's base URL too, and at that point it can easily configure a filter that uses the base URL under the hood.

AleksandrHovhannisyan avatar May 16 '22 11:05 AleksandrHovhannisyan

I'm trying Adding Social Share Links -- in my case with Eleventy v0.12.1 without eleventyConfig.addGlobalData and with Nunjucks, and therefore doing it like this as explained here:

_data/site.js

const isDev = process.env.ELEVENTY_ENV === 'development';

const baseUrl = isDev ? `localhost:8080` : `https://www.mydomain.com/`;

const site = {
  title: 'My site title',
  description: 'My site description',
  baseUrl,
}

module.exports = site;

.eleventy.js

  const site = require('./_data/site.js');

  /**
   * Prefixes the given URL with the site's base URL.
   * @param {string} url
   */
  const toAbsoluteUrl = (url) => {
    return new URL(url, site.baseUrl).href;
  }
  
  eleventyConfig.addFilter('toAbsoluteUrl', toAbsoluteUrl);

_includes/partials/components/social-share.njk

{% set pageUrl = page.url | toAbsoluteUrl %}

{% set stitle = page.title %}

<a href="https://www.facebook.com/sharer.php?u={{ pageUrl }}">Facebook</a> 
<a href="https://twitter.com/intent/tweet?url={{ pageUrl }}&text={{ stitle }}">Twitter</a> 
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ pageUrl }}">LinkedIn</a> 
<a href="https://reddit.com/submit?url={{pageUrl}}&title={{stitle}}">Reddit</a>

(but I get Error: filter not found: toAbsoluteUrl )-- ERRATA CORRIGE, now it works. See my comment below.

PS I do have in my .eleventy.js // https://www.11ty.dev/docs/plugins/rss/ const pluginRss = require("@11ty/eleventy-plugin-rss"); ... eleventyConfig.addPlugin(pluginRss);

rocc-o avatar Jan 03 '23 12:01 rocc-o

I'm an idiot. I forgot to add ".js" in const site = require('./_data/site');. Now it works. I'm correcting the above and leave it here for reference if someone need it.

rocc-o avatar Jan 03 '23 14:01 rocc-o

Can't reproduce either now... closing

tfrancart avatar Feb 27 '24 17:02 tfrancart

@tfrancart Not sure I follow; did you mean to comment that on a different issue?

AleksandrHovhannisyan avatar Feb 27 '24 17:02 AleksandrHovhannisyan

My bad, yes, sorry :-)

tfrancart avatar Feb 27 '24 17:02 tfrancart