Conventions

Carats resolves a handful of paths and filenames by convention. Deviating from them works until it does not, so it is worth keeping to them.

Page Folders

Each route lives in its own folder under src/client/pages, and the folder exports a default component from index.tsx next to a stylesheet of the same name. Folders that start with an underscore are not routes; they are shared building blocks referenced by the routes that need them.

src/client/pages/
├── _layout/          # shared building block, not a route
│   ├── index.tsx
│   └── _layout.sass
├── _error/           # suspense.error
│   └── index.tsx
├── _not_found/       # suspense.notFound
│   └── index.tsx
├── home/             # '/'
│   ├── index.tsx
│   └── home.sass
├── market/           # '/market'
│   ├── index.tsx
│   └── market.sass
├── profile/          # '/profile'
│   ├── index.tsx
│   └── profile.sass
└── trade/            # '/trade/:symbol'
    ├── index.tsx
    └── trade.sass

The leading underscore is what marks a folder as a building block rather than a page. It keeps shared components out of the way in the file tree and signals that they are not meant to be routed to.

Shared Layout

There is no file-convention layout like Next.js. A _layout component is imported by the pages that need it, which makes the composition explicit and lets a page opt out. The reference template wraps every page in the same layout to share the header, the footer and the mobile menu.

// src/client/pages/profile/index.tsx
import { Burnish } from '@carats/render';
import Layout from '../_layout';
import './profile.sass';

export default Burnish(function (user: User) {
    this.head = <>
        <title>Profile for {user.name}</title>
    </>;
    return (
        <Layout>
            <h1>{user.name}</h1>
        </Layout>
    );
});

The layout renders `props.children` where the page content belongs, which is the only composition mechanism. There is no nested-route tree and no automatic wrapping, so the dependency is visible in the import.

Error and Not Found Pages

The error and notFound suspense components are ordinary components. The error one receives the thrown Error, so it can read message and stack; the notFound one receives nothing. Both are worth wrapping in the shared layout so a failure still renders the site chrome.

Global DTOs

Interfaces placed in src/dto are global, because typeRoots points at that folder. That is what lets a culet be written as culetwith no import in both the server entrypoint and the page component. Keep these files free of imports and runtime code; they are declarations only.

// src/dto/user.d.ts
interface User {
  id: string
  name: string
  username: string
  email: string
  phone: string
  website: string
}

The interface has no `export`, which is what makes it global. With `typeRoots` pointing at `src/dto`, `culet` and `Burnish` resolve in every file without an import, and a rename in one place is picked up everywhere.

Stylesheets

A page or component imports its own SASS file, and that file imports base.sass with @use ... as *. The extension is optional in the path. Keep global resets and tokens in base.sass and nothing else there.

// src/client/pages/trade/trade.sass
@use '../../base' as *

#trade
  .trade-header
    display: flex

The extension in the `@use` path is optional, and the path is relative to the stylesheet rather than to the component. `base.sass` is the only file that defines tokens; every other file consumes them.

Naming

Route folders are named after the route segment, and the dynamic parameter name must be identical in the facet and in the culet. Culet files are named after the action they perform and default export the culet, which is then seated in the server entrypoint.