const fs = require('fs') const path = require("path"); const { execSync } = require("child_process"); const getSlugFromPath = (filePath) => { // Normalize the path - remove leading ./ if present const normalizedPath = filePath.startsWith('./') ? filePath.slice(2) : filePath; // For top-level files: recipes/foo.md -> slug is "foo" // For nested folders: recipes/bar/v1.md -> slug is "bar" const parts = normalizedPath.split(path.sep); // parts[0] should be 'recipes', parts[1] is the key part if (parts.length >= 2 && parts[0] === 'recipes') { const secondPart = parts[1]; // If it's a .md file at the top level (recipes/foo.md), strip the extension if (secondPart.endsWith('.md')) { return path.basename(secondPart, '.md'); } // Otherwise it's a folder name (recipes/bar/v1.md -> bar) return secondPart; } return path.basename(filePath, '.md'); } const getTitleFromSlug = (slug) => { return slug .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } const getGitCreatedTime = (filePath) => { try { const result = execSync( `git log --diff-filter=A --follow --format=%aI -- "${filePath}" | tail -1`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] } ).trim(); if (result) { return new Date(result).getTime(); } } catch (e) { // Git command failed, fall through to filesystem } return null; } const getFileCreatedTime = (filePath) => { // Try git first for accurate cross-system creation time const gitTime = getGitCreatedTime(filePath); if (gitTime) { return gitTime; } // Fall back to filesystem for untracked files try { const stats = fs.statSync(filePath); const time = stats.birthtime ?? stats.mtime; return time.getTime(); } catch (e) { return Date.now(); } } const getVersion = (filePath) => { const dirName = path.dirname(filePath) // Top-level files (directly in recipes/) always have version 0 if (dirName === 'recipes' || dirName === './recipes') { return 0 } const files = fs.readdirSync(dirName).filter(f => f.endsWith('.md')) const filesWithDates = files .map((file) => { const fullPath = path.join(dirName, file); return { file: fullPath, createdAt: getFileCreatedTime(fullPath), } }) .sort((a, b) => a.createdAt - b.createdAt) // oldest first (version 0) const normalizedFilePath = filePath.startsWith('./') ? filePath.slice(2) : filePath; const version = filesWithDates.findIndex(({ file }) => { const normalizedFile = file.startsWith('./') ? file.slice(2) : file; return normalizedFile === normalizedFilePath; }); return version === -1 ? 0 : version; } const isNewestVersion = (filePath) => { const dirName = path.dirname(filePath) // Top-level files are always the "newest" (only version) if (dirName === 'recipes' || dirName === './recipes') { return true } const files = fs.readdirSync(dirName).filter(f => f.endsWith('.md')) const filesWithDates = files .map((file) => { const fullPath = path.join(dirName, file); return { file: fullPath, createdAt: getFileCreatedTime(fullPath), } }) .sort((a, b) => b.createdAt - a.createdAt) // newest first const normalizedFilePath = filePath.startsWith('./') ? filePath.slice(2) : filePath; const newestFile = filesWithDates[0]?.file; const normalizedNewest = newestFile?.startsWith('./') ? newestFile.slice(2) : newestFile; return normalizedFilePath === normalizedNewest; } module.exports = { layout: "recipe.njk", tags: ["recipe"], eleventyComputed: { recipeSlug: (data) => { return getSlugFromPath(data.page.inputPath); }, title: (data) => { if (data.title && data.title !== data.page.fileSlug) { return data.title; } const slug = getSlugFromPath(data.page.inputPath); return getTitleFromSlug(slug); }, recipeVersion: (data) => { return getVersion(data.page.inputPath); }, isNewestVersion: (data) => { // Draft recipes are never considered "newest" for redirect purposes if (data.draft === true) { return false; } return isNewestVersion(data.page.inputPath); }, isDraft: (data) => { return data.draft === true; }, permalink: (data) => { const slug = getSlugFromPath(data.page.inputPath); const version = getVersion(data.page.inputPath); return `/recipe/${slug}/${version}/`; } } }