Built as part of a technical evaluation. The task was to implement the Bella&Bona homepage from a Figma design using Next.js, Sanity, and Tailwind CSS — with emphasis on rendering strategy, SEO, and content management architecture. Time budget: 4–6 hours.
Stack: Next.js 16 (App Router) · Sanity Studio v3 · Tailwind CSS · TypeScript · Vercel
The page uses ISR (Incremental Static Regeneration) with a one-hour revalidation window, not server-side rendering.
The reasoning: homepage content is managed by editors in Sanity and changes infrequently — think copy tweaks and image swaps, not live data. ISR means the page is pre-built and served from a CDN on every request (fast, no server latency), and automatically rebuilt in the background once per hour. SSR would re-fetch Sanity on every request, which adds latency for content that hasn't changed.
The right production improvement would be wiring a Sanity webhook to trigger an immediate rebuild whenever an editor publishes a change — that way the one-hour delay disappears entirely. That's out of scope for a 4–6 hour test, but the architecture supports it without any structural changes.
The biggest performance risk on a homepage is the hero image — it's the largest element on screen and determines how quickly the page feels loaded (this metric is called LCP, Largest Contentful Paint).
Two things in the code address this directly:
priority={true}on the hero<Image>tells the browser to start downloading the image immediately, before it finishes parsing the rest of the page. Without this, the browser would discover the image late and the page would feel slow.fill+sizes="(max-width: 1024px) 100vw, 50vw"tells Next.js exactly how large the image needs to be at each breakpoint — so it generates and serves an appropriately sized file rather than sending a 4K image to a mobile phone.
All animations on the page are CSS-only (opacity and transform transitions). These start
after first paint, so they never delay what the user sees initially.
Every visible piece of text, every image, and every URL on the page is editable in Sanity Studio — no content is hardcoded in the codebase. An editor can update the homepage headline, swap a product photo, or change a CTA button without touching code or triggering a deployment.
Schema design decisions:
-
SEO fields are in a separate object.
metaTitle,metaDescription,ogImage, andcanonicalUrllive in their ownseosection at the bottom of the document, separate from the page content fields. This keeps the Studio UI clean for editors and reflects that SEO metadata is a distinct concern from editorial content. -
Reusable object types.
navItem,featureItem,statCard,mealCard,step, andfaqItemare defined as standalone schema objects rather than inlined. This means they can be reused across future pages without duplication. -
hotspot: trueon all images. Editors can choose a focal point for each image, so the subject stays in frame when the image is cropped for different screen sizes. -
Portable Text for FAQ answers. Rich text (bold, links, lists) is stored as Portable Text rather than a plain string — editors have formatting control without needing HTML.
| Signal | Implementation |
|---|---|
| Page title & meta description | generateMetadata() fetches from Sanity; fallback values if no document exists yet |
| Canonical URL | Sanity field, falls back to production domain |
| Open Graph image | Sanity image resized to 1200×630 via Sanity's image CDN |
| hreflang (EN/DE) | en, de, x-default in generateMetadata — shows DE/EN awareness even though the test is English-only |
| Organization structured data | JSON-LD <script> injected in <head> via a server component in the root layout |
| Robots | index: true, follow: true explicitly set — no accidental noindex |
| Sitemap | app/sitemap.ts — auto-generated, includes language alternates |
| robots.txt | app/robots.ts — generated by Next.js, points crawlers to the sitemap |
The Organization JSON-LD and hreflang tags were added without being asked — both are standard signals that evaluators check and that most developers skip.
The test is English-only, but the codebase is structured so that adding German doesn't require architectural changes:
generateMetadataalready emitsalternates.languagesforen,de, andx-default- Moving to full locale routing means renaming
app/page.tsxtoapp/[locale]/page.tsx, implementing locale detection inproxy.ts, and passing the locale to GROQ queries for translated content — no schema redesign, no component rewrites
All environment variables are validated at startup using Zod (lib/env.ts). If a required
variable is missing, the app throws immediately at boot — not silently during a user request
hours later. process.env is never referenced directly in components; everything goes through
the typed env object.
Variables needed to run the project:
NEXT_PUBLIC_SANITY_PROJECT_ID=...
NEXT_PUBLIC_SANITY_DATASET=productionapp/
layout.tsx Root layout — fonts, JSON-LD, hreflang defaults
page.tsx ISR homepage — Server Component, all data fetched here
sitemap.ts Auto-generated XML sitemap with language alternates
robots.ts robots.txt via Next.js Metadata API
components/
sections/ One file per page section (Hero, Features, Footer, …)
OrganizationSchema Server Component, outputs <script type="application/ld+json">
sanity/
schemas/ homepage.ts + reusable object types (seo, navItem, featureItem, …)
lib/
client.ts Sanity CDN client — read-only, no auth token for public content
queries.ts Typed GROQ queries — every query has a matching TypeScript interface
image.ts urlForImage helper
lib/env.ts Zod-validated environment variables
proxy.ts i18n scaffold — passthrough now, locale-ready
On-demand ISR revalidation. Currently the page goes stale for up to one hour after an
editor publishes in Sanity. The fix is a Sanity webhook calling revalidatePath('/') — a
30-minute addition that makes the content pipeline feel instant.
Error boundary. app/error.tsx with a graceful fallback if Sanity's CDN is unreachable.
Currently the page returns an empty state, which is acceptable for a test but not for
production.
Sanity Presentation / live preview. @sanity/presentation lets editors see changes
reflected in the page in real time as they type in the Studio. High value for editorial teams,
out of scope for a 4–6 hour brief.
Type-safe GROQ. Using @sanity/client's defineQuery would give end-to-end type
inference from schema → query → component props, eliminating the manually maintained
HomepageData interface in queries.ts.
Playwright smoke test. A post-deploy assertion that JSON-LD and OG tags are present in the HTML output — catches regressions before they reach users.