feat: support external recipes in cookbook (#831)

* feat(schema): add external recipe fields to cookbook schema

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>

* feat(data): support external recipes in data generator

- 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>

* feat(website): render external recipe cards on cookbook page

- 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>

* feat(cookbook): add community samples section with first external recipe

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>

* feat(cookbook): add Copilot SDK Web App to community samples

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>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-03-02 19:36:10 +11:00
committed by GitHub
parent 0164092b2f
commit a998c2d38c
5 changed files with 189 additions and 13 deletions

View File

@@ -88,9 +88,42 @@
"items": {
"type": "string"
}
},
"external": {
"type": "boolean",
"description": "Whether this recipe links to an external repository",
"default": false
},
"url": {
"type": "string",
"description": "URL to the external repository or project (required when external is true)",
"format": "uri"
},
"author": {
"type": "object",
"description": "Author information for external recipes",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Author display name or GitHub username"
},
"url": {
"type": "string",
"description": "Author profile URL",
"format": "uri"
}
}
}
},
"if": {
"properties": { "external": { "const": true } },
"required": ["external"]
},
"then": {
"required": ["url"]
}
}
}
}
}

View File

@@ -69,3 +69,37 @@ cookbooks:
- playwright
- mcp
- wcag
- id: community-samples
name: Community Samples
description: Community-contributed projects and examples for GitHub Copilot
path: cookbook/community-samples
featured: false
languages: []
recipes:
- id: nodejs-agentic-issue-resolver
name: Node.js Agentic Issue Resolver
description: A resilient agentic workflow for autonomous codebase exploration and fixing, optimized for the Copilot SDK Technical Preview
external: true
url: https://github.com/Impesud/nodejs-copilot-issue-resolver
author:
name: Impesud
url: https://github.com/Impesud
tags:
- nodejs
- copilot-sdk
- agents
- community
- id: copilot-sdk-web-app
name: Copilot SDK Web App
description: A full-stack chat application built with the GitHub Copilot SDK, .NET Aspire, and React with GitHub OAuth, session history, and model selection
external: true
url: https://github.com/aaronpowell/copilot-sdk-web-app
author:
name: aaronpowell
url: https://github.com/aaronpowell
tags:
- dotnet
- copilot-sdk
- web-app
- community

View File

@@ -742,9 +742,12 @@ function generateSamplesData() {
const allTags = new Set();
let totalRecipes = 0;
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
// Collect languages
// First pass: collect all known language IDs across cookbooks
cookbookManifest.cookbooks.forEach((cookbook) => {
cookbook.languages.forEach((lang) => allLanguages.add(lang.id));
});
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
// Process recipes and add file paths
const recipes = cookbook.recipes.map((recipe) => {
@@ -753,6 +756,36 @@ function generateSamplesData() {
recipe.tags.forEach((tag) => allTags.add(tag));
}
totalRecipes++;
// External recipes link to an external URL — skip local file resolution
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: {},
};
}
// Build variants with file paths for each language
const variants = {};
cookbook.languages.forEach((lang) => {
@@ -771,13 +804,12 @@ function generateSamplesData() {
}
});
totalRecipes++;
return {
id: recipe.id,
name: recipe.name,
description: recipe.description,
tags: recipe.tags || [],
languages: Object.keys(variants),
variants,
};
});

View File

@@ -197,6 +197,39 @@ const base = import.meta.env.BASE_URL;
font-style: italic;
}
/* External recipe card */
.recipe-card.external {
border-style: dashed;
}
.recipe-badge.external-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--color-bg-secondary);
color: var(--color-text-muted);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
border: 1px solid var(--color-border);
white-space: nowrap;
}
.recipe-author-line {
margin-bottom: 12px;
font-size: 13px;
color: var(--color-text-muted);
}
.recipe-author-line a {
color: var(--color-link);
text-decoration: none;
}
.recipe-author-line a:hover {
text-decoration: underline;
}
/* Empty state */
.empty-state {
text-align: center;

View File

@@ -25,7 +25,11 @@ interface Recipe {
name: string;
description: string;
tags: string[];
languages: string[];
variants: Record<string, RecipeVariant>;
external?: boolean;
url?: string | null;
author?: { name: string; url?: string } | null;
}
interface Cookbook {
@@ -138,7 +142,7 @@ function setupFilters(): void {
languages.forEach((lang, id) => {
const option = document.createElement("option");
option.value = id;
option.textContent = `${lang.icon} ${lang.name}`;
option.textContent = lang.name;
languageSelect.appendChild(option);
});
@@ -257,10 +261,10 @@ function getFilteredRecipes(): {
);
}
// Apply language filter
// Apply language filter using per-recipe languages array
if (selectedLanguage) {
results = results.filter(
({ recipe }) => recipe.variants[selectedLanguage!]
results = results.filter(({ recipe }) =>
recipe.languages.includes(selectedLanguage!)
);
}
@@ -370,15 +374,55 @@ function renderRecipeCard(
const recipeKey = `${cookbook.id}-${recipe.id}`;
const isExpanded = expandedRecipes.has(recipeKey);
// Determine which language to show
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
const variant = recipe.variants[displayLang];
const tags = recipe.tags
.map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`)
.join("");
const langIndicators = cookbook.languages
// External recipe — link to external URL
if (recipe.external && recipe.url) {
const authorHtml = recipe.author
? `<span class="recipe-author">by ${
recipe.author.url
? `<a href="${escapeHtml(recipe.author.url)}" target="_blank" rel="noopener">${escapeHtml(recipe.author.name)}</a>`
: escapeHtml(recipe.author.name)
}</span>`
: "";
return `
<div class="recipe-card external${
isExpanded ? " expanded" : ""
}" data-recipe="${escapeHtml(recipeKey)}">
<div class="recipe-header">
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
<span class="recipe-badge external-badge" title="External project">
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"/>
</svg>
Community
</span>
</div>
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
${authorHtml ? `<div class="recipe-author-line">${authorHtml}</div>` : ""}
<div class="recipe-tags">${tags}</div>
<div class="recipe-actions">
<a href="${escapeHtml(recipe.url)}"
class="btn btn-primary btn-small" target="_blank" rel="noopener">
<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
</a>
</div>
</div>
`;
}
// Local recipe — existing behavior
// Determine which language to show
const displayLang = selectedLanguage || cookbook.languages?.[0]?.id || "nodejs";
const variant = recipe.variants[displayLang];
const langIndicators = (cookbook.languages ?? [])
.filter((lang) => recipe.variants[lang.id])
.map(
(lang) =>