# Configuration

> src/admin.config.ts is the only file you write: five keys naming the facts the CMS cannot derive, and the validator that catches the two typed mistakes.

Source: https://editor.astrocraftthemes.com/docs/configuration/

`src/admin.config.ts` is the one file you author. Everything in it is a fact about **your** site that the admin cannot derive: what to call itself, what it is editing, where it commits, which collections it manages, and where the page composer's building blocks live.

It sits outside `src/admin/` deliberately — a file you write cannot ship inside the thing you are adopting — and beside `src/content.config.ts`, because both are your site's rather than the framework's. Exactly one module in the whole tree reads it, which is what lets the values move later without touching a component.

## The whole file

```ts
import type { AdminConfig } from "./admin/js/adminConfig.ts";
import { siteDomain } from "./site.domain.ts";

export const admin = {
  appName: "AstroCraft",

  workspace: {
    name: "Meridian Coffee",
    domain: siteDomain,
  },

  git: {
    remote: "origin",
    branch: "main",
  },

  collections: {
    blog: {
      name: "blog", // the key in YOUR content.config.ts
      label: "Blog",
      publicPath: "/blog/", // where an entry lives on the published site
      titleField: "title",
      descriptionField: "description",
      socialImageField: "socialImage",
      socialTitleField: "socialTitle",
      socialDescriptionField: "socialDescription",
      dateField: "pubDate",
      updatedField: "updatedDate", // optional — beats dateField for "updated"
      draftField: "draft", // optional — marks an entry unpublished
    },
  },

  components: {
    sections: "src/components/sections",
    layouts: "src/layouts",
  },
} satisfies AdminConfig;
```

`satisfies AdminConfig` is really checked — `pnpm check` runs it — so a misspelled key is a type error rather than a surprise at runtime.

## `appName`

The CMS's own name, not the workspace's. Change it and every surface follows: the sidebar's home link, the sign-in sub-line, the invite caveat, the verification email's subject, the site panel, the review and library rails, the working-tree note and the settings strings.

## `workspace`

`name` is what your **team** calls the thing it edits. It is deliberately separate from `site.config.ts`'s `name`, which is what a **stranger** reads in a search result — some adopters want those to differ.

`domain` is a **bare host** — `example.com`, never `https://example.com`. It becomes `https://cms.<domain>/…` for invite and verification links and `https://<domain>/` wherever a screen points at the live site. It is **imported rather than typed**, from `src/site.domain.ts`, because that file is the single statement of the host and Settings → Site rewrites and commits it when an operator connects a domain. Type a literal here and the CMS's copy stops tracking the site's.

## `git`

The remote's **name** and the default publish branch. There is deliberately no repository URL: the checkout already knows where `origin` points, and every screen derives it from `git remote get-url`. A hand-written copy of that was wrong in this very repository once.

`branch` is only the **default** publish target. The settings store overrides it per deployment, and Publish reads the store — see [Settings](/docs/settings/).

## `collections`

The heart of it, and the place the no-second-schema promise is either real or aspirational.

Each key is a collection key from your `content.config.ts`, and **the key must equal its own `name`** — both are the same identifier and the validator refuses a mismatch. Every other value is the _name of a field that already exists in your schema_. This block cannot declare a field, cannot invent one, and cannot disagree with Zod about a type.

| Key                      | Required | What it names                                                                                                       |
| :----------------------- | :------- | :------------------------------------------------------------------------------------------------------------------ |
| `name`                   | yes      | the collection key in `content.config.ts`; must equal the object's own key                                          |
| `label`                  | yes      | what the sidebar and breadcrumbs call it                                                                            |
| `publicPath`             | yes      | where an entry lives on the published site, e.g. `/blog/` — display text, and the base of the editor's Preview link |
| `titleField`             | yes      | the field carrying the entry's title                                                                                |
| `dateField`              | yes      | the date the list sorts and dates by                                                                                |
| `updatedField`           | no       | a second date; beats `dateField` for "updated"                                                                      |
| `draftField`             | no       | the boolean marking an entry unpublished                                                                            |
| `descriptionField`       | no       | the search description the SEO tab edits                                                                            |
| `socialImageField`       | no       | the link-preview picture                                                                                            |
| `socialTitleField`       | no       | overrides the shared card's title                                                                                   |
| `socialDescriptionField` | no       | overrides the shared card's description                                                                             |

**An absent role is the honest answer.** If your schema has no second date, leave `updatedField` out; the editor renders the fields that exist and draws no control addressing a field that does not. This is worth showing with a second collection whose schema spells everything differently:

```ts
projects: {
  name: "projects",
  label: "Projects",
  publicPath: "/projects/",
  titleField: "title",
  descriptionField: "summary",    // not `description`
  socialImageField: "cover",      // not `socialImage`
  dateField: "completed",         // not `pubDate`
  draftField: "draft",
  // no updatedField — the schema has no second date
  // no social overrides — a project page shares one summary
},
```

That is the claim in the only form anybody can check by looking: the schema was not bent to suit the editor, the editor was told where to look.

Ask for a collection nobody configured and the build throws, because a typo that renders an empty list looks exactly like a collection with no entries in it.

## `components`

Where the page composer finds its building blocks. Two directories, walked rather than imported, so they are strings read off disk at request time:

```ts
components: {
  sections: "src/components/sections",
  layouts: "src/layouts",
  overrides: { /* … */ },
}
```

Both must exist even if empty, both go through path containment (a `..` here would list your file tree on a screen), and both must be under `src/` — a composed page imports through `@/*`, the only alias that reaches an arbitrary directory.

`overrides` is optional and covered in [The component library](/docs/component-library/). Leave a component out entirely and it still appears, drawn with the generic wireframe and whatever props its `interface Props` declares. You add an entry only to correct or complete what the source cannot state.

## `images`

Optional:

```ts
images: { avifQuality: 50 },
```

What the image library's **Optimise** verb encodes at. AVIF's quality scale is not JPEG's; 50 is roughly visually lossless for photography, and it is the default if you omit the object. It is configurable because "how hard may the CMS compress my photographs" is your call.

## The runtime validator

Beyond the type check, `checkAdminConfig` runs at import and throws with the file and the key named. It exists for the value-level mistakes a type cannot state, and there are two you will actually make:

- **A `domain` written as a URL.** `https://example.com` typechecks perfectly as a string and mails a dead link, because the invite builder prefixes `https://cms.`.
- **A collection key that disagrees with its own `name`.** Also a perfectly valid object.

## Troubleshooting

**The build throws naming `admin.config.ts` and a key.** That is the validator. The message names the file and the key; the two cases above are the ones that typecheck.

**A collection renders an empty list.** Check that its key and `name` match a collection actually defined in `content.config.ts`, and that its directory exists under `src/content/`.

**A control appears for a field you removed from the schema.** It cannot — but a _role_ pointing at a removed field can. Delete the role from `admin.config.ts` too; the editor will otherwise address a field Zod no longer declares.

**Renaming the app leaves the old name somewhere.** It should not; `appName` is read from one place. If you find a hard-coded string, that is a bug worth reporting.