Skip to content
AstroCraft Docs
On this theme

Project Structure

Urbic is two things in one repository: a studio site, and a CMS that edits it. The directory layout is drawn around keeping those two separable, because one of them gets replaced wholesale on an update and the other never should be.

src/
├── admin/                 the CMS — a package, replaced wholesale on update
├── components/
│   ├── Sections/<Page>/   layout-free page sections (Global/ for cross-page ones)
│   ├── Cards/             content-aware card compositions
│   ├── primitives/        a second primitive copy the CMS imports
│   ├── ui/<name>/         the primitive library — 45 of them
│   └── svg/icons/         the <Icon> system — 574 glyphs
├── config/                typed site config — the source of truth, never literals in components
├── data/<collection>/     content collections, Zod-validated
├── js/                    site utilities + their *.test.ts checks
├── layouts/               BaseLayout + BaseHead (all meta/SEO tags live here)
├── pages/                 file routes — thin shells owning BaseLayout + SEO
├── styles/                global.css entry, tailwind-theme.css tokens, motion/ catalog
├── admin.config.ts        the one file adopting the CMS asks you to write
├── content.config.ts      the collection schemas
└── site.domain.ts         the production host, rewritten by Settings → Site

The three tiers

Pages are thin route shells. A file in src/pages/ owns BaseLayout and the SEO props — title, description, noindex, JSON-LD — and then composes Sections. A section is a layout-free block of page content and never imports BaseLayout. Sections build on Cards (content-aware compositions like ProjectCard and NoteCard) and on ui primitives.

Each tier has a written contract in the repo: src/components/Sections/README.md, src/components/Cards/README.md and src/components/ui/README.md. They are worth reading before you add anything, because they describe what the existing code actually follows rather than an aspiration. The short version of the section rule: a section used by two or more pages moves to Sections/Global/, and data flows in from the route as typed props or the section reads config through the @js helpers — never both for the same data.

Section folders track the pages. Global/ holds eleven shared sections, Home/ eight bands, Blog/, Contact/ and Project/ six each, About/ and Services/ four, Work/ and NotFound/ two, Legal/ one, and UiCatalog/ nine that exist only for the dev-only showroom.

The package boundary

src/admin/ is the CMS, and it is a package that happens to live in your repo. It owns its own pages/, actions/, config/, components/ and middleware, and it hands them to Astro through integration hooks in src/admin/mount.ts rather than by putting files in yours. That is why adopting it adds nothing to src/pages/, and why an update can replace the whole directory.

Mounting it costs the host exactly four files, and src/admin/js/__tests__/boundary.test.ts asserts that list on every run so it cannot quietly grow:

  • astro.config.mjs — one import and adminPackage in integrations. That injects the admin routes, the session guard, and the check that keeps those routes on demand.
  • src/actions/index.ts — one re-export of the package’s own action manifest, spread beside the site’s own contact action.
  • src/admin.config.ts — your four facts: app name, workspace, git remote and branch, and which collections and components the CMS may touch. See Configuration.
  • src/site.domain.ts — the production host as a bare string. It is a host file because Settings → Site rewrites and commits it when an operator connects a domain.

Three directories belong to the package and get replaced on an update: src/admin/, src/components/primitives/ and src/components/svg/. src/components/primitives/ is a second copy of the primitives the admin imports, so the CMS has no dependency on your UI library and you can restyle src/components/ui/ freely without breaking an admin screen. primitiveDrift.test.ts counts what has diverged between the two, so the duplication is measured rather than forgotten.

Removing the CMS is the inverse: delete src/admin/, src/components/primitives/ and src/admin.config.ts, then undo the remaining host edits. The boundary test prints the list on every run.

The two asset directories

src/assets/images/ and src/assets/library/ look interchangeable and are not.

src/assets/library/ is the CMS’s. /admin/images/ lists exactly what is in it, an upload lands in it, and src/js/sectionImage.ts resolves a section’s photo by file name out of it — so a photo an author uploads is usable in a section the moment it arrives. It ships with 18 images.

src/assets/images/ keeps what is bound to a config or content record: projects/, blog/, team/, services/, partners/, testimonials/. Those reach their component through a loader that already exists, they carry alt text on the entry beside them, and they must not become a name an author can swap out from under the record that describes them.

The distinction is enforced rather than documented-only: src/admin/config/imageLibraryData.json.ts states the library path as librarySource, and js/sectionImage.test.ts fails if that constant and the glob in sectionImage.ts ever drift apart.

Where content lives

Three collections under src/data/, each an entry per folder with the folder name as the slug:

Collection Entries Holds
projects 9 The case studies behind /work/
blog 7 Journal notes
authors 1 Bylines, referenced by notes

Schemas are Zod, in src/content.config.ts, so bad frontmatter fails the build with the entry named. See Content Collections.

The dev-only catalog

src/pages/examples/[catalog].astro and src/components/Sections/UiCatalog/ are the primitive showroom at /examples/ui. It is noindex, filtered out of the sitemap, and emits no paths in a production build. It still costs you something, though — Tailwind scans its markup, so its demo classes sit in the stylesheet every page loads. Deleting both once you have finished picking primitives takes the shared stylesheet from 164,378 to 145,575 bytes and drops the @keyframes in it from 108 to 44. Layout covers why @source not is not a substitute for the delete.

NEXT STEPConfiguration