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

Optimize images #32

Merged
merged 16 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 153 additions & 2 deletions .eleventy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fs = require("fs");
const path = require("path");
const htmlmin = require("html-minifier-terser");
const tailwind = require('tailwindcss');
const postCss = require('postcss');
Expand All @@ -7,6 +8,15 @@ const cssnano = require('cssnano');
const mdit = require('markdown-it')
const mditAttrs = require('markdown-it-attrs');
const mditHighlight = require('markdown-it-highlightjs');
const Image = require('@11ty/eleventy-img');

// sizes and formats of resized images to make them responsive
// it can be overwriten when using the "Picture" short code
const Images = {
WIDTHS: [426, 460, 580, 768, 1200], // sizes of generated images
FORMATS: ['webp', 'jpeg'], // formats of generated images
SIZES: '(max-width: 1200px) 70vw, 1200px' // size of image rendered
}

module.exports = async function(eleventyConfig) {

Expand All @@ -25,6 +35,29 @@ module.exports = async function(eleventyConfig) {
typographer: true,
}
const mdLib = mdit(mditOptions).use(mditAttrs).use(mditHighlight, { inline: true }).disable('code')

// generate responsive images from Markdown
mdLib.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const imgSrc = env.eleventy.directories.input.slice(0, -1) + token.attrGet('src')
const imgAlt = token.content
const imgTitle = token.attrGet('title') ?? ''
const className = token.attrGet('class')
const ImgOptions = getImgOptions(env.page, imgSrc, imgAlt, className, Images.WIDTHS, Images.FORMATS, Images.SIZES);
const htmlOptions = {
alt: imgAlt,
class: className,
sizes: Images.SIZES,
loading: className?.includes('lazy') ? 'lazy' : undefined,
decoding: 'async',
title: imgTitle
}
Image(imgSrc, ImgOptions)
const metadata = Image.statsSync(imgSrc, ImgOptions)
const picture = Image.generateHTML(metadata, htmlOptions)

return picture
}
eleventyConfig.setLibrary('md', mdLib)

// Passthrough
Expand All @@ -39,6 +72,77 @@ module.exports = async function(eleventyConfig) {
// process css
eleventyConfig.addNunjucksAsyncFilter('postcss', postcssFilter);

// Image shortcode with <picture>
eleventyConfig.addShortcode("Picture", async (
page,
src,
alt,
className = undefined,
widths = Images.WIDTHS,
formats = Images.FORMATS,
sizes = Images.SIZES
) => {
if (!alt) {
throw new Error(`Missing \`alt\` on myImage from: ${src}`);
}
const srcImage = getSrcImage(page, src);
const options = getImgOptions(page, src, alt, className, widths, formats, sizes);
const imageMetadata = await Image(srcImage, options);
const sourceHtmlString = Object.values(imageMetadata)
// Map each format to the source HTML markup
.map((images) => {
// The first entry is representative of all the others
// since they each have the same shape
const { sourceType } = images[0];

// Use our util from earlier to make our lives easier
const sourceAttributes = stringifyAttributes({
type: sourceType,
// srcset needs to be a comma-separated attribute
srcset: images.map((image) => image.srcset).join(', '),
sizes,
});

// Return one <source> per format
return `<source ${sourceAttributes}>`;
})
.join('\n');

const getLargestImage = (format) => {
const images = imageMetadata[format];
return images[images.length - 1];
}

const largestUnoptimizedImg = getLargestImage(formats[0]);

const imgAttributes = stringifyAttributes({
src: largestUnoptimizedImg.url,
width: largestUnoptimizedImg.width,
height: largestUnoptimizedImg.height,
alt,
loading: className?.includes('lazy') ? 'lazy' : undefined,
decoding: 'async',
});

const imgHtmlString = `<img ${imgAttributes}>`;

const pictureAttributes = stringifyAttributes({
class: className,
});
const picture = `<picture ${pictureAttributes}>
${sourceHtmlString}
${imgHtmlString}
</picture>`;

return `${picture}`;
});


// Collections
eleventyConfig.addCollection("documentation", function (collection) {
return collection.getFilteredByGlob("./src/pages/documentation/**/*.md");
});

return {
dir: {
input: "src/pages",
Expand All @@ -47,10 +151,10 @@ module.exports = async function(eleventyConfig) {
data: '../_data',
output: '_site',
},
templateFormats: ['md', 'njk', 'jpg', 'gif', 'png', 'html'],
templateFormats: ['md', 'njk', 'jpg', 'gif', 'png', 'html', 'jpeg', 'webp'],
pathPrefix: process.env.BASE_HREF ? `/${process.env.BASE_HREF}/` : "/" // used with github pages
}
};
}; // end config

function htmlminTransform(content, outputPath) {
if( outputPath.endsWith(".html") ) {
Expand Down Expand Up @@ -81,3 +185,50 @@ const postcssFilter = (cssCode, done) => {
(e) => done(e, null)
);
}

/** Maps a config of attribute-value pairs to an HTML string
* representing those same attribute-value pairs.
*/
const stringifyAttributes = (attributeMap) => {
return Object.entries(attributeMap)
.map(([attribute, value]) => {
if (typeof value === 'undefined') return '';
return `${attribute}="${value}"`;
})
.join(' ');
};


const getSrcImage = (page, src) => {
let inputFolder = page.inputPath.split("/")
inputFolder.pop()
inputFolder = inputFolder.join("/");

return inputFolder+"/"+src;
}

const getImgOptions = (page, src, alt, className, widths, formats, sizes) => {
let outputFolder = page.outputPath.split("/")
outputFolder.pop() // remove index.html
outputFolder = outputFolder.join("/");

let urlPath = outputFolder.split("/")
urlPath.shift() // remove ./
urlPath.shift() // remove _site
urlPath = "/" + urlPath.join("/");

const options = {
widths: widths
.concat(widths.map((w) => w * 2)) // generate 2x sizes
.filter((v, i, s) => s.indexOf(v) === i), // dedupe
formats: [...formats, null],
outputDir: outputFolder,
urlPath: urlPath,
filenameFormat: function (id, src, width, format, options) {
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${width}w.${format}`;
}
}
return options;
}
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
# huwindty 🌬️

I wanted to use [Windty](https://github.com/distantcam/windty/) for my next [eleventy](https://www.11ty.dev/) project before I realised I need more than just a single page with [Tailwindcss](https://tailwindcss.com/). So I kept the good work and am adding more.
I wanted to use [Windty](https://github.com/distantcam/windty/) for my next [eleventy](https://www.11ty.dev/) project before I realised I need more than just a single page with [Tailwindcss](https://tailwindcss.com/). So I kept the good work and added more.

## What has been added
- CI to deploy via FTP to any server
## What was added
### Continuous Integration
- Publication to github pages
- Deployment to stand alone server via ssh (manual action)
- Lighthouse checks on key pages for each PR to keep the triple 💯
### Styles
- Tailwind css are processed directly by 11ty
- adding navigation menu generated from pages
### Navigation
- Navigation menu is directly generated from page structure
### Site output
- Handle markdown with style
- Process images to make them responsive
### Content Managment System
- Installed Decap CMS with content flow
- Possibility to use Sveltia CMS with the same config
### Documentation
- Documentation comes with the package as an example
- Explains how features are developped

## What I plan to add
- responsive image processor

## What is still missing
- better SEO metadata
- decap CMS
- in depth doc so I don't get lost when I get back here in 2 years
- documentation on the CMS
- maybe a nicer design

## How to use
## Install
1. [windty’s template](https://github.com/distantcam/windty/generate), or [clone this one](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository)
2. Install dependencies: `npm install`
3. Start development: `npm start`
Expand Down
Loading
Loading