Services
Urbic ships four services — interior concept, space planning, 3D modelling and detailing — and they are the one part of the content layer that is not a collection. They live in src/config/servicesData.json.ts, and that is a deliberate holding position rather than an oversight.
Where they live and why
Each entry in servicesData is shaped as exactly what a src/data/services/<slug>/ frontmatter would carry: title, href, image, alt, summary, deliverables, and a body. The day you promote it to a collection, the route swaps its array walk for getCollection("services") and no section changes.
Three pages read the array today. The homepage carousel reads title, href, image and alt. /services/ additionally reads summary and deliverables per entry, plus the shared servicesProcess. And /services/<slug>/ is the only reader of body.
The four titles and hrefs are stated twice — here and in navData.json.ts, which declares them as the header’s Services flyout. That duplication is real and it is guarded: servicesData.test.ts fails the build if the two lists drift. They are restated rather than derived because a service entry needs a photograph the nav has no business carrying, and a real collection entry would own the whole thing as one frontmatter block anyway.
The routes
src/pages/services/index.astro is a thin shell composing the shared PageHero, ServiceRows, Process, Testimonials and Faq. One file — src/pages/services/[slug].astro — serves all four detail routes, deriving each param through serviceSlug from @js/services, the same function the index rows use for their anchor ids.
It is [slug] rather than [...slug] on purpose: a rest param would also match /services/a/b/c/ and would type slug as possibly-undefined, while getStaticPaths only ever emits one segment. /work/[slug].astro makes the same call for the same reason.
The service photos are stored as dynamic-import thunks so pnpm test can load the config under plain Node, and the route resolves them in parallel in getStaticPaths before handing sections the plain asset a collection would have given.
The process band
servicesProcess is five phases — survey, concept, drawings, and so on — rendered as ruled rows rather than five too-narrow columns. It appears on both /services/ and each service page.
It is worth knowing what it is not: contactData’s nextSteps is three rows describing what happens after you send the enquiry form (reply, visit, fee) and it stops where work begins. These five are the work itself. The two lists meet at the site visit and then diverge, which is why they are separate arrays rather than one shared list.
Promoting to a collection
If you want authors editing services from the CMS, promote the array:
- Define a
servicescollection insrc/content.config.tswith the fieldsServiceEntryalready names. - Move each entry into
src/data/services/<slug>/index.md, with thebodybecoming the entry body. - Swap the array walk in
src/pages/services/[slug].astroandindex.astroforgetCollection("services"). - Add a
servicesblock tocollectionsinsrc/admin.config.ts, naming its title, description and social-image fields. - Keep
navData.json.tsin sync, or drop the nav parity check inservicesData.test.tsonce the config array is gone.
Until then, editing a service is editing a typed config module — a normal file change, committed by you rather than by the CMS.
Replacing the sample copy
The five process steps and the four service descriptions are placeholder copy written in the voice of the invented studio. The photograph on /services/ is borrowed from the homepage’s concept section until the page gets one of its own; that is flagged in servicesData.json.ts itself.