feat: support external recipes in cookbook#831
Conversation
Add optional external, url, and author fields to the recipe schema in cookbook.schema.json. When external is true, url is required via conditional validation. Author supports name (required) and url (optional) for attribution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- External recipes (external: true) skip local file validation - Validate URL format for external recipes - Pass through external, url, and author fields to output JSON - Add per-recipe languages array: derived from resolved variant keys for local recipes, and from tags matching known language IDs for external recipes - Collect language IDs in a first pass before processing recipes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extend Recipe interface with external, url, author, and languages - Render external recipes with Community badge, author attribution, and View on GitHub link instead of View Recipe/View Example buttons - Language filter uses per-recipe languages array uniformly - Remove Nerd Font icons from select dropdown options (native selects cannot render custom web fonts) - Add CSS for external recipe cards (dashed border, badge, author) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a Community Samples cookbook section to cookbook.yml with the Node.js Agentic Issue Resolver as the first external recipe entry, linking to https://github.com/Impesud/nodejs-copilot-issue-resolver. Resolves the use case from PR #613 for supporting external samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add aaronpowell/copilot-sdk-web-app — a full-stack chat app built with the GitHub Copilot SDK, .NET Aspire, and React. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for external community-contributed recipes in the cookbook system, enabling projects hosted in external repositories to be listed on the website's Cookbook page without requiring code to be hosted in this repository. The implementation includes schema extensions, data generation logic, frontend rendering, and styling for external recipe cards with appropriate badges and attribution.
Changes:
- Extended cookbook schema to support external recipes with URL, author, and external flag
- Added data generation logic to handle external recipes and derive languages from tags
- Implemented frontend rendering for external recipe cards with Community badge and GitHub links
- Fixed Nerd Font icon display issue in language filter dropdown
- Added first community sample (Node.js Agentic Issue Resolver) to demonstrate the feature
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
.schemas/cookbook.schema.json |
Added external, url, and author fields to recipe schema with conditional requirement for URL when external is true |
eng/generate-website-data.mjs |
Added external recipe processing logic, URL validation, and per-recipe languages array derivation from tags |
website/src/scripts/pages/samples.ts |
Added Recipe interface fields, fixed icon display in dropdown, implemented external recipe card rendering with Community badge, and updated language filtering to use per-recipe languages array |
website/src/pages/learning-hub/cookbook/index.astro |
Added CSS styling for external recipe cards, badges, and author attribution |
cookbook/cookbook.yml |
Added Community Samples cookbook section with first external recipe entry |
Comments suppressed due to low confidence (2)
website/src/scripts/pages/samples.ts:426
- The code calls
cookbook.languages.filter()which will throw a TypeError ifcookbook.languagesis undefined or null. This could happen if the YAML file has invalid structure. Consider adding a null check or using optional chaining with a fallback to an empty array.
const langIndicators = cookbook.languages
.filter((lang) => recipe.variants[lang.id])
eng/generate-website-data.mjs:791
- The code assumes
cookbook.languagesalways exists when iterating. For the "community-samples" cookbook which haslanguages: [], this is fine, but ifcookbook.languagesis undefined or null, this will throw a runtime error. Consider adding a null check or using optional chaining.
cookbook.languages.forEach((lang) => {
| const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs"; | ||
| const variant = recipe.variants[displayLang]; | ||
|
|
||
| const langIndicators = cookbook.languages |
There was a problem hiding this comment.
The code accesses cookbook.languages[0]?.id which could fail if cookbook.languages is undefined or null (not just an empty array). For consistency with the community-samples cookbook that has languages: [], consider adding a null check before accessing the array, such as cookbook.languages?.[0]?.id.
This issue also appears on line 425 of the same file.
| const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs"; | |
| const variant = recipe.variants[displayLang]; | |
| const langIndicators = cookbook.languages | |
| const displayLang = selectedLanguage || cookbook.languages?.[0]?.id || "nodejs"; | |
| const variant = recipe.variants[displayLang]; | |
| const langIndicators = (cookbook.languages ?? []) |
| <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true"> | ||
| <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/> | ||
| </svg> | ||
| View on GitHub |
There was a problem hiding this comment.
The button text "View on GitHub" is hardcoded, but the recipe.url could potentially point to repositories hosted on platforms other than GitHub (GitLab, Bitbucket, etc.). Consider making the button text more generic (e.g., "View Project" or "View Repository") or dynamically determining the text based on the URL domain.
| if (recipe.external) { | ||
| if (recipe.url) { | ||
| try { | ||
| new URL(recipe.url); | ||
| } catch { | ||
| console.warn(`Warning: Invalid URL for external recipe "${recipe.id}": ${recipe.url}`); | ||
| } | ||
| } else { | ||
| console.warn(`Warning: External recipe "${recipe.id}" is missing a url`); | ||
| } | ||
|
|
||
| // Derive languages from tags that match known language IDs | ||
| const recipeLanguages = (recipe.tags || []).filter((tag) => allLanguages.has(tag)); | ||
|
|
||
| return { | ||
| id: recipe.id, | ||
| name: recipe.name, | ||
| description: recipe.description, | ||
| tags: recipe.tags || [], | ||
| languages: recipeLanguages, | ||
| external: true, | ||
| url: recipe.url || null, | ||
| author: recipe.author || null, | ||
| variants: {}, | ||
| }; | ||
| } |
There was a problem hiding this comment.
If an external recipe is missing a URL (which triggers a warning at line 770), the recipe is still processed and added with url: null. However, the frontend rendering logic requires both recipe.external and recipe.url to be truthy to render as an external recipe card. If URL is missing, the code falls through to the local recipe rendering logic which expects variants to be populated, but external recipes have variants: {}. This will likely cause the recipe to display incorrectly or not at all. Consider either making the URL field required for external recipes (failing the build if missing) or adding explicit handling in the frontend for external recipes without URLs.
| cookbookManifest.cookbooks.forEach((cookbook) => { | ||
| cookbook.languages.forEach((lang) => allLanguages.add(lang.id)); | ||
| }); |
There was a problem hiding this comment.
The code assumes cookbook.languages always exists and is an array, but doesn't handle the case where it might be undefined or null. This could cause a runtime error when processing cookbooks with missing or invalid language configuration. Consider adding a null check or defaulting to an empty array.
This issue also appears on line 791 of the same file.
| return ` | ||
| <div class="recipe-card external${ | ||
| isExpanded ? " expanded" : "" | ||
| }" data-recipe="${recipeKey}"> |
There was a problem hiding this comment.
The data-recipe attribute value uses recipeKey which is constructed from cookbook.id and recipe.id without HTML escaping. While these values come from YAML and should follow the pattern constraint ^[a-z0-9-]+$, it's safer to escape the attribute value to prevent potential XSS if the validation is ever bypassed or modified.
| }" data-recipe="${recipeKey}"> | |
| }" data-recipe="${escapeHtml(recipeKey)}"> |
Summary
Adds support for external recipes in the cookbook system, allowing community-contributed projects hosted in external repositories to be listed alongside local recipes on the website's Cookbook page.
This addresses the use case from #613 — external samples like standalone projects can now be added to
cookbook.ymlwithout needing to host code in this repo.Changes
Schema (
.schemas/cookbook.schema.json)external(boolean),url(URI, required when external), andauthor(name + optional URL) fields to recipe itemsData generator (
eng/generate-website-data.mjs)external,url,authorfieldslanguagesarray — derived from resolved variant keys (local) or from tags matching known language IDs (external)Frontend (
website/src/scripts/pages/samples.ts,index.astro)languagesarray uniformly for both local and external recipes<select>dropdownCookbook manifest (
cookbook/cookbook.yml)How to add an external recipe
Add an entry to
cookbook/cookbook.ymlunder any cookbook section:Language tags (e.g.,
nodejs,python) are automatically matched to the language filter.Screenshots
External recipe card with Community badge and View on GitHub link
Closes #613