diff --git a/.schemas/cookbook.schema.json b/.schemas/cookbook.schema.json index 857bd84e7..f0465d88c 100644 --- a/.schemas/cookbook.schema.json +++ b/.schemas/cookbook.schema.json @@ -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"] } } } diff --git a/cookbook/cookbook.yml b/cookbook/cookbook.yml index 43f66f3fc..0134ebb7d 100644 --- a/cookbook/cookbook.yml +++ b/cookbook/cookbook.yml @@ -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 diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs index 9b145bd5b..79b28a1f8 100644 --- a/eng/generate-website-data.mjs +++ b/eng/generate-website-data.mjs @@ -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, }; }); diff --git a/website/src/pages/learning-hub/cookbook/index.astro b/website/src/pages/learning-hub/cookbook/index.astro index 34e1ba3c9..9187ea443 100644 --- a/website/src/pages/learning-hub/cookbook/index.astro +++ b/website/src/pages/learning-hub/cookbook/index.astro @@ -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; diff --git a/website/src/scripts/pages/samples.ts b/website/src/scripts/pages/samples.ts index cdc2e7d3a..2007c3dc1 100644 --- a/website/src/scripts/pages/samples.ts +++ b/website/src/scripts/pages/samples.ts @@ -25,7 +25,11 @@ interface Recipe { name: string; description: string; tags: string[]; + languages: string[]; variants: Record; + 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,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) => `${escapeHtml(tag)}`) .join(""); + // External recipe — link to external URL + if (recipe.external && recipe.url) { + const authorHtml = recipe.author + ? `by ${ + recipe.author.url + ? `${escapeHtml(recipe.author.name)}` + : escapeHtml(recipe.author.name) + }` + : ""; + + return ` +
+
+

${highlightedName || escapeHtml(recipe.name)}

+ + + Community + +
+

${escapeHtml(recipe.description)}

+ ${authorHtml ? `
${authorHtml}
` : ""} +
${tags}
+ +
+ `; + } + + // 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(