Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ jobs:
- name: Check component exports
run: bun scripts check:exports

- name: Check llms.txt response sizes
run: |
bun --cwd website velite build
bun scripts check:llms

- name: Check for same HTML tags
run: bun scripts check:nodes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import { Ark } from '@ark-ui/svelte/factory'
</script>

<Ark as="span">
{#snippet asChild(props)}
<a href="#" {...props()}>Ark UI</a>
{/snippet}
</Ark>
1 change: 1 addition & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"check:anatomy": "bun run src/check-anatomy.ts",
"check:exports": "bun run src/check-exports.ts",
"check:llms": "bun run src/check-llms-size.ts",
"check:nodes": "bun run src/check-nodes.ts",
"check:zag": "bun run src/check-zag-versions.ts",
"exports:files": "bun run src/exports-files.ts",
Expand Down
69 changes: 69 additions & 0 deletions scripts/src/check-llms-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Vercel rejects a prerendered response over 20MB (FALLBACK_BODY_TOO_LARGE).
* llms-full.txt crossed it by 37KB once, and `next build` does not catch it.
*
* Rendering the routes here would mean building the site, so this bounds them
* from their inputs instead. An llms-<framework> response is the page corpus
* with that framework's examples and prop tables inlined, so
* corpus + examples + types is an upper bound on the response.
*/
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import { join, resolve } from 'node:path'

const VERCEL_LIMIT_BYTES = 20_000_000
const BUDGET_BYTES = 12_000_000

const FRAMEWORKS = [
{ name: 'react', src: 'src' },
{ name: 'solid', src: 'src' },
{ name: 'vue', src: 'src' },
{ name: 'svelte', src: 'src/lib' },
] as const

const root = resolve('..')
const pagesPath = join(root, 'website/.velite/pages.json')

const dirBytes = (dir: string): number => {
if (!existsSync(dir)) return 0
return readdirSync(dir, { withFileTypes: true }).reduce((sum, entry) => {
const path = join(dir, entry.name)
return sum + (entry.isDirectory() ? dirBytes(path) : statSync(path).size)
}, 0)
}

const exampleBytes = (framework: (typeof FRAMEWORKS)[number]) => {
const componentsDir = join(root, 'packages', framework.name, framework.src, 'components')
if (!existsSync(componentsDir)) return 0
return readdirSync(componentsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.reduce((sum, entry) => sum + dirBytes(join(componentsDir, entry.name, 'examples')), 0)
}

const main = () => {
if (!existsSync(pagesPath)) {
console.error(`No velite output at ${pagesPath}. Run \`bunx velite build\` in website/ first.`)
process.exit(1)
}

const pages: { llm?: string }[] = JSON.parse(readFileSync(pagesPath, 'utf-8'))
const corpus = pages.reduce((sum, page) => sum + Buffer.byteLength(page.llm ?? '', 'utf8'), 0)
const mb = (bytes: number) => `${(bytes / 1e6).toFixed(2)}MB`

const bounds = FRAMEWORKS.map((framework) => ({
name: framework.name,
bytes: corpus + exampleBytes(framework) + dirBytes(join(root, 'website/src/content/types', framework.name)),
})).sort((a, b) => b.bytes - a.bytes)

const worst = bounds[0]
if (worst.bytes <= BUDGET_BYTES) {
console.log(`llms responses bound at ${mb(worst.bytes)} (${worst.name}), budget ${mb(BUDGET_BYTES)}.`)
return
}

console.error(`llms response bound is ${mb(worst.bytes)}, over the ${mb(BUDGET_BYTES)} budget.`)
console.error(`Vercel rejects any prerendered response over ${mb(VERCEL_LIMIT_BYTES)}.`)
for (const bound of bounds) console.error(` llms-${bound.name}.txt <= ${mb(bound.bytes)}`)
process.exit(1)
}

main()
6 changes: 5 additions & 1 deletion scripts/src/generate-example-registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { writeFileSync } from 'node:fs'
import { basename, join } from 'node:path'
import { globby } from 'globby'
import prettier from 'prettier'

const rootDir = join(import.meta.dirname, '../..')

Expand Down Expand Up @@ -165,7 +166,10 @@ export function hasExample(component: string, example: string): boolean {

// Write to website/src/lib/example-registry.ts
const outputPath = join(rootDir, 'website/src/lib/example-registry.ts')
writeFileSync(outputPath, output)

// format before writing, so a build never leaves the tree failing `prettier --check`
const config = await prettier.resolveConfig(outputPath)
writeFileSync(outputPath, await prettier.format(output, { ...config, parser: 'typescript' }))

console.log(`Generated example registry with ${allFiles.length} examples`)
console.log(`Output: ${outputPath}`)
Expand Down
8 changes: 8 additions & 0 deletions website/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ const nextConfig = {
destination: '/docs/overview/getting-started',
permanent: false,
},
{
// llms-full.txt duplicated every page across all four frameworks and
// outgrew Vercel's 20MB prerender cap. The per-framework files carry
// the same content, scoped.
source: '/llms-full.txt',
destination: '/llms.txt',
permanent: false,
},
{
// Exclude `api` so /api/docs* is not treated as a framework docs path.
source: '/:framework((?!api)[^/]+)/docs/:slug*',
Expand Down
30 changes: 0 additions & 30 deletions website/src/app/(llms)/llms-full.txt/route.ts

This file was deleted.

4 changes: 2 additions & 2 deletions website/src/app/(llms)/llms-react.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { cleanupPageContent } from '~/lib/llm-content'
import { getSidebarGroupsWithPages } from '~/lib/sidebar'
import type { Pages } from '.velite'

Expand All @@ -13,7 +13,7 @@ const generateCategorySection = async (group: { title: string; items: Pages[] })
}

export const GET = async () => {
const sidebarGroups = getSidebarGroupsWithPages()
const sidebarGroups = getSidebarGroupsWithPages('react')
const content = await Promise.all(sidebarGroups.map(generateCategorySection)).then((sections) =>
sections.join('\n\n'),
)
Expand Down
4 changes: 2 additions & 2 deletions website/src/app/(llms)/llms-solid.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { cleanupPageContent } from '~/lib/llm-content'
import { getSidebarGroupsWithPages } from '~/lib/sidebar'
import type { Pages } from '.velite'

Expand All @@ -13,7 +13,7 @@ const generateCategorySection = async (group: { title: string; items: Pages[] })
}

export const GET = async () => {
const sidebarGroups = getSidebarGroupsWithPages()
const sidebarGroups = getSidebarGroupsWithPages('solid')
const content = await Promise.all(sidebarGroups.map(generateCategorySection)).then((sections) =>
sections.join('\n\n'),
)
Expand Down
4 changes: 2 additions & 2 deletions website/src/app/(llms)/llms-svelte.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { cleanupPageContent } from '~/lib/llm-content'
import { getSidebarGroupsWithPages } from '~/lib/sidebar'
import type { Pages } from '.velite'

Expand All @@ -13,7 +13,7 @@ const generateCategorySection = async (group: { title: string; items: Pages[] })
}

export const GET = async () => {
const sidebarGroups = getSidebarGroupsWithPages()
const sidebarGroups = getSidebarGroupsWithPages('svelte')
const content = await Promise.all(sidebarGroups.map(generateCategorySection)).then((sections) =>
sections.join('\n\n'),
)
Expand Down
4 changes: 2 additions & 2 deletions website/src/app/(llms)/llms-vue.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { cleanupPageContent } from '~/lib/llm-content'
import { getSidebarGroupsWithPages } from '~/lib/sidebar'
import type { Pages } from '.velite'

Expand All @@ -13,7 +13,7 @@ const generateCategorySection = async (group: { title: string; items: Pages[] })
}

export const GET = async () => {
const sidebarGroups = getSidebarGroupsWithPages()
const sidebarGroups = getSidebarGroupsWithPages('vue')
const content = await Promise.all(sidebarGroups.map(generateCategorySection)).then((sections) =>
sections.join('\n\n'),
)
Expand Down
42 changes: 31 additions & 11 deletions website/src/app/(llms)/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,41 @@ import { getSidebarGroups } from '~/lib/sidebar'

export const dynamic = 'force-static'

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const LABELS: Record<string, string> = { react: 'React', solid: 'Solid', vue: 'Vue', svelte: 'Svelte' }

const SUMMARY =
'Ark UI is a headless component library for building design systems. It ships the same accessible, unstyled components for React, Solid, Svelte, and Vue, built on Zag.js state machines.'

const generateUrl = (framework: string, slug: string) => `https://ark-ui.com/${framework}/docs/${slug}`
export const GET = async () => {
const pageUrl = (framework: string, slug: string) => `https://ark-ui.com/${framework}/docs/${slug}`

const generatePageLinks = (page: { title: string; slug: string }) =>
frameworks.map((framework) => `- [${page.title}](${generateUrl(framework, page.slug)})`).join('\n')
// one H1, first line, then H2 sections (see https://llmstxt.org)
const intro = [
'# Ark UI',
'',
`> ${SUMMARY}`,
'',
'Every page exists once per framework. Links are prefixed with the framework they document.',
'',
'## Full documentation',
'',
...frameworks.map(
(f) => `- [llms-${f}.txt](https://ark-ui.com/llms-${f}.txt): All ${LABELS[f]} documentation in one file`,
),
'',
'- A single page as markdown: `https://ark-ui.com/llms.txt/{slug}`, for example [components/accordion](https://ark-ui.com/llms.txt/components/accordion). Add `?framework=vue` to switch framework.',
].join('\n')

const generateCategorySection = (group: (typeof sidebarGroups)[number]) => {
const header = `# ${group.title.toUpperCase()}\n`
const pageLinks = group.items.map(generatePageLinks).join('\n')
return `${header}\n${pageLinks}`
const section = (group: ReturnType<typeof getSidebarGroups>[number]) => {
const links = group.items.flatMap((page) =>
frameworks.map((f) => `- [${LABELS[f]}: ${page.title}](${pageUrl(f, page.slug)})`),
)
return `## ${group.title}\n\n${links.join('\n')}`
}

const content = sidebarGroups.map(generateCategorySection).join('\n\n')
const content = [intro, ...getSidebarGroups().map(section)].join('\n\n')

return new Response(content)
return new Response(content, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
})
}
2 changes: 1 addition & 1 deletion website/src/app/api/docs/[...slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ type Params = Promise<{ slug: string[] }>

export const GET = async (_request: Request, segmentData: { params: Params }) => {
const { slug } = await segmentData.params
const doc = getDoc(slug.join('/'))
const doc = await getDoc(slug.join('/'))

if (!doc) {
return Response.json({ error: `Documentation page not found: ${slug.join('/')}` }, { status: 404 })
Expand Down
7 changes: 6 additions & 1 deletion website/src/app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TableOfContent } from '~/components/table-of-content'
import { Heading } from '~/components/ui/heading'
import { Text } from '~/components/ui/text'
import { getFramework } from '~/lib/frameworks'
import { cleanupPageContent } from '~/lib/llm-content'
import { getAllPageSlugs, getPageBySlug, getPageNavigation } from '~/lib/pages'
import { getServerContext } from '~/lib/server-context'
import { MDXContent } from '~/mdx-content'
Expand Down Expand Up @@ -46,7 +47,11 @@ export default async function Page(props: Props) {
{currentPage.description}
</Text>
<Box position={{ md: 'absolute' }} top="2" right="2">
<CopyPageWidget slug={currentPage.slug} content={currentPage.llm} />
<CopyPageWidget
slug={currentPage.slug}
framework={framework}
content={await cleanupPageContent(currentPage, framework)}
/>
</Box>
<MDXContent code={currentPage.code} />
</article>
Expand Down
11 changes: 8 additions & 3 deletions website/src/app/llms.txt/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { notFound } from 'next/navigation'
import { getDoc } from '~/lib/docs'
import { type Framework, frameworks } from '~/lib/frameworks'

interface RouteContext {
params: Promise<{ slug: string[] }>
}

export async function GET(_request: Request, context: RouteContext) {
const isFramework = (value: string | null): value is Framework =>
!!value && (frameworks as readonly string[]).includes(value)

export async function GET(request: Request, context: RouteContext) {
const params = await context.params
const doc = getDoc(params.slug.join('/'))
const requested = new URL(request.url).searchParams.get('framework')
const doc = await getDoc(params.slug.join('/'), isFramework(requested) ? requested : 'react')

if (!doc) {
notFound()
}

return new Response(doc.content, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
})
Expand Down
11 changes: 6 additions & 5 deletions website/src/components/copy-page-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { getPublicUrl } from '~/lib/get-public-url'
interface CopyPageWidgetProps {
slug: string
content: string
framework: string
}

export const CopyPageWidget = (props: CopyPageWidgetProps) => {
const { slug, content } = props
const { slug, content, framework } = props
return (
<HStack gap="0" spaceX="-1px">
<CopyPageButton content={content} />
<ActionMenu slug={slug} />
<ActionMenu slug={slug} framework={framework} />
</HStack>
)
}
Expand All @@ -40,8 +41,8 @@ const CopyPageButton = (props: { content: string }) => {
)
}

const ActionMenu = (props: { slug: string }) => {
const { slug } = props
const ActionMenu = (props: { slug: string; framework: string }) => {
const { slug, framework } = props

const pageUrl = getPublicUrl(`/docs/${slug}`)
const readUrl = encodeURIComponent(
Expand All @@ -51,7 +52,7 @@ const ActionMenu = (props: { slug: string }) => {
const items = [
{
label: 'View as markdown',
href: `${pageUrl}.mdx`,
href: `${pageUrl}.mdx?framework=${framework}`,
icon: () => <SiMarkdown size={18} />,
},
{
Expand Down
2 changes: 0 additions & 2 deletions website/src/content/pages/ai/llms.txt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ We provide several LLMs.txt routes to help AI tools access our documentation:

- [llms.txt](https://ark-ui.com/llms.txt) - Contains a structured overview of all components and their documentation
links
- [llms-full.txt](https://ark-ui.com/llms-full.txt) - Provides comprehensive documentation including implementation
details and examples
- [llms-react.txt](https://ark-ui.com/llms-react.txt) - React-specific documentation and implementation details
- [llms-solid.txt](https://ark-ui.com/llms-solid.txt) - SolidJS-specific documentation and implementation details
- [llms-vue.txt](https://ark-ui.com/llms-vue.txt) - Vue-specific documentation and implementation details
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/pages/utilities/format-number.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
id: format
id: format-number
title: Format Number
description: Used to format numbers to a specific locale and options
---
Expand Down
Loading