Skip to content

feat: support external recipes in cookbook#831

Open
aaronpowell wants to merge 5 commits intostagedfrom
external-recipies
Open

feat: support external recipes in cookbook#831
aaronpowell wants to merge 5 commits intostagedfrom
external-recipies

Conversation

@aaronpowell
Copy link
Contributor

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.yml without needing to host code in this repo.

Changes

Schema (.schemas/cookbook.schema.json)

  • Added external (boolean), url (URI, required when external), and author (name + optional URL) fields to recipe items

Data generator (eng/generate-website-data.mjs)

  • External recipes skip local file validation and pass through external, url, author fields
  • URL format validation for external recipes
  • Added per-recipe languages array — derived from resolved variant keys (local) or from tags matching known language IDs (external)

Frontend (website/src/scripts/pages/samples.ts, index.astro)

  • External recipe cards render with "Community" badge, author attribution, and "View on GitHub" link
  • Language filter uses per-recipe languages array uniformly for both local and external recipes
  • Fixed Nerd Font icons showing as broken boxes in native <select> dropdown
  • CSS for external card styling (dashed border, badge, author link)

Cookbook manifest (cookbook/cookbook.yml)

How to add an external recipe

Add an entry to cookbook/cookbook.yml under any cookbook section:

- id: my-project
  name: My Project Name
  description: What the project does
  external: true
  url: https://github.com/user/repo
  author:
    name: username
    url: https://github.com/username
  tags:
    - nodejs
    - community

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

aaronpowell and others added 4 commits February 27, 2026 11:45
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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 if cookbook.languages is 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.languages always exists when iterating. For the "community-samples" cookbook which has languages: [], this is fine, but if cookbook.languages is undefined or null, this will throw a runtime error. Consider adding a null check or using optional chaining.
      cookbook.languages.forEach((lang) => {

Comment on lines +422 to 425
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
const variant = recipe.variants[displayLang];

const langIndicators = cookbook.languages
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 ?? [])

Copilot uses AI. Check for mistakes.
<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
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +762 to +787
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: {},
};
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +746 to +748
cookbookManifest.cookbooks.forEach((cookbook) => {
cookbook.languages.forEach((lang) => allLanguages.add(lang.id));
});
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
return `
<div class="recipe-card external${
isExpanded ? " expanded" : ""
}" data-recipe="${recipeKey}">
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}" data-recipe="${recipeKey}">
}" data-recipe="${escapeHtml(recipeKey)}">

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants