154 lines
No EOL
4.2 KiB
JavaScript
154 lines
No EOL
4.2 KiB
JavaScript
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}/`;
|
|
}
|
|
}
|
|
} |