From e03696a5bf34e148654fe1f294306746c307a82f Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 10 Jul 2026 13:23:53 +1000 Subject: [PATCH] Replace resource modals with dedicated detail pages and richer install options (#2246) * feat(website): dedicated agent detail pages instead of modal Replace the popup/modal viewer for agents with dedicated per-agent pages at /agent// for real URLs and better deep linking. - Build-time rendered docs (marked + gray-matter) with a details sidebar - Sidebar Actions card: Install split-button (VS Code/Insiders/Download/Copy markdown), Share, View on GitHub - Cards now link natively via anchors (no modal); card-render gains optional href (backward compatible for other types) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * feat(website): dedicated instruction detail pages + shared detail layout Extend the dedicated detail-page pattern (introduced for agents) to instructions, and factor the shared behavior/styles out for reuse: - Add instruction/[id].astro with build-time markdown render, breadcrumb, install split-button (VS Code/Insiders/Download/Copy markdown), Share, View on GitHub, and a details sidebar (Applies to / Source / Last updated) plus collapsible frontmatter. - Extract shared client behavior into resource-detail.ts (renamed from agent-detail.ts) keyed on [data-resource-detail]. - Move detail-page CSS into global.css under .resource-detail-page. - Point the agent detail page at the shared script/styles. - Instruction cards now link to /instruction// and the modal is removed from the instructions listing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * refactor(website): extract shared detail-page components Break the agent and instruction detail pages down into reusable Astro components under src/components/pages: Breadcrumb, Header, Main, Sidebar, SidebarChips, InstallButtons, and RawMarkdown. Both detail pages now compose these components instead of duplicating markup. Fix RawMarkdown to read the `markdown` prop (matching both call sites) and emit exact text via set:text, which restores the Copy markdown action that had silently broken when the hidden textarea stopped rendering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * refactor(website): share detail-page build logic in lib/detail-page Both detail pages duplicated the install/GitHub URL builders, the build-time markdown read+render, and the last-updated formatting. Move all of it into a DOM-free build-time helper (src/lib/detail-page.ts) exposing loadDetailPage(item, type), and reduce each [id].astro to a single call plus its type-specific chip data. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add dedicated skill detail pages with multi-file browser Migrate skills from the popup/modal viewer to dedicated per-item pages (/skill//) matching the agent and instruction detail pages. Skills need a few skill-specific mechanics: - Install via `gh skills install github/awesome-copilot ` (copyable), since skills have no VS Code install URL. - Download ZIP for the multi-file skill contents. - A file browser that defaults to SKILL.md and lets you inspect other files, with Shiki syntax highlighting for code and marked-rendered markdown. SKILL.md is embedded (rendered + raw) at build time; other files are lazy-fetched on demand and cached. Deep links via #file=. Also fixes a production build regression in the shared detail-page helper: repoRoot now resolves via process.cwd() instead of import.meta .url, which resolved incorrectly once bundled and silently returned empty markdown (breaking rendered docs + copy-markdown for agents and instructions too). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Replace skill file list with a dropdown selector The two-column file browser (side list + content pane) was cramped, splitting the already-narrow main column in half. Replace the side list with a dropdown in the file view header so the content pane spans the full width. - Multi-file skills get a change event instead of the removed file buttons; deep links, copy-file, Download ZIP, and Share all read the file list from the select options. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix skill sidebar card overflowing its column The install command in the actions card used white-space: nowrap, which gave the grid tracks a large min-content size. Grid items default to min-width: auto (won't shrink below content), so the actions card grew past the 352px sidebar, making it look wider than the agent/instruction sidebars. Add min-width: 0 down the sidebar grid chain so the command box stays within the column and scrolls horizontally instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Make skill file content pane grow to full page height Remove the fixed max-height/overflow on .skill-file-content so file content flows to natural height and the page scrolls, instead of a nested inner scroll region. Shiki
 keeps its own horizontal scroll.

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

* Center-align skill file picker label with the select

Reset the Starlight-injected margin-top on .skill-file-select so the
'File' label and the dropdown share a common vertical midline.

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

* Render code files edge-to-edge in skill file viewer

For code files, drop the container padding and the pre border/radius so
the highlighted code fills the full column width. Markdown files keep
their padded, bordered layout. Toggled via an is-code class on the
content pane based on the selected file kind.

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

* Add dedicated detail pages for hooks with a reusable file browser

Hooks had no good install path (manual copy or ZIP), so mirror the
Skills dedicated-page pattern: a multi-file browser plus a Download ZIP
action, replacing the modal on the hooks listing.

Generalise the Skills-specific file browser so both resource types share
one implementation:
- Rename SkillFileBrowser.astro -> FileBrowser.astro with neutral props.
- Rename skill-detail.ts -> file-browser.ts with neutral data attributes
  (data-file-browser-page, data-bundle-id, data-primary-file).
- Rescope install-slot styles under a shared .bundle-detail-page class.
- generate-website-data: rename getSkillFiles -> getFolderFiles and emit a
  files[] + readmeFileName for each hook.

Hooks list cards now deep-link to /hook// instead of opening a modal.

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

* Add dedicated detail pages for workflows

Migrate agentic workflows from the popup/modal viewer to dedicated per-item pages (/workflow//) with deep linking, matching the pattern used for agents, instructions, skills, and hooks.

Workflows are single .md files, so they reuse the single-file detail components (Main, Sidebar, Header, Breadcrumb, RawMarkdown) and the resource-detail client script. Since workflows have no VS Code install, the install slot instead documents the gh aw CLI flow and offers Download + Copy markdown actions.

Also rescope the shared install-slot CSS from .bundle-detail-page to .detail-actions-card so skill, hook, and workflow pages share it without a page-specific class, and drop the now-unused bundle-detail-page class from the skill and hook pages.

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

* Format workflow install note

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

* Add dedicated plugin detail pages

Replace the plugins modal with dedicated /plugin// pages that deep link, render the bundled README, and surface an Included items section linking each constituent agent/skill/instruction/hook to its own detail page (falling back to GitHub for items without a page, e.g. extensions).

- generate-website-data: resolve plugin items to detail URLs + titles, add readmeFile for local and extension-derived plugins
- new IncludedItems component groups bundled resources by kind
- plugin/[id].astro handles local, extension-derived, and external plugins with VS Code + CLI install actions
- resource-detail: copy-install handler shared with detail pages
- plugins list cards now deep link; modal wiring removed

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

* Show plugin version, ref, and commit in detail sidebar

Capture version from plugin.json (local + extension-derived) and external.json, and surface it in the plugin detail Details card. For external plugins, also show the pinned source ref and short commit SHA when present.

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

* Add dedicated canvas extension detail pages with preview gallery

Replace the extensions modal with per-extension pages at /extension//,
mirroring the skill/plugin detail layout. Each page shows a preview image
gallery, README docs (or an About fallback), and a sidebar with install
actions and details (version, canvas ID, keywords, author, commit).

- generate-website-data.mjs: emit readmeFile for extensions
- extensions-render.ts: deep-link cards to detail pages
- extensions.ts: strip modal/gallery wiring, keep filter/sort/copy actions
- resource-detail.ts: add copy-install-url action
- extension-gallery.ts: thumbnail switching for multi-image previews

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

* Add VS Code Insiders and GitHub Copilot app install links

Introduce a shared PluginInstall component used by both plugin and canvas
extension detail pages. It renders a split-button dropdown deep-linking into
VS Code, VS Code Insiders, and the GitHub Copilot app (ghapp://), plus the
CLI command for internal items.

- Internal plugins/extensions: ghapp://plugins/install?source=@awesome-copilot
- External plugins: ghapp://plugins/marketplace/add?source=
- ghapp source values are URL-encoded per the app's deep-link contract

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

* Make GitHub Copilot app the default install option

Promote the ghapp:// deep link to the primary split-button action and list
it first in the dropdown, ahead of VS Code and VS Code Insiders.

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

* Ignoring impeccable files

* Make Extensions grid Copy Install a direct Copilot app install

Replace the CLI-command copy button on extension cards with an
'Install in Copilot app' deep link (ghapp://plugins/install) for internal
extensions, matching the detail page. External extensions keep the Copy URL
fallback since they have no Copilot app install path.

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

* Address PR review feedback on detail pages

- Remove DOM innerHTML read/write round trip in file browser cache
  (CodeQL js/xss-through-dom); seed primary file from raw text and
  render lazily.
- Use Shiki dual light/dark themes in the file browser and add dark
  mode CSS overrides.
- Guard decodeURIComponent for #file= deep links against malformed
  percent-encoding.
- Slugify SidebarChips title before using it in the tag class name.
- Use a neutral aria-label on the shared detail Sidebar.
- URL-encode the external plugin source in VS Code and Insiders
  install links.

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

* Prevent client-side path traversal in file deep links

Addresses the CSPT-to-XSS class reported in github/open-source#1739.
A '#file=' hash value containing '../' sequences could resolve outside
the awesome-copilot repo prefix once normalized by fetch, loading
attacker-controlled content that was then rendered into the page.

- Add isSafeRepoFilePath() and enforce it at the raw-URL choke point
  (getRawGitHubUrl, fetchFileContent, downloadFile, getVSCodeInstallUrl),
  so no consumer can escape the repo prefix.
- Validate the decoded '#file=' path in the modal hash handler and guard
  its decodeURIComponent against malformed input.

The new hash-based file browser already restricts deep links to an
allowlist of build-time file descriptors; the choke-point guard is an
additional backstop.

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

* Fix accessibility violations on resource detail pages

Run the a11y audit against the new detail pages and resolve every
critical/serious axe violation it surfaced:

- Nest the detail column as a 
instead of a second
, fixing the duplicate/non-top-level/non-unique landmark rules on every detail page. - Add a shared enhanceMarkdownA11y() helper that makes rendered
/
  blocks keyboard focusable and gives task-list checkboxes a state-based
  accessible name; apply it at build time and in the client file browser.
- Make Shiki-highlighted code and install-command  blocks focusable.
- Underline links inside rendered docs and install notes so they are
  distinguishable without color, overriding the global #main-content reset.
- Extend the a11y audit to cover one representative page per detail type.

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

* Fix file browser GitHub URL handling

Encode selected file paths before assigning GitHub detail links and render load errors with DOM APIs so DOM-derived file names are not reinterpreted as markup.

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

* Address PR review feedback on detail pages and file browser

- Deep-link plugin extension items to their /extension// detail pages:
  generate canvas extensions before building the resource index, and index
  extensions (by id and folder basename) so resolvePluginItem can resolve them.
- Render image files in the bundle file browser via an  tag built from the
  safe raw URL instead of decoding binary assets as UTF-8 text; skip the
  copy-file action for images.
- Use a neutral 'Install' heading on internal plugin and extension pages so the
  split-button primary label accurately communicates the target.
- Warn (with the file path) when readResourceMarkdown fails instead of swallowing
  the error silently, keeping the build unbroken.

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

* Address PR review feedback on install links, dropdown a11y, and ZIP downloads

- externalRepoUrl (plugin detail pages) now builds the 'View on GitHub' tree
  URL from the pinned source.ref or source.sha, falling back to main only when
  neither is present, so the link matches the sidebar Ref/Commit chips.
- Install split-button dropdown on resource detail pages is now keyboard
  accessible: opening focuses the first item, ArrowUp/ArrowDown wrap, Home/End
  jump, and Escape closes and restores focus to the toggle, mirroring modal.ts.
- downloadZipBundle fetches each file as an ArrayBuffer and hands it to JSZip so
  binary assets (PNG/JPG/etc.) are preserved instead of corrupted by UTF-8 text
  decoding.

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

* Address PR review feedback on external repo links and workflow install note

- externalRepoUrl now uses a pinned source.ref/sha even when the external
  plugin has no path, returning /tree/ so "View on GitHub" points at the
  pinned revision and stays consistent with the sidebar Ref/Commit chips.
- Reflow the workflow install note so each inline code and link element stays on
  a single line (using explicit whitespace expressions for word spacing),
  removing the split end-tag artifacts while preserving the exact rendered text
  and spacing.

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Harden repo path validation and fix Included Items file links

- isSafeRepoFilePath now fails closed on any "%" so percent-encoded dot-segments
  (%2e%2e and double-encoded %252e%252e) cannot be normalized back into path
  traversal by the browser URL parser during fetch, and also rejects "." and
  empty path segments. Legitimate repo paths never contain these.
- IncludedItems githubHref now links file paths via /blob/ and directories via
  /tree/, detecting files by a trailing extension on the last path segment, so
  the fallback links for agent/instruction/command items no longer 404.

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

* Harden detail pages: sanitize markdown, fix search deep-links and external URLs

Addresses rubber-duck review items 1-5 for the detail-page migration:

1. Sanitize rendered markdown as untrusted HTML. Add isomorphic
   sanitize-html helper (isomorphic-dompurify) applied in the build-time
   pipeline (detail-page.ts) and the client file browser before a11y
   enhancement, so marked output can no longer inject scripts/handlers.
2. Point Pagefind search results at canonical /type/id/ detail pages
   instead of inert #file= listing hashes for types that have a detail page.
3. Sanitize external/generated URLs on plugin and extension detail pages
   and their render scripts so only http(s) links are emitted.
4. Add a shared externalRepoUrl helper that pins GitHub links to the
   source ref/sha (preferring sha) with encoded path segments, replacing
   the duplicated always-main logic in the pages, modal, and renderers.
5. Handle #file= hash navigation after initial load in the file browser
   via a hashchange listener.

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

* Potential fix for pull request finding

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

* Detail pages: clickable filter chips + code-block copy buttons (#2255)

* Add clickable filter chips on detail pages

Turn read-only metadata chips into navigation: tags (hooks/plugins),
keywords (extensions), and extensions (instructions) now link to their
list page pre-filtered by that value (e.g. /hooks/?tag=testing).

SidebarChips gains optional filterBase/filterParam props; when both are
set each chip renders as an  with an aria-label and hover/focus
styling, otherwise it stays a plain . Agent/skill/workflow chips
are unchanged. Filtering was verified end to end against each list
page's existing query-param handling.

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

* Add copy buttons to documentation code blocks

Detail-page markdown is rendered as plain 
 with no syntax
highlighter, so code and config snippets had no copy affordance. Add a
hover-revealed copy button to every code block in the rendered docs:
copies to the clipboard, shows a toast, and swaps to a check icon for
confirmation. Buttons are keyboard-accessible, always visible on touch
devices, and respect reduced-motion. The sidebar frontmatter block is
left untouched.

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

---------

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Fix detail-page GitHub links, empty-state link contrast, and file cache

Addresses four PR review comments on the detail-page migration:

- skill/hook detail pages: build the sidebar "View on GitHub" link from a
  /tree/main base instead of /blob/main, since item.path is a directory and
  /blob/ URLs 404. The FileBrowser githubBase stays on /blob for
  individual file links.
- global.css: include .detail-empty a in the underline override so links in
  empty-state notes stay distinguishable without relying on color alone
  (WCAG 1.4.1 / axe link-in-text-block).
- file-browser.ts: seed the primary file's cache with its already-rendered
  (frontmatter-stripped) HTML in addition to raw text, so re-selecting the
  primary file after navigating away no longer re-renders the raw source and
  surfaces frontmatter. Copy still uses the full raw text.

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com>
---
 .gitignore                                    |   1 +
 eng/generate-website-data.mjs                 | 144 ++++-
 website/package-lock.json                     | 549 +++++++++++++++++
 website/package.json                          |   1 +
 website/scripts/a11y-audit.mjs                |   9 +
 website/src/components/pages/Breadcrumb.astro |  25 +
 .../src/components/pages/FileBrowser.astro    | 145 +++++
 website/src/components/pages/Header.astro     |  13 +
 .../src/components/pages/IncludedItems.astro  |  93 +++
 .../src/components/pages/InstallButtons.astro |  61 ++
 website/src/components/pages/Main.astro       |  17 +
 .../src/components/pages/PluginInstall.astro  | 171 ++++++
 .../src/components/pages/RawMarkdown.astro    |   9 +
 website/src/components/pages/Sidebar.astro    | 104 ++++
 .../src/components/pages/SidebarChips.astro   |  51 ++
 .../src/integrations/pagefind-resources.ts    |  26 +-
 website/src/lib/detail-page.ts                | 125 ++++
 website/src/lib/external-source.ts            |  86 +++
 website/src/lib/file-types.ts                 | 108 ++++
 website/src/lib/markdown-a11y.ts              |  35 ++
 website/src/lib/sanitize-html.ts              |  54 ++
 website/src/pages/agent/[id].astro            | 116 ++++
 website/src/pages/agents.astro                |   2 -
 website/src/pages/extension/[id].astro        | 301 ++++++++++
 website/src/pages/extensions.astro            |   2 -
 website/src/pages/hook/[id].astro             | 159 +++++
 website/src/pages/hooks.astro                 |   2 -
 website/src/pages/instruction/[id].astro      | 115 ++++
 website/src/pages/instructions.astro          |   2 -
 website/src/pages/plugin/[id].astro           | 235 ++++++++
 website/src/pages/plugins.astro               |   2 -
 website/src/pages/skill/[id].astro            | 166 +++++
 website/src/pages/skills.astro                |   2 -
 website/src/pages/workflow/[id].astro         | 131 ++++
 website/src/pages/workflows.astro             |   2 -
 website/src/scripts/modal.ts                  |  30 +-
 website/src/scripts/pages/agents-render.ts    |  18 +-
 website/src/scripts/pages/agents.ts           | 100 ----
 website/src/scripts/pages/card-render.ts      |  23 +-
 .../src/scripts/pages/extension-gallery.ts    |  41 ++
 .../src/scripts/pages/extensions-render.ts    |  67 ++-
 website/src/scripts/pages/extensions.ts       | 327 +---------
 website/src/scripts/pages/file-browser.ts     | 407 +++++++++++++
 website/src/scripts/pages/hooks-render.ts     |  14 +
 website/src/scripts/pages/hooks.ts            |  84 +--
 .../src/scripts/pages/instructions-render.ts  |  18 +-
 website/src/scripts/pages/instructions.ts     |  82 ---
 website/src/scripts/pages/plugins-render.ts   |  17 +-
 website/src/scripts/pages/plugins.ts          | 131 ----
 website/src/scripts/pages/resource-detail.ts  | 250 ++++++++
 website/src/scripts/pages/skills-render.ts    |   8 +
 website/src/scripts/pages/skills.ts           | 100 +---
 website/src/scripts/pages/workflows-render.ts |   9 +
 website/src/scripts/pages/workflows.ts        |  87 ---
 website/src/scripts/utils.ts                  |  46 +-
 website/src/styles/global.css                 | 565 +++++++++++++++++-
 56 files changed, 4496 insertions(+), 992 deletions(-)
 create mode 100644 website/src/components/pages/Breadcrumb.astro
 create mode 100644 website/src/components/pages/FileBrowser.astro
 create mode 100644 website/src/components/pages/Header.astro
 create mode 100644 website/src/components/pages/IncludedItems.astro
 create mode 100644 website/src/components/pages/InstallButtons.astro
 create mode 100644 website/src/components/pages/Main.astro
 create mode 100644 website/src/components/pages/PluginInstall.astro
 create mode 100644 website/src/components/pages/RawMarkdown.astro
 create mode 100644 website/src/components/pages/Sidebar.astro
 create mode 100644 website/src/components/pages/SidebarChips.astro
 create mode 100644 website/src/lib/detail-page.ts
 create mode 100644 website/src/lib/external-source.ts
 create mode 100644 website/src/lib/file-types.ts
 create mode 100644 website/src/lib/markdown-a11y.ts
 create mode 100644 website/src/lib/sanitize-html.ts
 create mode 100644 website/src/pages/agent/[id].astro
 create mode 100644 website/src/pages/extension/[id].astro
 create mode 100644 website/src/pages/hook/[id].astro
 create mode 100644 website/src/pages/instruction/[id].astro
 create mode 100644 website/src/pages/plugin/[id].astro
 create mode 100644 website/src/pages/skill/[id].astro
 create mode 100644 website/src/pages/workflow/[id].astro
 create mode 100644 website/src/scripts/pages/extension-gallery.ts
 create mode 100644 website/src/scripts/pages/file-browser.ts
 create mode 100644 website/src/scripts/pages/resource-detail.ts

diff --git a/.gitignore b/.gitignore
index 342509c0..36d3cb06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ website/public/llms.txt
 *.sln
 obj/
 bin/
+.impeccable
diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs
index f10eafcf..2e646443 100755
--- a/eng/generate-website-data.mjs
+++ b/eng/generate-website-data.mjs
@@ -251,6 +251,10 @@ function generateHooksData(gitDates) {
       .replace(/\\/g, "/");
     const readmeRelativePath = `${relativePath}/README.md`;
 
+    // Get all files in the hook folder recursively (for the file browser and
+    // ZIP download on the detail page).
+    const files = getFolderFiles(hookPath, relativePath);
+
     // Track unique values
     (metadata.hooks || []).forEach((h) => allHookTypes.add(h));
     (metadata.tags || []).forEach((t) => allTags.add(t));
@@ -262,8 +266,10 @@ function generateHooksData(gitDates) {
       hooks: metadata.hooks || [],
       tags: metadata.tags || [],
       assets: metadata.assets || [],
+      files,
       path: relativePath,
       readmeFile: readmeRelativePath,
+      readmeFileName: "README.md",
       lastUpdated: gitDates.get(readmeRelativePath) || null,
     });
   }
@@ -465,7 +471,7 @@ function generateSkillsData(gitDates) {
         .replace(/\\/g, "/");
 
       // Get all files in the skill folder recursively
-      const files = getSkillFiles(skillPath, relativePath);
+      const files = getFolderFiles(skillPath, relativePath);
 
       // Get last updated from SKILL.md file
       const skillFilePath = `${relativePath}/SKILL.md`;
@@ -511,9 +517,9 @@ function generateSkillsData(gitDates) {
 }
 
 /**
- * Get all files in a skill folder recursively
+ * Get all files in a resource folder (skill or hook) recursively.
  */
-function getSkillFiles(skillPath, relativePath) {
+function getFolderFiles(skillPath, relativePath) {
   const files = [];
 
   function walkDir(dir, relDir) {
@@ -555,10 +561,89 @@ function getAgentFiles(agentDir, pluginRootPath) {
     }));
 }
 
+/**
+ * Build a lookup index of resource id -> { title, url } for the kinds that have
+ * dedicated detail pages, so plugin items can deep-link to them.
+ */
+function buildResourceIndex({ agents, skills, instructions, hooks, extensions }) {
+  const toMap = (items, urlPrefix) => {
+    const map = new Map();
+    for (const item of items || []) {
+      if (!item?.id) continue;
+      map.set(item.id, {
+        title: item.title || item.id,
+        url: `/${urlPrefix}/${item.id}/`,
+      });
+    }
+    return map;
+  };
+  const extensionMap = new Map();
+  for (const item of extensions || []) {
+    if (!item?.id) continue;
+
+    const entry = {
+      title: item.name || item.title || item.id,
+      url: `/extension/${item.id}/`,
+    };
+    const keys = [
+      item.id,
+      item.extensionId,
+      item.path ? pluginItemCandidateId(item.path) : null,
+    ].filter(Boolean);
+
+    for (const key of keys) {
+      if (!extensionMap.has(key)) {
+        extensionMap.set(key, entry);
+      }
+    }
+  }
+
+  return {
+    agent: toMap(agents, "agent"),
+    skill: toMap(skills, "skill"),
+    instruction: toMap(instructions, "instruction"),
+    hook: toMap(hooks, "hook"),
+    extension: extensionMap,
+  };
+}
+
+/**
+ * Derive the candidate resource id for a plugin item path (basename without a
+ * known resource extension), e.g. "./skills/foo/" -> "foo",
+ * "plugins/x/agents/bar.md" -> "bar".
+ */
+function pluginItemCandidateId(itemPath) {
+  const trimmed = String(itemPath || "")
+    .replace(/^\.\/+/, "")
+    .replace(/\/+$/, "");
+  const base = trimmed.split("/").pop() || "";
+  return base
+    .replace(/\.agent\.md$/i, "")
+    .replace(/\.prompt\.md$/i, "")
+    .replace(/\.instructions\.md$/i, "")
+    .replace(/\.md$/i, "");
+}
+
+/**
+ * Enrich a plugin item ({ kind, path }) with a display title and, when the item
+ * resolves to a known resource with a detail page, a detailUrl.
+ */
+function resolvePluginItem(item, resourceIndex) {
+  const candidateId = pluginItemCandidateId(item.path);
+  const lookup = resourceIndex?.[item.kind];
+  const match = lookup?.get(candidateId);
+
+  return {
+    ...item,
+    title: match?.title || candidateId || item.path,
+    detailUrl: match?.url || null,
+  };
+}
+
 /**
  * Generate plugins metadata
  */
-function generatePluginsData(gitDates) {
+function generatePluginsData(gitDates, resourceIndex = {}) {
   const plugins = [];
   const extensionEntriesByName = new Map();
 
@@ -594,16 +679,25 @@ function generatePluginsData(gitDates) {
           ? [...new Set(extensionPlugin.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
           : [];
         const relPath = `extensions/${extensionDirName}`;
-        const extensionItem = {
-          kind: "extension",
-          path: relPath,
-        };
+        const extensionItem = resolvePluginItem(
+          {
+            kind: "extension",
+            path: relPath,
+          },
+          resourceIndex
+        );
+        const extReadmePath = path.join(extensionDir, "README.md");
+        const extReadmeFile = fs.existsSync(extReadmePath)
+          ? `${relPath}/README.md`
+          : null;
 
         extensionEntriesByName.set(pluginName, {
           id: pluginName,
           name: pluginName,
           description: pluginDescription,
           path: relPath,
+          readmeFile: extReadmeFile,
+          version: normalizeText(extensionPlugin.version, null),
           tags: extensionKeywords,
           itemCount: 1,
           items: [extensionItem],
@@ -661,16 +755,23 @@ function generatePluginsData(gitDates) {
         ...(data.commands || []).map((p) => ({ kind: "prompt", path: p })),
         ...(data.skills || []).map((p) => ({ kind: "skill", path: p })),
         ...extensionItems,
-      ];
+      ].map((item) => resolvePluginItem(item, resourceIndex));
 
       const tags = data.keywords || data.tags || [];
       const pluginName = data.name || dir.name;
 
+      const readmePath = path.join(pluginDir, "README.md");
+      const readmeFile = fs.existsSync(readmePath)
+        ? `${relPath}/README.md`
+        : null;
+
       plugins.push({
         id: dir.name,
         name: pluginName,
         description: data.description || "",
         path: relPath,
+        readmeFile,
+        version: normalizeText(data.version, null),
         tags: tags,
         itemCount: items.length,
         items: items,
@@ -720,6 +821,7 @@ function generatePluginsData(gitDates) {
             name: ext.name,
             description: ext.description || "",
             path: `plugins/${ext.name}`,
+            version: normalizeText(ext.version, null),
             tags: tags,
             itemCount: 0,
             items: [],
@@ -1196,6 +1298,9 @@ function generateCanvasManifest(gitDates, commitSha) {
     );
     const extensionName = normalizeText(pluginJson.name, normalizeText(packageJson.name, dir.name));
     const extensionVersion = normalizeText(pluginJson.version, normalizeText(packageJson.version, "1.0.0"));
+    const readmeFile = fs.existsSync(path.join(extensionDir, "README.md"))
+      ? `${relPath}/README.md`
+      : null;
     const screenshots = resolveExtensionScreenshots(pluginJson, extensionDir, relPath, commitSha);
     const canvasFiles = getExtensionCanvasFiles(extensionDir);
     const canvases = [];
@@ -1224,6 +1329,7 @@ function generateCanvasManifest(gitDates, commitSha) {
         pluginName: extensionName,
         name: canvasName,
         version: extensionVersion,
+        readmeFile,
         description: canvasDescription,
         path: relPath,
         ref: commitSha,
@@ -1299,6 +1405,7 @@ function generateCanvasManifest(gitDates, commitSha) {
             pluginName: null,
             name,
             version: normalizeText(ext?.version, "1.0.0"),
+            readmeFile: null,
             description: normalizeText(ext?.description, "External canvas extension"),
             path: null,
             ref: null,
@@ -1686,12 +1793,6 @@ async function main() {
   const skills = skillsData.items;
   console.log(`✓ Generated ${skills.length} skills`);
 
-  const pluginsData = generatePluginsData(gitDates);
-  const plugins = pluginsData.items;
-  console.log(
-    `✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
-  );
-
   const extensionManifestData = generateCanvasManifest(gitDates, commitSha);
   const extensionsData = generateExtensionsData(extensionManifestData);
   const extensions = extensionsData.items;
@@ -1699,6 +1800,19 @@ async function main() {
     `✓ Generated ${extensions.length} extensions (${extensionsData.filters.keywords.length} keywords)`
   );
 
+  const resourceIndex = buildResourceIndex({
+    agents,
+    skills,
+    instructions,
+    hooks,
+    extensions,
+  });
+  const pluginsData = generatePluginsData(gitDates, resourceIndex);
+  const plugins = pluginsData.items;
+  console.log(
+    `✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
+  );
+
   const toolsData = generateToolsData();
   const tools = toolsData.items;
   console.log(
diff --git a/website/package-lock.json b/website/package-lock.json
index 57f5bd13..2d9917c1 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -15,6 +15,7 @@
         "choices.js": "^11.2.2",
         "front-matter": "^4.0.2",
         "gray-matter": "^4.0.3",
+        "isomorphic-dompurify": "^2.36.0",
         "jszip": "^3.10.1",
         "marked": "^18.0.2",
         "shiki": "^4.0.2"
@@ -24,6 +25,56 @@
         "playwright": "^1.49.0"
       }
     },
+    "node_modules/@acemir/cssom": {
+      "version": "0.9.31",
+      "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+      "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+      "license": "MIT"
+    },
+    "node_modules/@asamuzakjp/css-color": {
+      "version": "5.1.11",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+      "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/generational-cache": "^1.0.1",
+        "@csstools/css-calc": "^3.2.0",
+        "@csstools/css-color-parser": "^4.1.0",
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/dom-selector": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+      "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/nwsapi": "^2.3.9",
+        "bidi-js": "^1.0.3",
+        "css-tree": "^3.1.0",
+        "is-potential-custom-element-name": "^1.0.1",
+        "lru-cache": "^11.2.6"
+      }
+    },
+    "node_modules/@asamuzakjp/generational-cache": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+      "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/nwsapi": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+      "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+      "license": "MIT"
+    },
     "node_modules/@astrojs/compiler-binding": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.2.3.tgz",
@@ -415,6 +466,18 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@bramus/specificity": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+      "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+      "license": "MIT",
+      "dependencies": {
+        "css-tree": "^3.0.0"
+      },
+      "bin": {
+        "specificity": "bin/cli.js"
+      }
+    },
     "node_modules/@bruits/satteri-darwin-arm64": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.4.tgz",
@@ -568,6 +631,140 @@
         "node": ">= 20.12.0"
       }
     },
+    "node_modules/@csstools/color-helpers": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
+      "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@csstools/css-calc": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+      "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "4.1.9",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz",
+      "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^6.1.0",
+        "@csstools/css-calc": "^3.2.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+      "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-syntax-patches-for-csstree": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz",
+      "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "peerDependencies": {
+        "css-tree": "^3.2.1"
+      },
+      "peerDependenciesMeta": {
+        "css-tree": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+      "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
     "node_modules/@ctrl/tinycolor": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
@@ -1024,6 +1221,23 @@
         "node": ">=18"
       }
     },
+    "node_modules/@exodus/bytes": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+      "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@noble/hashes": "^1.8.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@noble/hashes": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@expressive-code/core": {
       "version": "0.44.0",
       "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.0.tgz",
@@ -2181,6 +2395,13 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/@types/unist": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2214,6 +2435,15 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/am-i-vibing": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz",
@@ -2445,6 +2675,15 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/bidi-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+      "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+      "license": "MIT",
+      "dependencies": {
+        "require-from-string": "^2.0.2"
+      }
+    },
     "node_modules/boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2723,6 +2962,34 @@
       "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
       "license": "CC0-1.0"
     },
+    "node_modules/cssstyle": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz",
+      "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==",
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/css-color": "^5.0.1",
+        "@csstools/css-syntax-patches-for-csstree": "^1.0.28",
+        "css-tree": "^3.1.0",
+        "lru-cache": "^11.2.6"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/data-urls": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+      "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/debug": {
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2740,6 +3007,12 @@
         }
       }
     },
+    "node_modules/decimal.js": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+      "license": "MIT"
+    },
     "node_modules/decode-named-character-reference": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
@@ -2877,6 +3150,15 @@
         "url": "https://github.com/fb55/domhandler?sponsor=1"
       }
     },
+    "node_modules/dompurify": {
+      "version": "3.4.11",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
+      "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+      "license": "(MPL-2.0 OR Apache-2.0)",
+      "optionalDependencies": {
+        "@types/trusted-types": "^2.0.7"
+      }
+    },
     "node_modules/domutils": {
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -3712,6 +3994,18 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/html-encoding-sniffer": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+      "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.6.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/html-escaper": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -3744,6 +4038,32 @@
       "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
       "license": "BSD-2-Clause"
     },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/i18next": {
       "version": "26.3.3",
       "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.3.tgz",
@@ -3912,6 +4232,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "license": "MIT"
+    },
     "node_modules/is-wsl": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
@@ -3933,6 +4259,19 @@
       "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
       "license": "MIT"
     },
+    "node_modules/isomorphic-dompurify": {
+      "version": "2.36.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.36.0.tgz",
+      "integrity": "sha512-E8YkGyPY3a/U5s0WOoc8Ok+3SWL/33yn2IHCoxCFLBUUPVy9WGa++akJZFxQCcJIhI+UvYhbrbnTIFQkHKZbgA==",
+      "license": "MIT",
+      "dependencies": {
+        "dompurify": "^3.3.1",
+        "jsdom": "^28.0.0"
+      },
+      "engines": {
+        "node": ">=20.19.5"
+      }
+    },
     "node_modules/js-yaml": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
@@ -3955,6 +4294,70 @@
         "js-yaml": "bin/js-yaml.js"
       }
     },
+    "node_modules/jsdom": {
+      "version": "28.1.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
+      "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
+      "license": "MIT",
+      "dependencies": {
+        "@acemir/cssom": "^0.9.31",
+        "@asamuzakjp/dom-selector": "^6.8.1",
+        "@bramus/specificity": "^2.4.2",
+        "@exodus/bytes": "^1.11.0",
+        "cssstyle": "^6.0.1",
+        "data-urls": "^7.0.0",
+        "decimal.js": "^10.6.0",
+        "html-encoding-sniffer": "^6.0.0",
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.6",
+        "is-potential-custom-element-name": "^1.0.1",
+        "parse5": "^8.0.0",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^6.0.0",
+        "undici": "^7.21.0",
+        "w3c-xmlserializer": "^5.0.0",
+        "webidl-conversions": "^8.0.1",
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.0",
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "canvas": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jsdom/node_modules/entities": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+      "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/jsdom/node_modules/parse5": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+      "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^8.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
     "node_modules/jsonc-parser": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
@@ -5821,6 +6224,15 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/radix3": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
@@ -6156,6 +6568,15 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/resolve-pkg-maps": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -6296,6 +6717,18 @@
         "node": ">=11.0.0"
       }
     },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
     "node_modules/section-matter": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -6543,6 +6976,12 @@
         "url": "https://opencollective.com/svgo"
       }
     },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "license": "MIT"
+    },
     "node_modules/tiny-inflate": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -6583,6 +7022,48 @@
         "url": "https://github.com/sponsors/SuperchupuDev"
       }
     },
+    "node_modules/tldts": {
+      "version": "7.4.7",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.7.tgz",
+      "integrity": "sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==",
+      "license": "MIT",
+      "dependencies": {
+        "tldts-core": "^7.4.7"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "7.4.7",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.7.tgz",
+      "integrity": "sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==",
+      "license": "MIT"
+    },
+    "node_modules/tough-cookie": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+      "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tldts": "^7.0.5"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+      "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
     "node_modules/trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -6628,6 +7109,15 @@
       "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
       "license": "MIT"
     },
+    "node_modules/undici": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+      "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
     "node_modules/undici-types": {
       "version": "7.16.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -7052,6 +7542,18 @@
         }
       }
     },
+    "node_modules/w3c-xmlserializer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/web-namespaces": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@@ -7062,6 +7564,38 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+      "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+      "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+      "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.11.0",
+        "tr46": "^6.0.0",
+        "webidl-conversions": "^8.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/which-pm-runs": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz",
@@ -7071,6 +7605,21 @@
         "node": ">=4"
       }
     },
+    "node_modules/xml-name-validator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "license": "MIT"
+    },
     "node_modules/xxhash-wasm": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
diff --git a/website/package.json b/website/package.json
index a9e3aa9a..d1c0f52e 100644
--- a/website/package.json
+++ b/website/package.json
@@ -25,6 +25,7 @@
     "choices.js": "^11.2.2",
     "front-matter": "^4.0.2",
     "gray-matter": "^4.0.3",
+    "isomorphic-dompurify": "^2.36.0",
     "jszip": "^3.10.1",
     "marked": "^18.0.2",
     "shiki": "^4.0.2"
diff --git a/website/scripts/a11y-audit.mjs b/website/scripts/a11y-audit.mjs
index e37dbcaf..ac130494 100644
--- a/website/scripts/a11y-audit.mjs
+++ b/website/scripts/a11y-audit.mjs
@@ -36,6 +36,15 @@ const routes = [
   '/contributors/',
   '/learning-hub/cookbook/',
   '/learning-hub/github-copilot-app/',
+  // Representative dedicated detail pages (one per resource type) so the audit
+  // covers the shared detail layout, sidebar, install buttons, and file browser.
+  '/agent/accessibility/',
+  '/instruction/a11y/',
+  '/skill/acquire-codebase-knowledge/',
+  '/hook/dependency-license-checker/',
+  '/workflow/daily-issues-report/',
+  '/plugin/accessibility-kanban/',
+  '/extension/accessibility-kanban/',
 ];
 
 const themes = ['dark', 'light'];
diff --git a/website/src/components/pages/Breadcrumb.astro b/website/src/components/pages/Breadcrumb.astro
new file mode 100644
index 00000000..fc399ecf
--- /dev/null
+++ b/website/src/components/pages/Breadcrumb.astro
@@ -0,0 +1,25 @@
+---
+export type Breadcrumb = {
+  paths: { name: string; href?: string }[];
+  title: string;
+};
+
+const { paths, title } = Astro.props as Breadcrumb;
+---
+
+
diff --git a/website/src/components/pages/FileBrowser.astro b/website/src/components/pages/FileBrowser.astro
new file mode 100644
index 00000000..ebc45eb1
--- /dev/null
+++ b/website/src/components/pages/FileBrowser.astro
@@ -0,0 +1,145 @@
+---
+import { getFileMeta } from "../../lib/file-types";
+
+export interface BrowserFile {
+  name: string;
+  path: string;
+  size?: number;
+}
+
+export interface FileBrowserProps {
+  files: BrowserFile[];
+  /** Full repo path of the primary file, e.g. skills/foo/SKILL.md or hooks/foo/README.md */
+  primaryPath: string;
+  /** Display name of the primary file, e.g. SKILL.md or README.md */
+  primaryName: string;
+  /** Pre-rendered HTML for the primary file (shown by default) */
+  primaryHtml: string;
+  /** GitHub blob base, e.g. https://github.com/github/awesome-copilot/blob/main */
+  githubBase: string;
+}
+
+const { files, primaryPath, primaryName, primaryHtml, githubBase } =
+  Astro.props as FileBrowserProps;
+
+// Primary file first, then everything else alphabetically by name.
+const sortedFiles = [...files].sort((a, b) => {
+  if (a.path === primaryPath) return -1;
+  if (b.path === primaryPath) return 1;
+  return a.name.localeCompare(b.name);
+});
+
+const otherFiles = sortedFiles.filter((f) => f.path !== primaryPath);
+const rootFiles = otherFiles.filter((f) => !f.name.includes("/"));
+
+// Group nested files by their top-level folder for optgroup rendering.
+const folderGroups = new Map();
+for (const file of otherFiles) {
+  const slash = file.name.indexOf("/");
+  if (slash === -1) continue;
+  const folder = file.name.slice(0, slash);
+  const bucket = folderGroups.get(folder) ?? [];
+  bucket.push(file);
+  folderGroups.set(folder, bucket);
+}
+const sortedFolders = [...folderGroups.keys()].sort((a, b) =>
+  a.localeCompare(b)
+);
+
+const hasMultipleFiles = sortedFiles.length > 1;
+
+function optionAttrs(file: BrowserFile) {
+  const meta = getFileMeta(file.name);
+  return {
+    value: file.path,
+    "data-file-name": file.name,
+    "data-file-lang": meta.lang,
+    "data-file-kind": meta.kind,
+  };
+}
+---
+
+
+
+
+ { + hasMultipleFiles ? ( +
+ + +
+ ) : ( + + {primaryName} + + ) + } +
+ + GitHub +
+
+ +
+ +
+
diff --git a/website/src/components/pages/Header.astro b/website/src/components/pages/Header.astro new file mode 100644 index 00000000..7aab026b --- /dev/null +++ b/website/src/components/pages/Header.astro @@ -0,0 +1,13 @@ +--- +export type HeaderItem = { + title: string; + description?: string; +}; + +const { item } = Astro.props as { item: HeaderItem }; +--- + +
+

{item.title}

+ {item.description &&

{item.description}

} +
diff --git a/website/src/components/pages/IncludedItems.astro b/website/src/components/pages/IncludedItems.astro new file mode 100644 index 00000000..118d95f1 --- /dev/null +++ b/website/src/components/pages/IncludedItems.astro @@ -0,0 +1,93 @@ +--- +export interface PluginIncludedItem { + kind: string; + path: string; + title?: string; + detailUrl?: string | null; +} + +export interface IncludedItemsProps { + items: PluginIncludedItem[]; + pluginPath: string; + githubBase: string; +} + +const { items, pluginPath, githubBase } = Astro.props as IncludedItemsProps; + +const KIND_LABELS: Record = { + agent: "Agents", + skill: "Skills", + instruction: "Instructions", + hook: "Hooks", + prompt: "Commands", + extension: "Extensions", +}; + +const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension"]; + +function githubHref(itemPath: string): string { + const normalized = itemPath.replace(/^\.\/+/, "").replace(/\/+$/, ""); + // Repo-root-relative paths (e.g. extensions/foo, plugins/foo) are used as-is; + // plugin-relative paths are resolved against the plugin folder. + const isRepoRelative = /^[a-z0-9._-]+\//i.test(normalized) && + (normalized.startsWith("extensions/") || + normalized.startsWith("plugins/") || + normalized.startsWith("agents/") || + normalized.startsWith("skills/") || + normalized.startsWith("instructions/") || + normalized.startsWith("hooks/")); + const repoPath = isRepoRelative ? normalized : `${pluginPath}/${normalized}`; + // GitHub uses /blob/ for files and /tree/ for directories; /tree// + // returns 404. Detect files by a trailing extension on the last path segment. + const lastSegment = repoPath.split("/").pop() ?? ""; + const isFile = /\.[a-z0-9]+$/i.test(lastSegment); + const base = isFile ? githubBase.replace("/tree/", "/blob/") : githubBase; + return `${base}/${repoPath}`; +} + +const grouped = KIND_ORDER.map((kind) => ({ + kind, + label: KIND_LABELS[kind] ?? kind, + entries: (items ?? []).filter((item) => item.kind === kind), +})).filter((group) => group.entries.length > 0); +--- + +{ + grouped.length > 0 && ( +
+

Included items

+

+ This plugin bundles the following resources. Installing the plugin brings + them all in together. +

+ {grouped.map((group) => ( +
+

{group.label}

+ +
+ ))} +
+ ) +} diff --git a/website/src/components/pages/InstallButtons.astro b/website/src/components/pages/InstallButtons.astro new file mode 100644 index 00000000..d9295534 --- /dev/null +++ b/website/src/components/pages/InstallButtons.astro @@ -0,0 +1,61 @@ +--- +const { vscodeUrl, insidersUrl, rawMarkdown } = Astro.props as { vscodeUrl: string; insidersUrl: string; rawMarkdown?: string }; +--- + + diff --git a/website/src/components/pages/Main.astro b/website/src/components/pages/Main.astro new file mode 100644 index 00000000..6bc7edfd --- /dev/null +++ b/website/src/components/pages/Main.astro @@ -0,0 +1,17 @@ +--- +const githubUrl = Astro.props.githubUrl as string; +const markdownHtml = Astro.props.markdownHtml as string; +--- + +
+ {markdownHtml ? ( +
+

Documentation

+
+
+ ) : ( +
+

Documentation preview is unavailable. View the file on GitHub.

+
+ )} +
diff --git a/website/src/components/pages/PluginInstall.astro b/website/src/components/pages/PluginInstall.astro new file mode 100644 index 00000000..12e4598c --- /dev/null +++ b/website/src/components/pages/PluginInstall.astro @@ -0,0 +1,171 @@ +--- +/** + * Install actions for plugins and canvas extensions. + * + * Renders a split-button dropdown that deep-links into VS Code, VS Code + * Insiders, and the GitHub Copilot app, plus an optional CLI command block. + * The dropdown open/close is wired by resource-detail.ts via `data-install-menu`. + */ +export interface PluginInstallProps { + isExternal?: boolean; + /** awesome-copilot plugin id (internal installs). */ + pluginId?: string | null; + /** Upstream marketplace source (owner/repo or git URL) for external installs. */ + externalSource?: string | null; + /** CLI install command (internal only). */ + installCommand?: string | null; + /** Heading shown above the buttons. */ + label?: string; + /** Note shown for external installs. */ + note?: string; +} + +const { + isExternal = false, + pluginId = null, + externalSource = null, + installCommand = null, + label, + note, +} = Astro.props as PluginInstallProps; + +const MARKETPLACE = "awesome-copilot"; + +let vscodeUrl: string | null = null; +let insidersUrl: string | null = null; +let ghappUrl: string | null = null; + +if (isExternal) { + if (externalSource) { + vscodeUrl = `vscode://chat-plugin/install?source=${encodeURIComponent( + externalSource + )}`; + insidersUrl = `vscode-insiders://chat-plugin/install?source=${encodeURIComponent( + externalSource + )}`; + ghappUrl = `ghapp://plugins/marketplace/add?source=${encodeURIComponent( + externalSource + )}`; + } +} else if (pluginId) { + const plugin = encodeURIComponent(pluginId); + vscodeUrl = `vscode://chat-plugin/install?source=github/awesome-copilot&plugin=${plugin}`; + insidersUrl = `vscode-insiders://chat-plugin/install?source=github/awesome-copilot&plugin=${plugin}`; + ghappUrl = `ghapp://plugins/install?source=${encodeURIComponent( + `${pluginId}@${MARKETPLACE}` + )}`; +} + +const ghappLabel = isExternal + ? "Open in GitHub Copilot app" + : "Install in GitHub Copilot app"; + +const primaryUrl = ghappUrl ?? vscodeUrl ?? insidersUrl; +const primaryLabel = + primaryUrl === ghappUrl + ? ghappLabel + : primaryUrl === vscodeUrl + ? "Install in VS Code" + : "Install in VS Code Insiders"; +const primaryIsGhapp = primaryUrl === ghappUrl; +const heading = label ?? (isExternal ? "Install this plugin" : "Install"); +--- + +
+

{heading}

+ {note &&

{note}

} + + { + primaryUrl && ( +
+ + + {primaryLabel} + + + + ) + } + + { + installCommand && ( + <> +

Or install with the CLI

+
+ {installCommand} + +
+ + ) + } +
diff --git a/website/src/components/pages/RawMarkdown.astro b/website/src/components/pages/RawMarkdown.astro new file mode 100644 index 00000000..87c55d0d --- /dev/null +++ b/website/src/components/pages/RawMarkdown.astro @@ -0,0 +1,9 @@ +--- +const { markdown } = Astro.props as { markdown?: string }; +--- + +{ + markdown && ( + + ) +} diff --git a/website/src/components/pages/Sidebar.astro b/website/src/components/pages/Sidebar.astro new file mode 100644 index 00000000..362a6031 --- /dev/null +++ b/website/src/components/pages/Sidebar.astro @@ -0,0 +1,104 @@ +--- +import type { SidebarChipsProps } from "./SidebarChips.astro"; +import SidebarChips from "./SidebarChips.astro"; + +export interface SidebarProps { + item: any; + lastUpdated?: string; + frontmatterText?: string; + githubUrl: string; + chips?: SidebarChipsProps[]; +} +const { + item, + lastUpdated, + frontmatterText, + githubUrl, + chips, +} = Astro.props as SidebarProps; +--- + + diff --git a/website/src/components/pages/SidebarChips.astro b/website/src/components/pages/SidebarChips.astro new file mode 100644 index 00000000..6c3811fd --- /dev/null +++ b/website/src/components/pages/SidebarChips.astro @@ -0,0 +1,51 @@ +--- +export type SidebarChipsProps = { + title: string; + items: string[]; + /** + * When both `filterBase` and `filterParam` are provided, each chip is + * rendered as a link to the resource's list page pre-filtered by that value + * (e.g. `/hooks/?tag=testing`), turning read-only metadata into navigation. + */ + filterBase?: string; + filterParam?: string; +}; + +const { title, items, filterBase, filterParam } = + Astro.props as SidebarChipsProps; +const slug = title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + +const linkable = Boolean(filterBase && filterParam); +const chipHref = (value: string) => + `${filterBase}?${filterParam}=${encodeURIComponent(value)}`; +--- + +
+
{title}
+
+ { + items.length ? ( +
+ {items.map((item) => + linkable ? ( + + {item} + + ) : ( + {item} + ) + )} +
+ ) : ( + Not specified + ) + } +
+
diff --git a/website/src/integrations/pagefind-resources.ts b/website/src/integrations/pagefind-resources.ts index 966f4bfd..f68e2e75 100644 --- a/website/src/integrations/pagefind-resources.ts +++ b/website/src/integrations/pagefind-resources.ts @@ -41,6 +41,19 @@ const TYPE_PAGES: Record = { tool: "/tools/", }; +// Resource types that have a dedicated detail page at ///. Search +// results for these should deep-link to the canonical detail page rather than +// the listing page with an inert #file= hash (listing pages no longer open a +// modal from that hash). +const DETAIL_ROUTE_TYPES = new Set([ + "agent", + "instruction", + "skill", + "hook", + "workflow", + "plugin", +]); + export default function pagefindResources(): AstroIntegration { let siteBase = "/"; @@ -97,12 +110,17 @@ export default function pagefindResources(): AstroIntegration { let added = 0; for (const record of records) { + const hasDetailPage = + DETAIL_ROUTE_TYPES.has(record.type) && Boolean(record.id); const typePage = TYPE_PAGES[record.type]; - if (!typePage) continue; + // Skip records we can neither deep-link nor point at a listing page. + if (!hasDetailPage && !typePage) continue; - const url = `${base}${typePage.slice(1)}#file=${encodeURIComponent( - record.path - )}`; + const url = hasDetailPage + ? `${base}${record.type}/${encodeURIComponent(record.id)}/` + : `${base}${typePage.slice(1)}#file=${encodeURIComponent( + record.path + )}`; const typeLabel = TYPE_LABELS[record.type] || record.type; const addResult = await index.addCustomRecord({ diff --git a/website/src/lib/detail-page.ts b/website/src/lib/detail-page.ts new file mode 100644 index 00000000..9932860d --- /dev/null +++ b/website/src/lib/detail-page.ts @@ -0,0 +1,125 @@ +import fs from "node:fs"; +import path from "node:path"; +import { marked } from "marked"; +import matter from "gray-matter"; +import { enhanceMarkdownA11y } from "./markdown-a11y"; +import { sanitizeHtml } from "./sanitize-html"; + +/** + * Build-time helpers shared by the resource detail pages + * (src/pages/agent/[id].astro, src/pages/instruction/[id].astro, ...). + * + * This module is intentionally free of any DOM/browser dependencies so it can + * run in Astro frontmatter at build time (unlike src/scripts/utils.ts, which is + * client-side). + */ + +const RAW_BASE = + "https://raw.githubusercontent.com/github/awesome-copilot/main"; +const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main"; + +// Per resource-type VS Code install configuration. +const INSTALL_CONFIG = { + agent: { + command: "chat-agent", + baseUrl: "https://aka.ms/awesome-copilot/install/agent", + }, + instruction: { + command: "chat-instructions", + baseUrl: "https://aka.ms/awesome-copilot/install/instructions", + }, +} as const satisfies Record; + +export type ResourceType = keyof typeof INSTALL_CONFIG; + +export interface DetailPageInput { + path: string; + lastUpdated?: string | null; +} + +export interface DetailPageData { + vscodeUrl: string; + insidersUrl: string; + githubUrl: string; + markdownHtml: string; + frontmatterText: string; + rawMarkdown: string; + lastUpdated: string | null; +} + +// Repo root. Astro runs (both `dev` and `build`) execute with the website/ +// directory as the working directory, so the repo root is its parent. This is +// resolved from the working directory rather than `import.meta.url` because the +// latter is unreliable once this module is bundled during a production build. +const repoRoot = path.resolve(process.cwd(), ".."); + +function buildInstallUrl( + filePath: string, + type: ResourceType, + insiders: boolean +): string { + const { command, baseUrl } = INSTALL_CONFIG[type]; + const rawUrl = `${RAW_BASE}/${filePath}`; + const editor = insiders ? "vscode-insiders" : "vscode"; + const innerUrl = `${editor}:${command}/install?url=${encodeURIComponent(rawUrl)}`; + return `${baseUrl}?url=${encodeURIComponent(innerUrl)}`; +} + +export function formatLastUpdated( + lastUpdated?: string | null +): string | null { + return lastUpdated + ? new Date(lastUpdated).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + }) + : null; +} + +/** + * Read a resource's markdown file at build time and return its rendered HTML, + * trimmed frontmatter block, and the raw file contents. + */ +export function readResourceMarkdown(filePath: string): { + markdownHtml: string; + frontmatterText: string; + rawMarkdown: string; +} { + let markdownHtml = ""; + let frontmatterText = ""; + let rawMarkdown = ""; + try { + const raw = fs.readFileSync(path.join(repoRoot, filePath), "utf-8"); + rawMarkdown = raw; + const parsed = matter(raw); + frontmatterText = parsed.matter?.trim() ?? ""; + markdownHtml = enhanceMarkdownA11y( + sanitizeHtml(marked.parse(parsed.content, { async: false }) as string) + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn( + `[detail-page] Failed to read or parse markdown for "${filePath}": ${message}` + ); + markdownHtml = ""; + } + return { markdownHtml, frontmatterText, rawMarkdown }; +} + +/** + * Build every piece of shared data a resource detail page needs: install URLs, + * the GitHub source URL, rendered/raw markdown, and the formatted update date. + */ +export function loadDetailPage( + item: DetailPageInput, + type: ResourceType +): DetailPageData { + return { + vscodeUrl: buildInstallUrl(item.path, type, false), + insidersUrl: buildInstallUrl(item.path, type, true), + githubUrl: `${GITHUB_BASE}/${item.path}`, + ...readResourceMarkdown(item.path), + lastUpdated: formatLastUpdated(item.lastUpdated), + }; +} diff --git a/website/src/lib/external-source.ts b/website/src/lib/external-source.ts new file mode 100644 index 00000000..f6420567 --- /dev/null +++ b/website/src/lib/external-source.ts @@ -0,0 +1,86 @@ +/** + * Shared helpers for building safe GitHub links to external (third-party) + * plugin/extension sources. + * + * This module is intentionally dependency-free (no DOM or node imports) so it + * can run both at build time (in Astro frontmatter, e.g. plugin/[id].astro and + * extension/[id].astro) and on the client (plugins-render.ts, modal.ts). + */ + +export interface ExternalSource { + source?: string; + repo?: string; + path?: string; + ref?: string; + sha?: string; +} + +const GITHUB_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; +// Tags, branch names, or a 40-char commit SHA. Deliberately excludes +// whitespace, "..", and characters that could break out of the path segment. +const GITHUB_REF_RE = /^(?!\/)(?!.*\/\/)(?!.*\.\.)(?!.*\/$)[A-Za-z0-9._/-]+$/; + +/** + * Allow only http(s) URLs; return "#" for anything else (mirrors the + * client-side sanitizeUrl in scripts/utils.ts). Prevents javascript:/data: + * schemes from reaching an href/src attribute. + */ +export function sanitizeHttpUrl(url: string | null | undefined): string { + if (!url) return "#"; + try { + const parsed = new URL(url); + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + return url; + } + } catch { + // Invalid URL + } + return "#"; +} + +function encodeRepoPath(rawPath: string): string { + return rawPath + .replace(/\\/g, "/") + .split("/") + .filter((segment) => segment !== "") + .map((segment) => encodeURIComponent(segment)) + .join("/"); +} + +/** + * Build a GitHub URL for an external plugin/extension source, pinned to the + * most immutable revision available (sha, then ref, then the default branch). + * Path segments are URL-encoded and a leading "/" (or a bare "/" path) is + * treated as "no path". + * + * When the source is not a valid GitHub repo reference, falls back to the first + * safe http(s) URL in `fallbackUrls`, or "#" if none is provided. + */ +export function externalRepoUrl( + source: ExternalSource | null | undefined, + fallbackUrls: Array = [] +): string { + if ( + source?.source === "github" && + typeof source.repo === "string" && + GITHUB_REPO_RE.test(source.repo) + ) { + const base = `https://github.com/${source.repo}`; + const candidateRef = source.sha || source.ref; + const ref = + candidateRef && GITHUB_REF_RE.test(candidateRef) ? candidateRef : ""; + const path = + source.path && source.path !== "/" ? encodeRepoPath(source.path) : ""; + + if (path) { + return `${base}/tree/${ref || "main"}/${path}`; + } + return ref ? `${base}/tree/${ref}` : base; + } + + for (const url of fallbackUrls) { + const safe = sanitizeHttpUrl(url); + if (safe !== "#") return safe; + } + return "#"; +} diff --git a/website/src/lib/file-types.ts b/website/src/lib/file-types.ts new file mode 100644 index 00000000..d17288e8 --- /dev/null +++ b/website/src/lib/file-types.ts @@ -0,0 +1,108 @@ +/** + * Map a file name to a Shiki language id and a coarse "kind" used by the file + * browser to decide how to render it (markdown prose, highlighted code, image, + * or a plain fallback). Shared between the build-time FileBrowser component + * and the client-side file-browser script, so both agree on languages. + */ + +export type FileKind = "markdown" | "code" | "image" | "other"; + +const EXT_LANG: Record = { + ts: "typescript", + tsx: "tsx", + js: "javascript", + jsx: "jsx", + mjs: "javascript", + cjs: "javascript", + py: "python", + rb: "ruby", + go: "go", + rs: "rust", + java: "java", + cs: "csharp", + c: "c", + cpp: "cpp", + h: "c", + php: "php", + swift: "swift", + kt: "kotlin", + sh: "bash", + bash: "bash", + zsh: "bash", + ps1: "powershell", + json: "json", + jsonc: "json", + yml: "yaml", + yaml: "yaml", + toml: "toml", + html: "html", + css: "css", + scss: "scss", + xml: "xml", + sql: "sql", + dockerfile: "docker", + md: "markdown", + markdown: "markdown", + txt: "text", +}; + +const CODE_LANGS = new Set([ + "typescript", + "tsx", + "javascript", + "jsx", + "python", + "ruby", + "go", + "rust", + "java", + "csharp", + "c", + "cpp", + "php", + "swift", + "kotlin", + "bash", + "powershell", + "json", + "yaml", + "toml", + "html", + "css", + "scss", + "xml", + "sql", + "docker", +]); + +const IMAGE_EXTS = new Set([ + "png", + "jpg", + "jpeg", + "gif", + "svg", + "webp", + "avif", + "bmp", + "ico", +]); + +export interface FileMeta { + ext: string; + lang: string; + kind: FileKind; +} + +export function getFileMeta(name: string): FileMeta { + const base = name.split("/").pop() ?? name; + const ext = + base.toLowerCase() === "dockerfile" + ? "dockerfile" + : (base.split(".").pop() ?? "").toLowerCase(); + const lang = EXT_LANG[ext] ?? "text"; + let kind: FileKind = "other"; + if (IMAGE_EXTS.has(ext)) kind = "image"; + else if (ext === "md" || ext === "markdown") kind = "markdown"; + else if (CODE_LANGS.has(lang)) kind = "code"; + return { ext, lang, kind }; +} diff --git a/website/src/lib/markdown-a11y.ts b/website/src/lib/markdown-a11y.ts new file mode 100644 index 00000000..2d416ad3 --- /dev/null +++ b/website/src/lib/markdown-a11y.ts @@ -0,0 +1,35 @@ +/** + * Accessibility post-processing for marked-generated markdown HTML. + * + * axe flags two issues on rendered markdown that we fix here rather than in the + * source content: + * - `scrollable-region-focusable`:
 and 
blocks can overflow on the + * x-axis, so they must be keyboard focusable to let keyboard users scroll them. + * - `label`: GitHub-style task-list checkboxes (`- [ ]` / `- [x]`) render as bare + * disabled elements with no accessible name. + * + * This module is pure (no DOM or node deps) so it can run both at build time + * (src/lib/detail-page.ts) and on the client (src/scripts/pages/file-browser.ts). + */ +export function enhanceMarkdownA11y(html: string): string { + if (!html) return html; + let out = html; + + // Make scrollable code/table blocks keyboard focusable. + out = out.replace(/]*\btabindex=)/g, '
]*\btabindex=)/g, '
]*\btype="checkbox"[^>]*)>/g, + (match, attrs) => { + if (/\baria-label=/.test(attrs)) return match; + const label = /\bchecked\b/.test(attrs) + ? "Completed task" + : "Incomplete task"; + return ``; + } + ); + + return out; +} diff --git a/website/src/lib/sanitize-html.ts b/website/src/lib/sanitize-html.ts new file mode 100644 index 00000000..7894d078 --- /dev/null +++ b/website/src/lib/sanitize-html.ts @@ -0,0 +1,54 @@ +/** + * Isomorphic HTML sanitizer for rendered markdown. + * + * `marked` allows raw HTML to pass through untouched, and the resulting string + * is injected via `set:html` / `innerHTML` on the resource detail pages and in + * the client-side file browser. Even though the markdown we render originates + * from this repository, a compromised or malicious resource file could + * otherwise introduce persistent XSS. Sanitizing the generated HTML gives us + * defense-in-depth on both the server (build time) and the client. + * + * `isomorphic-dompurify` resolves to a jsdom-backed DOMPurify in Node (so it + * works in Astro frontmatter during `astro build`) and to the native + * browser DOMPurify when bundled for the client. + */ +import DOMPurify from "isomorphic-dompurify"; + +let noopenerHookInstalled = false; + +function ensureNoopenerHook(): void { + if (noopenerHookInstalled) return; + noopenerHookInstalled = true; + + DOMPurify.addHook("afterSanitizeAttributes", (node) => { + const el = node as unknown as { + tagName?: string; + getAttribute?: (name: string) => string | null; + setAttribute?: (name: string, value: string) => void; + }; + + if (el?.tagName !== "A") return; + if (el.getAttribute?.("target") !== "_blank") return; + + const rel = el.getAttribute?.("rel") ?? ""; + const tokens = new Set(rel.split(/\s+/).filter(Boolean)); + tokens.add("noopener"); + tokens.add("noreferrer"); + el.setAttribute?.("rel", Array.from(tokens).join(" ")); + }); +} + +/** + * Sanitize a fragment of HTML produced from trusted-but-untrusted markdown, + * stripping scripts, event handlers, and dangerous URL schemes while keeping + * the formatting tags GitHub-flavored markdown commonly emits. + */ +export function sanitizeHtml(html: string): string { + if (!html) return html; + ensureNoopenerHook(); + return DOMPurify.sanitize(html, { + // Keep links that open in a new tab (target/rel) which some resource docs + // author directly as raw HTML. + ADD_ATTR: ["target", "rel"], + }); +} diff --git a/website/src/pages/agent/[id].astro b/website/src/pages/agent/[id].astro new file mode 100644 index 00000000..173c747c --- /dev/null +++ b/website/src/pages/agent/[id].astro @@ -0,0 +1,116 @@ +--- +import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; +import BackToTop from "../../components/BackToTop.astro"; +import agentsData from "../../../public/data/agents.json"; +import RawMarkdown from "../../components/pages/RawMarkdown.astro"; +import type { HeaderItem } from "../../components/pages/Header.astro"; +import DetailHeader from "../../components/pages/Header.astro"; +import Breadcrumb from "../../components/pages/Breadcrumb.astro"; +import Main from "../../components/pages/Main.astro"; +import InstallButtons from "../../components/pages/InstallButtons.astro"; +import Sidebar from "../../components/pages/Sidebar.astro"; +import { loadDetailPage } from "../../lib/detail-page"; + +interface AgentItem extends HeaderItem { + id: string; + title: string; + description?: string; + model?: string | string[]; + tools?: string[]; + hasHandoffs?: boolean; + handoffs?: string[]; + mcpServers?: string[]; + path: string; + filename?: string; + lastUpdated?: string | null; +} + +export function getStaticPaths() { + return (agentsData.items as AgentItem[]).map((item) => ({ + params: { id: item.id }, + props: { item }, + })); +} + +const { item } = Astro.props as { item: AgentItem }; + +const { + vscodeUrl, + insidersUrl, + githubUrl, + markdownHtml, + frontmatterText, + rawMarkdown, + lastUpdated, +} = loadDetailPage(item, "agent"); + +const models = Array.isArray(item.model) + ? item.model + : item.model + ? [item.model] + : []; +const tools = item.tools ?? []; +const handoffs = item.handoffs ?? []; +const mcpServers = item.mcpServers ?? []; +--- + + +
+ +
+ + + + +
+
+ + + + +
+
+
+ + + + +
diff --git a/website/src/pages/agents.astro b/website/src/pages/agents.astro index a43ae08d..b1e2f6db 100644 --- a/website/src/pages/agents.astro +++ b/website/src/pages/agents.astro @@ -5,7 +5,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import PageHeader from '../components/PageHeader.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import { renderAgentsHtml, sortAgents } from '../scripts/pages/agents-render'; const initialItems = sortAgents(agentsData.items, 'title'); @@ -43,7 +42,6 @@ const initialItems = sortAgents(agentsData.items, 'title'); - + diff --git a/website/src/pages/extensions.astro b/website/src/pages/extensions.astro index 9ebb8d13..a8c454fe 100644 --- a/website/src/pages/extensions.astro +++ b/website/src/pages/extensions.astro @@ -5,7 +5,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import PageHeader from '../components/PageHeader.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import { renderExtensionsHtml, sortExtensions } from '../scripts/pages/extensions-render'; const initialItems = sortExtensions(extensionsData.items, 'title'); @@ -53,7 +52,6 @@ const initialItems = sortExtensions(extensionsData.items, 'title'); - + diff --git a/website/src/pages/hooks.astro b/website/src/pages/hooks.astro index e74e5a26..e21f7d96 100644 --- a/website/src/pages/hooks.astro +++ b/website/src/pages/hooks.astro @@ -4,7 +4,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import PageHeader from '../components/PageHeader.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import hooksData from '../../public/data/hooks.json'; import { renderHooksHtml, sortHooks } from '../scripts/pages/hooks-render'; @@ -48,7 +47,6 @@ const initialItems = sortHooks(hooksData.items, 'title'); - + diff --git a/website/src/pages/instructions.astro b/website/src/pages/instructions.astro index 15dabebf..c398ec39 100644 --- a/website/src/pages/instructions.astro +++ b/website/src/pages/instructions.astro @@ -5,7 +5,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import PageHeader from '../components/PageHeader.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import { renderInstructionsHtml, sortInstructions } from '../scripts/pages/instructions-render'; const initialItems = sortInstructions(instructionsData.items, 'title'); @@ -48,7 +47,6 @@ const initialItems = sortInstructions(instructionsData.items, 'title'); - + diff --git a/website/src/pages/plugins.astro b/website/src/pages/plugins.astro index 89dfe944..60e0a8c4 100644 --- a/website/src/pages/plugins.astro +++ b/website/src/pages/plugins.astro @@ -5,7 +5,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import PageHeader from '../components/PageHeader.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import { renderPluginsHtml, sortPlugins } from '../scripts/pages/plugins-render'; const initialItems = sortPlugins(pluginsData.items, 'title'); @@ -57,7 +56,6 @@ const initialItems = sortPlugins(pluginsData.items, 'title'); - + diff --git a/website/src/pages/skills.astro b/website/src/pages/skills.astro index 644ea0e8..ca79a8b7 100644 --- a/website/src/pages/skills.astro +++ b/website/src/pages/skills.astro @@ -4,7 +4,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import PageHeader from '../components/PageHeader.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import skillsData from '../../public/data/skills.json'; import { renderSkillsHtml, sortSkills } from '../scripts/pages/skills-render'; @@ -43,7 +42,6 @@ const initialItems = sortSkills(skillsData.items, 'title'); - + diff --git a/website/src/pages/workflows.astro b/website/src/pages/workflows.astro index b6547d6f..4f3eba36 100644 --- a/website/src/pages/workflows.astro +++ b/website/src/pages/workflows.astro @@ -5,7 +5,6 @@ import ContributeCTA from '../components/ContributeCTA.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import PageHeader from '../components/PageHeader.astro'; import BackToTop from '../components/BackToTop.astro'; -import Modal from '../components/Modal.astro'; import { renderWorkflowsHtml, sortWorkflows } from '../scripts/pages/workflows-render'; const initialItems = sortWorkflows(workflowsData.items, 'title'); @@ -48,7 +47,6 @@ const initialItems = sortWorkflows(workflowsData.items, 'title'); -