Skip to content
AstroCraft Docs
On this theme

Work & Case Studies

/work/ is the studio’s archive, and it is the part of Urbic with the most machinery behind it. Nine case studies, a filter strip, a grid whose slots are fixed by the content rather than by the markup, and a detail page that renders its gallery three different ways.

The index

src/pages/work/index.astro is a thin route shell in the usual shape: it owns BaseLayout and the SEO props, sorts the collection through byOrder, and composes WorkHero, WorkGrid, the shared Testimonials band and the shared Faq.

The <h1> is the mock’s own title with its authored line break flattened, because a <title> tag is one line. That is a small thing worth copying if you write your own routes — the config carries the break, the route removes it for the tag.

byOrder throws on a duplicate order. Two projects claiming slot 4 would otherwise swap on every build, since the loader’s own order is the filesystem’s, and a grid that reshuffles between deploys is very hard to notice and very annoying to debug.

The filter strip

The three categories — homes, apartments, hospitality — come from the category enum on the schema, and the strip that filters by them is a radio group with no JavaScript. Checking a radio changes which cards a CSS rule hides.

One implementation detail there will save you an hour if you restyle it: the hide rules are unlayered on purpose. A display: none written inside @layer components loses to a card’s own flex utility, because a later layer wins regardless of specificity. The same trap applies to the gallery mode toggle on the detail page.

The grid

WorkGrid lays the sorted projects into a scatter — slots of different sizes, in the reading order the design intends. The slot table is zipped by position against the sorted array, which is the second reason order has to be unique and stable.

Each card is Cards/ProjectCard.astro: the cover image, the title, and a caption built from location and year. That is why the schema carries both a short location for the card and a locationFull for the detail hero — a caption and a hero want different lengths of the same fact.

A project page

src/pages/work/[slug].astro composes the six sections in Sections/Project/ plus the shared Pager, each one reading a different part of the frontmatter:

ProjectHero takes the cover, locationFull and the oversized unitless areaValue. ProjectFacts renders the fact pairs beside the 36px serif statement. ProjectStory renders the entry’s body as prose — the only part of a case study that is prose rather than data. MaterialSchedule renders the materials rows. ProjectGallery renders the gallery. And Pager renders the prev/next neighbours.

The pager wraps at both ends, and its neighbours come from src/js/projects.ts using the same order the grid used. The pager is also one of the two components declared by hand in src/admin.config.ts rather than read by the composer’s prop walker, because its neighbours carry a resolved ImageMetadata that only the dynamic route holds — the labels are editable, the entries are listed, locked and round-tripped untouched.

ProjectGallery renders the same gallery array three ways, and the reader picks with a chip group: DETAIL (a full-bleed lead photo followed by scattered plates), MASONRY (a columns-3 flow), and SLIDER (scroll-snap, with four controls that are a progressive enhancement — the slider scrolls perfectly well without them).

The modes are radios again, so switching costs no JavaScript. The mode list lives in galleryModes.ts rather than inside the component, and it carries a runnable check: _project-gallery.css needs one hide-rule per mode, no compiler can see that pairing, so galleryModes.test.ts reads the stylesheet and fails when the two drift.

The chip glyphs are inlined path data rather than registry icons, and that is a deliberate exception to the icon rule — the registry has no grid or column glyph, so there was nothing to reuse. They are drawn in currentColor so a chip that inverts on :checked takes its icon with it. If you add a fourth mode, add its hide-rule to the stylesheet in the same commit; the test will tell you if you forget.

Editing projects from the CMS

projects is mapped in src/admin.config.ts as Work, with title as the title, completed as the date, statement as the description and cover as the social image. It has no draft or updated field because the schema has neither.

completed rather than year as the date is not a toss-up. dateField is parsed with new Date(value), and the number 2025 reads as two seconds past the epoch — every project would sort into 1970. completed is a month-and-year string that parses correctly and orders the list the way the site’s own order reads.

The description role points at statement because that is what src/pages/work/[slug].astro already passes to BaseLayout as the page description. The search description is whatever the page says it is, not whatever a field is spelled.

Working with entries and The document editor cover the authoring side.

Replacing the sample archive

The nine projects are invented — the studio, the clients, the addresses and the photographs all are. Replacing them is nine folders under src/data/projects/, and the only rules the build enforces are the schema itself and unique order values.

The homepage’s project band is separate: src/config/projectsData.json.ts hand-picks six of the nine for the home page, so a project deleted from the collection needs removing there too. That band reads config rather than the collection because the homepage’s selection is an editorial choice, not the archive’s order.

NEXT STEPJournal