config: change directory and use ESM

This commit is contained in:
madrilene 2024-06-03 11:04:15 +02:00
parent a5073e4c19
commit b7c2308091
32 changed files with 392 additions and 298 deletions

View file

@ -1,28 +0,0 @@
/** All blog posts as a collection. */
const getAllPosts = collection => {
const posts = collection.getFilteredByGlob('./src/posts/**/*.md');
return posts.reverse();
};
/** All markdown files as a collection for sitemap.xml */
const onlyMarkdown = collection => {
return collection.getFilteredByGlob('./src/**/*.md');
};
/** All tags from all posts as a collection. */
const tagList = collection => {
const tagsSet = new Set();
collection.getAll().forEach(item => {
if (!item.data.tags) return;
item.data.tags
.filter(tag => !['posts', 'docs', 'all'].includes(tag))
.forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet).sort();
};
module.exports = {
getAllPosts,
onlyMarkdown,
tagList
};

View file

@ -1,90 +0,0 @@
const dayjs = require('dayjs');
const CleanCSS = require('clean-css');
const site = require('../../src/_data/meta');
const {throwIfNotType} = require('../utils');
const esbuild = require('esbuild');
/** Removes all tags from an HTML string. */
const stripHtml = str => {
throwIfNotType(str, 'string');
return str.replace(/<[^>]+>/g, '');
};
/** Formats the given string as an absolute url. */
const toAbsoluteUrl = url => {
throwIfNotType(url, 'string');
// Replace trailing slash, e.g., site.com/ => site.com
const siteUrl = site.url.replace(/\/$/, '');
// Replace starting slash, e.g., /path/ => path/
const relativeUrl = url.replace(/^\//, '');
return `${siteUrl}/${relativeUrl}`;
};
/** Converts the given date string to ISO8610 format. */
const toISOString = dateString => dayjs(dateString).toISOString();
/** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */
const formatDate = (date, format) => dayjs(date).format(format);
const minifyCss = code => new CleanCSS({}).minify(code).styles;
const minifyJs = async (code, ...rest) => {
const callback = rest.pop();
const cacheKey = rest.length > 0 ? rest[0] : null;
try {
if (cacheKey && jsminCache.hasOwnProperty(cacheKey)) {
const cacheValue = await Promise.resolve(jsminCache[cacheKey]); // Wait for the data, wrapped in a resolved promise in case the original value already was resolved
callback(null, cacheValue.code); // Access the code property of the cached value
} else {
const minified = esbuild.transform(code, {
minify: true
});
if (cacheKey) {
jsminCache[cacheKey] = minified; // Store the promise which has the minified output (an object with a code property)
}
callback(null, (await minified).code); // Await and use the return value in the callback
}
} catch (err) {
console.error('jsmin error: ', err);
callback(null, code); // Fail gracefully.
}
};
// source: https://github.com/bnijenhuis/bnijenhuis-nl/blob/main/.eleventy.js
const splitlines = (input, maxCharLength) => {
const parts = input.split(' ');
const lines = parts.reduce(function (acc, cur) {
if (!acc.length) {
return [cur];
}
let lastOne = acc[acc.length - 1];
if (lastOne.length + cur.length > maxCharLength) {
return [...acc, cur];
}
acc[acc.length - 1] = lastOne + ' ' + cur;
return acc;
}, []);
return lines;
};
const shuffleArray = array => {
return array.sort(() => Math.random() - 0.5);
};
module.exports = {
toISOString,
formatDate,
toAbsoluteUrl,
stripHtml,
minifyCss,
minifyJs,
splitlines,
shuffleArray
};

View file

@ -1,17 +0,0 @@
// Because Nunjucks's include doesn't like CSS with "{#". Source: https://github.com/nhoizey/pack11ty/blob/781248b92480701208f69e2161165e58d79a23ee/src/_11ty/shortcodes/include_raw.js
const fs = require('fs');
let memoizedIncludes = {};
const includeRaw = file => {
if (file in memoizedIncludes) {
return memoizedIncludes[file];
} else {
let content = fs.readFileSync(file, 'utf8');
memoizedIncludes[file] = content;
return content;
}
};
module.exports = includeRaw;

View file

@ -1,8 +0,0 @@
const imageShortcode = require('./image');
const includeRaw = require('./includeRaw');
const liteYoutube = require('./youtube-lite');
module.exports = {
imageShortcode,
includeRaw,
liteYoutube
};

View file

@ -1,10 +0,0 @@
const liteYoutube = (id, label) => {
return `
<div class="youtube-embed"> <lite-youtube videoid="${id}" style="background-image: url('https://i.ytimg.com/vi/${id}/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">${label}</span>
</button>
</lite-youtube></div>
`;
};
module.exports = liteYoutube;

View file

@ -1,37 +0,0 @@
// CSS and JavaScript as first-class citizens in Eleventy: https://pepelsbey.dev/articles/eleventy-css-js/
const postcss = require('postcss');
const postcssImport = require('postcss-import');
const postcssImportExtGlob = require('postcss-import-ext-glob');
const tailwindcss = require('tailwindcss');
const postcssRelativeColorSyntax = require('@csstools/postcss-relative-color-syntax');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
module.exports = eleventyConfig => {
eleventyConfig.addTemplateFormats('css');
eleventyConfig.addExtension('css', {
outputFileExtension: 'css',
compile: async (content, path) => {
if (path !== './src/assets/css/global.css') {
return;
}
return async () => {
let output = await postcss([
postcssImportExtGlob,
postcssImport,
tailwindcss,
postcssRelativeColorSyntax({preserve: true}),
autoprefixer,
cssnano
]).process(content, {
from: path
});
return output.css;
};
}
});
};

View file

@ -1,37 +0,0 @@
const esbuild = require('esbuild');
module.exports = eleventyConfig => {
eleventyConfig.addTemplateFormats('js');
eleventyConfig.addExtension('js', {
outputFileExtension: 'js',
compile: async (content, path) => {
if (!path.startsWith('./src/assets/scripts/')) {
return;
}
if (path === './src/assets/scripts/theme-toggle.js') {
await esbuild.build({
target: 'es2020',
entryPoints: [path],
outfile: './src/_includes/theme-toggle-inline.js',
bundle: true,
minify: true
});
return;
}
return async () => {
let output = await esbuild.build({
target: 'es2020',
entryPoints: [path],
minify: true,
bundle: true,
write: false
});
return output.outputFiles[0].text;
};
}
});
};

View file

@ -1,24 +0,0 @@
const slugify = require('slugify');
/** Converts string to a slug form. */
const slugifyString = str => {
return slugify(str, {
replacement: '-',
remove: /[#,&,+()$~%.'":*¿?¡!<>{}]/g,
lower: true
});
};
/** throw an error if the provided argument is not of the expected. */
const throwIfNotType = (arg, expectedType) => {
if (typeof arg !== expectedType) {
throw new Error(
`Expected argument of type ${expectedType} but instead got ${arg} (${typeof arg})`
);
}
};
module.exports = {
slugifyString,
throwIfNotType
};

View file

@ -0,0 +1,19 @@
/** All blog posts as a collection. */
export const getAllPosts = collection => {
return collection.getFilteredByGlob('./src/posts/**/*.md').reverse();
};
/** All markdown files as a collection for sitemap.xml */
export const onlyMarkdown = collection => {
return collection.getFilteredByGlob('./src/**/*.md');
};
/** All tags from all posts as a collection - excluding custom collections */
export const tagList = collection => {
const tagsSet = new Set();
collection.getAll().forEach(item => {
if (!item.data.tags) return;
item.data.tags.filter(tag => !['posts', 'docs', 'all'].includes(tag)).forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet).sort();
};

5
src/_config/events.js Normal file
View file

@ -0,0 +1,5 @@
import {svgToJpeg} from './events/svg-to-jpeg.js';
export default {
svgToJpeg
};

View file

@ -1,24 +1,17 @@
// https://bnijenhuis.nl/notes/automatically-generate-open-graph-images-in-eleventy/
// https://github.com/sophiekoonin/localghost/blob/main/src/plugins/og-to-png.js
// converts SVG to JPEG for open graph images
const fsPromises = require('fs/promises');
const fs = require('fs');
const path = require('path');
const Image = require('@11ty/eleventy-img');
import {promises as fsPromises, existsSync} from 'fs';
import path from 'node:path';
import Image from '@11ty/eleventy-img';
const ogImagesDir = './src/assets/og-images';
const svgToJpeg = async function () {
export const svgToJpeg = async function () {
const socialPreviewImagesDir = 'dist/assets/og-images/';
const files = await fsPromises.readdir(socialPreviewImagesDir);
if (files.length > 0) {
files.forEach(function (filename) {
files.forEach(async function (filename) {
const outputFilename = filename.substring(0, filename.length - 4);
if (
filename.endsWith('.svg') & !fs.existsSync(path.join(ogImagesDir, outputFilename))
) {
if (filename.endsWith('.svg') & !existsSync(path.join(ogImagesDir, outputFilename))) {
const imageUrl = socialPreviewImagesDir + filename;
Image(imageUrl, {
await Image(imageUrl, {
formats: ['jpeg'],
outputDir: ogImagesDir,
filenameFormat: function (id, src, width, format, options) {
@ -31,7 +24,3 @@ const svgToJpeg = async function () {
console.log('⚠ No social images found');
}
};
module.exports = {
svgToJpeg
};

20
src/_config/filters.js Normal file
View file

@ -0,0 +1,20 @@
import {toISOString, formatDate} from './filters/dates.js';
import {markdownFormat} from './filters/markdown-format.js';
import {shuffleArray} from './filters/sort-random.js';
import {sortAlphabetically} from './filters/sort-alphabetic.js';
import {splitlines} from './filters/splitlines.js';
import {striptags} from './filters/striptags.js';
import {toAbsoluteUrl} from './filters/to-absolute-url.js';
import {slugifyString} from './filters/slugify.js';
export default {
toISOString,
formatDate,
markdownFormat,
splitlines,
striptags,
toAbsoluteUrl,
shuffleArray,
sortAlphabetically,
slugifyString
};

View file

@ -0,0 +1,7 @@
import dayjs from 'dayjs';
/** Converts the given date string to ISO8610 format. */
export const toISOString = dateString => dayjs(dateString).toISOString();
/** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */
export const formatDate = (date, format) => dayjs(date).format(format);

View file

@ -0,0 +1,9 @@
// by Chris Burnell: https://chrisburnell.com/article/some-eleventy-filters/#markdown-format
import markdownParser from 'markdown-it';
const markdown = markdownParser();
export const markdownFormat = string => {
return markdown.render(string);
};

View file

@ -0,0 +1,10 @@
import slugify from 'slugify';
/** Converts string to a slug form. */
export const slugifyString = str => {
return slugify(str, {
replacement: '-',
remove: /[#,&,+()$~%.'":*¿?¡!<>{}]/g,
lower: true
});
};

View file

@ -0,0 +1,7 @@
export const sortAlphabetically = array => {
return array.sort((a, b) => {
if (a.data.title < b.data.title) return -1;
if (a.data.title > b.data.title) return 1;
return 0;
});
};

View file

@ -0,0 +1,3 @@
export const shuffleArray = array => {
return array.sort(() => Math.random() - 0.5);
};

View file

@ -0,0 +1,20 @@
export const splitlines = (input, maxCharLength) => {
const parts = input.split(' ');
const lines = parts.reduce(function (acc, cur) {
if (!acc.length) {
return [cur];
}
let lastOne = acc[acc.length - 1];
if (lastOne.length + cur.length > maxCharLength) {
return [...acc, cur];
}
acc[acc.length - 1] = lastOne + ' ' + cur;
return acc;
}, []);
return lines;
};

View file

@ -0,0 +1,3 @@
export const striptags = string => {
return string.replace(/<[^>]*>?/gm, '');
};

View file

@ -0,0 +1,12 @@
import {throwIfNotType} from '../utils/throw-if-not-type.js';
import {url as site} from '../../_data/meta.js';
/** Formats the given string as an absolute url. */
export const toAbsoluteUrl = url => {
throwIfNotType(url, 'string');
// Replace trailing slash, e.g., site.com/ => site.com
const siteUrl = site.url.replace(/\/$/, '');
// Replace starting slash, e.g., /path/ => path/
const relativeUrl = url.replace(/^\//, '');
return `${siteUrl}/${relativeUrl}`;
};

26
src/_config/plugins.js Normal file
View file

@ -0,0 +1,26 @@
// Eleventy
import {EleventyRenderPlugin} from '@11ty/eleventy';
import rss from '@11ty/eleventy-plugin-rss';
import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight';
import webc from '@11ty/eleventy-plugin-webc';
// custom
import {markdownLib} from './plugins/markdown.js';
// Custom transforms
import {htmlConfig} from './plugins/html-config.js';
// Custom template language
import {cssConfig} from './plugins/css-config.js';
import {jsConfig} from './plugins/js-config.js';
export default {
EleventyRenderPlugin,
rss,
syntaxHighlight,
webc,
markdownLib,
htmlConfig,
cssConfig,
jsConfig
};

View file

@ -0,0 +1,49 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import postcss from 'postcss';
import postcssImport from 'postcss-import';
import postcssImportExtGlob from 'postcss-import-ext-glob';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
export const cssConfig = eleventyConfig => {
eleventyConfig.addTemplateFormats('css');
eleventyConfig.addExtension('css', {
outputFileExtension: 'css',
compile: async (inputContent, inputPath) => {
const paths = [];
if (inputPath.endsWith('/src/assets/css/global/global.css')) {
paths.push('dist/assets/css/global.css');
paths.push('src/_includes/css/global.css'); // Assuming you might want to keep naming consistent for ease
} else if (inputPath.includes('/src/assets/css/bundle/')) {
const baseName = path.basename(inputPath);
paths.push(`src/_includes/css/${baseName}`);
} else if (inputPath.includes('/src/assets/css/components/')) {
const baseName = path.basename(inputPath);
paths.push(`dist/assets/css/components/${baseName}`);
} else {
return;
}
return async () => {
let result = await postcss([
postcssImportExtGlob,
postcssImport,
tailwindcss,
autoprefixer,
cssnano
]).process(inputContent, {from: inputPath});
// Write the output to all specified paths
for (const outputPath of paths) {
await fs.mkdir(path.dirname(outputPath), {recursive: true});
await fs.writeFile(outputPath, result.css);
}
return result.css;
};
}
});
};

View file

@ -1,7 +1,8 @@
const htmlmin = require('html-minifier-terser');
import htmlmin from 'html-minifier-terser';
const isProduction = process.env.ELEVENTY_ENV === 'production';
module.exports = eleventyConfig => {
export const htmlConfig = eleventyConfig => {
eleventyConfig.addTransform('html-minify', (content, path) => {
if (path && path.endsWith('.html') && isProduction) {
return htmlmin.minify(content, {

View file

@ -0,0 +1,45 @@
import esbuild from 'esbuild';
import path from 'node:path';
export const jsConfig = eleventyConfig => {
eleventyConfig.addTemplateFormats('js');
eleventyConfig.addExtension('js', {
outputFileExtension: 'js',
compile: async (content, inputPath) => {
// Skip processing if not in the designated scripts directories
if (!inputPath.startsWith('./src/assets/scripts/')) {
return;
}
// Inline scripts processing
if (inputPath.startsWith('./src/assets/scripts/bundle/')) {
const filename = path.basename(inputPath);
const outputFilename = filename;
const outputPath = `./src/_includes/scripts/${outputFilename}`;
await esbuild.build({
target: 'es2020',
entryPoints: [inputPath],
outfile: outputPath,
bundle: true,
minify: true
});
return;
}
// Default handling for other scripts, excluding inline scripts
return async () => {
let output = await esbuild.build({
target: 'es2020',
entryPoints: [inputPath],
bundle: true,
minify: true,
write: false
});
return output.outputFiles[0].text;
};
}
});
};

View file

@ -1,17 +1,17 @@
const markdownIt = require('markdown-it');
const markdownItPrism = require('markdown-it-prism');
const markdownItAnchor = require('markdown-it-anchor');
const markdownItClass = require('@toycode/markdown-it-class');
const markdownItLinkAttributes = require('markdown-it-link-attributes');
const markdownItEmoji = require('markdown-it-emoji').full;
const markdownItEleventyImg = require('markdown-it-eleventy-img');
const markdownItFootnote = require('markdown-it-footnote');
const markdownitMark = require('markdown-it-mark');
const markdownitAbbr = require('markdown-it-abbr');
const {slugifyString} = require('../utils');
const path = require('path');
import markdownIt from 'markdown-it';
import markdownItPrism from 'markdown-it-prism';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItClass from '@toycode/markdown-it-class';
import markdownItLinkAttributes from 'markdown-it-link-attributes';
import {full as markdownItEmoji} from 'markdown-it-emoji';
import markdownItEleventyImg from 'markdown-it-eleventy-img';
import markdownItFootnote from 'markdown-it-footnote';
import markdownitMark from 'markdown-it-mark';
import markdownitAbbr from 'markdown-it-abbr';
import {slugifyString} from '../filters/slugify.js';
import path from 'node:path';
const markdownLib = markdownIt({
export const markdownLib = markdownIt({
html: true,
breaks: true,
linkify: true,
@ -84,5 +84,3 @@ const markdownLib = markdownIt({
.use(markdownItFootnote)
.use(markdownitMark)
.use(markdownitAbbr);
module.exports = markdownLib;

View file

@ -5,13 +5,13 @@ import path from 'node:path';
const dataPath = './src/_data/builtwith.json';
const screenshotDir = path.join(
path.dirname(new URL(import.meta.url).pathname),
'../../src/assets/images/screenshots'
'../../assets/images/screenshots'
);
async function fetchScreenshot(url, filePath) {
const waitCondition = 'wait:2';
const timeout = 'timeout:5';
const apiUrl = `https://v1.screenshot.11ty.dev/${encodeURIComponent(url)}/opengraph/_${waitCondition}_${timeout}/`;
const apiUrl = `https://v1.screenshot.11ty.dev/${encodeURIComponent(url)}/large/_${waitCondition}_${timeout}/`;
const buffer = await fetch(apiUrl, {
duration: '1d',

View file

@ -0,0 +1,4 @@
import {imageShortcode} from './shortcodes/image.js';
import {svgShortcode} from './shortcodes/svg.js';
export default {imageShortcode, svgShortcode};

View file

@ -1,7 +1,12 @@
const Image = require('@11ty/eleventy-img');
const path = require('path');
const htmlmin = require('html-minifier-terser');
import Image from '@11ty/eleventy-img';
import path from 'node:path';
import htmlmin from 'html-minifier-terser';
/**
* Converts an attribute map object to a string of HTML attributes.
* @param {Object} attributeMap - The attribute map object.
* @returns {string} - The string of HTML attributes.
*/
const stringifyAttributes = attributeMap => {
return Object.entries(attributeMap)
.map(([attribute, value]) => {
@ -11,14 +16,26 @@ const stringifyAttributes = attributeMap => {
.join(' ');
};
const imageShortcode = async (
/**
* Generates an HTML image element with responsive images and optional caption.
* @param {string} src - The path to the image source file.
* @param {string} [alt=''] - The alternative text for the image.
* @param {string} [caption=''] - The caption for the image.
* @param {string} [loading='lazy'] - The loading attribute for the image.
* @param {string} [className] - The CSS class name for the image element.
* @param {string} [sizes='90vw'] - The sizes attribute for the image.
* @param {number[]} [widths=[440, 650, 960, 1200]] - The widths for generating responsive images.
* @param {string[]} [formats=['avif', 'webp', 'jpeg']] - The formats for generating responsive images.
* @returns {string} - The HTML image element.
*/
export const imageShortcode = async (
src,
alt = '',
caption,
caption = '',
loading = 'lazy',
className,
sizes = '90vw',
widths = [440, 880, 1024, 1360],
widths = [440, 650, 960, 1200],
formats = ['avif', 'webp', 'jpeg']
) => {
const metadata = await Image(src, {
@ -62,7 +79,7 @@ const imageShortcode = async (
});
const imageElement = caption
? `<figure class="flow ${className ? `${className}` : ''}">
? `<figure slot="image" class="flow ${className ? `${className}` : ''}">
<picture>
${imageSources}
<img
@ -70,7 +87,7 @@ const imageShortcode = async (
</picture>
<figcaption>${caption}</figcaption>
</figure>`
: `<picture class="flow ${className ? `${className}` : ''}">
: `<picture slot="image" class="flow ${className ? `${className}` : ''}">
${imageSources}
<img
${imgageAttributes}>
@ -78,5 +95,3 @@ const imageShortcode = async (
return htmlmin.minify(imageElement, {collapseWhitespace: true});
};
module.exports = imageShortcode;

View file

@ -0,0 +1,23 @@
/**
* Generates an optimized SVG shortcode with optional attributes.
*
* @param {string} svgName - The name of the SVG file (without the .svg extension).
* @param {string} [ariaName=''] - The ARIA label for the SVG.
* @param {string} [className=''] - The CSS class name for the SVG.
* @param {string} [styleName=''] - The inline style for the SVG.
* @returns {Promise<string>} The optimized SVG shortcode.
*/
import {optimize} from 'svgo';
import {readFileSync} from 'node:fs';
export const svgShortcode = async (svgName, ariaName = '', className = '', styleName = '') => {
const svgData = readFileSync(`./src/assets/svg/${svgName}.svg`, 'utf8');
const {data} = await optimize(svgData);
return data.replace(
/<svg(.*?)>/,
`<svg$1 ${ariaName ? `aria-label="${ariaName}"` : 'aria-hidden="true"'} ${className ? `class="${className}"` : ''} ${styleName ? `style="${styleName}"` : ''} >`
);
};

View file

@ -0,0 +1,43 @@
/**
* Credits:
* - © Andy Bell - https://buildexcellentwebsit.es/
*/
/**
* Takes an array of tokens and sends back and array of name
* and clamp pairs for CSS fluid values.
*
* @param {array} tokens array of {name: string, min: number, max: number}
* @returns {array} {name: string, value: string}
*/
import viewports from '../../_data/designTokens/viewports.json';
export const clampGenerator = tokens => {
const rootSize = 16;
return tokens.map(({name, min, max}) => {
if (min === max) {
return `${min / rootSize}rem`;
}
// Convert the min and max sizes to rems
const minSize = min / rootSize;
const maxSize = max / rootSize;
// Convert the pixel viewport sizes into rems
const minViewport = viewports.min / rootSize;
const maxViewport = viewports.max / rootSize;
// Slope and intersection allow us to have a fluid value but also keep that sensible
const slope = (maxSize - minSize) / (maxViewport - minViewport);
const intersection = -1 * minViewport * slope + minSize;
return {
name,
value: `clamp(${minSize}rem, ${intersection.toFixed(2)}rem + ${(slope * 100).toFixed(
2
)}vw, ${maxSize}rem)`
};
});
};

View file

@ -0,0 +1,13 @@
/**
* Throws an error if the argument is not of the expected type.
*
* @param {*} arg - The argument to check the type of.
* @param {string} expectedType - The expected type of the argument.
* @throws {Error} If the argument is not of the expected type.
*/
export const throwIfNotType = (arg, expectedType) => {
if (typeof arg !== expectedType) {
throw new Error(`Expected argument of type ${expectedType} but instead got ${arg} (${typeof arg})`);
}
};

View file

@ -0,0 +1,24 @@
/**
* Credits:
* - © Andy Bell - https://buildexcellentwebsit.es/
*/
/**
* Converts human readable tokens into tailwind config friendly ones
*
* @param {array} tokens {name: string, value: any}
* @return {object} {key, value}
*/
import slugify from 'slugify';
export const tokensToTailwind = tokens => {
const nameSlug = text => slugify(text, {lower: true});
let response = {};
tokens.forEach(({name, value}) => {
response[nameSlug(name)] = value;
});
return response;
};