Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimizing Images with the 11ty Image Plugin #118

Open
AleksandrHovhannisyan opened this issue Oct 31, 2021 · 39 comments
Open

Optimizing Images with the 11ty Image Plugin #118

AleksandrHovhannisyan opened this issue Oct 31, 2021 · 39 comments
Labels
comments Comments section for an article.

Comments

@AleksandrHovhannisyan
Copy link
Owner

No description provided.

@AleksandrHovhannisyan AleksandrHovhannisyan added the comments Comments section for an article. label Oct 31, 2021
@solution-loisir
Copy link

solution-loisir commented Nov 30, 2021

Really nice article (and very nice web site by the way)! You just inspire me to refactor my custom and hacky solution to use Eleventy-img instead. I reused some of your concepts and some of your code (I hope you don't mind). Here's what I came up with. I had the need to be able to choose between lazy and eager mode and to work without JavaScript with the noscript tag.

const Image = require("@11ty/eleventy-img");
const outdent = require("outdent");
const path = require("path");

const placeholder = 22;

module.exports = async ({
    input,
    width = [300, 600],
    alt = "",
    baseFormat = "jpeg",
    optimalFormat = ["avif", "webp"],
    lazy = false,
    className = ["shadow-black-transparent"],
    sizes = "100vw"
}) => {
    const { dir, base } = path.parse(input);
    const inputPath = path.join(".", dir, base);

    const metadata = await Image(inputPath, {
        widths: [placeholder, ...width],
        formats: [...optimalFormat, baseFormat],
        urlPath: dir,
        outputDir: path.join("docs", dir)
    });
    
    const lowSrc = metadata[baseFormat][0];
    const highSrc = metadata[baseFormat][metadata[baseFormat].length - 1];
    
    if(lazy) {
      return outdent`
    <picture class="lazy-picture" data-lazy-state="unseen">
    ${Object.values(metadata).map(entry => {
      return `<source type="${entry[0].sourceType}" srcset="${entry[0].srcset}" data-srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}" class="lazy">`;
    }).join("\n")}
    <img
      src="${lowSrc.url}"
      data-src="${highSrc.url}"
      width="${highSrc.width}"
      height="${highSrc.height}"
      alt="${alt}"
      class="lazy ${className.join(" ")}"
      loading="lazy">
    </picture>
    <noscript>
    <picture>
    ${Object.values(metadata).map(entry => {
      return `<source type="${entry[0].sourceType}" srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}">`;
    }).join("\n")}
    <img
      src="${highSrc.url}"
      width="${highSrc.width}"
      height="${highSrc.height}"
      alt="${alt}"
      class="${className.join(" ")}">
    </picture>
    </noscript>`;

    } else if(!lazy) {
      return outdent`
      <picture>
      ${Object.values(metadata).map(entry => {
        return `<source type="${entry[0].sourceType}" srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}">`;
      }).join("\n")}
      <img
        src="${highSrc.url}"
        width="${highSrc.width}"
        height="${highSrc.height}"
        alt="${alt}"
        class="${className.join(" ")}"
      </picture>`;
    }
}

Thanks, really enjoy reading your blog! 👍

@AleksandrHovhannisyan
Copy link
Owner Author

@solution-loisir Thanks, really glad to hear it! I've updated the post to mention noscript as an enhancement.

@bronze
Copy link

bronze commented Jan 8, 2022

Hi @AleksandrHovhannisyan do you have the final code for your post? Im trying to follow up on it but im getting Expected positive integer for width but received 0 of type number.

const ImageWidths = {
  ORIGINAL: null,
  PLACEHOLDER: 24,
};

const imageShortcode = async (
  relativeSrc,
  alt,
  widths = [400, 800, 1280],
  baseFormat = 'jpeg',
  optimizedFormats = ['webp', 'avif'],
  sizes = '100vw'
) => {
  const { dir: imgDir } = path.parse(relativeSrc);
  const fullSrc = path.join('src', relativeSrc);

  const imageMetadata = await Image(fullSrc, {
    widths: [ImageWidths.ORIGINAL, ImageWidths.PLACEHOLDER, ...widths],
    formats: [...optimizedFormats, baseFormat],
    outputDir: path.join('dist', imgDir),
    urlPath: imgDir,
    filenameFormat: (hash, src, width, format) => {
      const suffix = width === ImageWidths.PLACEHOLDER ? 'placeholder' : width;
      const extension = path.extname(src);
      const name = path.basename(src, extension);
      return `${name}-${hash}-${suffix}.${format}`;
    },
  });

  // Map each unique format (e.g., jpeg, webp) to its smallest and largest images
  const formatSizes = Object.entries(imageMetadata).reduce((formatSizes, [format, images]) => {
    if (!formatSizes[format]) {
      const placeholder = images.find((image) => image.width === ImageWidths.PLACEHOLDER);
      // 11ty sorts the sizes in ascending order under the hood
      const largestVariant = images[images.length - 1];

      formatSizes[format] = {
        placeholder,
        largest: largestVariant,
      };
    }
    return formatSizes;
  }, {});


  // Chain class names w/ the classNames package; optional
  // const picture = `<picture class="${classNames('lazy-picture', className)}"> //removed to use without classNames
  const picture = `<picture class="lazy-picture">
  ${Object.values(imageMetadata)
    // Map each format to the source HTML markup
    .map((formatEntries) => {
      // The first entry is representative of all the others since they each have the same shape
      const { format: formatName, sourceType } = formatEntries[0];

      const placeholderSrcset = formatSizes[formatName].placeholder.url;
      const actualSrcset = formatEntries
        // We don't need the placeholder image in the srcset
        .filter((image) => image.width !== ImageWidths.PLACEHOLDER)
        // All non-placeholder images get mapped to their srcset
        .map((image) => image.srcset)
        .join(', ');

      return `<source type="${sourceType}" srcset="${placeholderSrcset}" data-srcset="${actualSrcset}" data-sizes="${sizes}">`;
    })
    .join('\n')}
    <img
      src="${formatSizes[baseFormat].placeholder.url}"
      data-src="${formatSizes[baseFormat].largest.url}"
      width="${width}"
      height="${height}"
      alt="${alt}"
      class="lazy-img"
      loading="lazy">
  </picture>`;

  return picture;


};

@AleksandrHovhannisyan
Copy link
Owner Author

AleksandrHovhannisyan commented Jan 9, 2022

@bronze Looks like my post may have a typo. I believe it should be this for the image width and height attributes:

width="${formatSizes[baseFormat].largest.width}"
height="${formatSizes[baseFormat].largest.height}"

@KingScroll
Copy link

Hey @AleksandrHovhannisyan! Your article is amazing. It got met set up and running on my local servers and on Netlify dev. I'm just having an issue which has turned out to be quite a headache -- when I'm deploying to Netlify it just doesn't want to play nice. I get this error:

10:24:28 AM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode Image (via Template render error) 10:24:28 AM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)

I've tried everything from modifying the fullSrc object, the frontmatter values for my posts, etc... and it always works well on the local server but I can't quite crack it on the actual netlify deploy. Any ideas?

@KingScroll
Copy link

KingScroll commented Apr 28, 2022

Actually, I fixed it. The real problem was the fact that I was trying to run a Synchronous version of the Image shortcode. The reason for that is that I have a nunjucks macro I was trying to get images in, and the error comes from the synchronous code. The Asynchronous code works perfectly.

I need to either figure out an alternative to macros, or get the synchronous version of the code right. If you have any ideas or insights, that would be cool!

@AleksandrHovhannisyan
Copy link
Owner Author

AleksandrHovhannisyan commented Apr 28, 2022

@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: 11ty/eleventy#1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.

@KingScroll
Copy link

@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: 11ty/eleventy#1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.

Hello again! So I got rid of the macro (it was just for one element), so no more synchronous stuff! Bad news: it just doesn't work. I'm still getting the same error, unfortunately, which means that my problem wasn't quite what I thought it was. I'm still getting this error on Netlify:

4:12:49 PM: [11ty] Problem writing Eleventy templates: (more in DEBUG output) 4:12:49 PM: [11ty] 1. Having trouble rendering njk template ./src/content/projects/projects.njk (via TemplateContentRenderError) 4:12:49 PM: [11ty] 2. (./src/content/projects/projects.njk) 4:12:49 PM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode Image (via Template render error) 4:12:49 PM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)

I suspect it might have something to do with the path modifications in the shortcode function.

My file structure is as follows:

image

The images copy over in their optimized format to public/images/uploads/ via PassthroughCopy.

It all works in my local server, but Netlify doesn't seem to want it. Do you think you can help me with this? I really can't quite crack it

@werls
Copy link

werls commented May 10, 2022

Many thanks for this article, everything is explained in an easy and objective way. Unfortunately, I'm having a problem with my code and I believe is something with my outputDir and urlPath configuration (which is weird since the structure is very similar to the one exemplified in the article). The only difference is that I use /dist/ as the output directory, not /_site/.

So i just changed this line of code
outputDir: path.join('_site', imgDir),

To this
outputDir: path.join('dist', imgDir),

The images were correctly copied to the /dist/assets/images/ directory, but instead of the image I'm receiving a text written "undefined" on my website.

Here's my imageShortcode:
const imageShortcode = async ( relativeSrc, alt, className, widths = [null, 400, 800, 1280], formats = ['jpeg', 'webp'], sizes = '100vw' ) => { const { dir: imgDir } = path.parse(relativeSrc); const fullSrc = path.join('src', relativeSrc); const imageMetadata = await Image(fullSrc, { widths, formats, outputDir: path.join('dist', imgDir), urlPath: imgDir }); };

And this is how I'm using the shortcode inside the njk file:
{% image "/assets/images/image-1.jpg", "image alt text", "(min-width: 30em) 50vw, 100vw" %}

Any idea what could be happening?
(I apologize in advance if this is not the right place for my question)

@AleksandrHovhannisyan
Copy link
Owner Author

@werls This is the right place to ask, no worries. Sounds like your shortcode maybe isn't returning anything. Either that or some sort of async issue.

@werls
Copy link

werls commented May 10, 2022

Oops, my bad. Actually my shortcode wasn't returning anything. Solved now. Thank you!

@muratcorlu
Copy link

Do you see the possibility of having this flow over classical markdown image tags instead of having a liquid shortcodes?

I just want to keep images as simple markdown

![Some alternative text](images/example.jpg]

@AleksandrHovhannisyan
Copy link
Owner Author

@muratcorlu I wish! I tried to get that to work at some point but hit some roadblocks along the way. There's an open issue here where I've provided more context on the problem: 11ty/eleventy#2428 (comment). Ben Holmes created a demo here that I almost got working: https://github.com/Holben888/11ty-image-optimization-demo. The TL;DR of the issue is that if you add a custom Markdown extension via 11ty's addExtension API, you opt out of 11ty processing your Markdown files for templating, so things like shortcodes and partials won't work.

@AleksandrHovhannisyan AleksandrHovhannisyan changed the title Lazily Loading Images with the 11ty Image Plugin Optimizing Images with the 11ty Image Plugin Jul 20, 2022
@solution-loisir
Copy link

I think it would be possible to write a markdown-it plugin similar to this one (very old), but which would use the 11ty image plugin for processing images. It could be interesting to have both a regular 11ty shortcode and a markdown plugin. I might test this over the weekend (if I find the time) just to see what's possible...

@AleksandrHovhannisyan
Copy link
Owner Author

@solution-loisir Ooh, that's a clever idea! Let me know what you figure out.

@solution-loisir
Copy link

solution-loisir commented Jul 24, 2022

Hi @muratcorlu and @AleksandrHovhannisyan, I wrote a markdown-it plugin which uses the synchronous version of the eleventy-img plugin. This my first markdown-it plugin so it's probably not perfect. It serves as a proof of concept for the discussion. Here's the code. I did not publish it so it's used as a local function via the regular markdown-it API like:

const markdownIt = require('markdown-it');
const markdownItEleventyImg = require("./markdown-it/markdown-it-eleventy-img");

module.exports = function(config) {
  config.setLibrary('md', markdownIt ({
    html: true,
    breaks: true,
    linkify: true
  })
  .use(markdownItEleventyImg, {
    widths: [800, 500, 300],
    lazy: false
  });
} 

I think that the shortcode is more flexible and is much easier to write and maybe to maintain. But still, I see some value in using modern image format while keeping the authoring simple and comfortable. Especially if you have standard dimension for images in markdown. This could be developed much further (adding <figure>, controlling loading, etc.) Tell me what you think. Feel free to ask questions. Thanks for the challenge!

@AleksandrHovhannisyan
Copy link
Owner Author

@solution-loisir Very cool! I wish markdown-it supported async renderers 😞 (And had better docs for how to write plugins.) I bet you could take this idea further and have the plugin take a custom image rendering function as an option. That way, users can either supply a renderer that uses the 11ty image plugin or use something else entirely.

@solution-loisir
Copy link

I bet you could take this idea further and have the plugin take a custom image rendering function as an option.

That's a very good idea, and still provide a default function. I like that, I may fiddle with this a little. If it takes shape enough, I may consider publishing eventually. Thanks for your input! ☺️

@solution-loisir
Copy link

solution-loisir commented Jul 28, 2022

Hey, just to let you know markdown-it-eleventy-img is now live! @AleksandrHovhannisyan, I did consider your idea of providing a callback function to the user, but decided to go a different way. The main idea here is to provide the ability to use modern image formats while keeping the simplicity and the essence of markdown. I'm pretty new to all this so, check it out, use it, let me know what you think! :-)

@AleksandrHovhannisyan
Copy link
Owner Author

@solution-loisir Nice work! I'll take this for a spin when I have some downtime 🙂 My main reasoning for not using the 11ty image plugin directly is that it would make the plugin's API simpler (you wouldn't need to forward 11ty image's options to the plugin), and it would also give users more control over how they want to render their images. For example, my custom 11ty image shortcode is a bit more involved and has some custom rendering logic. But this sounds promising for simpler use cases.

@solution-loisir
Copy link

and it would also give users more control over how they want to render their images.

Fair point. I think it could be implemented side by side for a do it your way use case. It would complete the plugin nicely.

@MarkBuskbjerg
Copy link

@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: 11ty/eleventy#1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.

Hello again! So I got rid of the macro (it was just for one element), so no more synchronous stuff! Bad news: it just doesn't work. I'm still getting the same error, unfortunately, which means that my problem wasn't quite what I thought it was. I'm still getting this error on Netlify:

4:12:49 PM: [11ty] Problem writing Eleventy templates: (more in DEBUG output) 4:12:49 PM: [11ty] 1. Having trouble rendering njk template ./src/content/projects/projects.njk (via TemplateContentRenderError) 4:12:49 PM: [11ty] 2. (./src/content/projects/projects.njk) 4:12:49 PM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode Image (via Template render error) 4:12:49 PM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)

I suspect it might have something to do with the path modifications in the shortcode function.

My file structure is as follows:

image

The images copy over in their optimized format to public/images/uploads/ via PassthroughCopy.

It all works in my local server, but Netlify doesn't seem to want it. Do you think you can help me with this? I really can't quite crack it

Hi @KingScroll

I get the exact same error on Netlify.

But I only get it when it is images with transparency (png) I try to convert.

Everything works perfectly on my local machine. But when I try to build on Netlify it fails with the exact same error as you get.

If I the use the image (still png) but without transparency - it works like a charm on Netlify.

Do you know - @AleksandrHovhannisyan - if something related to images with a transparent background could be the cause of trouble?

@AleksandrHovhannisyan
Copy link
Owner Author

@MarkBuskbjerg Wish I could help, but it's hard to say without seeing the code for your site. My guess is that this is still a Nunjucks async issue in disguise, although if you say non-transparent PNGs work, that might not be the issue.

@miklb
Copy link

miklb commented Nov 27, 2022

I'm having a hard time wrapping my head around how to pass different widths in the shortcode than the defaults. If I'm using your defaults and would rather the img be 200px and 480px what would the shortcode look like?

@AleksandrHovhannisyan
Copy link
Owner Author

AleksandrHovhannisyan commented Nov 27, 2022

@miklb Since Nunjucks supports array expressions natively, you could do:

{% image 'src', 'alt', [100, 200, etc.] %}

Or, if you're using an object argument:

{% image src: 'src', alt: 'alt', widths: [100, 200, etc.] %}

In Liquid, things are unfortunately not as easy because it doesn't support array expressions out of the box; you have to split strings on a delimiter, like this:

{% assign widths = "100,200,300" | split: "," %}

That's a bit of a problem in situations like this where you want to have an array of numbers, not an array of strings. On my site, what I do is create an intermediate include that assembles my arguments as JSON and forwards them to my image shortcode:

{%- comment -%}
https://www.aleksandrhovhannisyan.com/blog/passing-object-arguments-to-liquid-shortcodes-in-11ty/
The props below get assembled as a JSON string and parsed into an object prop.
This is a workaround for the fact that 11ty doesn't support named shortcode arguments in Liquid.
{%- endcomment -%}
{%- capture props -%}
{
"src": "{{ src }}"
{%- if alt -%},"alt": "{{ alt }}"{%- endif -%}
{%- if baseFormat -%},"baseFormat": "{{ baseFormat }}"{%- endif -%}
{%- if optimizedFormats -%},"optimizedFormats": {{ optimizedFormats }}{%- endif -%}
{%- if widths -%},"widths": {{ widths }}{%- endif -%}
{%- if sizes -%},"sizes": "{{ sizes }}"{%- endif -%}
{%- comment -%}Explicit nil check since isLinked is a boolean prop.{%- endcomment -%}
{%- if isLinked != nil -%},"isLinked": {{ isLinked }}{%- endif -%}
{%- if className -%},"className": "{{ className }}"{%- endif -%}
{%- if imgClassName -%},"imgClassName": "{{ imgClassName }}"{%- endif -%}
{%- if fileName -%},"fileName": "{{ fileName }}"{%- endif -%}
{%- if isLazy != nil -%},"isLazy": {{ isLazy }}{%- endif -%}
}
{%- endcapture -%}
{%- assign props = props | fromJson -%}
{%- comment -%}Wrapper around custom image shortcode to forward named arguments.{%- endcomment -%}
{%- image props -%}

Allowing me to do this in Liquid:

{% include image.html src: "src", alt: "alt", widths: "[100, 200, 300]" %}

Such that the string of arrays, when JSON-parsed, becomes an array of numbers. A bit convoluted, but I don't know of any other workarounds. If you find one, do let me know!

@miklb
Copy link

miklb commented Nov 29, 2022

Thanks. Seems a little too convoluted for my needs. I may opt for two different shortcodes—one for full content width images and one for floated images.

@AleksandrHovhannisyan
Copy link
Owner Author

@miklb That makes a lot more sense! Good call.

@miklb
Copy link

miklb commented Nov 30, 2022

@AleksandrHovhannisyan just wanted to say I re-read your post and realized you already covered my question and after reading https://www.aleksandrhovhannisyan.com/blog/passing-object-arguments-to-liquid-shortcodes-in-11ty/ I better understand your include. I hated the idea of duplicating code for one argument. Cheers.

@AleksandrHovhannisyan
Copy link
Owner Author

AleksandrHovhannisyan commented Nov 30, 2022

@miklb Fwiw, I think your proposed solution would've also worked. This is what I imagined:

const specialImage = async (args) => {
  const image = await imageShortcode({ ...args, widths: [100, 200, etc.] });
  return image;
}

And then you could register that as its own shortcode and use it:

{% specialImage 'src', 'alt' %}

Either way works, though! The include approach is a little more flexible in Liquid in case you need to vary other arguments as well and want to use named arguments.

@truleighsyd
Copy link

Hello, so I'm using image.liquid not image.html in my _includes to create an intermediate include but I'm running into the following issue:

Screen Shot 2022-12-19 at 9 49 17 AM

The include in the index looks like this:

{% include 'image', src: 'assets/image-01.jpg', alt: 'this is s test' %}

And the JS shortcode looks like this as following your guide

const imageShortcode = async ( src, alt, className = undefined, widths = [400, 800, 1280], formats = ['webp', 'jpeg'], sizes = '100vw' ) => { const imageMetadata = await Image(src, { widths: [...widths, null], formats: [...formats, null], outputDir: '_site/assets/images', urlPath: '/assets', }) const imageAttributes = { alt, sizes, loading: 'lazy', decoding: 'async', } return Image.generateHTML(imageMetadata, imageAttributes) }

Along with the global filter

eleventyConfig.addFilter('fromJson', JSON.parse)

Am I missing something?

@AleksandrHovhannisyan
Copy link
Owner Author

@truleighsyd This error usually occurs when you pass in the wrong path for the shortcode name, so 11ty/Liquid cannot find the partial (image.liquid in this case). Can you try 'image.liquid' with an explicit extension? If that doesn't work, please share your 11ty config's return value (directory config) and your project structure.

@truleighsyd
Copy link

truleighsyd commented Dec 19, 2022

With liquid includes/renders you do not need to include the extension name. Unless this is specific to working with 11ty shortcodes? Here's the the eleventy config return value.

UserConfig {
  events: AsyncEventEmitter {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    [Symbol(kCapture)]: false
  },
  benchmarkManager: BenchmarkManager {
    benchmarkGroups: { Configuration: [BenchmarkGroup], Aggregate: [BenchmarkGroup] },
    isVerbose: true,
    start: 46588.24823799729
  },
  benchmarks: {
    config: BenchmarkGroup {
      benchmarks: [Object],
      isVerbose: true,
      logger: [ConsoleLogger],
      minimumThresholdMs: 0,
      minimumThresholdPercent: 8
    },
    aggregate: BenchmarkGroup {
      benchmarks: {},
      isVerbose: false,
      logger: [ConsoleLogger],
      minimumThresholdMs: 0,
      minimumThresholdPercent: 8
    }
  },
  collections: {},
  precompiledCollections: {},
  templateFormats: undefined,
  liquidOptions: {},
  liquidTags: {},
  liquidFilters: {
    slug: [Function],
    slugify: [Function],
    url: [Function],
    log: [Function],
    serverlessUrl: [Function],
    getCollectionItem: [Function],
    getPreviousCollectionItem: [Function],
    getNextCollectionItem: [Function],
    makeUppercase: [Function],
    toISOString: [Function],
    toJson: [Function],
    fromJson: [Function]
  },
  liquidShortcodes: { image: [Function] },
  liquidPairedShortcodes: {},
  nunjucksEnvironmentOptions: {},
  nunjucksFilters: {
    slug: [Function],
    slugify: [Function],
    url: [Function],
    log: [Function],
    serverlessUrl: [Function],
    getCollectionItem: [Function],
    getPreviousCollectionItem: [Function],
    getNextCollectionItem: [Function],
    toJson: [Function],
    fromJson: [Function]
  },
  nunjucksAsyncFilters: {},
  nunjucksTags: {},
  nunjucksGlobals: {},
  nunjucksShortcodes: { image: [Function] },
  nunjucksAsyncShortcodes: {},
  nunjucksPairedShortcodes: {},
  nunjucksAsyncPairedShortcodes: {},
  handlebarsHelpers: {
    slug: [Function],
    slugify: [Function],
    url: [Function],
    log: [Function],
    serverlessUrl: [Function],
    getCollectionItem: [Function],
    getPreviousCollectionItem: [Function],
    getNextCollectionItem: [Function],
    toJson: [Function],
    fromJson: [Function]
  },
  handlebarsShortcodes: { image: [Function] },
  handlebarsPairedShortcodes: {},
  javascriptFunctions: {
    slug: [Function],
    slugify: [Function],
    url: [Function],
    log: [Function],
    serverlessUrl: [Function],
    getCollectionItem: [Function],
    getPreviousCollectionItem: [Function],
    getNextCollectionItem: [Function],
    toJson: [Function],
    fromJson: [Function],
    image: [Function]
  },
  pugOptions: {},
  ejsOptions: {},
  markdownHighlighter: null,
  libraryOverrides: {},
  passthroughCopies: { 'assets/*.js': 'assets', 'assets/*.css': 'assets' },
  layoutAliases: {},
  linters: {},
  transforms: {},
  activeNamespace: '',
  DateTime: [Function: DateTime],
  dynamicPermalinks: true,
  useGitIgnore: true,
  ignores: Set { 'node_modules/**' },
  dataDeepMerge: true,
  extensionMap: Set {},
  watchJavaScriptDependencies: true,
  additionalWatchTargets: [],
  browserSyncConfig: {},
  globalData: {},
  chokidarConfig: {},
  watchThrottleWaitTime: 0,
  dataExtensions: Map {},
  quietMode: false,
  plugins: [],
  _pluginExecution: false,
  useTemplateCache: true,
  dataFilterSelectors: Set {},
  dir: undefined,
  logger: ConsoleLogger {
    _isVerbose: true,
    outputStream: Readable {
      _readableState: [ReadableState],
      readable: true,
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      [Symbol(kCapture)]: false
    }
  }
}

And here's project structure. It's just a test project to experiment with different 11ty features.

Screen Shot 2022-12-19 at 10 43 08 AM

@AleksandrHovhannisyan
Copy link
Owner Author

@truleighsyd The reason I mentioned including the extension is because I have some partials that are HTML (with embedded liquid), and I always have to do {% include 'partial.html' %}. Give that a shot, but if it doesn't work I'm not sure. Looks like you're just using the default 11ty dir config, so that should work.

@truleighsyd
Copy link

Hmm weird it doesn't work this way but the following does. The issue is that it reads it as an obj so need to pass the obj. The following code works. Thanks : )

Screen Shot 2022-12-19 at 4 05 53 PM

@bulecampur
Copy link

Hi, I was trying to use your short code. However, I have one question: Can it be automated, i.e. if iterating over data, can I include front matter variable in the short code? I tried just plugging it in, but it did not work. Thanks for the hint, if it is possible

@AleksandrHovhannisyan
Copy link
Owner Author

@bulecampur Should be possible. For example, if src is a front-matter variable available in the scope where you're using the shortcode, you should be able to do:

{% image src, 'alt', etc. %}

(Doesn't have to be src)

@bulecampur
Copy link

Thank you Aleksandr, the front matter variable is not in the scope but pulled from either collection or it is a variable from global data. For example on the homepage:
{% for origin in origins %} <img src="{{ origin.imgurl }}" alt="{{ origin.alt }}" />
{% endfor %}
I am trying to replace the with the shortcode but it returns an error that it could not find the variable. Maybe it's just not possible?
I am new to Eleventy and only have rudimentary coding skills (was using Hugo for a little bit before).

@NateWr
Copy link

NateWr commented Mar 31, 2023

Thanks @AleksandrHovhannisyan, this was very helpful. I am new to 11ty and I was looking for a simple way to process some source images (compression, resizing) that aren't going to end up in <picture> tags (for example, generating site icons from a svg). It was very simple in the end -- just add the outputDir to the config -- but I wanted to share what I did in case it is useful for others who don't necessarily need to work through the full shortcode solution.

const Image = require("@11ty/eleventy-img");
const path = require("path");

module.exports = function(config) {
  config.addPassthroughCopy("img/*.svg");

  (async () => {
    /**
     * Preserve the original file names
     */
    const filenameFormat = function (id, src, width, format, options) {
      const extension = path.extname(src);
      const name = path.basename(src, extension);

      return `${name}-${width}.${format}`;
    };

    [
      'img/footer.png',
      'img/footer-dark.png',
      'img/header.png',
      'img/header-dark.png',

    ].forEach(async image => {
      await Image(image, {
        formats: ['webp', 'jpeg'],
        widths: [480, 768, 1200],
        outputDir: '_site/img',
        filenameFormat
      })
    });

    await Image('img/site-icon.svg', {
      formats: ['png'],
      widths: [32, 180, 192, 512],
      outputDir: '_site/img',
      filenameFormat
    });
};

Happy to be informed if this is a bad way to go about it, generally. 👍

@AleksandrHovhannisyan
Copy link
Owner Author

@NateWr That works! Alternatively, you could add a dedicated shortcode for just your favicons. That's what I do on my site. Although your version is probably faster because you only run that logic once in the config, whereas the shortcode approach would run it on every template build (images would be cached, but still).

/** Returns link tags for the site's favicon. */
async function faviconShortcode(src) {
const props = {
widths: [16, 32, 57, 76, 96, 128, APPLE_TOUCH_ICON_WIDTH, 192, 228],
formats: [FAVICON_FORMAT],
outputDir: path.join(imagePaths.output, 'favicons'),
urlPath: path.join(withoutBaseDirectory(imagePaths.output), 'favicons'),
filenameFormat: (_hash, _src, width, format) => {
return `favicon-${width}.${format}`;
},
};
const metadata = await Image(src, props);
return metadata[FAVICON_FORMAT].map((image) => {
const isAppleTouchIcon = image.width === APPLE_TOUCH_ICON_WIDTH;
const attributes = stringifyAttributes({
href: image.url,
rel: isAppleTouchIcon ? 'apple-touch-icon' : 'icon',
...(isAppleTouchIcon ? {} : { sizes: `${image.width}x${image.width}` }),
});
return `<link ${attributes}>`;
}).join('\n');
}

{%- comment -%}Favicons{%- endcomment -%}
{% favicon 'src/assets/images/favicons/favicon.png' %}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comments Comments section for an article.
Projects
None yet
Development

No branches or pull requests