Skip to content

Commit a998c2d

Browse files
aaronpowellCopilotCopilot
authored
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>
1 parent 0164092 commit a998c2d

5 files changed

Lines changed: 189 additions & 13 deletions

File tree

.schemas/cookbook.schema.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,40 @@
8888
"items": {
8989
"type": "string"
9090
}
91+
},
92+
"external": {
93+
"type": "boolean",
94+
"description": "Whether this recipe links to an external repository",
95+
"default": false
96+
},
97+
"url": {
98+
"type": "string",
99+
"description": "URL to the external repository or project (required when external is true)",
100+
"format": "uri"
101+
},
102+
"author": {
103+
"type": "object",
104+
"description": "Author information for external recipes",
105+
"required": ["name"],
106+
"properties": {
107+
"name": {
108+
"type": "string",
109+
"description": "Author display name or GitHub username"
110+
},
111+
"url": {
112+
"type": "string",
113+
"description": "Author profile URL",
114+
"format": "uri"
115+
}
116+
}
91117
}
118+
},
119+
"if": {
120+
"properties": { "external": { "const": true } },
121+
"required": ["external"]
122+
},
123+
"then": {
124+
"required": ["url"]
92125
}
93126
}
94127
}

cookbook/cookbook.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,37 @@ cookbooks:
6969
- playwright
7070
- mcp
7171
- wcag
72+
73+
- id: community-samples
74+
name: Community Samples
75+
description: Community-contributed projects and examples for GitHub Copilot
76+
path: cookbook/community-samples
77+
featured: false
78+
languages: []
79+
recipes:
80+
- id: nodejs-agentic-issue-resolver
81+
name: Node.js Agentic Issue Resolver
82+
description: A resilient agentic workflow for autonomous codebase exploration and fixing, optimized for the Copilot SDK Technical Preview
83+
external: true
84+
url: https://github.com/Impesud/nodejs-copilot-issue-resolver
85+
author:
86+
name: Impesud
87+
url: https://github.com/Impesud
88+
tags:
89+
- nodejs
90+
- copilot-sdk
91+
- agents
92+
- community
93+
- id: copilot-sdk-web-app
94+
name: Copilot SDK Web App
95+
description: A full-stack chat application built with the GitHub Copilot SDK, .NET Aspire, and React with GitHub OAuth, session history, and model selection
96+
external: true
97+
url: https://github.com/aaronpowell/copilot-sdk-web-app
98+
author:
99+
name: aaronpowell
100+
url: https://github.com/aaronpowell
101+
tags:
102+
- dotnet
103+
- copilot-sdk
104+
- web-app
105+
- community

eng/generate-website-data.mjs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -742,9 +742,12 @@ function generateSamplesData() {
742742
const allTags = new Set();
743743
let totalRecipes = 0;
744744

745-
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
746-
// Collect languages
745+
// First pass: collect all known language IDs across cookbooks
746+
cookbookManifest.cookbooks.forEach((cookbook) => {
747747
cookbook.languages.forEach((lang) => allLanguages.add(lang.id));
748+
});
749+
750+
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
748751

749752
// Process recipes and add file paths
750753
const recipes = cookbook.recipes.map((recipe) => {
@@ -753,6 +756,36 @@ function generateSamplesData() {
753756
recipe.tags.forEach((tag) => allTags.add(tag));
754757
}
755758

759+
totalRecipes++;
760+
761+
// External recipes link to an external URL — skip local file resolution
762+
if (recipe.external) {
763+
if (recipe.url) {
764+
try {
765+
new URL(recipe.url);
766+
} catch {
767+
console.warn(`Warning: Invalid URL for external recipe "${recipe.id}": ${recipe.url}`);
768+
}
769+
} else {
770+
console.warn(`Warning: External recipe "${recipe.id}" is missing a url`);
771+
}
772+
773+
// Derive languages from tags that match known language IDs
774+
const recipeLanguages = (recipe.tags || []).filter((tag) => allLanguages.has(tag));
775+
776+
return {
777+
id: recipe.id,
778+
name: recipe.name,
779+
description: recipe.description,
780+
tags: recipe.tags || [],
781+
languages: recipeLanguages,
782+
external: true,
783+
url: recipe.url || null,
784+
author: recipe.author || null,
785+
variants: {},
786+
};
787+
}
788+
756789
// Build variants with file paths for each language
757790
const variants = {};
758791
cookbook.languages.forEach((lang) => {
@@ -771,13 +804,12 @@ function generateSamplesData() {
771804
}
772805
});
773806

774-
totalRecipes++;
775-
776807
return {
777808
id: recipe.id,
778809
name: recipe.name,
779810
description: recipe.description,
780811
tags: recipe.tags || [],
812+
languages: Object.keys(variants),
781813
variants,
782814
};
783815
});

website/src/pages/learning-hub/cookbook/index.astro

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,39 @@ const base = import.meta.env.BASE_URL;
197197
font-style: italic;
198198
}
199199

200+
/* External recipe card */
201+
.recipe-card.external {
202+
border-style: dashed;
203+
}
204+
205+
.recipe-badge.external-badge {
206+
display: inline-flex;
207+
align-items: center;
208+
gap: 4px;
209+
background: var(--color-bg-secondary);
210+
color: var(--color-text-muted);
211+
padding: 4px 10px;
212+
border-radius: 12px;
213+
font-size: 12px;
214+
border: 1px solid var(--color-border);
215+
white-space: nowrap;
216+
}
217+
218+
.recipe-author-line {
219+
margin-bottom: 12px;
220+
font-size: 13px;
221+
color: var(--color-text-muted);
222+
}
223+
224+
.recipe-author-line a {
225+
color: var(--color-link);
226+
text-decoration: none;
227+
}
228+
229+
.recipe-author-line a:hover {
230+
text-decoration: underline;
231+
}
232+
200233
/* Empty state */
201234
.empty-state {
202235
text-align: center;

website/src/scripts/pages/samples.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ interface Recipe {
2525
name: string;
2626
description: string;
2727
tags: string[];
28+
languages: string[];
2829
variants: Record<string, RecipeVariant>;
30+
external?: boolean;
31+
url?: string | null;
32+
author?: { name: string; url?: string } | null;
2933
}
3034

3135
interface Cookbook {
@@ -138,7 +142,7 @@ function setupFilters(): void {
138142
languages.forEach((lang, id) => {
139143
const option = document.createElement("option");
140144
option.value = id;
141-
option.textContent = `${lang.icon} ${lang.name}`;
145+
option.textContent = lang.name;
142146
languageSelect.appendChild(option);
143147
});
144148

@@ -257,10 +261,10 @@ function getFilteredRecipes(): {
257261
);
258262
}
259263

260-
// Apply language filter
264+
// Apply language filter using per-recipe languages array
261265
if (selectedLanguage) {
262-
results = results.filter(
263-
({ recipe }) => recipe.variants[selectedLanguage!]
266+
results = results.filter(({ recipe }) =>
267+
recipe.languages.includes(selectedLanguage!)
264268
);
265269
}
266270

@@ -370,15 +374,55 @@ function renderRecipeCard(
370374
const recipeKey = `${cookbook.id}-${recipe.id}`;
371375
const isExpanded = expandedRecipes.has(recipeKey);
372376

373-
// Determine which language to show
374-
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
375-
const variant = recipe.variants[displayLang];
376-
377377
const tags = recipe.tags
378378
.map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`)
379379
.join("");
380380

381-
const langIndicators = cookbook.languages
381+
// External recipe — link to external URL
382+
if (recipe.external && recipe.url) {
383+
const authorHtml = recipe.author
384+
? `<span class="recipe-author">by ${
385+
recipe.author.url
386+
? `<a href="${escapeHtml(recipe.author.url)}" target="_blank" rel="noopener">${escapeHtml(recipe.author.name)}</a>`
387+
: escapeHtml(recipe.author.name)
388+
}</span>`
389+
: "";
390+
391+
return `
392+
<div class="recipe-card external${
393+
isExpanded ? " expanded" : ""
394+
}" data-recipe="${escapeHtml(recipeKey)}">
395+
<div class="recipe-header">
396+
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
397+
<span class="recipe-badge external-badge" title="External project">
398+
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
399+
<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"/>
400+
</svg>
401+
Community
402+
</span>
403+
</div>
404+
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
405+
${authorHtml ? `<div class="recipe-author-line">${authorHtml}</div>` : ""}
406+
<div class="recipe-tags">${tags}</div>
407+
<div class="recipe-actions">
408+
<a href="${escapeHtml(recipe.url)}"
409+
class="btn btn-primary btn-small" target="_blank" rel="noopener">
410+
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
411+
<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"/>
412+
</svg>
413+
View on GitHub
414+
</a>
415+
</div>
416+
</div>
417+
`;
418+
}
419+
420+
// Local recipe — existing behavior
421+
// Determine which language to show
422+
const displayLang = selectedLanguage || cookbook.languages?.[0]?.id || "nodejs";
423+
const variant = recipe.variants[displayLang];
424+
425+
const langIndicators = (cookbook.languages ?? [])
382426
.filter((lang) => recipe.variants[lang.id])
383427
.map(
384428
(lang) =>

0 commit comments

Comments
 (0)