More website tweaks#977
Conversation
Render resource listing pages in Astro for first paint and hydrate client filtering/search behavior on top. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR modernizes the website’s client-side rendering and modal preview experience by embedding page JSON into the HTML, refactoring list rendering into shared render helpers, and adding markdown rendering + syntax highlighting support.
Changes:
- Embed
public/data/*.jsoninto pages (via a newEmbeddedPageDatacomponent) and havefetchData()read embedded JSON before making network requests. - Refactor multiple listing pages (agents/instructions/hooks/skills/workflows/tools/plugins/samples) to use shared
*-render.tshelpers and delegated event handling. - Upgrade the modal to support rendered markdown (front matter stripped) and highlighted code preview, plus add a skills file switcher and lazy-load JSZip for downloads.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/styles/global.css | Updates modal/listing toolbar styling; adjusts font path and adds new modal/file-switcher styles. |
| website/src/scripts/utils.ts | Adds embedded-data support to fetchData() and introduces loadJSZip() lazy loader; tweaks escaping and resource typing. |
| website/src/scripts/pages/workflows.ts | Switches to shared renderer/sorter and delegated click handling. |
| website/src/scripts/pages/workflows-render.ts | New shared HTML renderer + sorter for workflows. |
| website/src/scripts/pages/tools.ts | Switches to shared renderer, delegated copy handlers, and one-time init guard. |
| website/src/scripts/pages/tools-render.ts | New shared renderer for tools with URL sanitization. |
| website/src/scripts/pages/skills.ts | Uses shared renderer/sorter; adds delegated handlers; lazy-loads JSZip for downloads. |
| website/src/scripts/pages/skills-render.ts | New shared HTML renderer + sorter for skills. |
| website/src/scripts/pages/samples.ts | Refactors rendering/results-count logic into shared renderer utilities; adds init guard and debounced search. |
| website/src/scripts/pages/samples-render.ts | New shared renderer + results-count formatter for cookbook/samples page. |
| website/src/scripts/pages/plugins.ts | Uses shared renderer; removes featured filter; adds delegated click handling. |
| website/src/scripts/pages/plugins-render.ts | New shared HTML renderer for plugins (includes external URL handling). |
| website/src/scripts/pages/instructions.ts | Uses shared renderer/sorter and delegated click handling; widens applyTo typing. |
| website/src/scripts/pages/instructions-render.ts | New shared HTML renderer + sorter for instructions. |
| website/src/scripts/pages/hooks.ts | Uses shared renderer/sorter; delegated click + lazy-load JSZip for downloads. |
| website/src/scripts/pages/hooks-render.ts | New shared HTML renderer + sorter for hooks. |
| website/src/scripts/pages/agents.ts | Uses shared renderer/sorter and delegated click handling; updates results count initialization. |
| website/src/scripts/pages/agents-render.ts | New shared HTML renderer + sorter for agents. |
| website/src/scripts/modal.ts | Adds markdown rendering (marked + front-matter), Shiki highlighting, view-mode toggles, and a skills file switcher. |
| website/src/scripts/embedded-data.ts | New helper for embedding and reading JSON from <script type="application/json">. |
| website/src/pages/*.astro + website/src/pages/learning-hub/cookbook/index.astro | Server-renders initial lists and embeds page JSON for fast client init. |
| website/src/components/Modal.astro | Updates modal layout, adds Render/Raw controls, and adds a file switcher UI. |
| website/src/components/EmbeddedPageData.astro | New component to serialize/inline page JSON payloads. |
| website/package.json + website/package-lock.json | Adds markdown/front matter + highlighting deps (marked/front-matter/shiki) and lock updates. |
Files not reviewed (1)
- website/package-lock.json: Language not supported
You can also share your feedback on Copilot code review. Take the survey.
| </button> | ||
| <a href="${getGitHubUrl( | ||
| item.path | ||
| )}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
There was a problem hiding this comment.
This link opens a new tab (target="_blank") without rel="noopener noreferrer", which allows reverse-tabnabbing. Add rel="noopener noreferrer" to external links opened in a new tab.
| )}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> | |
| )}" class="btn btn-secondary" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
| </button> | ||
| <a href="${getGitHubUrl( | ||
| item.path | ||
| )}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
There was a problem hiding this comment.
This link opens a new tab (target="_blank") without rel="noopener noreferrer", which allows reverse-tabnabbing. Add rel="noopener noreferrer" to external links opened in a new tab.
| )}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> | |
| )}" class="btn btn-secondary" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
| ${getActionButtonsHtml(item.path, true)} | ||
| <a href="${getGitHubUrl( | ||
| item.path | ||
| )}" class="btn btn-secondary btn-small" target="_blank" onclick="event.stopPropagation()" title="View on GitHub"> |
There was a problem hiding this comment.
This link opens a new tab (target="_blank") without rel="noopener noreferrer", which allows reverse-tabnabbing. Add rel="noopener noreferrer" to external links opened in a new tab.
| )}" class="btn btn-secondary btn-small" target="_blank" onclick="event.stopPropagation()" title="View on GitHub"> | |
| )}" class="btn btn-secondary btn-small" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="View on GitHub"> |
| @font-face { | ||
| font-family: 'Monaspace Argon NF'; | ||
| src: url('../fonts/MonaspaceArgonNF-Regular.woff2') format('woff2'); | ||
| src: url('../../public/fonts/MonaspaceArgonNF-Regular.woff2') format('woff2'); | ||
| font-weight: 400; |
There was a problem hiding this comment.
The font-face URL points into ../../public/..., which won’t resolve correctly once Astro/Vite bundles src/styles/global.css. Since the font lives in website/public/fonts, reference it via an absolute public path (e.g. /fonts/...) instead of a relative path into public.
| .modal-header-top { | ||
| gap: 16px; | ||
| min-width: 0; | ||
| } |
There was a problem hiding this comment.
.modal-header-top sets gap and sizing but never sets display: flex, so the title/actions won’t be laid out as intended (and gap will have no effect). Add an explicit layout (e.g. display:flex plus alignment/justification) or move these rules back onto .modal-header.
| if (isMarkdownFile(currentFilePath) && currentViewMode === "rendered") { | ||
| const container = ensureDivContent("modal-rendered-content"); | ||
| if (!container) return; | ||
|
|
||
| const { body: markdownBody } = fm(currentFileContent); | ||
| container.innerHTML = marked(markdownBody, { async: false }); | ||
| } else { |
There was a problem hiding this comment.
Markdown is rendered with marked(...) and injected via innerHTML without sanitization. Because repo content can include raw HTML, this enables XSS in the modal. Sanitize the generated HTML (e.g. DOMPurify) or configure the renderer to disallow raw HTML before assigning to innerHTML.
| const { body: markdownBody } = fm(currentFileContent); | ||
| container.innerHTML = marked(markdownBody, { async: false }); |
There was a problem hiding this comment.
fm(currentFileContent) can throw on malformed front matter, and marked(...) can also throw; currently that would reject openFileModal() and leave the modal in a broken state. Wrap front-matter parsing/markdown rendering in a try/catch and fall back to raw/highlighted rendering (or a safe error message) on failure.
| const { body: markdownBody } = fm(currentFileContent); | |
| container.innerHTML = marked(markdownBody, { async: false }); | |
| try { | |
| const { body: markdownBody } = fm(currentFileContent); | |
| container.innerHTML = marked(markdownBody, { async: false }); | |
| } catch (error) { | |
| // If front-matter parsing or markdown rendering fails, fall back to highlighted view | |
| // rather than leaving the modal in a broken state. | |
| // eslint-disable-next-line no-console | |
| console.error( | |
| "Failed to render markdown content, falling back to highlighted view.", | |
| error | |
| ); | |
| await renderHighlightedCode(currentFileContent, currentFilePath); | |
| } |
| @@ -438,27 +865,12 @@ export async function openFileModal( | |||
| return; | |||
| } | |||
There was a problem hiding this comment.
When opening a plugin modal, the new Render/Raw buttons aren’t hidden/reset. If a user previously opened a markdown file, the plugin modal can still show these controls even though they don’t apply. Consider explicitly hiding the render/raw controls (or calling a shared reset like updateViewButtons() after clearing currentFilePath/currentFileContent) in the plugin branch.
| categoryFilter.innerHTML = | ||
| '<option value="">All Categories</option>' + | ||
| data.filters.categories | ||
| .map( | ||
| (c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>` | ||
| (c) => `<option value="${c}">${c}</option>` | ||
| ) | ||
| .join(""); |
There was a problem hiding this comment.
Category options are built via innerHTML using values from tools.json without escaping. If a category string contains HTML special characters, this can become an injection vector. Prefer creating <option> elements and setting textContent/value, or escape the strings before concatenating into HTML.
See below for a potential fix:
// Clear any existing options
categoryFilter.innerHTML = "";
// Add default "All Categories" option
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = "All Categories";
categoryFilter.appendChild(defaultOption);
// Add options for each category
data.filters.categories.forEach((c) => {
const option = document.createElement("option");
option.value = c;
option.textContent = c;
categoryFilter.appendChild(option);
});
| </div> | ||
| <div class="resource-actions"> | ||
| ${getActionButtonsHtml(item.path)} | ||
| <a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
There was a problem hiding this comment.
This link opens a new tab (target="_blank") without rel="noopener noreferrer", which allows reverse-tabnabbing. Add rel="noopener noreferrer" to external links opened in a new tab.
| <a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> | |
| <a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a> |
This pull request updates dependencies and introduces new packages to the
websiteproject, primarily adding support for parsing and rendering markdown and front matter, as well as upgrading syntax highlighting libraries. Additionally, a new Astro component is added for embedding page data as JSON. These changes collectively improve content processing and code highlighting capabilities.