config: change directory and use ESM
This commit is contained in:
parent
a5073e4c19
commit
b7c2308091
32 changed files with 392 additions and 298 deletions
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
const imageShortcode = require('./image');
|
|
||||||
const includeRaw = require('./includeRaw');
|
|
||||||
const liteYoutube = require('./youtube-lite');
|
|
||||||
module.exports = {
|
|
||||||
imageShortcode,
|
|
||||||
includeRaw,
|
|
||||||
liteYoutube
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
19
src/_config/collections.js
Normal file
19
src/_config/collections.js
Normal 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
5
src/_config/events.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {svgToJpeg} from './events/svg-to-jpeg.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
svgToJpeg
|
||||||
|
};
|
||||||
|
|
@ -1,24 +1,17 @@
|
||||||
// https://bnijenhuis.nl/notes/automatically-generate-open-graph-images-in-eleventy/
|
import {promises as fsPromises, existsSync} from 'fs';
|
||||||
// https://github.com/sophiekoonin/localghost/blob/main/src/plugins/og-to-png.js
|
import path from 'node:path';
|
||||||
// converts SVG to JPEG for open graph images
|
import Image from '@11ty/eleventy-img';
|
||||||
|
|
||||||
const fsPromises = require('fs/promises');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const Image = require('@11ty/eleventy-img');
|
|
||||||
const ogImagesDir = './src/assets/og-images';
|
const ogImagesDir = './src/assets/og-images';
|
||||||
|
|
||||||
const svgToJpeg = async function () {
|
export const svgToJpeg = async function () {
|
||||||
const socialPreviewImagesDir = 'dist/assets/og-images/';
|
const socialPreviewImagesDir = 'dist/assets/og-images/';
|
||||||
const files = await fsPromises.readdir(socialPreviewImagesDir);
|
const files = await fsPromises.readdir(socialPreviewImagesDir);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
files.forEach(function (filename) {
|
files.forEach(async function (filename) {
|
||||||
const outputFilename = filename.substring(0, filename.length - 4);
|
const outputFilename = filename.substring(0, filename.length - 4);
|
||||||
if (
|
if (filename.endsWith('.svg') & !existsSync(path.join(ogImagesDir, outputFilename))) {
|
||||||
filename.endsWith('.svg') & !fs.existsSync(path.join(ogImagesDir, outputFilename))
|
|
||||||
) {
|
|
||||||
const imageUrl = socialPreviewImagesDir + filename;
|
const imageUrl = socialPreviewImagesDir + filename;
|
||||||
Image(imageUrl, {
|
await Image(imageUrl, {
|
||||||
formats: ['jpeg'],
|
formats: ['jpeg'],
|
||||||
outputDir: ogImagesDir,
|
outputDir: ogImagesDir,
|
||||||
filenameFormat: function (id, src, width, format, options) {
|
filenameFormat: function (id, src, width, format, options) {
|
||||||
|
|
@ -31,7 +24,3 @@ const svgToJpeg = async function () {
|
||||||
console.log('⚠ No social images found');
|
console.log('⚠ No social images found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
svgToJpeg
|
|
||||||
};
|
|
||||||
20
src/_config/filters.js
Normal file
20
src/_config/filters.js
Normal 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
|
||||||
|
};
|
||||||
7
src/_config/filters/dates.js
Normal file
7
src/_config/filters/dates.js
Normal 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);
|
||||||
9
src/_config/filters/markdown-format.js
Normal file
9
src/_config/filters/markdown-format.js
Normal 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);
|
||||||
|
};
|
||||||
10
src/_config/filters/slugify.js
Normal file
10
src/_config/filters/slugify.js
Normal 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
|
||||||
|
});
|
||||||
|
};
|
||||||
7
src/_config/filters/sort-alphabetic.js
Normal file
7
src/_config/filters/sort-alphabetic.js
Normal 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;
|
||||||
|
});
|
||||||
|
};
|
||||||
3
src/_config/filters/sort-random.js
Normal file
3
src/_config/filters/sort-random.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const shuffleArray = array => {
|
||||||
|
return array.sort(() => Math.random() - 0.5);
|
||||||
|
};
|
||||||
20
src/_config/filters/splitlines.js
Normal file
20
src/_config/filters/splitlines.js
Normal 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;
|
||||||
|
};
|
||||||
3
src/_config/filters/striptags.js
Normal file
3
src/_config/filters/striptags.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const striptags = string => {
|
||||||
|
return string.replace(/<[^>]*>?/gm, '');
|
||||||
|
};
|
||||||
12
src/_config/filters/to-absolute-url.js
Normal file
12
src/_config/filters/to-absolute-url.js
Normal 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
26
src/_config/plugins.js
Normal 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
|
||||||
|
};
|
||||||
49
src/_config/plugins/css-config.js
Normal file
49
src/_config/plugins/css-config.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
const htmlmin = require('html-minifier-terser');
|
import htmlmin from 'html-minifier-terser';
|
||||||
|
|
||||||
const isProduction = process.env.ELEVENTY_ENV === 'production';
|
const isProduction = process.env.ELEVENTY_ENV === 'production';
|
||||||
|
|
||||||
module.exports = eleventyConfig => {
|
export const htmlConfig = eleventyConfig => {
|
||||||
eleventyConfig.addTransform('html-minify', (content, path) => {
|
eleventyConfig.addTransform('html-minify', (content, path) => {
|
||||||
if (path && path.endsWith('.html') && isProduction) {
|
if (path && path.endsWith('.html') && isProduction) {
|
||||||
return htmlmin.minify(content, {
|
return htmlmin.minify(content, {
|
||||||
45
src/_config/plugins/js-config.js
Normal file
45
src/_config/plugins/js-config.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
const markdownIt = require('markdown-it');
|
import markdownIt from 'markdown-it';
|
||||||
const markdownItPrism = require('markdown-it-prism');
|
import markdownItPrism from 'markdown-it-prism';
|
||||||
const markdownItAnchor = require('markdown-it-anchor');
|
import markdownItAnchor from 'markdown-it-anchor';
|
||||||
const markdownItClass = require('@toycode/markdown-it-class');
|
import markdownItClass from '@toycode/markdown-it-class';
|
||||||
const markdownItLinkAttributes = require('markdown-it-link-attributes');
|
import markdownItLinkAttributes from 'markdown-it-link-attributes';
|
||||||
const markdownItEmoji = require('markdown-it-emoji').full;
|
import {full as markdownItEmoji} from 'markdown-it-emoji';
|
||||||
const markdownItEleventyImg = require('markdown-it-eleventy-img');
|
import markdownItEleventyImg from 'markdown-it-eleventy-img';
|
||||||
const markdownItFootnote = require('markdown-it-footnote');
|
import markdownItFootnote from 'markdown-it-footnote';
|
||||||
const markdownitMark = require('markdown-it-mark');
|
import markdownitMark from 'markdown-it-mark';
|
||||||
const markdownitAbbr = require('markdown-it-abbr');
|
import markdownitAbbr from 'markdown-it-abbr';
|
||||||
const {slugifyString} = require('../utils');
|
import {slugifyString} from '../filters/slugify.js';
|
||||||
const path = require('path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const markdownLib = markdownIt({
|
export const markdownLib = markdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
|
|
@ -84,5 +84,3 @@ const markdownLib = markdownIt({
|
||||||
.use(markdownItFootnote)
|
.use(markdownItFootnote)
|
||||||
.use(markdownitMark)
|
.use(markdownitMark)
|
||||||
.use(markdownitAbbr);
|
.use(markdownitAbbr);
|
||||||
|
|
||||||
module.exports = markdownLib;
|
|
||||||
|
|
@ -5,13 +5,13 @@ import path from 'node:path';
|
||||||
const dataPath = './src/_data/builtwith.json';
|
const dataPath = './src/_data/builtwith.json';
|
||||||
const screenshotDir = path.join(
|
const screenshotDir = path.join(
|
||||||
path.dirname(new URL(import.meta.url).pathname),
|
path.dirname(new URL(import.meta.url).pathname),
|
||||||
'../../src/assets/images/screenshots'
|
'../../assets/images/screenshots'
|
||||||
);
|
);
|
||||||
|
|
||||||
async function fetchScreenshot(url, filePath) {
|
async function fetchScreenshot(url, filePath) {
|
||||||
const waitCondition = 'wait:2';
|
const waitCondition = 'wait:2';
|
||||||
const timeout = 'timeout:5';
|
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, {
|
const buffer = await fetch(apiUrl, {
|
||||||
duration: '1d',
|
duration: '1d',
|
||||||
4
src/_config/shortcodes.js
Normal file
4
src/_config/shortcodes.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import {imageShortcode} from './shortcodes/image.js';
|
||||||
|
import {svgShortcode} from './shortcodes/svg.js';
|
||||||
|
|
||||||
|
export default {imageShortcode, svgShortcode};
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
const Image = require('@11ty/eleventy-img');
|
import Image from '@11ty/eleventy-img';
|
||||||
const path = require('path');
|
import path from 'node:path';
|
||||||
const htmlmin = require('html-minifier-terser');
|
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 => {
|
const stringifyAttributes = attributeMap => {
|
||||||
return Object.entries(attributeMap)
|
return Object.entries(attributeMap)
|
||||||
.map(([attribute, value]) => {
|
.map(([attribute, value]) => {
|
||||||
|
|
@ -11,14 +16,26 @@ const stringifyAttributes = attributeMap => {
|
||||||
.join(' ');
|
.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,
|
src,
|
||||||
alt = '',
|
alt = '',
|
||||||
caption,
|
caption = '',
|
||||||
loading = 'lazy',
|
loading = 'lazy',
|
||||||
className,
|
className,
|
||||||
sizes = '90vw',
|
sizes = '90vw',
|
||||||
widths = [440, 880, 1024, 1360],
|
widths = [440, 650, 960, 1200],
|
||||||
formats = ['avif', 'webp', 'jpeg']
|
formats = ['avif', 'webp', 'jpeg']
|
||||||
) => {
|
) => {
|
||||||
const metadata = await Image(src, {
|
const metadata = await Image(src, {
|
||||||
|
|
@ -62,7 +79,7 @@ const imageShortcode = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageElement = caption
|
const imageElement = caption
|
||||||
? `<figure class="flow ${className ? `${className}` : ''}">
|
? `<figure slot="image" class="flow ${className ? `${className}` : ''}">
|
||||||
<picture>
|
<picture>
|
||||||
${imageSources}
|
${imageSources}
|
||||||
<img
|
<img
|
||||||
|
|
@ -70,7 +87,7 @@ const imageShortcode = async (
|
||||||
</picture>
|
</picture>
|
||||||
<figcaption>${caption}</figcaption>
|
<figcaption>${caption}</figcaption>
|
||||||
</figure>`
|
</figure>`
|
||||||
: `<picture class="flow ${className ? `${className}` : ''}">
|
: `<picture slot="image" class="flow ${className ? `${className}` : ''}">
|
||||||
${imageSources}
|
${imageSources}
|
||||||
<img
|
<img
|
||||||
${imgageAttributes}>
|
${imgageAttributes}>
|
||||||
|
|
@ -78,5 +95,3 @@ const imageShortcode = async (
|
||||||
|
|
||||||
return htmlmin.minify(imageElement, {collapseWhitespace: true});
|
return htmlmin.minify(imageElement, {collapseWhitespace: true});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = imageShortcode;
|
|
||||||
23
src/_config/shortcodes/svg.js
Normal file
23
src/_config/shortcodes/svg.js
Normal 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}"` : ''} >`
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/_config/utils/clamp-generator.js
Normal file
43
src/_config/utils/clamp-generator.js
Normal 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)`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
13
src/_config/utils/throw-if-not-type.js
Normal file
13
src/_config/utils/throw-if-not-type.js
Normal 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})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
24
src/_config/utils/tokens-to-tailwind.js
Normal file
24
src/_config/utils/tokens-to-tailwind.js
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue