Sanity project configuration, GROQ patterns, and CMS hook architecture
This project uses Sanity.io as the headless CMS.
| Key | Value |
|---|---|
| Project ID | nk38o90y |
| Dataset | production |
| Studio URL | https://hs-dental-clinic.sanity.studio/ |
| Studio config | studio/sanity.config.ts |
| CLI config | studio/sanity.cli.ts |
| Client module | src/lib/sanityClient.ts |
[!CAUTION] A previous agent used a different project ID () in build scripts. The correct ID is . Always verify this in if unsure.
yvwb8oibnk38o90ystudio/sanity.config.ts17 schemas registered in studio/schemas/index.ts:
Singletons (page settings — one document each):
siteSettings — Global site config (logo, social links, contact info, OG image)hero — Homepage hero section (title, subtitle, CTA, background image)homepageSettings — Homepage features, testimonials toggleaboutSettings — Doctor bio, credentials, timelineservicesPageSettings — Services index page configtechnologySettings — Technology page configdsdSettings — Digital Smile Design page configtourismSettings — Tourism page config (pricing, FAQ, VIP program)Collections (multiple documents):
service — Service documents (slug-based, used on Services page)testimonial — Patient testimonialsteamMember — Clinic team membersfaq — Frequently asked questionstourismPricing — Tourism treatment pricing tiersbeforeAfterCase — Gallery before/after cases with imagesyoutubeVideo — YouTube video embeds with category filteringservicePillar — 4 SEO pillar pages (dental-implants, tmj-tmd-treatment, clear-aligners, full-arch-rehabilitation)page — Generic CMS pages (slug-based, Portable Text body)[!CAUTION]
gallerySettingsandpostdo NOT exist as schemas despite being referenced in older documentation. Do not attempt to query these types.
All CMS hooks live in src/hooks/useCmsData.ts. They follow this pattern:
export function useServicePillar(slug: string) {
const [pillar, setPillar] = useState<ServicePillarData>(DEFAULTS[slug]);
useEffect(() => {
sanityClient.fetch(QUERY, { slug }).then((data) => {
if (data) setPillar({ ...DEFAULTS[slug], ...data });
});
}, [slug]);
return { pillar };
}
Key principle: Every CMS field uses ?? fallback to hardcoded defaults. The site never breaks if CMS is empty.
| Hook | Used By |
|---|---|
useSiteSettings() | Layout, all pages |
useHomepageSettings() | Home.tsx |
useAboutSettings() | About.tsx |
useGallerySettings() | Gallery.tsx |
useTourismSettings() | DentalTourism.tsx |
useDsdSettings() | DigitalSmileDesign.tsx |
useServicePillar(slug) | 4 service pillar pages |
useSanityImage(ref, width) | Any page with CMS images |
useYoutubeVideos(category) | Video sections |
// Fetch a singleton (prefer known _id, fallback to any)
coalesce(*[_type == "hero" && _id == "hero"][0], *[_type == "hero"][0])
// Fetch a singleton (simple)
*[_type == "siteSettings"][0]
// Fetch a service pillar by slug
*[_type == "servicePillar" && slug.current == $slug][0]
// Fetch all blog posts
*[_type == "post" && defined(slug.current)] | order(_createdAt desc)
// Fetch slugs + types for prerendering
*[_type in ["post", "service"] && defined(slug.current)]{_type, "slug": slug.current}
[!CAUTION]
order()does NOT support booleandesc. Usingorder(_id == "hero" desc)causes a 400 error. For "prefer this document" logic, always usecoalesce()with two filtered queries.
A webhook named "Netlify Rebuild on Publish" is configured in the Sanity dashboard:
This means: publish content in Sanity Studio → Netlify auto-rebuilds → live site updates.