# Project Structures

A Uniweb project is a pnpm workspace. [Building with Uniweb](./building-with-uniweb.md) introduces the two-package setup — foundation and site, side by side. That's the starting point for most projects. But as a workspace grows — a second site sharing the same foundation, an extension adding specialized section types, multiple brands under one repo — the layout matters.

This guide covers the workspace structures we've found work well, when to use each, and the concrete wiring that connects the pieces.

> **The naming convention.** A site is pure content; a foundation is the site's source code. That's why a single-foundation workspace puts content in `site/` and code in `src/`. The foundation package's `name` is just `src` — folder and package name match. See [Folder name vs package name](#4-folder-name-vs-package-name) below for the multi-foundation cases where they need to differ.

> **Terminology note:** "Workspace" means the top-level directory created by `uniweb create` — the pnpm monorepo root. In co-located layouts, each subdirectory (e.g., `marketing/`, `docs/`) is called a **project** — a self-contained group of a foundation and its sites. Use `uniweb add project` to create one in a step, or `--project` on `add foundation` / `add site` for a project's foundation (`<project>/src/`) or its one site (`<project>/site/`). A project with several sites keeps them in `<project>/sites/` — see [Projects with several sites](#projects-with-several-sites).

---

## How the Pieces Connect

Every Uniweb workspace uses three wiring mechanisms. Understanding them makes all the recipes below predictable.

### 1. Workspace packages

`pnpm-workspace.yaml` declares which directories are packages. The default globs cover only the single layout:

```yaml
# pnpm-workspace.yaml
packages:
  - src
  - site
```

`src` and `site` match sibling packages.

The entries are folder patterns, and the CLI reads them the way pnpm does: `*`, `?` and `[...]` within a folder name, `**` for any depth, `{a,b}` for alternatives, and a leading `!` to exclude what the others match. A folder a wildcard finds counts when it holds a `package.json`. When `uniweb add` creates a package, it lists the folder only if no pattern covers it already, and it edits just this list — comments and the file's other settings stay as they are.

### 2. `file:` dependency

The site's `package.json` references its foundation as a local dependency:

```json
{
  "dependencies": {
    "src": "file:../src"
  }
}
```

The key name (`"src"`) is the foundation package's `name` field — what the foundation declared in its own `package.json`. The path is relative to the site's `package.json`. When packages move, this path changes — that's the main wiring adjustment between layouts.

### 3. `foundation:` in site.yml

The site needs to know which package provides the foundation. `site.yml` declares this:

```yaml
foundation: src
```

The value matches the package name from `package.json`. The build finds the foundation by that name: through the site's `file:` dependency of that name, a foundation in a folder of that name next to the site, or one in `../../foundations/<name>`. It never looks in `node_modules` — a foundation is loaded by the site, not installed as a library.

### 4. Folder name vs package name

These are independent. The folder is what you see on disk; the package name is what pnpm and `site.yml` reference. A package's `name` is whatever its `package.json::name` says — the folder name is just a place.

The convention this guide follows:

| Layout | Folder | Package `name` |
| --- | --- | --- |
| Single foundation | `src/` | `src` (folder name = package name) |
| Co-located, multi-foundation | `marketing/src/`, `docs/src/`, … | `marketing-src`, `docs-src`, … (the `-src` suffix disambiguates them within one workspace, since pnpm requires unique package names) |
| Segregated, multi-foundation | `foundations/marketing/`, `foundations/docs/`, … | `marketing`, `docs`, … (the parent `foundations/` is the disambiguator at the folder level; package names match the leaf folder name) |

In every layout, **folder and package name are the same string** — except in the co-located multi-foundation case, where pnpm's workspace-uniqueness rule forces a per-project suffix. The single-foundation case keeps the names matching: folder `src/`, package `src`.

That's the complete wiring. Workspace globs tell pnpm what's a package, `file:` tells the site where the foundation lives on disk, and `foundation:` tells the build which package it is. Everything else — directory names, nesting depth, number of packages — is convention.

---

## Recipes

Each recipe shows a workspace layout with its wiring. Pick the one that matches your situation, or combine pieces from several.

### Single (siblings)

The default when you run `pnpm create uniweb`. Foundation and site are siblings at the workspace root.

```
my-project/
├── src/                       ← the foundation package
│   ├── package.json           ← name: "src"
│   ├── main.js                ← foundation declarations (name, vars, defaultLayout)
│   ├── styles.css
│   ├── sections/
│   ├── components/
│   └── vite.config.js
├── site/
│   ├── package.json           ← "src": "file:../src"
│   ├── site.yml               ← foundation: src
│   ├── pages/
│   └── vite.config.js
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - src
  - site
```

A site is pure content. A foundation is the site's source code — that's why the foundation package lives in `src/`. Folder and package name match: the folder is `src/` and `package.json::name` is `"src"`.

**When to use it:** One foundation, one site. The workspace root stays clean. Most projects start here and many never need more.

**Trade-off:** Adding a second foundation or site puts it next to the first at the root level. That works for two or three packages, but gets noisy beyond that.

### Segregated

Foundations and sites in separate directories. Create a `blank` workspace and use `uniweb add` with paths — `uniweb add foundation foundations/default` — to build this layout.

```
my-project/
├── foundations/
│   ├── default/
│   │   ├── package.json      ← name: "default"
│   │   └── sections/
│   └── blog/
│       ├── package.json      ← name: "blog"
│       └── sections/
├── sites/
│   ├── main/
│   │   ├── package.json      ← "default": "file:../../foundations/default"
│   │   └── site.yml          ← foundation: default
│   └── marketing/
│       ├── package.json      ← "default": "file:../../foundations/default"
│       └── site.yml          ← foundation: default
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - foundations/*
  - sites/*
```

`uniweb add` registers each folder it creates by its exact path; globs like these cover every folder under `foundations/` and `sites/` at once. Either works.

The `file:` path goes up two levels from `sites/main/` to the workspace root, then down into `foundations/default/`.

**When to use it:** Multiple sites sharing a foundation, or multiple foundations with clear separation. The foundation is often the primary deliverable — registered to the Uniweb registry and delivered via CDN to sites. The site packages serve as development harnesses for testing the foundation.

**Trade-off:** Deeper nesting means longer `file:` paths. A single-foundation project doesn't benefit from the extra directories.

**Build order:**

Foundations must build before sites — prerender needs each foundation's built `entry.js`. `uniweb build` at the workspace root takes care of it: it builds every foundation, then every extension, then every site. The CLI writes it into the root `package.json` as the `build` script, next to `dev`:

```json
{
  "scripts": {
    "build": "uniweb build",
    "dev": "uniweb dev",
    "dev:marketing": "uniweb dev marketing"
  }
}
```

With several sites, `uniweb dev` runs the first one, `uniweb dev <site>` any other, and `uniweb add site` adds a `dev:<site>` script for each site after the first.

### Co-located projects

Each project is a self-contained directory with its own foundation and site. This is the pattern we used for all of our company sites.

```
my-workspace/
├── marketing/
│   ├── src/
│   │   └── package.json      ← name: "marketing-src"
│   └── site/
│       ├── package.json      ← "marketing-src": "file:../src"
│       └── site.yml          ← foundation: marketing-src
├── docs/
│   ├── src/
│   │   └── package.json      ← name: "docs-src"
│   └── site/
│       ├── package.json      ← "docs-src": "file:../src"
│       └── site.yml          ← foundation: docs-src
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - "*/src"
  - "*/site"
```

The `*/src` and `*/site` globs discover every project's packages automatically. Adding a new project means creating a new directory — no workspace config changes.

**When to use it:** Multiple independent brands or products that share a repo for convenience but don't share foundations. Each project has its own build, its own foundation, its own site. Teams can work in their own directory without stepping on each other.

**Trade-off:** Foundation package names must be globally unique in the workspace — pnpm needs to distinguish them. If two projects both name their foundation `"src"`, pnpm can't link them correctly. Use project-scoped names (`"marketing-src"`, `"docs-src"`) or scoped packages (`"@acme/marketing"`).

**One foundation can't be shared** across projects in this layout without reaching across directory boundaries (a `file:../../other/src` path, which defeats the co-location purpose). If two projects need the same foundation, the segregated layout is a better fit.

### Projects with several sites

Co-located projects whose foundation serves more than one site — one product or solution type per project, its sites side by side.

```
my-workspace/
├── memberships/
│   ├── src/
│   │   └── package.json      ← name: "memberships-src"
│   └── sites/
│       ├── fitness/
│       │   ├── package.json  ← "memberships-src": "file:../../src"
│       │   └── site.yml      ← foundation: memberships-src
│       └── society/
├── courses/
│   ├── src/                  ← name: "courses-src"
│   └── sites/
│       └── academy/
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - "*/src"
  - "*/sites/*"
```

Two lines cover every project and every site in each, so a new project or site needs no change to the file. Listing each project's folders instead (`memberships/src`, `memberships/sites/*`, …) works the same way and names the projects in one place, at two lines each.

Grow it by naming the folders:

```bash
uniweb add foundation --project courses                         # courses/src/, package courses-src
uniweb add site courses/sites/academy --foundation courses-src  # courses/sites/academy/
```

With `*/src` and `*/sites/*` listed, both commands create the folder and leave `pnpm-workspace.yaml` as it is. A site's package name is the last folder of its path, and package names are workspace-wide: two projects can't both hold a site package named `fitness`.

Extensions nest the same way. A project's own go in `memberships/extensions/<name>/`, and one shared by every project goes in `extensions/<name>/` at the workspace root; each site loads the nearest one with the name it asks for.

**When to use it:** Several products or solution types in one repository, each a foundation with several sites.

**Trade-off:** One folder level more than co-located projects, so a site's `file:` path goes up two levels (`file:../../src`).

### Extensions as siblings

An extension adds specialized section types without modifying the primary foundation. It's a separate foundation, loaded at runtime from a URL — for an extension in the workspace, a URL on the site's own origin. The `extensions` template creates this layout.

```
my-project/
├── src/
│   ├── package.json          ← name: "src"
│   └── sections/             ← Header, Hero, Features, Footer
├── extensions/
│   └── effects/
│       ├── package.json      ← name: "effects"
│       └── sections/         ← ParticleHero, AnimatedCounter
├── site/
│   ├── package.json          ← "src": "file:../src"
│   └── site.yml              ← see below
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - src
  - extensions/effects
  - site
```

The extension isn't a `file:` dependency of the site — it's loaded by URL at runtime. `site.yml` declares it:

```yaml
# site/site.yml
foundation: src

extensions:
  - /effects/entry.js
```

`/effects/entry.js` names the extension `effects`. A site finds it in a folder of that name — `effects/` or `extensions/effects/` — in the folder the site is in or a folder above it, up to the workspace root; the nearest one wins. So `effects/` next to `site/` works as well as `extensions/effects/`, and so does an extension at the workspace root for a site in `sites/main/`. The same folder serves the URL everywhere:

- **Dev:** `uniweb dev` builds the extension first if it isn't built, or if its sources changed since, and the dev server serves its build at that URL. After editing the extension while the server runs, run `uniweb build` and reload.
- **Production:** `uniweb build` builds extensions before sites, and each site's build copies the extension's built files into its own `dist/`, under `effects/`.

No root script or Vite plugin is needed for either.

An extension builds like any foundation — same Vite plugin, same output. The difference is in role: it contributes section types but doesn't provide Layout or theme variables. Its `main.js` declares `extension: true`:

```js
// extensions/effects/main.js
export default {
  extension: true
}
```

This causes the build to write `role: "extension"` to the extension's `schema.json`, and to warn if the extension declares `vars` or layouts (which belong to the primary foundation).

Content authors use section types from extensions the same way — `type: ParticleHero` in frontmatter. The runtime resolves the type from whichever foundation provides it. Primary foundation wins if both define the same name; extensions are checked in declared order.

**When to use it:** Adding specialized capabilities (visual effects, data visualization, interactive widgets) that don't belong in a general-purpose foundation. The extension can be developed, tested, and registered independently.

**Trade-off:** Extensions are always runtime-loaded, which means an additional network request on first page load.

See [Extending Your Site](./extending-your-site.md) for the broader extensibility model, including Vite plugins and `head.html` scripts.

### Extensions in segregated layout

When extensions are shared across multiple sites, the segregated layout accommodates them naturally:

```
my-project/
├── foundations/
│   └── default/
│       └── package.json      ← name: "default"
├── extensions/
│   ├── effects/
│   │   └── package.json      ← name: "effects"
│   └── data-viz/
│       └── package.json      ← name: "data-viz"
├── sites/
│   ├── marketing/
│   │   └── site.yml          ← extensions: [/effects/entry.js]
│   └── analytics/
│       └── site.yml          ← extensions: [/effects/entry.js, /data-viz/entry.js]
├── pnpm-workspace.yaml
└── package.json
```

```yaml
# pnpm-workspace.yaml
packages:
  - foundations/*
  - extensions/*
  - sites/*
```

The wiring is the same as the sibling extension recipe, organized for scale. Each site declares the extensions it needs, and finds them by the same rule: a site in `sites/marketing/` looks in `sites/` first, then at the workspace root, where `extensions/effects/` is. Its build copies each one into its own `dist/`.

**When to use it:** Multiple extensions shared across multiple sites — typically a larger project with a team maintaining the extension library.

**Trade-off:** More folders for the same wiring. For a single extension used by one site, the sibling layout is less ceremony.

---

## Choosing a Layout

| Situation | Layout |
| --- | --- |
| One foundation, one site | Single |
| Registering a portable foundation | Segregated (site is the dev harness) |
| Multiple sites sharing a foundation | Segregated |
| Multiple independent brands | Co-located |
| Several products, each a foundation with several sites | Projects with several sites |
| Adding specialized section types | Extensions as siblings |
| Extensions shared across sites | Extensions segregated |

These are starting points. The patterns compose — a co-located project can have a sibling extension, a segregated layout can mount external content with `paths:`. Start with the pattern that matches your primary need, and adjust as the project grows.

---

## Growing a Project

Projects evolve. The `uniweb add` command handles scaffolding, workspace globs, and root scripts — you don't wire things manually.

> **Running the CLI:** Install globally with `npm i -g uniweb` for the best experience — then `uniweb` works everywhere, and inside a project it runs the project's own copy. Keep it current: `create` and `clone` start a project on the CLI that runs them, so a global install stops them when a newer release is out. Without a global install, use `npx uniweb@latest create` to start a project and `pnpm uniweb` inside one (after `pnpm install`).

### Starting from blank

When you know you need a non-default layout — co-located projects, multiple foundations, or a segregated structure — start with a blank workspace and build it up:

```bash
pnpm create uniweb my-workspace --blank
cd my-workspace

# Add a co-located project
uniweb add project main

# Now install and run
pnpm install
pnpm dev
```

The `add project` command creates a co-located foundation + site pair in one step. The same flow works for any layout — the CLI creates the folder you ask for, exactly as you ask:

```bash
# Co-located: grouped by project (preferred for multiple projects)
uniweb add project marketing
uniweb add project docs

# Bare names — folder is at the workspace root
uniweb add foundation marketing       # → ./marketing/   (package: marketing)
uniweb add site main --foundation marketing
uniweb add site docs --foundation blog

# Segregated: pass the path explicitly with a slash
uniweb add foundation foundations/marketing   # → ./foundations/marketing/
uniweb add foundation foundations/blog        # → ./foundations/blog/
uniweb add site sites/main --foundation marketing
uniweb add site sites/docs --foundation blog
```

The CLI creates the directory, writes `package.json` and config files, lists the package in `pnpm-workspace.yaml` (unless a pattern there covers it already), wires the `file:` dependency from site to foundation, and updates root scripts — removing the ones it wrote for a site that no longer exists. Run `pnpm install` once after adding all packages. If a target folder or package name already exists, the command stops with a precise error.

### Adding to an existing workspace

After `pnpm install`, the CLI is available as a project dependency too:

```bash
# Add a second site sharing the existing foundation
uniweb add site blog
pnpm install
```

This creates `blog/` at the workspace root, registers it in `pnpm-workspace.yaml`, and wires the site to the existing foundation. The original site at `site/` stays where it is. To group sites under a `sites/` parent, write the path explicitly: `uniweb add site sites/blog`.

```
my-project/
├── src/                          ← foundation package (name: "src")
├── site/                         ← original
├── blog/                         ← new, wired to the src foundation
└── pnpm-workspace.yaml
```

Adding a named foundation works the same way: `uniweb add foundation marketing` creates `marketing/`, and `uniweb add foundation foundations/marketing` groups it under `foundations/`.

### Adding a co-located project

If you started with a single layout and now need independent projects:

```bash
uniweb add project docs
pnpm install
```

This creates `docs/src/` and `docs/site/`, and lists `*/src` and `*/site` — each only if the workspace's patterns don't cover that folder already. Package names are `docs-src` and `docs-site`. Use `--from` to apply template content: `add project docs --from academic`.

Your original `src/` and `site/` still work — they match their existing globs. You can keep the hybrid layout or move them into a project directory when you're ready.

For a project with several sites, add each one by its folder — `uniweb add site docs/sites/guide --foundation docs-src` — as in [Projects with several sites](#projects-with-several-sites). `--project docs` places the project's foundation (`docs/src/`) or its one site (`docs/site/`) and takes no name of its own: given one, the command stops and names the folder form instead.

### Adding an extension

```bash
uniweb add extension effects --site site
pnpm install
```

This creates `effects/`, lists it in `pnpm-workspace.yaml`, and wires `/effects/entry.js` — where the site loads it from — into the specified site's `site.yml`. Extensions are placed like foundations: to group them, name the folder (`uniweb add extension extensions/effects`). The extension builds like a foundation — run `uniweb build` from the workspace root to build everything in order.

The URL is written only when the site will find this extension there — the folder has to be one the site looks in (see [Extensions as siblings](#extensions-as-siblings)). For any other folder, the command creates the extension, leaves `site.yml` alone, and says where the site looks.

Extensions are always runtime-loaded (via URL, not `file:` dependency). `uniweb dev` builds the extension when it needs to and serves its build; `uniweb build` copies it into the site's `dist/`. See [Extending Your Site](./extending-your-site.md) for the broader extensibility model.

---

## Working with External Content

A site can mount content from outside its own `pages/` directory using `paths:` in `site.yml`. This is useful when content lives in a separate repository, a shared directory, or a submodule.

```yaml
# site/site.yml
paths:
  pages/docs: ../../../docs
```

This mounts the `docs/` directory as if it were `pages/docs/` inside the site. Pages and library files in the external directory are discovered normally.

The path is relative to the site directory. `defineSiteConfig()` in the site's `vite.config.js` reads `paths:` and auto-computes `server.fs.allow` so Vite can serve files from outside the project root — no manual `server.fs.allow` configuration needed.

**When to use it:** Documentation content shared across sites, content maintained by a separate team, or submodule-based content repositories. The Uniweb site itself uses this pattern to mount `packages/docs/` into its page tree.

---

## See Also

- **[Building with Uniweb](./building-with-uniweb.md)** — The two-package model and how CCA works
- **[Templates](../getting-started/templates.md)** — Built-in and official templates
- **[Extending Your Site](./extending-your-site.md)** — Vite plugins, `head.html`, and the full extensibility model
- **[Foundation Categories](./foundation-categories.md)** — Bundled vs portable foundations and when portability matters
- **[Site Configuration](../reference/site-configuration.md)** — Full `site.yml` reference including `paths:`, `extensions:`, and `foundation:`