03Client · Luxury creative agency
Noevella Group
A media-forward agency site on a headless CMS: divisions, work, journal and a shop, with a design language locked with the client before a page was built.
- 11CMS collections
- 5divisions
- 9research briefs
01
The problem
- A founder running events, brand strategy, talent and a creative hub needed one story, not five service pages.
- She needed to publish work and products herself, without a developer.
02
Architecture
CMSPayload 3 inside the Next.js app: Divisions, Projects, Products, Orders, Inquiries, Insights, Services, Team, Testimonials, Media, Users.
StoragePostgres for content, Vercel Blob for media, scripted media imports.
Front endClay and glass panels, sticker cards, type-on hero, floating pill nav with a mega panel.
GrowthMeta and GA4 pixels with campaign attribution carried into every order and WhatsApp message.
ResilienceEvery CMS query falls back to labelled static content, so pages render before any entry exists.
03
Key decisions
01
No WebGL, on purpose
The design DNA keeps motion in CSS and GSAP so video stays the hero and phones stay fast.
TradeoffLess spectacle, more speed and content.
02
Research before layout
Nine research briefs fed one content master plan; the core message became 'one integrated team'.
TradeoffSlower start, far fewer rewrites.
04
Real code
src/lib/queries.tsexcerpt
async function safe<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
try {
return await fn()
} catch (err) {
console.warn('[queries] falling back:', (err as Error).message)
return fallback
}
}
export async function getFeaturedProjects(limit = 9) {
const fromCms = await safe(async () => { /* payload.find featured projects … */ }, [])
// Real featured work leads, clearly labelled demo work fills the rest
return [...fromCms, ...DEMO_PROJECTS].slice(0, limit)
}src/components/Reveal.tsxexcerpt
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (reduce) {
el.classList.add('is-in')
return
}
// Arm the CSS effect (idempotent across instances).
document.documentElement.setAttribute('data-reveal-armed', '')
// … IntersectionObserver adds .is-in on entry …
// Safety net: never leave content hidden.
const failSafe = window.setTimeout(() => el.classList.add('is-in'), 3000)05
How it was verified
- Typed collections with generated types.
- Every animation respects reduced motion, with a global kill switch.
- Orders and inquiries accept public creates but only signed-in staff can read them.
06
Results
- The founder edits content, work and products herself.
07
What I'd do next
- Remove demo labels from shop items before launch.
- Move from a Gmail address to a domain email.

