Files
stanza/apps/web/src/lib/module-search.ts
T
jake 5ca679cf32 feat: add module detail pages, ⌘K search, dynamic OG images, sitemap, and SEO groundwork for apps/web
- Add `/m/$slot/$id` route (`apps/web/src/routes/m.$slot.$id.tsx`) with `module-detail.ts` server function — full adapter switcher (peer chip rows), deps/devDeps/scripts table, env-var table, templates list with click-to-expand Shiki preview, and a "Try it" copy-command bar; new detail components land under `apps/web/src/components/detail/`
- Add ⌘K search popover (`apps/web/src/components/search/site-search.tsx`) backed by `apps/web/src/lib/module-search.ts` — fuzzy-matches id/label/description/slot exactly as the CLI does; wired into the header
- Add dynamic OG image generation via `@vercel/og` + Satori: `server/routes/og/[slot]/[id].ts` renders a per-module card, `server/routes/og.ts` renders the default fallback; both served as Nitro route handlers with appropriate CDN cache headers; OG React components in `apps/web/src/server/og-card.tsx`
- Add `server/routes/sitemap.xml.ts` (enumerates `/` + all `/m/$slot/$id` URLs from registry index) and `public/robots.txt` pointing at it
- Add `apps/web/src/lib/seo.ts` — `buildHead()` helper that emits `title`, `og:*`, `twitter:*`, and `canonical` tags; consumed by `__root.tsx`, `index.tsx`, and `m.$slot.$id.tsx`
- Extract `ModuleLogo` into `apps/web/src/components/module-logo.tsx` — shared between slot-cards and the new detail page; remove the duplicate from `slot-cards.tsx`
- Add new shadcn-style UI primitives: `command.tsx` (cmdk wrapper), `dialog.tsx`, `input-group.tsx`, `kbd.tsx`, `popover.tsx`, `textarea.tsx`
- Add `apps/web/src/server/registry-base.ts` (filesystem reads to avoid SSR loopback-fetch deadlock), `registry-index.ts`, and `module-detail.ts` as the server-side data layer
- Add `@vercel/og` and `cmdk` to `apps/web` dependencies; add `vitest run` as `pnpm test` in web package
- Mark all corresponding TODO items done
2026-05-20 19:36:34 -04:00

14 lines
436 B
TypeScript

import type { ModuleSummary, SlotId } from "@stanza/registry";
export function groupBySlot(
modules: ModuleSummary[],
): Array<{ slot: SlotId; modules: ModuleSummary[] }> {
const groups = new Map<SlotId, ModuleSummary[]>();
for (const m of modules) {
const list = groups.get(m.slot) ?? [];
list.push(m);
groups.set(m.slot, list);
}
return [...groups.entries()].map(([slot, mods]) => ({ slot, modules: mods }));
}