---
title: Project structure
description: The videos source root and the special files Odori discovers, inherits, and compiles.
---

Odori's primary convention is a root-level `videos/` folder. It can coexist with Next.js, live in a standalone repository, or use another configured source root without changing the meaning of its files. Odori discovers resources by entry filename, not by directory name.

<FileTree>

- videos/
  - layout.tsx
  - components/
    - title-reveal/
      - title-reveal.tsx
      - title-reveal.preview.tsx
    - product/
      - deployment-card.tsx
  - lib/
  - brands/
    - paper.ts
  - launch/
    - video.tsx
    - schema.ts
    - prepare.ts
    - scenes/
  - social/
    - layout.tsx
    - announcement/
      - video.tsx
- components/
  - dashboard/
  - deployments/
- public/
  - fonts/
- .odori/
  - imports.generated.ts
  - catalog.json
  - components.json
  - builds/
- odori.config.ts

</FileTree>

## Special files

| File | Role |
| --- | --- |
| `video.tsx` | Required React entry and static video metadata |
| `layout.tsx` | Inherited format, brand, safe areas, motion, and audio policy |
| `schema.ts` | Serializable input contract and defaults |
| `prepare.ts` | Async work that resolves once before playback or render |
| `brands/*.ts` | Brand modules Studio can preview any video with |
| `scenes/*.tsx` | Ordinary source organization with no discovery semantics |
| `*.preview.tsx` | Development fixture that makes a component available in Studio |

Odori treats every `videos/**/video.tsx` module as an exportable video, every `videos/**/*.preview.tsx` module as a component preview, and every module under a `brands/` directory as a source of brand tokens. Other `.tsx` files remain ordinary source unless a discovered entry imports them.

## Directories are ids

A video's id is its directory path under `videos/`, nested to any depth. The
tree above holds `launch` and `social/announcement`. Two videos cannot collide,
because two directories cannot share a path.

```bash
odori export social/announcement    # writes out/social-announcement.mp4
```

Ids are paths and output files are flat, so an id's separators become dashes in
a written file.

Set `metadata.id` to override the path. That keeps an id stable when a video
moves, at the cost of the guarantee above: an override that names another
video's id fails discovery, reporting both files.

## Configuration

`odori.config.ts` is optional. It names the source root, the export directory, the audio library, the Studio port, the Chrome executable, and any static asset references.

```ts title="odori.config.ts"
import {defineConfig} from "@odori/cli";

export default defineConfig({
  videosDir: "videos",
  exportDir: "out",
  audioDir: "public/audio",
  port: 4300,
  assets: [{reference: "brand-mark", url: "/brand/mark.svg"}],
});
```

Files in `public/` are served at the root of the Studio dev server and by the
render worker, so a font, logo, or sound resolves at the same URL in preview and
in export. `audioDir` is the slice of it Studio lists as an audio library.

## Organize components by ownership

Keep shared video-native components in `videos/components/`. These components can use Odori frame state, timeline contracts, brand context, asset readiness, and video-only transitions.

Keep real application interface components in the application's existing `components/` directory. Videos can import deterministic application components directly:

```tsx title="videos/launch/scenes/product-proof.tsx"
import {DeploymentCard} from "@/components/deployments/deployment-card";

export function ProductProof({deployment}) {
  return <DeploymentCard deployment={deployment} interactive={false} />;
}
```

If an application component depends on routing, live data, or browser state, add a video adapter under `videos/components/product/`. The adapter receives frozen props from `prepare.ts` and removes interactions that cannot produce deterministic frames.

Keep one-off scene components beside their composition. Move a component to `videos/components/` after another video needs it.

## Preview reusable components

A `*.preview.tsx` module is a development-only fixture. It supplies representative props, duration, canvas dimensions, controls, and edge cases so Studio can display a reusable component without turning it into an exportable video.

```tsx title="videos/components/title-reveal/title-reveal.preview.tsx"
import {defineComponentPreview} from "odori/preview";
import {TitleReveal} from "./title-reveal";

export default defineComponentPreview({
  title: "Title reveal",
  category: "Typography",
  component: TitleReveal,
  canvas: {width: 1920, height: 1080, duration: "4s"},
  examples: [
    {name: "Default", props: {title: "Ship the story."}},
    {name: "Two lines", props: {title: "Build videos\nlike applications."}},
  ],
});
```

Studio can play, pause, seek, loop, switch brands, vary props, and test formats against this fixture. Production builds exclude preview modules.

## Generated files

Odori scans video, preview, and brand modules and writes static imports to `.odori/imports.generated.ts`. Studio uses all three. The embedded Player, tests, and render workers consume only the production video graph.

```ts title=".odori/imports.generated.ts"
import Launch, {metadata as launchMetadata} from "../videos/launch/video";
import Social, {metadata as socialMetadata} from "../videos/social/announcement/video";

export const videos = [
  {component: Launch, metadata: launchMetadata},
  {component: Social, metadata: socialMetadata},
];
```

This generated module keeps the module graph compatible with Next.js, tests, browsers, and Node render workers. Odori regenerates `.odori/`, so projects should not edit or commit its cache and build output.

## Why not put videos under `app/`?

`app/` maps URLs to web routes. `videos/` maps IDs to time-based React entries.
Keeping them adjacent makes Next.js compatibility explicit without forcing a
video-only project to adopt application routing.
