From 1380872e960fecae7c5f75e5f2495e8ed350b10b Mon Sep 17 00:00:00 2001 From: bntvllnt Date: Wed, 13 May 2026 00:15:59 +0200 Subject: [PATCH 1/2] feat: add starter templates gallery --- apps/registry/app/llms-full.txt/route.ts | 27 ++- apps/registry/app/llms.txt/route.ts | 18 ++ apps/registry/app/sitemap.ts | 21 +- apps/registry/app/templates/[slug]/page.tsx | 228 ++++++++++++++++++ apps/registry/app/templates/page.tsx | 142 +++++++++++ apps/registry/app/vs/shadcn/page.tsx | 10 +- apps/registry/components/header/header.tsx | 23 +- apps/registry/components/landing/landing.tsx | 33 +-- apps/registry/lib/jsonld.ts | 42 +++- apps/registry/lib/sidebar-sections.ts | 1 + apps/registry/lib/templates.ts | 203 ++++++++++++++++ .../public/template-screenshots/ai-chat.svg | 1 + .../public/template-screenshots/dashboard.svg | 1 + .../public/template-screenshots/docs-site.svg | 1 + .../template-screenshots/next-starter.svg | 1 + .../public/template-screenshots/saas.svg | 1 + templates/ai-chat/README.md | 23 ++ templates/ai-chat/screenshots/preview.svg | 1 + templates/ai-chat/template.json | 8 + templates/dashboard/README.md | 23 ++ templates/dashboard/screenshots/preview.svg | 1 + templates/dashboard/template.json | 8 + templates/docs-site/README.md | 23 ++ templates/docs-site/screenshots/preview.svg | 1 + templates/docs-site/template.json | 8 + templates/next-starter/README.md | 23 ++ .../next-starter/screenshots/preview.svg | 1 + templates/next-starter/template.json | 8 + templates/saas/README.md | 23 ++ templates/saas/screenshots/preview.svg | 1 + templates/saas/template.json | 8 + 31 files changed, 885 insertions(+), 28 deletions(-) create mode 100644 apps/registry/app/templates/[slug]/page.tsx create mode 100644 apps/registry/app/templates/page.tsx create mode 100644 apps/registry/lib/templates.ts create mode 100644 apps/registry/public/template-screenshots/ai-chat.svg create mode 100644 apps/registry/public/template-screenshots/dashboard.svg create mode 100644 apps/registry/public/template-screenshots/docs-site.svg create mode 100644 apps/registry/public/template-screenshots/next-starter.svg create mode 100644 apps/registry/public/template-screenshots/saas.svg create mode 100644 templates/ai-chat/README.md create mode 100644 templates/ai-chat/screenshots/preview.svg create mode 100644 templates/ai-chat/template.json create mode 100644 templates/dashboard/README.md create mode 100644 templates/dashboard/screenshots/preview.svg create mode 100644 templates/dashboard/template.json create mode 100644 templates/docs-site/README.md create mode 100644 templates/docs-site/screenshots/preview.svg create mode 100644 templates/docs-site/template.json create mode 100644 templates/next-starter/README.md create mode 100644 templates/next-starter/screenshots/preview.svg create mode 100644 templates/next-starter/template.json create mode 100644 templates/saas/README.md create mode 100644 templates/saas/screenshots/preview.svg create mode 100644 templates/saas/template.json diff --git a/apps/registry/app/llms-full.txt/route.ts b/apps/registry/app/llms-full.txt/route.ts index c1c5fb42..2396a892 100644 --- a/apps/registry/app/llms-full.txt/route.ts +++ b/apps/registry/app/llms-full.txt/route.ts @@ -2,6 +2,12 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import registry from "../../registry.json"; +import { + getTemplateGithubUrl, + getTemplateInstallCommand, + getTemplatePath, + TEMPLATES, +} from "../../lib/templates"; const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; @@ -61,7 +67,6 @@ async function buildLlmsFullTxt(): Promise { lines.push(""); lines.push("```bash"); lines.push(`pnpm dlx shadcn@latest add ${SITE_URL}/r/.json`); - lines.push(`# Or with npm: npx shadcn@latest add ${SITE_URL}/r/.json`); lines.push("```"); lines.push(""); @@ -76,6 +81,26 @@ async function buildLlmsFullTxt(): Promise { lines.push(""); } + lines.push("## Templates"); + lines.push(""); + lines.push( + "Starter kits pair complete app shapes with component lists and a one-line pnpm install command.", + ); + lines.push(""); + + for (const template of TEMPLATES) { + lines.push(`### ${template.title}`); + lines.push(""); + lines.push(`- Slug: \`${template.slug}\``); + lines.push(`- Page: ${SITE_URL}${getTemplatePath(template)}`); + lines.push(`- Demo: ${template.demoUrl}`); + lines.push(`- Source: ${getTemplateGithubUrl(template)}`); + lines.push(`- Install: \`${getTemplateInstallCommand(template)}\``); + lines.push(`- Audience: ${template.audience}`); + lines.push(`- Components: ${template.components.join(", ")}`); + lines.push(""); + } + lines.push("## Components"); lines.push(""); lines.push( diff --git a/apps/registry/app/llms.txt/route.ts b/apps/registry/app/llms.txt/route.ts index 32130fb2..7f2c90b9 100644 --- a/apps/registry/app/llms.txt/route.ts +++ b/apps/registry/app/llms.txt/route.ts @@ -1,4 +1,9 @@ import registry from "../../registry.json"; +import { + getTemplateInstallCommand, + getTemplatePath, + TEMPLATES, +} from "../../lib/templates"; const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; @@ -79,6 +84,19 @@ function buildLlmsTxt(): string { lines.push( `- [Components index](${SITE_URL}/components): browse all components by category`, ); + lines.push( + `- [Templates](${SITE_URL}/templates): starter kits for full VLLNT UI apps`, + ); + lines.push(""); + + lines.push("## Templates"); + lines.push(""); + for (const template of TEMPLATES) { + lines.push( + `- [${template.title}](${SITE_URL}${getTemplatePath(template)}): ` + + `${template.description} Install: \`${getTemplateInstallCommand(template)}\``, + ); + } lines.push(""); lines.push("## Registry API"); diff --git a/apps/registry/app/sitemap.ts b/apps/registry/app/sitemap.ts index 816f585b..07db7acb 100644 --- a/apps/registry/app/sitemap.ts +++ b/apps/registry/app/sitemap.ts @@ -1,5 +1,6 @@ import type { MetadataRoute } from "next"; +import { getTemplatePath, TEMPLATES } from "../lib/templates"; import registry from "../registry.json"; const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; @@ -36,8 +37,21 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "monthly", priority: 0.6, }, + { + url: `${SITE_URL}/templates`, + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, ]; + const templateRoutes: MetadataRoute.Sitemap = TEMPLATES.map((template) => ({ + url: `${SITE_URL}${getTemplatePath(template)}`, + lastModified, + changeFrequency: "weekly", + priority: 0.7, + })); + const items = (registry as { readonly items: readonly RegistryItem[] }).items; const componentRoutes: MetadataRoute.Sitemap = items.map((item) => ({ @@ -62,5 +76,10 @@ export default function sitemap(): MetadataRoute.Sitemap { })), ]; - return [...staticRoutes, ...componentRoutes, ...registryEndpoints]; + return [ + ...staticRoutes, + ...templateRoutes, + ...componentRoutes, + ...registryEndpoints, + ]; } diff --git a/apps/registry/app/templates/[slug]/page.tsx b/apps/registry/app/templates/[slug]/page.tsx new file mode 100644 index 00000000..ecc86c7b --- /dev/null +++ b/apps/registry/app/templates/[slug]/page.tsx @@ -0,0 +1,228 @@ +import { Breadcrumb, CodeBlock, Sidebar } from "@vllnt/ui"; +import { ExternalLink, Github } from "lucide-react"; +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import Script from "next/script"; + +import { + breadcrumbLd, + jsonLdScriptAttributes, + softwareApplicationLd, +} from "@/lib/jsonld"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; +import { canonical } from "@/lib/seo"; +import { getSidebarSections } from "@/lib/sidebar-sections"; +import { + getTemplate, + getTemplateGithubUrl, + getTemplateInstallCommand, + getTemplatePath, + TEMPLATES, +} from "@/lib/templates"; + +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; + +type Props = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams(): { slug: string }[] { + return TEMPLATES.map((template) => ({ slug: template.slug })); +} + +export async function generateMetadata(props: Props): Promise { + const { slug } = await props.params; + const template = getTemplate(slug); + + if (!template) { + return { + title: "Templates - VLLNT UI", + }; + } + + const title = `${template.title} Template - VLLNT UI`; + + return { + alternates: { canonical: canonical(getTemplatePath(template)) }, + description: template.description, + openGraph: generateOGMetadata({ + description: template.description, + title, + type: "docs", + }), + title, + twitter: generateTwitterMetadata({ + description: template.description, + title, + type: "docs", + }), + }; +} + +export default async function TemplatePage(props: Props) { + const { slug } = await props.params; + const template = getTemplate(slug); + + if (!template) { + notFound(); + } + + const installCommand = getTemplateInstallCommand(template); + const templatePath = getTemplatePath(template); + const templateUrl = `${SITE_URL}${templatePath}`; + const githubUrl = getTemplateGithubUrl(template); + + return ( + <> +