AleksandrHovhannisyan / aleksandrhovhannisyan.com

My online resume and blog, created with 11ty, Sass, and JavaScript.
https://aleksandrhovhannisyan.com/
109 stars 26 forks source link

Optimizing Images with the 11ty Image Plugin #118

Open AleksandrHovhannisyan opened 2 years ago

solution-loisir commented 2 years ago

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! :+1:

AleksandrHovhannisyan commented 2 years ago

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

bronze commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 shortcodeImage(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 commented 2 years ago

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 commented 2 years ago

@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: https://github.com/11ty/eleventy/issues/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 commented 2 years ago

@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 shortcodeImage(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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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

muratcorlu commented 1 year ago

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 commented 1 year ago

@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: https://github.com/11ty/eleventy/issues/2428#issuecomment-1152703912. 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.

solution-loisir commented 1 year ago

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 commented 1 year ago

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

solution-loisir commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 shortcodeImage(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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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:

https://github.com/AleksandrHovhannisyan/aleksandrhovhannisyan.com/blob/7ed63df3300ce0e1fc09d0f1219a8d38dad9c6ea/src/_includes/image.html#L1-L24

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 commented 1 year ago

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 commented 1 year ago

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

miklb commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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. :+1:

AleksandrHovhannisyan commented 1 year ago

@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).

https://github.com/AleksandrHovhannisyan/aleksandrhovhannisyan.com/blob/15195f76c87cd15df4112ac486ea2ced6617deda/config/shortcodes/favicon.js#L9-L31

https://github.com/AleksandrHovhannisyan/aleksandrhovhannisyan.com/blob/15195f76c87cd15df4112ac486ea2ced6617deda/src/_layouts/base.html#L46-L47