Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions apps/registry/app/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import { Breadcrumb, MDXContent, Sidebar } from "@vllnt/ui";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import Script from "next/script";

import { getPageContent } from "@/lib/content";
import { DOCS_PAGES, getDocsPage, getDocsPath } from "@/lib/docs-pages";
import {
breadcrumbLd,
jsonLdScriptAttributes,
techArticleLd,
} from "@/lib/jsonld";
import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og";
import { canonical } from "@/lib/seo";
import { getSidebarSections } from "@/lib/sidebar-sections";

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 DOCS_PAGES.map((page) => ({ slug: page.slug }));
}

const ROOT_CHANGELOG_PATH = path.join(process.cwd(), "..", "..", "CHANGELOG.md");
const PACKAGE_CHANGELOG_PATH = path.join(
process.cwd(),
"..",
"..",
"packages",
"ui",
"CHANGELOG.md",
);

async function readChangelogFile(filePath: string): Promise<string> {
try {
return (await readFile(filePath, "utf8")).trim();
} catch {
return "";
}
}

async function readChangelog(): Promise<string> {
const [rootChangelog, packageChangelog] = await Promise.all([
readChangelogFile(ROOT_CHANGELOG_PATH),
readChangelogFile(PACKAGE_CHANGELOG_PATH),
]);

return [
rootChangelog ? `## Repository changelog\n\n${rootChangelog}` : "",
packageChangelog ? `## Package changelog\n\n${packageChangelog}` : "",
]
.filter(Boolean)
.join("\n\n");
}

export async function generateMetadata(props: Props): Promise<Metadata> {
const { slug } = await props.params;
const docsPage = getDocsPage(slug);

if (!docsPage) {
return {
title: "Documentation",
};
}

const { frontmatter } = await getPageContent(`docs/${docsPage.slug}`);
const og = frontmatter.og;
const href = getDocsPath(docsPage);

return {
alternates: { canonical: canonical(href) },
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 DocsSlugPage(props: Props) {
const { slug } = await props.params;
const docsPage = getDocsPage(slug);

if (!docsPage) {
notFound();
}

const { content, frontmatter } = await getPageContent(`docs/${docsPage.slug}`);
const pageContent =
docsPage.slug === "changelog"
? `${content}\n\n${await readChangelog()}`
: content;
const pageUrl = `${SITE_URL}${getDocsPath(docsPage)}`;

return (
<>
<Script
id={`docs-${docsPage.slug}-json-ld`}
{...jsonLdScriptAttributes([
breadcrumbLd([
{ name: "Home", url: SITE_URL },
{ name: "Docs", url: `${SITE_URL}/docs` },
{ name: frontmatter.title, url: pageUrl },
]),
techArticleLd({
description: frontmatter.description,
title: frontmatter.title,
url: pageUrl,
}),
])}
/>
<Sidebar sections={getSidebarSections()} />
<main className="flex-1 overflow-y-auto bg-background">
<div className="container mx-auto px-4 py-16 lg:px-8">
<div className="mb-8">
<Breadcrumb
className="mb-4 text-muted-foreground"
items={[
{ href: "/", label: "Home" },
{ href: "/docs", label: "Docs" },
{ label: frontmatter.title },
]}
/>
<h1 className="text-4xl font-semibold mb-4">
{frontmatter.title}
</h1>
<p className="text-muted-foreground text-lg">
{frontmatter.description}
</p>
</div>

<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="prose prose-lg dark:prose-invert max-w-none prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white prose-p:leading-7 prose-blockquote:mt-6 prose-blockquote:border-l prose-blockquote:pl-6 prose-blockquote:italic prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ol:my-6 prose-ol:ml-6 prose-ol:list-decimal prose-code:relative prose-code:rounded prose-code:bg-muted prose-code:px-[0.3rem] prose-code:py-[0.3rem] prose-code:text-sm prose-pre:my-6 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:py-4 prose-pre:text-sm prose-pre:text-white prose-pre:shadow-lg dark:prose-pre:bg-zinc-900 prose-hr:my-8 prose-hr:border-border prose-table:w-full prose-table:border-collapse prose-table:border prose-table:border-border prose-th:border prose-th:border-border prose-th:bg-muted prose-th:p-2 prose-th:text-left prose-th:font-medium prose-td:border prose-td:border-border prose-td:p-2 prose-img:rounded-lg prose-img:border prose-img:border-border prose-img:shadow-lg prose-a:font-medium prose-a:text-primary prose-a:underline prose-a:underline-offset-4 hover:prose-a:text-primary/80 prose-strong:font-semibold prose-em:italic prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground">
<MDXContent content={pageContent} />
</div>
</div>
</div>
</main>
</>
);
}
55 changes: 43 additions & 12 deletions apps/registry/app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Breadcrumb, MDXContent, Sidebar } from "@vllnt/ui";
import type { Metadata } from "next";
import Link from "next/link";
import Script from "next/script";

import { getPageContent } from "@/lib/content";
import { breadcrumbLd, jsonLdScript } from "@/lib/jsonld";
import { DOCS_PAGES, getDocsPath } from "@/lib/docs-pages";
import {
breadcrumbLd,
jsonLdScriptAttributes,
techArticleLd,
} from "@/lib/jsonld";
import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og";
import { canonical } from "@/lib/seo";
import { getSidebarSections } from "@/lib/sidebar-sections";
Expand Down Expand Up @@ -35,16 +42,19 @@ export default async function DocumentationPage() {

return (
<>
<script
dangerouslySetInnerHTML={{
__html: jsonLdScript(
breadcrumbLd([
{ name: "Home", url: SITE_URL },
{ name: "Docs", url: `${SITE_URL}/docs` },
]),
),
}}
type="application/ld+json"
<Script
id="docs-json-ld"
{...jsonLdScriptAttributes([
breadcrumbLd([
{ name: "Home", url: SITE_URL },
{ name: "Docs", url: `${SITE_URL}/docs` },
]),
techArticleLd({
description: "Learn how to use VLLNT UI components in your projects.",
title: "Documentation",
url: `${SITE_URL}/docs`,
}),
])}
/>
<Sidebar sections={getSidebarSections()} />
<main className="flex-1 overflow-y-auto bg-background">
Expand All @@ -63,8 +73,29 @@ export default async function DocumentationPage() {
</p>
</div>

<nav aria-label="Documentation pages" className="mb-12">
<ul className="grid gap-4 md:grid-cols-2">
{DOCS_PAGES.map((page) => (
<li
className="rounded-lg border border-border bg-card p-4"
key={page.slug}
>
<Link
className="font-medium text-foreground underline underline-offset-4"
href={getDocsPath(page)}
>
{page.title}
</Link>
<p className="mt-2 text-sm text-muted-foreground">
{page.description}
</p>
</li>
))}
</ul>
</nav>

<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="prose prose-lg dark:prose-invert max-w-none prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white prose-p:leading-7 prose-blockquote:mt-6 prose-blockquote:border-l-2 prose-blockquote:pl-6 prose-blockquote:italic prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ol:my-6 prose-ol:ml-6 prose-ol:list-decimal prose-code:relative prose-code:rounded prose-code:bg-muted prose-code:px-[0.3rem] prose-code:py-[0.3rem] prose-code:text-sm prose-pre:my-6 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-black prose-pre:py-4 prose-pre:text-sm prose-pre:text-white prose-pre:shadow-lg dark:prose-pre:bg-zinc-900 prose-hr:my-8 prose-hr:border-border prose-table:w-full prose-table:border-collapse prose-table:border prose-table:border-border prose-th:border prose-th:border-border prose-th:bg-muted prose-th:p-2 prose-th:text-left prose-th:font-medium prose-td:border prose-td:border-border prose-td:p-2 prose-img:rounded-lg prose-img:border prose-img:border-border prose-img:shadow-lg prose-a:font-medium prose-a:text-primary prose-a:underline prose-a:underline-offset-4 hover:prose-a:text-primary/80 prose-strong:font-semibold prose-em:italic prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground">
<div className="prose prose-lg dark:prose-invert max-w-none prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white prose-p:leading-7 prose-blockquote:mt-6 prose-blockquote:border-l prose-blockquote:pl-6 prose-blockquote:italic prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ol:my-6 prose-ol:ml-6 prose-ol:list-decimal prose-code:relative prose-code:rounded prose-code:bg-muted prose-code:px-[0.3rem] prose-code:py-[0.3rem] prose-code:text-sm prose-pre:my-6 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:py-4 prose-pre:text-sm prose-pre:text-white prose-pre:shadow-lg dark:prose-pre:bg-zinc-900 prose-hr:my-8 prose-hr:border-border prose-table:w-full prose-table:border-collapse prose-table:border prose-table:border-border prose-th:border prose-th:border-border prose-th:bg-muted prose-th:p-2 prose-th:text-left prose-th:font-medium prose-td:border prose-td:border-border prose-td:p-2 prose-img:rounded-lg prose-img:border prose-img:border-border prose-img:shadow-lg prose-a:font-medium prose-a:text-primary prose-a:underline prose-a:underline-offset-4 hover:prose-a:text-primary/80 prose-strong:font-semibold prose-em:italic prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground">
<MDXContent content={content} />
</div>
</div>
Expand Down
25 changes: 17 additions & 8 deletions apps/registry/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import { DOCS_PAGES, getDocsPath } from "../../lib/docs-pages";
import registry from "../../registry.json";

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai";
Expand All @@ -14,11 +15,20 @@ type RegistryItem = {
readonly registryDependencies?: readonly string[];
};

const DOC_PAGES: ReadonlyArray<{ slug: string; title: string }> = [
{ slug: "home", title: "Get Started" },
{ slug: "docs", title: "Documentation" },
{ slug: "philosophy", title: "Philosophy" },
{ slug: "components", title: "Components Overview" },
const REFERENCE_PAGES: readonly {
href: string;
slug: string;
title: string;
}[] = [
{ href: "/", slug: "home", title: "Get Started" },
{ href: "/docs", slug: "docs", title: "Documentation" },
...DOCS_PAGES.map((page) => ({
href: getDocsPath(page),
slug: `docs/${page.slug}`,
title: page.title,
})),
{ href: "/philosophy", slug: "philosophy", title: "Philosophy" },
{ href: "/components", slug: "components", title: "Components Overview" },
];

function stripFrontmatter(source: string): string {
Expand Down Expand Up @@ -61,16 +71,15 @@ async function buildLlmsFullTxt(): Promise<string> {
lines.push("");
lines.push("```bash");
lines.push(`pnpm dlx shadcn@latest add ${SITE_URL}/r/<name>.json`);
lines.push(`# Or with npm: npx shadcn@latest add ${SITE_URL}/r/<name>.json`);
lines.push("```");
lines.push("");

for (const page of DOC_PAGES) {
for (const page of REFERENCE_PAGES) {
const body = await readDocPage(page.slug);
if (!body) continue;
lines.push(`## ${page.title}`);
lines.push("");
lines.push(`Source: ${SITE_URL}/${page.slug === "home" ? "" : page.slug}`);
lines.push(`Source: ${SITE_URL}${page.href}`);
lines.push("");
lines.push(body);
lines.push("");
Expand Down
6 changes: 6 additions & 0 deletions apps/registry/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DOCS_PAGES, getDocsPath } from "../../lib/docs-pages";
import registry from "../../registry.json";

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai";
Expand Down Expand Up @@ -73,6 +74,11 @@ function buildLlmsTxt(): string {
lines.push(
`- [Documentation](${SITE_URL}/docs): theming, registry usage, conventions`,
);
for (const page of DOCS_PAGES) {
lines.push(
`- [${page.title}](${SITE_URL}${getDocsPath(page)}): ${page.description}`,
);
}
lines.push(
`- [Philosophy](${SITE_URL}/philosophy): design principles and component patterns`,
);
Expand Down
15 changes: 14 additions & 1 deletion apps/registry/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MetadataRoute } from "next";

import { DOCS_PAGES, getDocsPath } from "../lib/docs-pages";
import registry from "../registry.json";

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai";
Expand Down Expand Up @@ -38,6 +39,13 @@ export default function sitemap(): MetadataRoute.Sitemap {
},
];

const docsRoutes: MetadataRoute.Sitemap = DOCS_PAGES.map((page) => ({
url: `${SITE_URL}${getDocsPath(page)}`,
lastModified,
changeFrequency: "monthly",
priority: 0.75,
}));

const items = (registry as { readonly items: readonly RegistryItem[] }).items;

const componentRoutes: MetadataRoute.Sitemap = items.map((item) => ({
Expand All @@ -62,5 +70,10 @@ export default function sitemap(): MetadataRoute.Sitemap {
})),
];

return [...staticRoutes, ...componentRoutes, ...registryEndpoints];
return [
...staticRoutes,
...docsRoutes,
...componentRoutes,
...registryEndpoints,
];
}
23 changes: 15 additions & 8 deletions apps/registry/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,29 @@ type Registry = {
};

export function Header() {
const router = useRouter();
const { push } = useRouter();
const registry = registryData as Registry;

const navItems = [
{ href: "/", title: "Get Started" },
{ href: "/docs", title: "Docs" },
{ href: "/philosophy", title: "Philosophy" },
{ href: "/components", title: "Components" },
];

const searchItems = registry.items
.filter((item) => item.type === "registry:component")
.map((item) => ({
const searchItems = registry.items.reduce<
{ description?: string; id: string; title: string }[]
>((items, item) => {
if (item.type !== "registry:component") return items;

items.push({
description: item.description,
id: item.name,
title: item.title,
}));
});

return items;
}, []);

return (
<NavbarSaas
Expand All @@ -42,18 +49,18 @@ export function Header() {
groupHeading="Components"
items={searchItems}
onSelect={(item) => {
router.push(`/components/${item.id}`);
push(`/components/${item.id}`);
}}
searchPlaceholder="Search components..."
/>
<a
aria-label="VLLNT UI on GitHub"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
className="inline-flex size-9 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
href={GITHUB_URL}
rel="noreferrer"
target="_blank"
>
<Github className="h-4 w-4" />
<Github className="size-4" />
</a>
</div>
}
Expand Down
Loading
Loading