feat(website): add samples/cookbook page with recipe browser

Integrates the cookbook/ folder into the website's Samples page:

Data Structure:
- Add cookbook/cookbook.yml manifest defining cookbooks and recipes
- Add .schemas/cookbook.schema.json for validation
- Add COOKBOOK_DIR constant to eng/constants.mjs

Build Integration:
- Add generateSamplesData() to generate samples.json from cookbook.yml
- Include recipe variants with file paths for each language
- Add samples count to manifest.json

Website UI:
- Create samples.ts with FuzzySearch, language/tag filtering
- Replace placeholder samples.astro with functional recipe browser
- Recipe cards with language indicators and action buttons
- Language tabs for switching between implementations
- View Recipe/View Example buttons open modal
- GitHub link for each recipe

Features:
- Search recipes by name/description
- Filter by programming language (Node.js, Python, .NET, Go)
- Filter by tags (multi-select with Choices.js)
- 5 recipes across 4 languages = 20 recipe variants
This commit is contained in:
Aaron Powell
2026-02-02 15:11:12 +11:00
parent 23f08bbb63
commit b8d93a0344
14 changed files with 1699 additions and 67 deletions

View File

@@ -16,6 +16,7 @@ import {
PROMPTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR,
COOKBOOK_DIR,
ROOT_FOLDER,
} from "./constants.mjs";
import {
@@ -559,6 +560,89 @@ function generateSearchIndex(agents, prompts, instructions, skills, collections)
return index;
}
/**
* Generate samples/cookbook data from cookbook.yml
*/
function generateSamplesData() {
const cookbookYamlPath = path.join(COOKBOOK_DIR, "cookbook.yml");
if (!fs.existsSync(cookbookYamlPath)) {
console.warn("Warning: cookbook/cookbook.yml not found, skipping samples generation");
return { cookbooks: [], totalRecipes: 0, totalCookbooks: 0, filters: { languages: [], tags: [] } };
}
const cookbookManifest = parseYamlFile(cookbookYamlPath);
if (!cookbookManifest || !cookbookManifest.cookbooks) {
console.warn("Warning: Invalid cookbook.yml format");
return { cookbooks: [], totalRecipes: 0, totalCookbooks: 0, filters: { languages: [], tags: [] } };
}
const allLanguages = new Set();
const allTags = new Set();
let totalRecipes = 0;
const cookbooks = cookbookManifest.cookbooks.map(cookbook => {
// Collect languages
cookbook.languages.forEach(lang => allLanguages.add(lang.id));
// Process recipes and add file paths
const recipes = cookbook.recipes.map(recipe => {
// Collect tags
if (recipe.tags) {
recipe.tags.forEach(tag => allTags.add(tag));
}
// Build variants with file paths for each language
const variants = {};
cookbook.languages.forEach(lang => {
const docPath = `${cookbook.path}/${lang.id}/${recipe.id}.md`;
const examplePath = `${cookbook.path}/${lang.id}/recipe/${recipe.id}${lang.extension}`;
// Check if files exist
const docFullPath = path.join(ROOT_FOLDER, docPath);
const exampleFullPath = path.join(ROOT_FOLDER, examplePath);
if (fs.existsSync(docFullPath)) {
variants[lang.id] = {
doc: docPath,
example: fs.existsSync(exampleFullPath) ? examplePath : null
};
}
});
totalRecipes++;
return {
id: recipe.id,
name: recipe.name,
description: recipe.description,
tags: recipe.tags || [],
variants
};
});
return {
id: cookbook.id,
name: cookbook.name,
description: cookbook.description,
path: cookbook.path,
featured: cookbook.featured || false,
languages: cookbook.languages,
recipes
};
});
return {
cookbooks,
totalRecipes,
totalCookbooks: cookbooks.length,
filters: {
languages: Array.from(allLanguages).sort(),
tags: Array.from(allTags).sort()
}
};
}
/**
* Main function
*/
@@ -592,6 +676,9 @@ async function main() {
const tools = toolsData.items;
console.log(`✓ Generated ${tools.length} tools (${toolsData.filters.categories.length} categories)`);
const samplesData = generateSamplesData();
console.log(`✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)`);
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
console.log(`✓ Generated search index with ${searchIndex.length} items`);
@@ -626,6 +713,11 @@ async function main() {
JSON.stringify(toolsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "samples.json"),
JSON.stringify(samplesData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "search-index.json"),
JSON.stringify(searchIndex, null, 2)
@@ -641,6 +733,7 @@ async function main() {
skills: skills.length,
collections: collections.length,
tools: tools.length,
samples: samplesData.totalRecipes,
total: searchIndex.length,
},
};