Docs

This page isn't translated yet

next

Project structure

The three-tier layout every Systhema project shares and where your own code belongs.

On this page

Use these file-ownership rules to decide where to put your code. Payload projects have managed routes and starter scaffolds; HTML and Next.js projects without Payload do not have the managed CMS route group.

The three tiersLink to this section

Every file in src/ falls into exactly one bucket:

  1. Framework-managed: refreshed during systhema upgrade, with local edits preserved for review beside a .new file. Carries a DO NOT MODIFY banner. Treat as part of @systhemaui/*; do not edit.
  2. Starter scaffolds, written once at scaffold time, preserved on every subsequent upgrade. Yours to customize (logo, fonts, providers, metadata, 404 design). Carries a "Starter file scaffolded by Systhema" banner.
  3. Your territory, entirely yours. Custom blocks, components, lib utilities, access functions, custom routes, anything not produced by the framework.

The (systhema)/ route group and src/proxy.ts are framework-managed. The (payload)/ route group is Payload's own: create-app-files writes it once when it is missing and upgrades never rewrite it; edit only its custom.scss, and let payload generate:importmap rewrite admin/importMap.js. src/app/layout.tsx, src/app/(site)/template.tsx, src/app/(site)/globals.css, src/app/(site)/not-found.tsx, and src/app/robots.txt/route.ts are starter scaffolds. Everything else, src/blocks/, src/components/, src/lib/, src/access/, custom routes, is yours.

TierBehaviour on upgradeExamples
Framework-managedRefreshed; local edits produce .newsrc/proxy.ts, src/app/(site)/(systhema)/[[...segments]]/page.tsx, (systhema)/(sitemaps)/sitemap.xml/route.ts, (systhema)/sys/[route]/route.ts
Starter scaffoldsWritten once, never overwrittensrc/app/layout.tsx, src/app/robots.txt/route.ts, src/app/(site)/template.tsx, src/app/(site)/globals.css, src/app/(site)/not-found.tsx
Your territoryUntouched by Systhemasrc/blocks/, src/components/, src/lib/, src/access/, src/app/about/page.tsx, anything you author

With frontend locales enabledLink to this section

Configuring locales adds no glue files, src/proxy.ts and the catch-all page.tsx stay byte-identical. The one file that changes shape is the site shell, and the tiers still hold:

FileTierNotes
src/app/(site)/shell.tsxStarter scaffoldReplaces template.tsx. Your fonts, branding, header/footer, receives the route locale as a prop.
(site)/(systhema)/[[...segments]]/layout.tsxFramework-managedBoundary layout: resolves the route locale and injects it into your shell.
(site)/(systhema)/[[...segments]]/not-found.tsxFramework-managedRe-exports your (site)/not-found.tsx so a thrown notFound() renders inside the shell.

Your 404 stays where it was, src/app/(site)/not-found.tsx is still the file you edit. Disabling locales reverses all of this and restores template.tsx.

Project shapesLink to this section

The three shapes match the HTML, Next.js and Payload starter templates. Match the template, don't invent your own layout.

HTMLLink to this section

project-root/
├── index.html                      # entry — loads dist/style.css
├── package.json
├── systhema.config.js              # JS variant (HTML projects don't have TS)
└── src/
    ├── style.css                   # @import '@systhemaui/core/tailwind'
    └── tokens/                     # design tokens (your territory)

No router, no app shell. The HTML shape only consumes @systhemaui/core.

Next.js without CMSLink to this section

project-root/
├── next.config.ts
├── package.json
├── systhema.config.ts
├── public/
└── src/
    ├── app/
    │   ├── globals.css             # @import '@systhemaui/core/tailwind'  ← starter scaffold
    │   ├── layout.tsx              # RootLayout — fonts, metadata, providers  ← starter scaffold
    │   └── page.tsx                # home page — your territory
    ├── blocks/                     # custom blocks — your territory (optional)
    ├── components/                 # custom React components — your territory (optional)
    ├── lib/                        # shared utilities — your territory (optional)
    └── tokens/                     # design tokens

Flat src/app/, no route groups. Add src/blocks/, src/components/, src/lib/ when you need them. There is no (site)/ segment in a Next-only project; Payload is the reason (site)/ exists in the next shape.

Next.js + PayloadLink to this section

project-root/
├── next.config.ts                       # wraps in withPayload(...)
├── register-env.sh                      # loads .env into shell for systhema-core CLI
├── .env / .env.example                  # PAYLOAD_SECRET, DATABASE_URI, NEXT_PUBLIC_SERVER_URL
├── public/                              # static assets + uploads/ symlink
├── storage/                             # SQLite database + filesystem upload store
└── src/
    ├── payload.config.ts                # buildConfig wrapped in withSysthema(...)
    ├── payload-types.ts                 # generated by `payload generate:types`
    ├── proxy.ts                         # systhemaGateway(...)  ← framework-managed
    ├── tokens/                          # design tokens
    ├── blocks/                          # custom blocks — your territory (see "Block mirror pattern")
    ├── components/                      # custom React components — your territory
    ├── lib/                             # shared utilities — your territory
    ├── access/                          # custom capabilities + access functions — your territory
    └── app/
        ├── layout.tsx                   # outer layout — starter scaffold
        ├── robots.txt/route.ts          # host-aware robots.txt — starter scaffold
        ├── (payload)/                   # Payload admin segment — Payload's, written once
        │   ├── layout.tsx
        │   ├── custom.scss
        │   ├── admin/
        │   └── api/
        └── (site)/                      # public-facing site
            ├── globals.css              # @import '@systhemaui/core/tailwind'  ← starter scaffold
            ├── template.tsx             # RootLayout + RootHeader + RootFooter  ← starter scaffold
            ├── not-found.tsx            # 404 page — starter scaffold
            ├── (systhema)/              # CMS-driven routes — framework-managed
            │   ├── [[...segments]]/page.tsx
            │   ├── (sitemaps)/sitemap.xml/route.ts
            │   └── sys/[route]/route.ts
            └── about/page.tsx           # custom static page — your territory (example)

The (site)/ segment exists so the public site can coexist with the (payload)/ admin segment; both share src/app/layout.tsx. Your custom pages mount under (site)/ so they inherit template.tsx's header/footer.

What's shared across all threeLink to this section

  • src/tokens/, always, every shape. Tokens are the design-system source of truth.
  • systhema.config.{ts,js} at the project root, always, every shape.
  • "Your territory" folders (src/blocks/, src/components/, src/lib/, etc.), always optional, always your call to create.

Block mirror patternLink to this section

A Systhema custom block has two halves: a Payload schema and a React component. Colocate them in one folder rather than splitting them across src/blocks/ and src/components/:

src/blocks/Hero/
├── index.ts            # Payload schema — fields, lexical features, customBlocks registration
└── component.tsx       # React renderer — receives Payload props, returns JSX

Register the block objects through buildConfig(withSysthema(baseConfig, options)), with options.customBlocks containing each schema and converter. See Custom blocks for the complete registration contract. The same colocation works in Next-only projects. src/blocks/Hero/component.tsx can be imported and rendered with hardcoded props until Payload arrives. The systhema:building-sites agent skill covers the pattern in detail.

lib/ over utils/Link to this section

One name for shared utilities: src/lib/, never src/utils/. Backend helpers, shared formatters, request/response builders all live here. Frontend-only utilities can colocate next to the consuming component (src/components/foo/utils.ts) or share via src/lib/ if cross-cutting.

This avoids the most common drift in real Systhema projects, half use lib/, half use utils/, a few use both.

Upgrading from Next-only to Next + PayloadLink to this section

Adding Payload to a Next-only project is mostly additive. Existing src/blocks/, src/components/, src/lib/ survive untouched; the integration adds Payload's app-router segment and the framework-managed routes.

StepWhat changes
1. Install packagesAdd @systhemaui/payload, payload, db driver (@payloadcms/db-sqlite, @payloadcms/db-postgres, …).
2. Create src/payload.config.tsNew file. Wrap buildConfig({...}) in withSysthema(...). Set secret, db.
3. Move src/app/page.tsx → src/app/(site)/page.tsx, same for globals.cssThe only file-move step. Everything else is additive.
4. Run systhema-core payload create-app-filesScaffolds src/proxy.ts, src/app/(site)/(systhema)/, src/app/(site)/template.tsx, not-found.tsx, and src/app/(payload)/ when the project has none. Then run payload generate:importmap.
5. Wire existing custom blocksAdd CustomBlock objects to customBlocks: [myBlock] in the plugin options. Each object provides type, data, editor and converter; register browser converters separately for client preview.
6. Set packages: { payload: true } in systhema.config.tsActivates Payload-specific token outputs and admin-UI utilities.
7. Run pnpm syncRegenerates artifacts and the configured Payload outputs. If you changed package versions, review the separate upgrade and database migration plan.

The full guide, including diagnosis of what survives, the customBlocks wiring, and token migrations, is in the systhema:building-sites agent skill.

Customizing the content modelLink to this section

The editor's content model is the page. Start from the assumption that every page is a default page (the hero group plus one root content field composed from native blocks) and that the home page can use the same default template. Add a custom template only when its shell or data requirements differ. Add schema only where the system has no answer, and decide top down:

You need…Use
A pageA default page composed from native blocks.
A band repeated on several pagescomponents: true and the component block.
A page whose renderer needs data or a bespoke layoutA custom page template: one root content field, resolveData for the fetch. A listing page reuses the archive's editorTabs shape and createPostsListingFields().
A kind of postposts.types plus a thin post template (root content and the two or three typed fields the kind cannot render without).
A band no native block renders, or one that runs a queryA custom block, registered at the editors its nearest native block uses, using the same tier as the section or region its renderer supplies. Add design controls only when the native composition cannot express the requirement.
Data with its own identity, read from more than one placeA collection via customCollections.

Typed template fields are for what the page breaks without (a listing's config, a hero, an embed URL); prose is blocks. Custom blocks used on exactly one page, typed prose fields, single-reader collections and a settings global that duplicates the Footer global are the shapes that get rebuilt later. withSysthema replaces the base config's collections and globals arrays with its own set plus customCollections / customGlobals; extend a Systhema-owned collection after the call, not by passing collections.

When in doubtLink to this section

  • File is in src/app/(site)/(systhema)/: framework-managed, keep your application code outside it. src/proxy.ts is also managed; preserve project security changes and review its .new file during upgrades.
  • File is in src/app/(payload)/ → Payload's route group, never rewritten by upgrades; edit only custom.scss.
  • File has a "Starter file scaffolded by Systhema" banner → starter scaffold, edit freely.
  • File is anywhere else under src/ → your territory.

The starter templates are the canonical layouts. Match them; don't invent.