From a8c81ae3bcd540b4552bfe2fea249a0f062500dd Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:42:26 +0100 Subject: [PATCH 1/5] feat(registry): add OG image generation for all pages Dynamic OG images via /api/og route using next/og ImageResponse. Frontmatter-driven config adapted from bntvllnt pattern. - Add lib/og.ts (Zod schema, URL builder, metadata generators) - Add lib/og-templates.ts (4 templates: home, component, docs, page) - Add app/api/og/route.ts (ImageResponse with category badges, footer) - Migrate static page content from inline to MDX files with frontmatter - Add lib/content.ts + lib/schemas.ts (gray-matter loader, Zod validation) - Wire generateMetadata with OG + Twitter cards on all 5 routes - Add metadataBase to layout.tsx for absolute OG URLs Closes #100 --- apps/registry/app/api/og/route.ts | 220 ++++++++++++++++++ apps/registry/app/components/[slug]/page.tsx | 41 ++-- apps/registry/app/components/page.tsx | 23 ++ apps/registry/app/docs/page.tsx | 50 ++-- apps/registry/app/layout.tsx | 3 + apps/registry/app/page.tsx | 50 ++-- apps/registry/app/philosophy/page.tsx | 79 ++----- .../component-preview/component-preview.tsx | 10 +- apps/registry/content/pages/components.mdx | 9 + apps/registry/content/pages/docs.mdx | 33 +++ apps/registry/content/pages/home.mdx | 33 +++ apps/registry/content/pages/philosophy.mdx | 62 +++++ apps/registry/lib/content.ts | 22 ++ apps/registry/lib/og-templates.ts | 62 +++++ apps/registry/lib/og.ts | 72 ++++++ apps/registry/lib/schemas.ts | 18 ++ apps/registry/package.json | 7 +- pnpm-lock.yaml | 89 ++++++- 18 files changed, 749 insertions(+), 134 deletions(-) create mode 100644 apps/registry/app/api/og/route.ts create mode 100644 apps/registry/content/pages/components.mdx create mode 100644 apps/registry/content/pages/docs.mdx create mode 100644 apps/registry/content/pages/home.mdx create mode 100644 apps/registry/content/pages/philosophy.mdx create mode 100644 apps/registry/lib/content.ts create mode 100644 apps/registry/lib/og-templates.ts create mode 100644 apps/registry/lib/og.ts create mode 100644 apps/registry/lib/schemas.ts diff --git a/apps/registry/app/api/og/route.ts b/apps/registry/app/api/og/route.ts new file mode 100644 index 0000000..1deca16 --- /dev/null +++ b/apps/registry/app/api/og/route.ts @@ -0,0 +1,220 @@ +import React from "react"; + +import { ImageResponse } from "next/og"; +import type { NextRequest } from "next/server"; +import { z } from "zod"; + +import { OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from "@/lib/og"; +import { + getTemplate, + type OGTemplateType, + truncateText, +} from "@/lib/og-templates"; + +const parametersSchema = z.object({ + category: z.string().max(100).optional(), + description: z.string().max(500).optional(), + title: z.string().min(1).max(200).default("VLLNT UI"), + type: z.enum(["home", "component", "docs", "page"]).default("page"), + url: z.string().max(200).optional(), +}); + +type ValidatedParameters = z.infer; + +function parseSearchParameters( + searchParameters: URLSearchParams, +): Record { + return Object.fromEntries( + [...searchParameters.entries()].filter(([, value]) => value), + ); +} + +export const revalidate = 604_800; + +const h = React.createElement; + +function renderCategoryBadge(category: string): React.ReactElement { + return h( + "div", + { + style: { + alignItems: "center", + display: "flex", + fontSize: 48, + width: "100%", + }, + }, + h( + "span", + { + style: { + border: "2px solid rgba(255, 255, 255, 0.3)", + borderRadius: 9999, + color: "rgba(255, 255, 255, 0.9)", + padding: "12px 32px", + textTransform: "capitalize" as const, + }, + }, + category.replaceAll("-", " ").toLowerCase(), + ), + ); +} + +function renderTitle(title: string, fontSize: number): React.ReactElement { + return h( + "h1", + { + style: { + color: "white", + fontSize, + letterSpacing: "-0.03em", + lineHeight: 1.2, + margin: 0, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, + }, + title, + ); +} + +function renderDescription(text: string): React.ReactElement { + return h( + "p", + { + style: { + color: "rgba(255, 255, 255, 0.75)", + fontSize: 64, + lineHeight: 1.4, + marginTop: 83, + }, + }, + text, + ); +} + +function renderMainContent( + title: string, + titleSize: number, + description?: string, +): React.ReactElement { + return h( + "div", + { + style: { + display: "flex", + flex: 1, + flexDirection: "column", + justifyContent: "center", + maxWidth: 1600, + }, + }, + renderTitle(title, titleSize), + description ? renderDescription(description) : undefined, + ); +} + +function renderFooterLabel(label: string): React.ReactElement { + return h( + "span", + { + style: { + color: "rgba(255, 255, 255, 0.5)", + fontSize: 42, + letterSpacing: "0.15em", + }, + }, + label, + ); +} + +function renderFooter(footerLabel?: string): React.ReactElement { + return h( + "div", + { + style: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + width: "100%", + }, + }, + h( + "span", + { style: { color: "rgba(255, 255, 255, 0.8)", fontSize: 48 } }, + "ui.vllnt.com", + ), + footerLabel ? renderFooterLabel(footerLabel) : undefined, + ); +} + +function renderOGElement( + parameters: ValidatedParameters, + templateType: OGTemplateType, +): React.ReactElement { + const template = getTemplate(templateType); + const displayTitle = truncateText(parameters.title, template.titleMaxLength); + const displayDescription = + parameters.description && template.descriptionMaxLength + ? truncateText(parameters.description, template.descriptionMaxLength) + : parameters.description; + + return h( + "div", + { + style: { + alignItems: "flex-start", + backgroundColor: "black", + color: "white", + display: "flex", + flexDirection: "column", + fontFamily: "system-ui, sans-serif", + height: "100%", + justifyContent: "space-between", + padding: 120, + width: "100%", + }, + }, + template.showCategory && parameters.category + ? renderCategoryBadge(parameters.category) + : h("div", { style: { display: "flex" } }), + renderMainContent(displayTitle, template.titleSize, displayDescription), + renderFooter(template.footerLabel), + ); +} + +function formatZodErrors(zodError: z.ZodError): string { + return zodError.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join(", "); +} + +function handleError(error: unknown): Response { + if (error instanceof z.ZodError) { + return new Response(`Invalid parameters: ${formatZodErrors(error)}`, { + status: 400, + }); + } + return new Response( + `Failed to generate image: ${error instanceof Error ? error.message : "Unknown error"}`, + { status: 500 }, + ); +} + +export function GET(request: NextRequest): ImageResponse | Response { + try { + const { searchParams } = new URL(request.url); + const raw = parseSearchParameters(searchParams); + const parameters = parametersSchema.parse(raw); + const element = renderOGElement(parameters, parameters.type); + + return new ImageResponse(element, { + height: OG_IMAGE_HEIGHT, + width: OG_IMAGE_WIDTH, + }); + } catch (error) { + return handleError(error); + } +} diff --git a/apps/registry/app/components/[slug]/page.tsx b/apps/registry/app/components/[slug]/page.tsx index 6365917..43ff41b 100644 --- a/apps/registry/app/components/[slug]/page.tsx +++ b/apps/registry/app/components/[slug]/page.tsx @@ -9,6 +9,7 @@ import { notFound } from "next/navigation"; import { ComponentPreview } from "@/components/component-preview"; import { QuickAdd } from "@/components/quick-add"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; import { getCategoryForComponent, getSidebarSections, @@ -87,31 +88,33 @@ export async function generateMetadata(props: Props): Promise { const componentDirectory = getComponentDirectory(component); const headerPath = path.join(componentDirectory, "header.mdx"); + const category = getCategoryForComponent(slug); + + let title = `${component.title} - VLLNT UI`; + let description = component.description; try { const headerContent = await readFile(headerPath, "utf8"); const { metadata } = parseFrontmatter(headerContent); - - return { - description: metadata.description || component.description, - openGraph: { - description: metadata.description || component.description, - title: metadata.title || component.title, - type: "website", - }, - title: metadata.title || `${component.title} - VLLNT UI`, - twitter: { - card: "summary_large_image", - description: metadata.description || component.description, - title: metadata.title || component.title, - }, - }; + title = metadata.title || title; + description = metadata.description || description; } catch { - return { - description: component.description, - title: `${component.title} - VLLNT UI`, - }; + // header.mdx is optional — use registry.json defaults } + + const ogParameters = { + category, + description, + title: component.title, + type: "component" as const, + }; + + return { + description, + openGraph: generateOGMetadata(ogParameters), + title, + twitter: generateTwitterMetadata(ogParameters), + }; } export default async function ComponentPage(props: Props) { diff --git a/apps/registry/app/components/page.tsx b/apps/registry/app/components/page.tsx index d3cd5e7..e4d9023 100644 --- a/apps/registry/app/components/page.tsx +++ b/apps/registry/app/components/page.tsx @@ -1,13 +1,36 @@ import { Sidebar } from "@vllnt/ui"; +import type { Metadata } from "next"; import Link from "next/link"; import { ComponentPreview } from "@/components/component-preview/component-preview"; +import { getPageContent } from "@/lib/content"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; import { components, getSidebarSections, groupedComponents, } from "@/lib/sidebar-sections"; +export async function generateMetadata(): Promise { + const { frontmatter } = await getPageContent("components"); + const og = frontmatter.og; + + return { + description: frontmatter.description, + openGraph: generateOGMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + title: frontmatter.title, + twitter: generateTwitterMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + }; +} + export default function ComponentsPage() { return ( <> diff --git a/apps/registry/app/docs/page.tsx b/apps/registry/app/docs/page.tsx index 14e707c..a11cb66 100644 --- a/apps/registry/app/docs/page.tsx +++ b/apps/registry/app/docs/page.tsx @@ -1,32 +1,32 @@ import { MDXContent, Sidebar } from "@vllnt/ui"; +import type { Metadata } from "next"; +import { getPageContent } from "@/lib/content"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; import { getSidebarSections } from "@/lib/sidebar-sections"; -export default async function DocumentationPage() { - const gettingStarted = `# Getting Started - -Welcome to VLLNT UI. This is a component registry built with shadcn/ui. - -## Installation - -Install components using the shadcn CLI: - -\`\`\`bash -pnpm dlx shadcn@latest add https://ui.vllnt.com/r/[component-name].json -\`\`\` - -## Usage - -Import components from \`@vllnt/ui\`: - -\`\`\`tsx -import { Button } from '@vllnt/ui' - -export function MyComponent() { - return +export async function generateMetadata(): Promise { + const { frontmatter } = await getPageContent("docs"); + const og = frontmatter.og; + + return { + description: frontmatter.description, + openGraph: generateOGMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + title: frontmatter.title, + twitter: generateTwitterMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + }; } -\`\`\` -`; + +export default async function DocumentationPage() { + const { content } = await getPageContent("docs"); return ( <> @@ -42,7 +42,7 @@ export function MyComponent() {
- +
diff --git a/apps/registry/app/layout.tsx b/apps/registry/app/layout.tsx index 3985332..9f1468d 100644 --- a/apps/registry/app/layout.tsx +++ b/apps/registry/app/layout.tsx @@ -13,6 +13,9 @@ import "./globals.css"; export const metadata: Metadata = { description: "VLLNT UI Component Registry", + metadataBase: new URL( + process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.com", + ), title: "VLLNT UI - Component Registry", }; diff --git a/apps/registry/app/page.tsx b/apps/registry/app/page.tsx index d1ea430..5e5eea3 100644 --- a/apps/registry/app/page.tsx +++ b/apps/registry/app/page.tsx @@ -1,32 +1,32 @@ import { MDXContent, Sidebar } from "@vllnt/ui"; +import type { Metadata } from "next"; +import { getPageContent } from "@/lib/content"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; import { getSidebarSections } from "@/lib/sidebar-sections"; -export default async function HomePage() { - const gettingStarted = `# Getting Started - -Welcome to VLLNT UI. This is a component registry built with shadcn/ui. - -## Installation - -Install components using the shadcn CLI: - -\`\`\`bash -pnpm dlx shadcn@latest add https://ui.vllnt.com/r/[component-name].json -\`\`\` - -## Usage - -Import components from \`@vllnt/ui\`: - -\`\`\`tsx -import { Button } from '@vllnt/ui' - -export function MyComponent() { - return +export async function generateMetadata(): Promise { + const { frontmatter } = await getPageContent("home"); + const og = frontmatter.og; + + return { + description: frontmatter.description, + openGraph: generateOGMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + title: frontmatter.title, + twitter: generateTwitterMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + }; } -\`\`\` -`; + +export default async function HomePage() { + const { content } = await getPageContent("home"); return ( <> @@ -42,7 +42,7 @@ export function MyComponent() {
- +
diff --git a/apps/registry/app/philosophy/page.tsx b/apps/registry/app/philosophy/page.tsx index 51e59cf..bacefc8 100644 --- a/apps/registry/app/philosophy/page.tsx +++ b/apps/registry/app/philosophy/page.tsx @@ -1,61 +1,32 @@ import { MDXContent, Sidebar } from "@vllnt/ui"; +import type { Metadata } from "next"; +import { getPageContent } from "@/lib/content"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; import { getSidebarSections } from "@/lib/sidebar-sections"; -export default async function PhilosophyPage() { - const philosophy = `# Philosophy - -VLLNT UI is built on a set of core principles that guide every design decision and implementation choice. - -## Minimalist Design - -Less is more. Every component is designed with simplicity at its core. We believe that good design should be invisible—it should work seamlessly without drawing unnecessary attention to itself. Our components are: - -- Clean and uncluttered -- Focused on essential functionality -- Free from decorative elements -- Purposeful in every detail - -## Dark Mode First - -Dark mode isn't an afterthought—it's the primary design direction. Every component is crafted with dark backgrounds in mind, ensuring optimal readability and visual comfort. Light mode is available, but dark mode is where VLLNT UI truly shines. - -- Designed for low-light environments -- Reduced eye strain -- Modern aesthetic -- Professional appearance - -## No Emoji Style - -We maintain a professional, text-based approach. Emojis and decorative icons are kept to a minimum, ensuring our components remain timeless and universally applicable. - -- Text-first communication -- Professional tone -- Universal compatibility -- Clean typography - -## Fast Components - -Performance is not negotiable. Every component is optimized for speed and efficiency: - -- Lightweight implementations -- Minimal dependencies -- Optimized rendering -- Fast load times - -## Technical Excellence - -Beyond aesthetics, VLLNT UI prioritizes: - -- **Type Safety**: Built with TypeScript for reliability -- **Accessibility**: WCAG compliant components -- **Customization**: Easy to extend and modify -- **Modern Standards**: Built on latest web technologies - -## Getting Started +export async function generateMetadata(): Promise { + const { frontmatter } = await getPageContent("philosophy"); + const og = frontmatter.og; + + return { + description: frontmatter.description, + openGraph: generateOGMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + title: frontmatter.title, + twitter: generateTwitterMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + }; +} -All components follow these principles. When you use VLLNT UI, you're not just adding components—you're adopting a philosophy of clean, fast, and purposeful design. -`; +export default async function PhilosophyPage() { + const { content } = await getPageContent("philosophy"); return ( <> @@ -71,7 +42,7 @@ All components follow these principles. When you use VLLNT UI, you're not just a
- +
diff --git a/apps/registry/components/component-preview/component-preview.tsx b/apps/registry/components/component-preview/component-preview.tsx index df7c939..4714c91 100644 --- a/apps/registry/components/component-preview/component-preview.tsx +++ b/apps/registry/components/component-preview/component-preview.tsx @@ -1183,13 +1183,13 @@ function AlertDialogPreview() { function HorizontalScrollRowPreview() { return ( - - {Array.from({ length: 6 }, (_, i) => ( + + {Array.from({ length: 6 }, (_, index) => (
- Card {i + 1} + Card {index + 1}
))}
@@ -1199,11 +1199,11 @@ function HorizontalScrollRowPreview() { function ViewSwitcherPreview() { return ( ); diff --git a/apps/registry/content/pages/components.mdx b/apps/registry/content/pages/components.mdx new file mode 100644 index 0000000..106173e --- /dev/null +++ b/apps/registry/content/pages/components.mdx @@ -0,0 +1,9 @@ +--- +title: Components +description: Explore all components available in the VLLNT UI library. +type: component +og: + title: Components + description: Explore all VLLNT UI components + type: component +--- diff --git a/apps/registry/content/pages/docs.mdx b/apps/registry/content/pages/docs.mdx new file mode 100644 index 0000000..185000f --- /dev/null +++ b/apps/registry/content/pages/docs.mdx @@ -0,0 +1,33 @@ +--- +title: Documentation +description: Learn how to use VLLNT UI components in your projects. +type: docs +og: + title: Documentation + description: Learn how to use VLLNT UI components + type: docs +--- + +# Getting Started + +Welcome to VLLNT UI. This is a component registry built with shadcn/ui. + +## Installation + +Install components using the shadcn CLI: + +```bash +pnpm dlx shadcn@latest add https://ui.vllnt.com/r/[component-name].json +``` + +## Usage + +Import components from `@vllnt/ui`: + +```tsx +import { Button } from '@vllnt/ui' + +export function MyComponent() { + return +} +``` diff --git a/apps/registry/content/pages/home.mdx b/apps/registry/content/pages/home.mdx new file mode 100644 index 0000000..01dd851 --- /dev/null +++ b/apps/registry/content/pages/home.mdx @@ -0,0 +1,33 @@ +--- +title: VLLNT UI - Component Registry +description: A component registry built with shadcn/ui. Install components using the shadcn CLI. +type: home +og: + title: VLLNT UI + description: Component Registry built with shadcn/ui + type: home +--- + +# Getting Started + +Welcome to VLLNT UI. This is a component registry built with shadcn/ui. + +## Installation + +Install components using the shadcn CLI: + +```bash +pnpm dlx shadcn@latest add https://ui.vllnt.com/r/[component-name].json +``` + +## Usage + +Import components from `@vllnt/ui`: + +```tsx +import { Button } from '@vllnt/ui' + +export function MyComponent() { + return +} +``` diff --git a/apps/registry/content/pages/philosophy.mdx b/apps/registry/content/pages/philosophy.mdx new file mode 100644 index 0000000..e81dae1 --- /dev/null +++ b/apps/registry/content/pages/philosophy.mdx @@ -0,0 +1,62 @@ +--- +title: Philosophy +description: The principles that guide VLLNT UI design and development. +type: page +og: + title: Philosophy + description: The principles behind VLLNT UI + type: page +--- + +# Philosophy + +VLLNT UI is built on a set of core principles that guide every design decision and implementation choice. + +## Minimalist Design + +Less is more. Every component is designed with simplicity at its core. We believe that good design should be invisible—it should work seamlessly without drawing unnecessary attention to itself. Our components are: + +- Clean and uncluttered +- Focused on essential functionality +- Free from decorative elements +- Purposeful in every detail + +## Dark Mode First + +Dark mode isn't an afterthought—it's the primary design direction. Every component is crafted with dark backgrounds in mind, ensuring optimal readability and visual comfort. Light mode is available, but dark mode is where VLLNT UI truly shines. + +- Designed for low-light environments +- Reduced eye strain +- Modern aesthetic +- Professional appearance + +## No Emoji Style + +We maintain a professional, text-based approach. Emojis and decorative icons are kept to a minimum, ensuring our components remain timeless and universally applicable. + +- Text-first communication +- Professional tone +- Universal compatibility +- Clean typography + +## Fast Components + +Performance is not negotiable. Every component is optimized for speed and efficiency: + +- Lightweight implementations +- Minimal dependencies +- Optimized rendering +- Fast load times + +## Technical Excellence + +Beyond aesthetics, VLLNT UI prioritizes: + +- **Type Safety**: Built with TypeScript for reliability +- **Accessibility**: WCAG compliant components +- **Customization**: Easy to extend and modify +- **Modern Standards**: Built on latest web technologies + +## Getting Started + +All components follow these principles. When you use VLLNT UI, you're not just adding components—you're adopting a philosophy of clean, fast, and purposeful design. diff --git a/apps/registry/lib/content.ts b/apps/registry/lib/content.ts new file mode 100644 index 0000000..bec5047 --- /dev/null +++ b/apps/registry/lib/content.ts @@ -0,0 +1,22 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import matter from "gray-matter"; + +import { type PageFrontmatter, pageFrontmatterSchema } from "./schemas"; + +type PageContent = { + content: string; + frontmatter: PageFrontmatter; +}; + +const CONTENT_DIR = path.join(process.cwd(), "content", "pages"); + +export async function getPageContent(slug: string): Promise { + const filePath = path.join(CONTENT_DIR, `${slug}.mdx`); + const raw = await readFile(filePath, "utf8"); + const { content, data } = matter(raw); + const frontmatter = pageFrontmatterSchema.parse(data); + + return { content, frontmatter }; +} diff --git a/apps/registry/lib/og-templates.ts b/apps/registry/lib/og-templates.ts new file mode 100644 index 0000000..e0d1abd --- /dev/null +++ b/apps/registry/lib/og-templates.ts @@ -0,0 +1,62 @@ +type OGTemplate = { + descriptionMaxLength?: number; + footerLabel?: string; + showCategory: boolean; + titleMaxLength: number; + titleSize: number; +}; + +const DESCRIPTION_MAX = 120; +const TITLE_MAX_LARGE = 25; +const TITLE_MAX_SMALL = 30; + +export const OG_TEMPLATES = { + component: { + descriptionMaxLength: DESCRIPTION_MAX, + footerLabel: "COMPONENT", + showCategory: true, + titleMaxLength: TITLE_MAX_SMALL, + titleSize: 140, + }, + docs: { + descriptionMaxLength: DESCRIPTION_MAX, + footerLabel: "DOCS", + showCategory: false, + titleMaxLength: TITLE_MAX_LARGE, + titleSize: 160, + }, + home: { + descriptionMaxLength: DESCRIPTION_MAX, + footerLabel: undefined, + showCategory: false, + titleMaxLength: TITLE_MAX_LARGE, + titleSize: 160, + }, + page: { + descriptionMaxLength: DESCRIPTION_MAX, + footerLabel: undefined, + showCategory: false, + titleMaxLength: TITLE_MAX_LARGE, + titleSize: 160, + }, +} satisfies Record; + +export type OGTemplateType = keyof typeof OG_TEMPLATES; + +export function getTemplate(type: string): OGTemplate { + if (type in OG_TEMPLATES) { + return OG_TEMPLATES[type as OGTemplateType]; + } + return OG_TEMPLATES.page; +} + +export function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + + const truncated = text.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(" "); + const cutPoint = lastSpace > 0 ? lastSpace : maxLength; + const clean = truncated.slice(0, cutPoint).replace(/[\s,.:;]+$/, ""); + + return `${clean}...`; +} diff --git a/apps/registry/lib/og.ts b/apps/registry/lib/og.ts new file mode 100644 index 0000000..8deeb0d --- /dev/null +++ b/apps/registry/lib/og.ts @@ -0,0 +1,72 @@ +import type { Metadata } from "next"; +import { z } from "zod"; + +export const OG_IMAGE_WIDTH = 2400; +export const OG_IMAGE_HEIGHT = 1260; + +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.com"; + +export const ogImageParametersSchema = z.object({ + category: z.string().max(100).optional(), + description: z.string().max(500).optional(), + title: z.string().min(1).max(200).default("VLLNT UI"), + type: z.enum(["home", "component", "docs", "page"]).default("page"), + url: z.string().max(200).optional(), +}); + +export type OGImageParametersInput = z.input; +export type OGImageParameters = z.infer; + +export function generateOGImageURL(parameters: OGImageParametersInput): string { + const validated = ogImageParametersSchema.parse(parameters); + + const searchParameters = new URLSearchParams({ + title: validated.title, + type: validated.type, + ...(validated.description && { description: validated.description }), + ...(validated.category && { category: validated.category }), + ...(validated.url && { url: validated.url }), + }); + + return `/api/og?${searchParameters.toString()}`; +} + +export function generateOGMetadata( + parameters: OGImageParametersInput, +): Metadata["openGraph"] { + const validated = ogImageParametersSchema.parse(parameters); + const ogImageURL = new URL( + generateOGImageURL(validated), + SITE_URL, + ).toString(); + + return { + description: validated.description, + images: [ + { + alt: validated.title, + height: OG_IMAGE_HEIGHT, + url: ogImageURL, + width: OG_IMAGE_WIDTH, + }, + ], + siteName: "VLLNT UI", + title: validated.title, + type: "website", + url: SITE_URL, + }; +} + +export function generateTwitterMetadata( + parameters: OGImageParametersInput, +): Metadata["twitter"] { + const validated = ogImageParametersSchema.parse(parameters); + const ogImageURL = generateOGImageURL(validated); + + return { + card: "summary_large_image", + description: validated.description, + images: [ogImageURL], + title: validated.title, + }; +} diff --git a/apps/registry/lib/schemas.ts b/apps/registry/lib/schemas.ts new file mode 100644 index 0000000..031538a --- /dev/null +++ b/apps/registry/lib/schemas.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const ogImageFrontmatterSchema = z.object({ + category: z.string().optional(), + description: z.string().optional(), + title: z.string().optional(), + type: z.enum(["home", "component", "docs", "page"]).optional(), +}); + +export const pageFrontmatterSchema = z.object({ + description: z.string().min(1), + og: ogImageFrontmatterSchema.optional(), + title: z.string().min(1), + type: z.enum(["home", "component", "docs", "page"]).default("page"), +}); + +export type PageFrontmatter = z.infer; +export type OGImageFrontmatter = z.infer; diff --git a/apps/registry/package.json b/apps/registry/package.json index a0e0c8b..b32b338 100644 --- a/apps/registry/package.json +++ b/apps/registry/package.json @@ -20,23 +20,26 @@ "@vllnt/ui": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.468.0", "next": "16.1.6", "react": "^19.2.0", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", "shadcn": "canary", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zod": "^4.3.6" }, "devDependencies": { - "eslint": "^9.39.1", "@tailwindcss/typography": "^0.5.16", + "@types/mdx": "^2.0.13", "@types/node": "^22", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.6", "@vllnt/eslint-config": "^1.0.0", "@vllnt/typescript": "^1.0.0", "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", "postcss": "^8.5", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c6e6d7..822a71a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 lucide-react: specifier: ^0.468.0 version: 0.468.0(react@19.2.4) @@ -68,10 +71,16 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.4.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)) + '@types/mdx': + specifier: ^2.0.13 + version: 2.0.13 '@types/node': specifier: ^22 version: 22.19.10 @@ -2589,6 +2598,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3456,6 +3468,10 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3671,6 +3687,10 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -3878,6 +3898,10 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4043,6 +4067,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -4094,6 +4122,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -4947,6 +4979,10 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5045,6 +5081,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -5112,6 +5151,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -5753,6 +5796,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -7940,6 +7986,10 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -8774,8 +8824,8 @@ snapshots: '@babel/parser': 7.29.0 eslint: 9.39.2(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 3.25.76 - zod-validation-error: 4.0.2(zod@3.25.76) + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -9018,6 +9068,10 @@ snapshots: transitivePeerDependencies: - supports-color + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -9225,6 +9279,13 @@ snapshots: graphql@16.12.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -9464,6 +9525,8 @@ snapshots: is-decimal@2.0.1: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -9606,6 +9669,11 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -9670,6 +9738,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + kleur@3.0.3: {} kleur@4.1.5: {} @@ -10773,6 +10843,11 @@ snapshots: scheduler@0.27.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.4: {} @@ -10952,6 +11027,8 @@ snapshots: space-separated-tokens@2.0.2: {} + sprintf-js@1.0.3: {} + stackback@0.0.2: {} statuses@2.0.2: {} @@ -11048,6 +11125,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-final-newline@2.0.0: {} @@ -11656,12 +11735,14 @@ snapshots: dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@3.25.76): + zod-validation-error@4.0.2(zod@4.3.6): dependencies: - zod: 3.25.76 + zod: 4.3.6 zod@3.25.76: {} + zod@4.3.6: {} + zustand@4.5.7(@types/react@19.2.13)(react@19.2.4): dependencies: use-sync-external-store: 1.6.0(react@19.2.4) From c4fe6f3acd48e4a2acdbfe347dc9525d99a89a6a Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:39:51 +0100 Subject: [PATCH 2/5] feat(registry): add OG image preview page for visual review Temporary /og-preview page showing all generated OG images: - 4 static pages + all component pages grouped by category - Lazy-loaded images with URL display for debugging --- apps/registry/app/api/og/route.ts | 37 +++++---- apps/registry/app/og-preview/page.tsx | 114 ++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 apps/registry/app/og-preview/page.tsx diff --git a/apps/registry/app/api/og/route.ts b/apps/registry/app/api/og/route.ts index 1deca16..aba7b05 100644 --- a/apps/registry/app/api/og/route.ts +++ b/apps/registry/app/api/og/route.ts @@ -150,6 +150,19 @@ function renderFooter(footerLabel?: string): React.ReactElement { ); } +const ROOT_STYLE = { + alignItems: "flex-start", + backgroundColor: "black", + color: "white", + display: "flex", + flexDirection: "column", + fontFamily: "system-ui, sans-serif", + height: "100%", + justifyContent: "space-between", + padding: 120, + width: "100%", +} as const; + function renderOGElement( parameters: ValidatedParameters, templateType: OGTemplateType, @@ -161,25 +174,15 @@ function renderOGElement( ? truncateText(parameters.description, template.descriptionMaxLength) : parameters.description; - return h( - "div", - { - style: { - alignItems: "flex-start", - backgroundColor: "black", - color: "white", - display: "flex", - flexDirection: "column", - fontFamily: "system-ui, sans-serif", - height: "100%", - justifyContent: "space-between", - padding: 120, - width: "100%", - }, - }, + const topSection = template.showCategory && parameters.category ? renderCategoryBadge(parameters.category) - : h("div", { style: { display: "flex" } }), + : h("div", { style: { display: "flex" } }); + + return h( + "div", + { style: ROOT_STYLE }, + topSection, renderMainContent(displayTitle, template.titleSize, displayDescription), renderFooter(template.footerLabel), ); diff --git a/apps/registry/app/og-preview/page.tsx b/apps/registry/app/og-preview/page.tsx new file mode 100644 index 0000000..b27b559 --- /dev/null +++ b/apps/registry/app/og-preview/page.tsx @@ -0,0 +1,114 @@ +import { generateOGImageURL } from "@/lib/og"; +import { components, groupedComponents } from "@/lib/sidebar-sections"; + +const STATIC_PAGES = [ + { + description: "Component Registry built with shadcn/ui", + title: "VLLNT UI", + type: "home" as const, + }, + { + description: "Explore all VLLNT UI components", + title: "Components", + type: "component" as const, + }, + { + description: "Learn how to use VLLNT UI components", + title: "Documentation", + type: "docs" as const, + }, + { + description: "The principles behind VLLNT UI", + title: "Philosophy", + type: "page" as const, + }, +]; + +function OGCard({ + label, + ogUrl, + subtitle, +}: { + label: string; + ogUrl: string; + subtitle?: string; +}): React.JSX.Element { + return ( +
+
+ {label} + {subtitle ? ( + {subtitle} + ) : undefined} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`OG +
+ + {ogUrl} + +
+ ); +} + +export default function OGPreviewPage(): React.JSX.Element { + return ( +
+
+
+

OG Image Preview

+

+ {components.length} components + {STATIC_PAGES.length} static pages + = {components.length + STATIC_PAGES.length} OG images +

+
+ +
+

Static Pages

+
+ {STATIC_PAGES.map((page) => ( + + ))} +
+
+ + {groupedComponents.map((group) => ( +
+

+ {group.label} ({group.items.length}) +

+
+ {group.items.map((component) => ( + + ))} +
+
+ ))} +
+
+ ); +} From 4988845aed92da6377bdcbf1a3a4790716823a5d Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:44:15 +0100 Subject: [PATCH 3/5] fix(registry): use ui.vllnt.com as homepage OG title, remove footer URL --- apps/registry/app/api/og/route.ts | 19 +++++-------------- apps/registry/content/pages/home.mdx | 2 +- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/apps/registry/app/api/og/route.ts b/apps/registry/app/api/og/route.ts index aba7b05..c0159f2 100644 --- a/apps/registry/app/api/og/route.ts +++ b/apps/registry/app/api/og/route.ts @@ -131,22 +131,13 @@ function renderFooterLabel(label: string): React.ReactElement { } function renderFooter(footerLabel?: string): React.ReactElement { + if (!footerLabel) { + return h("div", { style: { display: "flex" } }); + } return h( "div", - { - style: { - alignItems: "center", - display: "flex", - justifyContent: "space-between", - width: "100%", - }, - }, - h( - "span", - { style: { color: "rgba(255, 255, 255, 0.8)", fontSize: 48 } }, - "ui.vllnt.com", - ), - footerLabel ? renderFooterLabel(footerLabel) : undefined, + { style: { alignItems: "center", display: "flex", justifyContent: "flex-end", width: "100%" } }, + renderFooterLabel(footerLabel), ); } diff --git a/apps/registry/content/pages/home.mdx b/apps/registry/content/pages/home.mdx index 01dd851..831c1a5 100644 --- a/apps/registry/content/pages/home.mdx +++ b/apps/registry/content/pages/home.mdx @@ -3,7 +3,7 @@ title: VLLNT UI - Component Registry description: A component registry built with shadcn/ui. Install components using the shadcn CLI. type: home og: - title: VLLNT UI + title: ui.vllnt.com description: Component Registry built with shadcn/ui type: home --- From aabcbf7d9ad30447385b5ccf429e695bd87534ec Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:58:34 +0100 Subject: [PATCH 4/5] fix(registry): sync og-preview homepage title to ui.vllnt.com --- apps/registry/app/og-preview/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/registry/app/og-preview/page.tsx b/apps/registry/app/og-preview/page.tsx index b27b559..f035d1a 100644 --- a/apps/registry/app/og-preview/page.tsx +++ b/apps/registry/app/og-preview/page.tsx @@ -4,7 +4,7 @@ import { components, groupedComponents } from "@/lib/sidebar-sections"; const STATIC_PAGES = [ { description: "Component Registry built with shadcn/ui", - title: "VLLNT UI", + title: "ui.vllnt.com", type: "home" as const, }, { From 7a2b0b87bf640e0a4ab988c5ebc9ee3c2b3f0747 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:03:51 +0100 Subject: [PATCH 5/5] chore(registry): remove temporary og-preview page --- apps/registry/app/og-preview/page.tsx | 114 -------------------------- 1 file changed, 114 deletions(-) delete mode 100644 apps/registry/app/og-preview/page.tsx diff --git a/apps/registry/app/og-preview/page.tsx b/apps/registry/app/og-preview/page.tsx deleted file mode 100644 index f035d1a..0000000 --- a/apps/registry/app/og-preview/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { generateOGImageURL } from "@/lib/og"; -import { components, groupedComponents } from "@/lib/sidebar-sections"; - -const STATIC_PAGES = [ - { - description: "Component Registry built with shadcn/ui", - title: "ui.vllnt.com", - type: "home" as const, - }, - { - description: "Explore all VLLNT UI components", - title: "Components", - type: "component" as const, - }, - { - description: "Learn how to use VLLNT UI components", - title: "Documentation", - type: "docs" as const, - }, - { - description: "The principles behind VLLNT UI", - title: "Philosophy", - type: "page" as const, - }, -]; - -function OGCard({ - label, - ogUrl, - subtitle, -}: { - label: string; - ogUrl: string; - subtitle?: string; -}): React.JSX.Element { - return ( -
-
- {label} - {subtitle ? ( - {subtitle} - ) : undefined} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`OG -
- - {ogUrl} - -
- ); -} - -export default function OGPreviewPage(): React.JSX.Element { - return ( -
-
-
-

OG Image Preview

-

- {components.length} components + {STATIC_PAGES.length} static pages - = {components.length + STATIC_PAGES.length} OG images -

-
- -
-

Static Pages

-
- {STATIC_PAGES.map((page) => ( - - ))} -
-
- - {groupedComponents.map((group) => ( -
-

- {group.label} ({group.items.length}) -

-
- {group.items.map((component) => ( - - ))} -
-
- ))} -
-
- ); -}