The CMS
Urbic ships with a CMS at /admin/. It is the AstroCraft editor, and it is source in src/admin/ rather than a hosted service or a plugin calling home. It reads your Zod schemas, it writes files into your repository, and every edit lands as a real git commit authored by the person who made it.
This chapter covers what Urbic wires up. The operator’s manual — every screen, every setting, the whole authoring workflow — lives at editor.astrocraftthemes.com/docs. These pages do not duplicate it; they tell you what is specific to this theme and link across.
What “git-backed” actually means
There is no separate content database. A saved entry is a file in src/data/, an uploaded image is a file in src/assets/library/, and a composed page is a real .astro file in src/pages/. The CMS commits them.
That has three consequences worth understanding before you deploy:
The running server is a checkout. It needs the source tree, .git and node_modules, because Publish runs your build command in place. This is why Urbic’s Dockerfile is one stage instead of the usual build-then-slim-runtime pattern. See Deployment.
Content review is code review, if you want it to be. Point the publish branch in src/admin.config.ts at something other than main and every content change arrives as commits on a branch a human merges.
There is no parallel schema. src/admin.config.ts can only ever name fields that src/content.config.ts already declares. It cannot invent one and it cannot disagree about a type, so the constraint you write once in Zod is the constraint the editor enforces. How the schema becomes the admin explains which Zod type becomes which control.
How it is mounted
src/admin/mount.ts is an Astro integration. Astro’s file router only looks at srcDir/pages and its middleware only at srcDir/middleware.*, so a package that owns routes hands them over through integration hooks — which is what makes src/admin/ liftable rather than merely tidy. Your src/pages/ stays yours; adopting the CMS adds nothing to it.
Mounting costs four host files, and boundary.test.ts asserts that list on every run so it cannot quietly grow. See Project Structure for what each one does.
Two things in astro.config.mjs exist because of the CMS and should not be changed casually. output: "server" is non-negotiable — the session guard is middleware, and middleware never sees a visitor on a prerendered route, so a statically built admin would ship its HTML and drafts straight past the guard while looking perfect. adminPackage refuses the build under output: "static" and names the route. And trailingSlash: "ignore" is there because the admin serves three URL shapes Astro will not answer under "always"; see Routing for why the failure mode is invisible.
The site itself is unaffected: 31 of 32 routes still prerender.
The screens
Thirteen pages and two endpoints. What each one does in Urbic:
Overview (/admin/) is the dashboard — what is in flight, what is waiting on you, and whether the live site matches the repository.
Sections — one list screen per collection named in src/admin.config.ts. Urbic maps three: Journal (blog), Work (projects) and People (authors). The sidebar generates this group from the config, so a collection added there arrives with its screen rather than as a link to a 404.
The document editor (/admin/editor/<collection>/<slug>/) is where an entry is written. Its side panels carry the frontmatter — the Details, SEO and History tabs — mapped onto whichever fields admin.config.ts named for those roles.
The image library (/admin/images/) lists exactly what is in src/assets/library/. An upload lands there, and src/js/sectionImage.ts resolves a section’s photo by file name out of the same directory — so a photo an author uploads is usable in a section the moment it arrives. The library’s optimise action resamples down to a width ceiling and re-encodes as AVIF; the savings the panel promises are measured with the same function the action applies, so the number shown is the number the commit delivers.
The page composer (/admin/pages/) builds real .astro files out of your section components. It walks src/components/Sections — capital S in the config, because the composer walks that literal path and a case-sensitive filesystem finds nothing under the lowercase spelling.
Review and publish (/admin/review/) is the queue and the Publish button. See Publishing.
Team (/admin/team/) handles roles and invites, Settings (/admin/settings/) holds the build command, the domain and the licence, and the four auth routes handle sign-in, sign-up and the emailed verify and invite links.
Full walkthroughs: The dashboard, Working with entries, The document editor, Details, SEO and History, The image library, Composing pages, Team, roles and invites and Settings.
What Urbic declares for the composer
The composer reads a section’s props off a self-contained interface Props. Most of Urbic’s sections state theirs that way and need no help. Five cannot, and each is declared in src/admin.config.ts for a reason that is a fact about the component rather than an oversight:
Header, PageTransition and NotFoundIllustration take no content at all — the first two are the shell’s own chrome that BaseLayout renders on every page, and the third is an inline SVG whose only props are native <svg> attributes. They are declared as props: [], which says “nothing to edit here” — a different thing from “nobody has told the CMS what this takes”, and the inspector distinguishes the two.
Pager and EnquiryField do take props, but ones a page cannot write: the pager’s neighbours carry a resolved ImageMetadata only the dynamic route holds, and the enquiry field’s option list is also the server’s validation allow-list. Their editable halves are declared; the rest is listed, locked and round-tripped untouched.
BaseLayout gets an override too, mapping its title, description and image props onto the three SEO roles — it is a declaration rather than a derivation because this layout calls its link picture image, which no parser can know is the social image.
If you write a new section that the CMS should compose, state its props as a plain interface Props. Do not extend anything: the prop walker refuses an extending interface outright, because the members it could see would look like the whole list and a prop nobody saw would vanish on the next save.
Accounts and sessions
The first account created at /admin/signup/ is the owner; everyone else arrives by invite. Accounts, sessions, settings, drafts and invite tokens live in a libSQL database pointed at by ASTROCRAFT_DB_URL. Unset, the node adapter installs its filesystem session default — fine locally, and the reason a redeploy logs everyone out if you leave it unset in production.
MAIL_FROM is the sender for invites and verification. It is optional as a value but structurally required: the admin’s actions import it from astro:env/server, so removing the line from the env schema kills the build with [MISSING_EXPORT] rather than warning.
See Your first account, Environment variables and Bring a database.
Removing it
Delete src/admin/, src/components/primitives/ and src/admin.config.ts, then undo the three remaining host edits. boundary.test.ts prints the list on every run. The site keeps output: "server" only for /contact/ — restore prerender = true there and drop contactServer from src/actions/index.ts and it is fully static again.