Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .schemas/cookbook.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,40 @@
"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"]
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions cookbook/cookbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 36 additions & 4 deletions eng/generate-website-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
Comment on lines +746 to +748
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.

const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {

// Process recipes and add file paths
const recipes = cookbook.recipes.map((recipe) => {
Expand All @@ -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: {},
};
}
Comment on lines +762 to +787
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.

// Build variants with file paths for each language
const variants = {};
cookbook.languages.forEach((lang) => {
Expand All @@ -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,
};
});
Expand Down
33 changes: 33 additions & 0 deletions website/src/pages/learning-hub/cookbook/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
60 changes: 52 additions & 8 deletions website/src/scripts/pages/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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!)
);
}

Expand Down Expand Up @@ -370,14 +374,54 @@ 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("");

// 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="${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.
<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
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.
</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
Comment on lines +422 to 425
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.
.filter((lang) => recipe.variants[lang.id])
.map(
Expand Down