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 culet
// 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
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.