Skip to content
Join UI

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

text
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 model

The 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.

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

@/lib/utils must export cn:

lib/utils.ts
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:

lib/site.ts
export const siteConfig = {
  namespace: "@joinui",
  installTarget: "components/joinui",
  // …
} as const

Changing 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:

tsconfig.json
{
  "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

ScriptWhat it does
pnpm devDevelopment server
pnpm buildRuns registry:build, then the production build
pnpm registry:buildRegenerates registry.json and public/r/*.json
pnpm registry:validateSchema-checks metadata, files, previews and links
pnpm typechecktsc --noEmit
pnpm lintESLint
pnpm checkValidate, typecheck and lint in one pass