AstroCraft — home
AstroCraft
Buy a licence
All documentation

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.

Last updated

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

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 hostexample.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.

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:

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:

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. 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:

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.