Project setup
Directory layout, aliases and conventions.
This page describes how this repository is organised and which conventions the components assume. You do not have to copy the layout — but the two aliases in the last section are load-bearing.
Directory layout
app/
(site)/ # header + footer shell
(marketing)/page.tsx # /
components/ # /components and /components/[slug]
docs/[[...slug]]/ # /docs and /docs/[slug]
(preview)/preview/[slug]/ # bare full-page previews
layout.tsx # html, fonts, metadata, toaster
globals.css # design tokens — the neutral ramp, hue families, motion
opengraph-image.tsx # site-wide social card
icon.png / apple-icon.png / favicon.ico # the Join Way monogram, by convention
manifest.ts / sitemap.ts / robots.ts
components/
site/ # documentation chrome
previews/ # one live demo per component
ui/ # local shadcn-style primitives
content/
docs/ # MDX guides
lib/
registry/ # metadata, helpers, build + catalog projections
prompts/ # AI prompt generation
search/ # local search index and scorer
docs/ # navigation and sidebar trees
mdx/ # MDX loading, TOC extraction, rehype plugin
highlight.ts # Shiki, one highlighter for the whole build
shiki-theme.ts # low-chroma syntax themes, one per mode
registry/
components/ # the components the CLI ships
public/
r/ # generated registry items (git-ignored)
scripts/
build-registry.ts
validate-registry.ts
types/
registry.ts # the content modelThe separation that matters
Four kinds of code live in this repository, and they are deliberately kept apart:
Path aliases
Both aliases below appear inside registry component source, so they must resolve in the consuming project too.
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}@/lib/utils must export cn:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}The shadcn CLI creates this file for you during init, and every Join UI
registry item lists utils in its registryDependencies so it is installed
automatically when missing.
Where components land
The registry writes to components/joinui/<name>.tsx. That is set once, in
lib/site.ts:
export const siteConfig = {
namespace: "@joinui",
installTarget: "components/joinui",
// …
} as constChanging installTarget changes the target path in every registry item, every
install instruction and every generated AI prompt, because all three are derived
from it.
TypeScript configuration
The project runs with strict plus two extras worth adopting:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true
}
}noUncheckedIndexedAccess is why array lookups in the components are guarded
rather than assumed — items[index] is T | undefined, and the components
handle that instead of asserting it away.
Scripts
| Script | What it does |
|---|---|
pnpm dev | Development server |
pnpm build | Runs registry:build, then the production build |
pnpm registry:build | Regenerates registry.json and public/r/*.json |
pnpm registry:validate | Schema-checks metadata, files, previews and links |
pnpm typecheck | tsc --noEmit |
pnpm lint | ESLint |
pnpm check | Validate, typecheck and lint in one pass |