Skip to content

Structuring a Growing Project

The Project Structure page maps what ships in the box. This page is how to grow it without turning src/ into a junk drawer — and it comes down to two rules.

  1. Match the folder to what the code is. Use the table below — one lookup, first match wins.
  2. Keep code as local as possible. Move it up only when a second, unrelated thing needs it.

Rule 2 is the whole game. Colocation, when to promote a component into a shared folder, when a single file should become a folder — they are all the same idea. A component used by one block lives in that block. It graduates to a shared folder the moment a second, unrelated caller appears — not before. Don’t pre-promote, and don’t scatter a unit’s files across mirrored styles/, types/, and tests/ trees; keep what changes together, together.

I’m adding…FolderExample
A content type editors managesrc/collections/posts.ts
A site-wide content setsrc/globals/site-header.ts
A block editors add to a pagesrc/blocks/blocks/text/
A reusable Payload fieldsrc/fields/fields/link/
A Payload plugin configsrc/plugins/payload-seo.ts
A whole feature — UI plus its own logicsrc/features/<name>/features/contact-form/
A page-type compositionsrc/templates/<name>/templates/home/
A generic UI primitivesrc/components/ui/ui/rich-text/
Page chrome (header, footer, nav)src/components/layout/layout/site-header/
A component that knows your collectionssrc/components/content/content/post-list/
A reusable React hooksrc/hooks/use-button-hover.ts
A shared constantsrc/config/slugs.ts
A pure helper (no React)src/lib/lib/url/get-url.ts
A TypeScript type guardsrc/guards/guards/array.ts
A page or routesrc/app/(frontend)/[slug]/page.tsx

Three rows need a judgement call. The rest of the page is those calls.

The boilerplate ships src/components/ as ui/ and layout/; add content/ yourself once you have components tied to your collections. The split is by what the component knows about:

  • Directorycomponents/
    • Directoryui/ Generic primitives — would exist on any site (cms-link, rich-text)
    • Directorylayout/ Page chrome — the frame around content (site-header, primary-navigation)
    • Directorycontent/ Knows about your collections (post-list) — add this yourself

If a component is used by only one block, feature, or template, it is not shared — it stays there (rule 2), not in components/.

A component renders the props it is given. A feature owns its own data flow — a server action, a validation schema, an email, a third-party call — bundled with its UI in src/features/<name>/.

A button is a component. The contact form is a feature: its folder holds the form, its server action, its schema, its mail template, and its spam check together. Rule of thumb: if you would describe it as a thing the site does rather than a thing on the page, it is a feature.

src/templates/<page-type>/ holds one page-template component per page type (home/home.tsx, work/work.tsx) — the thing that composes a page from blocks and content. A component used by only one template lives inside it (templates/home/components/) and moves to components/content/ only when a second template needs it. That is rule 2 again.

The moment a thing needs a second file, give it a folder and colocate both:

  • collections/pages.tscollections/pages/ (with a fields/ folder) when it grows custom fields.
  • A block is always a folder — it has at least a config and a component.
  • A component becomes a folder when it gains styles, types, or a client/server split.

Don’t pre-create folders for files that will never grow. The folder earns its place when the second file arrives.

  • kebab-case for every file and folder — contact-form.tsx, not ContactForm.tsx.
  • -client suffix for the client half of a server/client split (primary-navigation-client.tsx); the server component keeps the plain name.
  • Import via @/@/components/ui/rich-text/rich-text, never ../../../ chains.
  • One folder, one concern — if you can’t name a folder without “and”, it is two folders.