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.
The two rules
Section titled “The two rules”- Match the folder to what the code is. Use the table below — one lookup, first match wins.
- 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.
Where does it go?
Section titled “Where does it go?”| I’m adding… | Folder | Example |
|---|---|---|
| A content type editors manage | src/collections/ | posts.ts |
| A site-wide content set | src/globals/ | site-header.ts |
| A block editors add to a page | src/blocks/ | blocks/text/ |
| A reusable Payload field | src/fields/ | fields/link/ |
| A Payload plugin config | src/plugins/ | payload-seo.ts |
| A whole feature — UI plus its own logic | src/features/<name>/ | features/contact-form/ |
| A page-type composition | src/templates/<name>/ | templates/home/ |
| A generic UI primitive | src/components/ui/ | ui/rich-text/ |
| Page chrome (header, footer, nav) | src/components/layout/ | layout/site-header/ |
| A component that knows your collections | src/components/content/ | content/post-list/ |
| A reusable React hook | src/hooks/ | use-button-hover.ts |
| A shared constant | src/config/ | slugs.ts |
| A pure helper (no React) | src/lib/ | lib/url/get-url.ts |
| A TypeScript type guard | src/guards/ | guards/array.ts |
| A page or route | src/app/(frontend)/ | [slug]/page.tsx |
Three rows need a judgement call. The rest of the page is those calls.
components/ splits three ways
Section titled “components/ splits three ways”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/.
Component or feature?
Section titled “Component or feature?”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.
Template-specific components
Section titled “Template-specific components”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.
When a file becomes a folder
Section titled “When a file becomes a folder”The moment a thing needs a second file, give it a folder and colocate both:
collections/pages.ts→collections/pages/(with afields/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.
Naming conventions
Section titled “Naming conventions”- kebab-case for every file and folder —
contact-form.tsx, notContactForm.tsx. -clientsuffix 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.