mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-07-16 18:05:58 -04:00
feat: add /registry root index page, sort detail tables, and rename OG routes to .webp URLs
- Add `registry.index.tsx` — a root registry landing page at `/registry` listing all modules; emit `/registry` in `prerender.ts` so it is prerendered alongside category and module paths
- Sort deps, scripts, env vars, and templates alphabetically (env vars: required-first then alpha; templates: by scope rank then dest) for consistent display order
- Rename OG image routes to include the `.webp` extension in the URL (`og.index.ts` → `og[.]webp.ts`, `og.docs.$.ts` → `og.docs.{$}[.]webp.ts`, `og.registry.$category.$id.ts` → `og.registry.$category.{$id}[.]webp.ts`) and update the default `ogImage` fallback in `seo.ts` to `/og.webp`
- Swap version/icon layout in `DepsTable` so version is hidden on hover and the external-link icon appears in its place
This commit is contained in:
@@ -5,7 +5,7 @@ import { Section, SectionList } from "@/components/detail/section";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export function DepsTable({ title, entries }: { title: string; entries: Record<string, string> }) {
|
||||
const items = Object.entries(entries);
|
||||
const items = Object.entries(entries).toSorted(([a], [b]) => a.localeCompare(b));
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -20,14 +20,18 @@ export function DepsTable({ title, entries }: { title: string; entries: Record<s
|
||||
aria-label={`${name} ${version} (opens in new tab)`}
|
||||
className="group flex items-center justify-between gap-3 px-3 py-2 font-mono text-xs transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<span className="inline-flex min-w-0 items-center gap-1 text-foreground group-hover:underline group-hover:underline-offset-1">
|
||||
<span className="truncate">{name}</span>
|
||||
<span className="min-w-0 truncate text-foreground group-hover:underline group-hover:underline-offset-1">
|
||||
{name}
|
||||
</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1">
|
||||
<span className="text-muted-foreground tabular-nums group-hover:hidden">
|
||||
{version}
|
||||
</span>
|
||||
<IconExternalLink
|
||||
className="size-3 shrink-0 text-muted-foreground/60"
|
||||
className="hidden size-3 shrink-0 text-muted-foreground/60 group-hover:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<span className="shrink-0 text-muted-foreground tabular-nums">{version}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
@@ -37,7 +41,7 @@ export function DepsTable({ title, entries }: { title: string; entries: Record<s
|
||||
}
|
||||
|
||||
export function ScriptsTable({ entries }: { entries: Record<string, string> }) {
|
||||
const items = Object.entries(entries);
|
||||
const items = Object.entries(entries).toSorted(([a], [b]) => a.localeCompare(b));
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -63,10 +67,14 @@ export function ScriptsTable({ entries }: { entries: Record<string, string> }) {
|
||||
export function EnvTable({ env }: { env: EnvVar[] }) {
|
||||
if (env.length === 0) return null;
|
||||
|
||||
const items = env.toSorted(
|
||||
(a, b) => Number(b.required) - Number(a.required) || a.name.localeCompare(b.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<Section title="Environment Variables" count={env.length}>
|
||||
<SectionList>
|
||||
{env.map((e) => (
|
||||
{items.map((e) => (
|
||||
<li key={e.name} className="grid gap-1 px-3 py-2.5 text-xs">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono font-medium text-foreground">{e.name}</span>
|
||||
|
||||
@@ -17,10 +17,13 @@ export function TemplatesList({
|
||||
previews: Record<string, Preview>;
|
||||
}) {
|
||||
if (templates.length === 0) return null;
|
||||
const sorted = templates.toSorted(
|
||||
(a, b) => scopeRank(a.scope) - scopeRank(b.scope) || a.dest.localeCompare(b.dest),
|
||||
);
|
||||
return (
|
||||
<Section title="Templates" count={templates.length}>
|
||||
<SectionList>
|
||||
{templates.map((tpl) => (
|
||||
{sorted.map((tpl) => (
|
||||
<TemplateRow key={tpl.dest} template={tpl} preview={previews[tpl.dest]} />
|
||||
))}
|
||||
</SectionList>
|
||||
@@ -80,3 +83,9 @@ function scopeLabel(scope: TemplateRef["scope"]): string {
|
||||
if (scope === "package") return "package";
|
||||
return "app";
|
||||
}
|
||||
|
||||
function scopeRank(scope: TemplateRef["scope"]): number {
|
||||
if (scope === "package") return 1;
|
||||
if (scope === "app") return 2;
|
||||
return 0; // repo (or undefined default)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ function listRegistryPaths(): string[] {
|
||||
};
|
||||
const categoryPaths = index.categories.map((c) => `/registry/${c.id}`);
|
||||
const modulePaths = index.modules.map((m) => `/registry/${m.category}/${m.id}`);
|
||||
return [...categoryPaths, ...modulePaths].toSorted((a, b) => a.localeCompare(b));
|
||||
return ["/registry", ...categoryPaths, ...modulePaths].toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function listPrerenderPages() {
|
||||
|
||||
@@ -40,7 +40,7 @@ export function buildHead(input: HeadInput): HeadOutput {
|
||||
input.titleOverride ?? (input.title ? `${input.title} · ${DEFAULT_TITLE}` : DEFAULT_TITLE);
|
||||
const description = input.description ?? DEFAULT_DESCRIPTION;
|
||||
const url = abs(input.path);
|
||||
const ogImage = abs(input.ogImage ?? "/og");
|
||||
const ogImage = abs(input.ogImage ?? "/og.webp");
|
||||
const type = input.type ?? "website";
|
||||
|
||||
const links: Array<Record<string, string>> = [{ rel: "canonical", href: url }];
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as StatsRouteImport } from './routes/stats'
|
||||
import { Route as OgDotwebpRouteImport } from './routes/og[.]webp'
|
||||
import { Route as DocsDotmdRouteImport } from './routes/docs[.]md'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as OgIndexRouteImport } from './routes/og.index'
|
||||
import { Route as RegistryIndexRouteImport } from './routes/registry.index'
|
||||
import { Route as DocsChar123Char125DotmdRouteImport } from './routes/docs.{$}[.]md'
|
||||
import { Route as DocsLlmsDottxtRouteImport } from './routes/docs.llms[.]txt'
|
||||
import { Route as DocsLlmsFullDottxtRouteImport } from './routes/docs.llms-full[.]txt'
|
||||
@@ -20,16 +21,21 @@ import { Route as DocsSplatRouteImport } from './routes/docs.$'
|
||||
import { Route as ApiEventsRouteImport } from './routes/api.events'
|
||||
import { Route as RegistryCategoryIndexRouteImport } from './routes/registry.$category.index'
|
||||
import { Route as RegistryCategoryIdRouteImport } from './routes/registry.$category.$id'
|
||||
import { Route as OgDocsSplatRouteImport } from './routes/og.docs.$'
|
||||
import { Route as OgDocsChar123Char125DotwebpRouteImport } from './routes/og.docs.{$}[.]webp'
|
||||
import { Route as ApiSearchModulesRouteImport } from './routes/api.search.modules'
|
||||
import { Route as ApiSearchDocsRouteImport } from './routes/api.search.docs'
|
||||
import { Route as OgRegistryCategoryIdRouteImport } from './routes/og.registry.$category.$id'
|
||||
import { Route as OgRegistryCategoryChar123idChar125DotwebpRouteImport } from './routes/og.registry.$category.{$id}[.]webp'
|
||||
|
||||
const StatsRoute = StatsRouteImport.update({
|
||||
id: '/stats',
|
||||
path: '/stats',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgDotwebpRoute = OgDotwebpRouteImport.update({
|
||||
id: '/og.webp',
|
||||
path: '/og.webp',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DocsDotmdRoute = DocsDotmdRouteImport.update({
|
||||
id: '/docs.md',
|
||||
path: '/docs.md',
|
||||
@@ -40,9 +46,9 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgIndexRoute = OgIndexRouteImport.update({
|
||||
id: '/og/',
|
||||
path: '/og/',
|
||||
const RegistryIndexRoute = RegistryIndexRouteImport.update({
|
||||
id: '/registry/',
|
||||
path: '/registry/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DocsChar123Char125DotmdRoute = DocsChar123Char125DotmdRouteImport.update({
|
||||
@@ -80,11 +86,12 @@ const RegistryCategoryIdRoute = RegistryCategoryIdRouteImport.update({
|
||||
path: '/registry/$category/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgDocsSplatRoute = OgDocsSplatRouteImport.update({
|
||||
id: '/og/docs/$',
|
||||
path: '/og/docs/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgDocsChar123Char125DotwebpRoute =
|
||||
OgDocsChar123Char125DotwebpRouteImport.update({
|
||||
id: '/og/docs/{$}.webp',
|
||||
path: '/og/docs/{$}.webp',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiSearchModulesRoute = ApiSearchModulesRouteImport.update({
|
||||
id: '/api/search/modules',
|
||||
path: '/api/search/modules',
|
||||
@@ -95,134 +102,142 @@ const ApiSearchDocsRoute = ApiSearchDocsRouteImport.update({
|
||||
path: '/api/search/docs',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgRegistryCategoryIdRoute = OgRegistryCategoryIdRouteImport.update({
|
||||
id: '/og/registry/$category/$id',
|
||||
path: '/og/registry/$category/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OgRegistryCategoryChar123idChar125DotwebpRoute =
|
||||
OgRegistryCategoryChar123idChar125DotwebpRouteImport.update({
|
||||
id: '/og/registry/$category/{$id}.webp',
|
||||
path: '/og/registry/$category/{$id}.webp',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/docs.md': typeof DocsDotmdRoute
|
||||
'/og.webp': typeof OgDotwebpRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/events': typeof ApiEventsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/llms-full.txt': typeof DocsLlmsFullDottxtRoute
|
||||
'/docs/llms.txt': typeof DocsLlmsDottxtRoute
|
||||
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
|
||||
'/og/': typeof OgIndexRoute
|
||||
'/registry/': typeof RegistryIndexRoute
|
||||
'/api/search/docs': typeof ApiSearchDocsRoute
|
||||
'/api/search/modules': typeof ApiSearchModulesRoute
|
||||
'/og/docs/$': typeof OgDocsSplatRoute
|
||||
'/og/docs/{$}.webp': typeof OgDocsChar123Char125DotwebpRoute
|
||||
'/registry/$category/$id': typeof RegistryCategoryIdRoute
|
||||
'/registry/$category/': typeof RegistryCategoryIndexRoute
|
||||
'/og/registry/$category/$id': typeof OgRegistryCategoryIdRoute
|
||||
'/og/registry/$category/{$id}.webp': typeof OgRegistryCategoryChar123idChar125DotwebpRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/docs.md': typeof DocsDotmdRoute
|
||||
'/og.webp': typeof OgDotwebpRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/events': typeof ApiEventsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/llms-full.txt': typeof DocsLlmsFullDottxtRoute
|
||||
'/docs/llms.txt': typeof DocsLlmsDottxtRoute
|
||||
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
|
||||
'/og': typeof OgIndexRoute
|
||||
'/registry': typeof RegistryIndexRoute
|
||||
'/api/search/docs': typeof ApiSearchDocsRoute
|
||||
'/api/search/modules': typeof ApiSearchModulesRoute
|
||||
'/og/docs/$': typeof OgDocsSplatRoute
|
||||
'/og/docs/{$}.webp': typeof OgDocsChar123Char125DotwebpRoute
|
||||
'/registry/$category/$id': typeof RegistryCategoryIdRoute
|
||||
'/registry/$category': typeof RegistryCategoryIndexRoute
|
||||
'/og/registry/$category/$id': typeof OgRegistryCategoryIdRoute
|
||||
'/og/registry/$category/{$id}.webp': typeof OgRegistryCategoryChar123idChar125DotwebpRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/docs.md': typeof DocsDotmdRoute
|
||||
'/og.webp': typeof OgDotwebpRoute
|
||||
'/stats': typeof StatsRoute
|
||||
'/api/events': typeof ApiEventsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/llms-full.txt': typeof DocsLlmsFullDottxtRoute
|
||||
'/docs/llms.txt': typeof DocsLlmsDottxtRoute
|
||||
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
|
||||
'/og/': typeof OgIndexRoute
|
||||
'/registry/': typeof RegistryIndexRoute
|
||||
'/api/search/docs': typeof ApiSearchDocsRoute
|
||||
'/api/search/modules': typeof ApiSearchModulesRoute
|
||||
'/og/docs/$': typeof OgDocsSplatRoute
|
||||
'/og/docs/{$}.webp': typeof OgDocsChar123Char125DotwebpRoute
|
||||
'/registry/$category/$id': typeof RegistryCategoryIdRoute
|
||||
'/registry/$category/': typeof RegistryCategoryIndexRoute
|
||||
'/og/registry/$category/$id': typeof OgRegistryCategoryIdRoute
|
||||
'/og/registry/$category/{$id}.webp': typeof OgRegistryCategoryChar123idChar125DotwebpRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/docs.md'
|
||||
| '/og.webp'
|
||||
| '/stats'
|
||||
| '/api/events'
|
||||
| '/docs/$'
|
||||
| '/docs/llms-full.txt'
|
||||
| '/docs/llms.txt'
|
||||
| '/docs/{$}.md'
|
||||
| '/og/'
|
||||
| '/registry/'
|
||||
| '/api/search/docs'
|
||||
| '/api/search/modules'
|
||||
| '/og/docs/$'
|
||||
| '/og/docs/{$}.webp'
|
||||
| '/registry/$category/$id'
|
||||
| '/registry/$category/'
|
||||
| '/og/registry/$category/$id'
|
||||
| '/og/registry/$category/{$id}.webp'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/docs.md'
|
||||
| '/og.webp'
|
||||
| '/stats'
|
||||
| '/api/events'
|
||||
| '/docs/$'
|
||||
| '/docs/llms-full.txt'
|
||||
| '/docs/llms.txt'
|
||||
| '/docs/{$}.md'
|
||||
| '/og'
|
||||
| '/registry'
|
||||
| '/api/search/docs'
|
||||
| '/api/search/modules'
|
||||
| '/og/docs/$'
|
||||
| '/og/docs/{$}.webp'
|
||||
| '/registry/$category/$id'
|
||||
| '/registry/$category'
|
||||
| '/og/registry/$category/$id'
|
||||
| '/og/registry/$category/{$id}.webp'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/docs.md'
|
||||
| '/og.webp'
|
||||
| '/stats'
|
||||
| '/api/events'
|
||||
| '/docs/$'
|
||||
| '/docs/llms-full.txt'
|
||||
| '/docs/llms.txt'
|
||||
| '/docs/{$}.md'
|
||||
| '/og/'
|
||||
| '/registry/'
|
||||
| '/api/search/docs'
|
||||
| '/api/search/modules'
|
||||
| '/og/docs/$'
|
||||
| '/og/docs/{$}.webp'
|
||||
| '/registry/$category/$id'
|
||||
| '/registry/$category/'
|
||||
| '/og/registry/$category/$id'
|
||||
| '/og/registry/$category/{$id}.webp'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
DocsDotmdRoute: typeof DocsDotmdRoute
|
||||
OgDotwebpRoute: typeof OgDotwebpRoute
|
||||
StatsRoute: typeof StatsRoute
|
||||
ApiEventsRoute: typeof ApiEventsRoute
|
||||
DocsSplatRoute: typeof DocsSplatRoute
|
||||
DocsLlmsFullDottxtRoute: typeof DocsLlmsFullDottxtRoute
|
||||
DocsLlmsDottxtRoute: typeof DocsLlmsDottxtRoute
|
||||
DocsChar123Char125DotmdRoute: typeof DocsChar123Char125DotmdRoute
|
||||
OgIndexRoute: typeof OgIndexRoute
|
||||
RegistryIndexRoute: typeof RegistryIndexRoute
|
||||
ApiSearchDocsRoute: typeof ApiSearchDocsRoute
|
||||
ApiSearchModulesRoute: typeof ApiSearchModulesRoute
|
||||
OgDocsSplatRoute: typeof OgDocsSplatRoute
|
||||
OgDocsChar123Char125DotwebpRoute: typeof OgDocsChar123Char125DotwebpRoute
|
||||
RegistryCategoryIdRoute: typeof RegistryCategoryIdRoute
|
||||
RegistryCategoryIndexRoute: typeof RegistryCategoryIndexRoute
|
||||
OgRegistryCategoryIdRoute: typeof OgRegistryCategoryIdRoute
|
||||
OgRegistryCategoryChar123idChar125DotwebpRoute: typeof OgRegistryCategoryChar123idChar125DotwebpRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -234,6 +249,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof StatsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/og.webp': {
|
||||
id: '/og.webp'
|
||||
path: '/og.webp'
|
||||
fullPath: '/og.webp'
|
||||
preLoaderRoute: typeof OgDotwebpRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs.md': {
|
||||
id: '/docs.md'
|
||||
path: '/docs.md'
|
||||
@@ -248,11 +270,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/og/': {
|
||||
id: '/og/'
|
||||
path: '/og'
|
||||
fullPath: '/og/'
|
||||
preLoaderRoute: typeof OgIndexRouteImport
|
||||
'/registry/': {
|
||||
id: '/registry/'
|
||||
path: '/registry'
|
||||
fullPath: '/registry/'
|
||||
preLoaderRoute: typeof RegistryIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs/{$}.md': {
|
||||
@@ -304,11 +326,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof RegistryCategoryIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/og/docs/$': {
|
||||
id: '/og/docs/$'
|
||||
path: '/og/docs/$'
|
||||
fullPath: '/og/docs/$'
|
||||
preLoaderRoute: typeof OgDocsSplatRouteImport
|
||||
'/og/docs/{$}.webp': {
|
||||
id: '/og/docs/{$}.webp'
|
||||
path: '/og/docs/{$}.webp'
|
||||
fullPath: '/og/docs/{$}.webp'
|
||||
preLoaderRoute: typeof OgDocsChar123Char125DotwebpRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/search/modules': {
|
||||
@@ -325,11 +347,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ApiSearchDocsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/og/registry/$category/$id': {
|
||||
id: '/og/registry/$category/$id'
|
||||
path: '/og/registry/$category/$id'
|
||||
fullPath: '/og/registry/$category/$id'
|
||||
preLoaderRoute: typeof OgRegistryCategoryIdRouteImport
|
||||
'/og/registry/$category/{$id}.webp': {
|
||||
id: '/og/registry/$category/{$id}.webp'
|
||||
path: '/og/registry/$category/{$id}.webp'
|
||||
fullPath: '/og/registry/$category/{$id}.webp'
|
||||
preLoaderRoute: typeof OgRegistryCategoryChar123idChar125DotwebpRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
@@ -338,19 +360,21 @@ declare module '@tanstack/react-router' {
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
DocsDotmdRoute: DocsDotmdRoute,
|
||||
OgDotwebpRoute: OgDotwebpRoute,
|
||||
StatsRoute: StatsRoute,
|
||||
ApiEventsRoute: ApiEventsRoute,
|
||||
DocsSplatRoute: DocsSplatRoute,
|
||||
DocsLlmsFullDottxtRoute: DocsLlmsFullDottxtRoute,
|
||||
DocsLlmsDottxtRoute: DocsLlmsDottxtRoute,
|
||||
DocsChar123Char125DotmdRoute: DocsChar123Char125DotmdRoute,
|
||||
OgIndexRoute: OgIndexRoute,
|
||||
RegistryIndexRoute: RegistryIndexRoute,
|
||||
ApiSearchDocsRoute: ApiSearchDocsRoute,
|
||||
ApiSearchModulesRoute: ApiSearchModulesRoute,
|
||||
OgDocsSplatRoute: OgDocsSplatRoute,
|
||||
OgDocsChar123Char125DotwebpRoute: OgDocsChar123Char125DotwebpRoute,
|
||||
RegistryCategoryIdRoute: RegistryCategoryIdRoute,
|
||||
RegistryCategoryIndexRoute: RegistryCategoryIndexRoute,
|
||||
OgRegistryCategoryIdRoute: OgRegistryCategoryIdRoute,
|
||||
OgRegistryCategoryChar123idChar125DotwebpRoute:
|
||||
OgRegistryCategoryChar123idChar125DotwebpRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -86,7 +86,7 @@ export const Route = createFileRoute("/docs/$")({
|
||||
title,
|
||||
description,
|
||||
path,
|
||||
ogImage: loaderData ? `/og${loaderData.url}` : undefined,
|
||||
ogImage: loaderData ? `/og${loaderData.url}.webp` : undefined,
|
||||
markdownPath: loaderData ? `${path}.md` : undefined,
|
||||
jsonLd:
|
||||
loaderData && title
|
||||
|
||||
@@ -5,11 +5,11 @@ import { source } from "@/lib/source";
|
||||
import { OgDocs } from "@/server/og-card.server";
|
||||
|
||||
/**
|
||||
* `/og/docs/$splat` — per-docs-page OG card, mirroring the public
|
||||
* `/og/docs/$splat.webp` — per-docs-page OG card, mirroring the public
|
||||
* `/docs/$splat` URL. The splat resolves to fumadocs page slugs the same way
|
||||
* `routes/docs.$.tsx` does. Bails 404 when the page is unknown.
|
||||
*/
|
||||
export const Route = createFileRoute("/og/docs/$")({
|
||||
export const Route = createFileRoute("/og/docs/{$}.webp")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: ({ params }) => {
|
||||
+3
-3
@@ -6,11 +6,11 @@ import { OgCard } from "@/server/og-card.server";
|
||||
import { loadRegistryFile } from "@/server/registry-base.server";
|
||||
|
||||
/**
|
||||
* `/og/registry/$category/$id` — per-module OG card (e.g. `/og/registry/auth/clerk`), mirroring
|
||||
* the public `/registry/$category/$id` URL. Dynamically rendered at request time via
|
||||
* `/og/registry/$category/$id.webp` — per-module OG card (e.g. `/og/registry/auth/clerk.webp`),
|
||||
* mirroring the public `/registry/$category/$id` URL. Dynamically rendered at request time via
|
||||
* Takumi. Bails 404 when the module is unknown.
|
||||
*/
|
||||
export const Route = createFileRoute("/og/registry/$category/$id")({
|
||||
export const Route = createFileRoute("/og/registry/$category/{$id}.webp")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async ({ params }) => {
|
||||
@@ -4,12 +4,12 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { OgDefault } from "@/server/og-card.server";
|
||||
|
||||
/**
|
||||
* `/og` — default OG used by `/`, `/search`, and anywhere without a more
|
||||
* `/og.webp` — default OG used by `/`, `/search`, and anywhere without a more
|
||||
* specific image. Static-ish content; the SWR header lets the CDN serve it
|
||||
* stale for a long time. A TanStack Start server route (no page component):
|
||||
* the `GET` handler streams a WebP straight back.
|
||||
*/
|
||||
export const Route = createFileRoute("/og/")({
|
||||
export const Route = createFileRoute("/og.webp")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: () =>
|
||||
@@ -45,7 +45,7 @@ export const Route = createFileRoute("/registry/$category/$id")({
|
||||
title: module.label,
|
||||
description: module.description,
|
||||
path,
|
||||
ogImage: `/og/registry/${params.category}/${params.id}`,
|
||||
ogImage: `/og${path}.webp`,
|
||||
type: "article",
|
||||
jsonLd: [
|
||||
getSoftwareSourceCodeJsonLd({
|
||||
@@ -118,7 +118,7 @@ function ModuleDetailPage() {
|
||||
<h1 className="text-2xl font-medium tracking-tight text-balance" translate="no">
|
||||
{module.label}
|
||||
</h1>
|
||||
<Badge variant="default">{categoryLabel(module.category)}</Badge>
|
||||
<Badge variant="secondary">{categoryLabel(module.category)}</Badge>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm text-pretty text-muted-foreground">{module.description}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-xs">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CategoryId } from "@stanza/registry";
|
||||
import { categoryLabel, isCategoryId } from "@stanza/registry";
|
||||
import { categoryDescription, categoryLabel, isCategoryId } from "@stanza/registry";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { Link, createFileRoute, notFound, useLoaderData } from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
@@ -28,21 +27,6 @@ export const Route = createFileRoute("/registry/$category/")({
|
||||
component: CategoryLandingPage,
|
||||
});
|
||||
|
||||
const CATEGORY_BLURBS: Record<CategoryId, string> = {
|
||||
framework: "Web and native app frameworks.",
|
||||
ui: "Styling systems and component primitives.",
|
||||
db: "Database engines.",
|
||||
orm: "Typed query layers over your database.",
|
||||
auth: "Authentication providers and session handling.",
|
||||
payments: "Checkout, customer portal, and webhooks.",
|
||||
email: "Transactional email providers and templates.",
|
||||
ai: "AI SDK and provider wiring.",
|
||||
tooling: "Linter and formatter toolchains.",
|
||||
testing: "Test runners — unit and end-to-end.",
|
||||
deploy: "Deploy targets.",
|
||||
monorepo: "Workspace task orchestrators.",
|
||||
};
|
||||
|
||||
function CategoryLandingPage() {
|
||||
const { category } = Route.useLoaderData();
|
||||
const { registry } = useLoaderData({ from: "__root__" });
|
||||
@@ -74,7 +58,7 @@ function CategoryLandingPage() {
|
||||
<h1 className="text-2xl font-medium tracking-tight text-balance">{label}</h1>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm text-pretty text-muted-foreground">
|
||||
{CATEGORY_BLURBS[category]}
|
||||
{categoryDescription(category)}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { Category, ModuleMetadata } from "@stanza/registry";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { Link, createFileRoute, useLoaderData } from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { SectionList } from "@/components/detail/section";
|
||||
import { ModuleLogo } from "@/components/module-logo";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { buildHead, getWebSiteJsonLd } from "@/lib/seo";
|
||||
|
||||
export const Route = createFileRoute("/registry/")({
|
||||
head: () =>
|
||||
buildHead({
|
||||
title: "Registry",
|
||||
description: "Every first-party module Stanza can install, grouped by category.",
|
||||
path: "/registry",
|
||||
jsonLd: [getWebSiteJsonLd()],
|
||||
}),
|
||||
component: RegistryIndexPage,
|
||||
});
|
||||
|
||||
type CategoryGroup = { category: Category; modules: ModuleMetadata[] };
|
||||
|
||||
function RegistryIndexPage() {
|
||||
const { registry } = useLoaderData({ from: "__root__" });
|
||||
|
||||
// Group modules under their category, preserving the index's topological
|
||||
// category order. Every category is rendered (even empty ones) so the page
|
||||
// links to all category landing pages — it doubles as a registry sitemap.
|
||||
const groups = useMemo<CategoryGroup[]>(() => {
|
||||
const byCategory = new Map<string, ModuleMetadata[]>();
|
||||
for (const m of registry.modules) {
|
||||
const list = byCategory.get(m.category);
|
||||
if (list) list.push(m);
|
||||
else byCategory.set(m.category, [m]);
|
||||
}
|
||||
return registry.categories.map((category) => ({
|
||||
category,
|
||||
modules: (byCategory.get(category.id) ?? []).toSorted((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
),
|
||||
}));
|
||||
}, [registry]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-10 sm:px-6">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<IconArrowLeft className="size-3" aria-hidden="true" />
|
||||
Back to builder
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-2xl font-medium tracking-tight text-balance">Registry</h1>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm text-pretty text-muted-foreground">
|
||||
Every first-party module Stanza can install, grouped by category.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Separator className="my-8" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{groups.map(({ category, modules }) => (
|
||||
<CategoryGroupSection key={category.id} category={category} modules={modules} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryGroupSection({
|
||||
category,
|
||||
modules,
|
||||
}: {
|
||||
category: Category;
|
||||
modules: ModuleMetadata[];
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Link to="/registry/$category" params={{ category: category.id }}>
|
||||
<h2 className="text-[13px] font-medium tracking-tight text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
{category.label}
|
||||
</h2>
|
||||
</Link>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-auto px-1.5 py-1 font-mono text-[11px] leading-none tabular-nums"
|
||||
>
|
||||
{modules.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{modules.length === 0 ? (
|
||||
<p className="mt-4 text-xs text-muted-foreground">No modules in this category (yet).</p>
|
||||
) : (
|
||||
<SectionList>
|
||||
{modules.map((m) => (
|
||||
<li key={m.id}>
|
||||
<Link
|
||||
to="/registry/$category/$id"
|
||||
params={{ category: m.category, id: m.id }}
|
||||
className="flex items-start gap-3 p-3 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<ModuleLogo logo={m.logo} label={m.label} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-sm leading-tight font-medium">{m.label}</h3>
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{m.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</SectionList>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -26,8 +26,8 @@ const PAGE: CSSProperties = {
|
||||
};
|
||||
|
||||
const HEADER_ROW: CSSProperties = { display: "flex", alignItems: "center", gap: "12px" };
|
||||
const DOT: CSSProperties = { color: "#52525b", fontSize: "20px" };
|
||||
const SLOT: CSSProperties = { color: "#a1a1aa", fontSize: "20px" };
|
||||
const SEPARATOR: CSSProperties = { color: "#52525b", fontSize: "24px" };
|
||||
const SLOT: CSSProperties = { color: "#a1a1aa", fontSize: "24px" };
|
||||
|
||||
// Brand mark sized and colored for the always-dark OG background. The viewer
|
||||
// `<img>` sets the rendered dimensions; the explicit fill keeps it visible
|
||||
@@ -84,7 +84,9 @@ const FOOTER: CSSProperties = {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
color: "#6c6c6c",
|
||||
fontSize: "20px",
|
||||
fontSize: "22px",
|
||||
fontWeight: 500,
|
||||
fontFamily: "Geist Mono, monospace",
|
||||
};
|
||||
|
||||
const DEFAULT_BODY: CSSProperties = {
|
||||
@@ -100,7 +102,6 @@ const TAGLINE: CSSProperties = {
|
||||
lineHeight: 1.3,
|
||||
maxWidth: "900px",
|
||||
};
|
||||
const DEFAULT_FOOTER: CSSProperties = { color: "#52525b", fontSize: "22px" };
|
||||
|
||||
export function OgCard({ meta }: { meta: ModuleMetadata }): ReactElement {
|
||||
const logo = meta.logo;
|
||||
@@ -113,7 +114,7 @@ export function OgCard({ meta }: { meta: ModuleMetadata }): ReactElement {
|
||||
<div style={PAGE}>
|
||||
<div style={HEADER_ROW}>
|
||||
<img src={svgToDataUri(BRAND_LOGO_SVG)} width={32} height={32} alt="Stanza" />
|
||||
<span style={DOT}>·</span>
|
||||
<span style={SEPARATOR}>/</span>
|
||||
<span style={SLOT}>{categoryLabel(meta.category)}</span>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +135,7 @@ export function OgCard({ meta }: { meta: ModuleMetadata }): ReactElement {
|
||||
|
||||
<div style={FOOTER}>
|
||||
<span>
|
||||
stanza.tools/registry/{meta.category}/{meta.id}
|
||||
npx stanza-cli add {meta.category} {meta.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +160,7 @@ export function OgDocs({
|
||||
<div style={PAGE}>
|
||||
<div style={HEADER_ROW}>
|
||||
<img src={svgToDataUri(BRAND_LOGO_SVG)} width={32} height={32} alt="Stanza" />
|
||||
<span style={DOT}>·</span>
|
||||
<span style={SEPARATOR}>/</span>
|
||||
<span style={SLOT}>Docs</span>
|
||||
</div>
|
||||
|
||||
@@ -182,10 +183,10 @@ export function OgDefault(): ReactElement {
|
||||
return (
|
||||
<div style={PAGE}>
|
||||
<div style={DEFAULT_BODY}>
|
||||
<img src={svgToDataUri(BRAND_LOGO_SVG)} width={160} height={160} alt="Stanza" />
|
||||
<img src={svgToDataUri(BRAND_LOGO_SVG)} width={120} height={120} alt="Stanza" />
|
||||
<div style={TAGLINE}>Modular monorepo template builder.</div>
|
||||
</div>
|
||||
<div style={DEFAULT_FOOTER}>npm init stanza my-app</div>
|
||||
<div style={FOOTER}>npm init stanza my-revolutionary-app</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
PEER_CATEGORIES,
|
||||
PACKAGE_DIRS,
|
||||
categoryLabel,
|
||||
categoryDescription,
|
||||
categoryHome,
|
||||
categoryCardinality,
|
||||
isMulti,
|
||||
|
||||
@@ -43,70 +43,70 @@ export const CATEGORIES = [
|
||||
{
|
||||
id: "framework",
|
||||
label: "Framework",
|
||||
description: "Web/native app framework.",
|
||||
description: "Web and native app frameworks.",
|
||||
cardinality: "one",
|
||||
home: { kind: "app" },
|
||||
},
|
||||
{
|
||||
id: "ui",
|
||||
label: "UI",
|
||||
description: "Styling system + component primitives.",
|
||||
description: "Styling systems and component primitives.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "ui" },
|
||||
},
|
||||
{
|
||||
id: "db",
|
||||
label: "Database",
|
||||
description: "Database engine.",
|
||||
description: "Database engines.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "db" },
|
||||
},
|
||||
{
|
||||
id: "orm",
|
||||
label: "ORM",
|
||||
description: "Database query layer.",
|
||||
description: "Typed query layers over your database.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "db" },
|
||||
},
|
||||
{
|
||||
id: "auth",
|
||||
label: "Auth",
|
||||
description: "Authentication provider.",
|
||||
description: "Authentication providers and session handling.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "auth" },
|
||||
},
|
||||
{
|
||||
id: "payments",
|
||||
label: "Payments",
|
||||
description: "Checkout + webhooks.",
|
||||
description: "Checkout, customer portal, and webhooks.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "payments" },
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
description: "Transactional email.",
|
||||
description: "Transactional email providers and templates.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "email" },
|
||||
},
|
||||
{
|
||||
id: "ai",
|
||||
label: "AI",
|
||||
description: "AI SDK + provider wiring.",
|
||||
description: "AI SDK and provider wiring.",
|
||||
cardinality: "one",
|
||||
home: { kind: "package", dir: "ai" },
|
||||
},
|
||||
{
|
||||
id: "tooling",
|
||||
label: "Tooling",
|
||||
description: "Linter + formatter toolchain.",
|
||||
description: "Linter and formatter toolchains.",
|
||||
cardinality: "one",
|
||||
home: { kind: "repo" },
|
||||
},
|
||||
{
|
||||
id: "testing",
|
||||
label: "Testing",
|
||||
description: "Test runners (unit, e2e).",
|
||||
description: "Test runners — unit and end-to-end.",
|
||||
cardinality: "many",
|
||||
home: { kind: "app" },
|
||||
},
|
||||
@@ -120,7 +120,7 @@ export const CATEGORIES = [
|
||||
{
|
||||
id: "monorepo",
|
||||
label: "Monorepo",
|
||||
description: "Workspace task orchestrator.",
|
||||
description: "Workspace task orchestrators.",
|
||||
cardinality: "one",
|
||||
home: { kind: "repo" },
|
||||
},
|
||||
@@ -161,6 +161,11 @@ export function categoryLabel(id: CategoryId): string {
|
||||
return CATEGORY_BY_ID[id].label;
|
||||
}
|
||||
|
||||
/** One-line blurb — used as the category landing-page subtitle. */
|
||||
export function categoryDescription(id: CategoryId): string {
|
||||
return CATEGORY_BY_ID[id].description;
|
||||
}
|
||||
|
||||
/** Install destination for a category's modules. */
|
||||
export function categoryHome(id: CategoryId): InstallHome {
|
||||
return CATEGORY_BY_ID[id].home;
|
||||
|
||||
Reference in New Issue
Block a user