const markdownIt = require("markdown-it"); const markdownItFootnoteCustom = require('./lib/footnotes/plugin'); const markdownItMermaid = require('markdown-it-mermaid').default const markdownItTaskLists = require('markdown-it-task-lists'); const markdownItMeasurements = require('./lib/measurements/plugin'); const markdownItHashtag = require('./lib/hashtags/plugin'); const markdownItStripTrailingHashtags = require('./lib/strip-trailing-hashtags/plugin'); const markdownItDetails = require('./lib/details/plugin'); const { extractTags, expandHierarchicalTags, getPostTags, getRecipeTags } = require('./lib/tags/plugin'); const { cacheBustingPlugin } = require('./lib/cache-busting/plugin'); const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); const fs = require("fs"); const { DateTime } = require("luxon"); const siteConfig = require("./_data/site.js"); const isReleased = (post) => { return post.data.released !== false; } const sharedPlugins = [ markdownItFootnoteCustom, markdownItHashtag, markdownItMermaid, [markdownItTaskLists, { enabled: true, label: true, labelAfter: false }], markdownItDetails, ]; const applyPlugins = (md, plugins) => plugins.reduce((instance, plugin) => { const [pluginFn, options] = Array.isArray(plugin) ? plugin : [plugin]; return instance.use(pluginFn, options), instance; }, md); const createMarkdownInstance = (extraPlugins = []) => { const md = markdownIt({ html: true, breaks: true, linkify: true }); applyPlugins(md, [...sharedPlugins, ...extraPlugins]); return md; }; const md = createMarkdownInstance([markdownItStripTrailingHashtags]); const recipeMd = createMarkdownInstance([markdownItStripTrailingHashtags, markdownItMeasurements]); const tagExtractorMd = createMarkdownInstance(); module.exports = (eleventyConfig) => { eleventyConfig.addPlugin(syntaxHighlight); eleventyConfig.addPlugin(cacheBustingPlugin, { rootDir: __dirname }); eleventyConfig.addFilter("extractTags", (content) => extractTags(content, tagExtractorMd)); eleventyConfig.addFilter("extractTagsFromFile", (filePath) => { try { const content = fs.readFileSync(filePath, 'utf-8'); return extractTags(content, tagExtractorMd); } catch (e) { return []; } }); eleventyConfig.addFilter("renderRecipeMarkdown", (content) => { if (!content) return ''; return recipeMd.render(content); }); eleventyConfig.setLibrary("md", md); eleventyConfig.addFilter("dateTimeFormat", (date) => { const dt = date instanceof Date ? DateTime.fromJSDate(date, { zone: 'utc' }) : DateTime.fromISO(date, { zone: 'utc' }); // Convert to site timezone for display const displayDt = dt.setZone(siteConfig.timezone); return displayDt.toFormat('MMMM d, yyyy h:mm a ZZZZ'); }); eleventyConfig.addFilter("isoDateTime", (date) => { const dt = date instanceof Date ? DateTime.fromJSDate(date, { zone: 'utc' }) : DateTime.fromISO(date, { zone: 'utc' }); return dt.toISO(); }); eleventyConfig.addFilter("isoDate", (date) => { const dt = date instanceof Date ? DateTime.fromJSDate(date, { zone: 'utc' }) : DateTime.fromISO(date, { zone: 'utc' }); return dt.toISODate(); }); eleventyConfig.addFilter("isMoreThanHourAfter", (date1, date2) => { if (!date1 || !date2) return false; const toDateTime = (d) => { if (DateTime.isDateTime(d)) return d; if (d instanceof Date) return DateTime.fromJSDate(d, { zone: 'utc' }); return DateTime.fromISO(d, { zone: 'utc' }); }; const dt1 = toDateTime(date1); const dt2 = toDateTime(date2); const diff = dt1.diff(dt2, 'hours').hours; return Math.abs(diff) > 1; }); eleventyConfig.addCollection("posts", (collectionApi) => { return collectionApi.getFilteredByGlob("posts/**/*.md") .filter(isReleased) .sort((a, b) => { const aDate = a.data.createdAt || a.date; const bDate = b.data.createdAt || b.date; return aDate - bDate; }); }); eleventyConfig.addCollection("allPosts", (collectionApi) => { return collectionApi.getFilteredByGlob("posts/**/*.md").sort((a, b) => { const aDate = a.data.createdAt || a.date; const bDate = b.data.createdAt || b.date; return aDate - bDate; }); }); eleventyConfig.addCollection("postsBySlug", (collectionApi) => { const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased); const slugify = eleventyConfig.getFilter("slugify"); const grouped = {}; posts.forEach(post => { const slug = slugify(post.data.title); if (!grouped[slug]) { grouped[slug] = []; } grouped[slug].push(post); }); Object.values(grouped).forEach(postList => { postList.sort((a, b) => { const aDate = a.data.createdAt || a.date; const bDate = b.data.createdAt || b.date; return aDate - bDate; }); }); return grouped; }); eleventyConfig.addCollection("postsByTag", (collectionApi) => { const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased); const tagMap = {}; posts.forEach(post => { const rawTags = getPostTags(post, tagExtractorMd); const tags = expandHierarchicalTags(rawTags); tags.forEach((tag) => { tagMap[tag] = { name: tag, posts: [post, ...(tagMap[tag]?.posts ?? [])], recipes: tagMap[tag]?.recipes ?? [], } }) }); Object.values(tagMap).forEach(tagData => { tagData.posts = [...new Set(tagData.posts)].sort((a, b) => { const aDate = a.data.createdAt || a.date; const bDate = b.data.createdAt || b.date; return aDate - bDate; }); }); return tagMap; }); // Recipe collections eleventyConfig.addCollection("recipes", (collectionApi) => { return collectionApi.getFilteredByGlob("recipes/**/*.md") .filter(recipe => recipe.data.draft !== true); }); eleventyConfig.addCollection("allRecipes", (collectionApi) => { return collectionApi.getFilteredByGlob("recipes/**/*.md"); }); eleventyConfig.addCollection("recipesBySlug", (collectionApi) => { const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md"); // Group recipes by slug using reduce const grouped = recipes.reduce((acc, recipe) => { const slug = recipe.data.recipeSlug; return { ...acc, [slug]: [...(acc[slug] || []), recipe], }; }, {}); // Transform grouped recipes into final structure with sorted versions and newest non-draft return Object.entries(grouped).reduce((acc, [slug, recipeList]) => { const versions = [...recipeList].sort((a, b) => a.data.recipeVersion - b.data.recipeVersion); const newest = [...versions].reverse().find(r => r.data.draft !== true) || null; return { ...acc, [slug]: { versions, newest }, }; }, {}); }); eleventyConfig.addCollection("contentTags", (collectionApi) => { const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased); const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md") .filter(r => r.data.isNewestVersion && r.data.draft !== true); const postTags = posts.flatMap(post => getPostTags(post, tagExtractorMd)); const recipeTags = recipes.flatMap(recipe => getRecipeTags(recipe, tagExtractorMd)); const allTags = expandHierarchicalTags([...postTags, ...recipeTags]); return [...new Set(allTags)].sort(); }); eleventyConfig.addCollection("contentByTag", (collectionApi) => { const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased); const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md") .filter(r => r.data.isNewestVersion && r.data.draft !== true); const sortByDate = (a, b) => { const aDate = a.data.createdAt || a.date; const bDate = b.data.createdAt || b.date; return aDate - bDate; }; const postTagMap = posts.reduce((acc, post) => { const rawTags = getPostTags(post, tagExtractorMd); const tags = expandHierarchicalTags(rawTags); return tags.reduce((innerAcc, tag) => ({ ...innerAcc, [tag]: { name: tag, posts: [...(innerAcc[tag]?.posts || []), post], recipes: innerAcc[tag]?.recipes || [], }, }), acc); }, {}); const tagMap = recipes.reduce((acc, recipe) => { const rawTags = getRecipeTags(recipe, tagExtractorMd); const tags = expandHierarchicalTags(rawTags); return tags.reduce((innerAcc, tag) => ({ ...innerAcc, [tag]: { name: tag, posts: innerAcc[tag]?.posts || [], recipes: [...(innerAcc[tag]?.recipes || []), recipe], }, }), acc); }, postTagMap); return Object.entries(tagMap).reduce((acc, [tag, tagData]) => ({ ...acc, [tag]: { ...tagData, posts: [...new Set(tagData.posts)].sort(sortByDate), recipes: [...new Set(tagData.recipes)], }, }), {}); }); eleventyConfig.addPassthroughCopy("robots.txt"); eleventyConfig.addPassthroughCopy("simulations"); eleventyConfig.addPassthroughCopy("js"); eleventyConfig.ignores.add("README.md"); return { dir: { includes: "_includes", output: "_site" }, markdownTemplateEngine: "njk", htmlTemplateEngine: "njk" }; };