The Belgian real estate web is a graveyard of agency templates. Sofoximmo.be is a fair example of the genre clean enough, functional, but built on patterns that haven’t aged well. The dark navy header, the four-dropdown search bar, the heavily blurred hero image: every Wallonia agency site looks roughly like this, and most of them load slowly, rank poorly, and convert worse than they should.
This piece walks through how a modern Next.js build improves on that baseline, with the actual code to recreate the sofoximmo hero and the design decisions worth changing before you ship.
Setup Build A Site Layout
Create a new Next.js project and install the icon library:
npx create-next-app@latest sofox-clone --typescript --tailwind --app
cd sofox-clone
npm install lucide-react
Drop a hero photo at public/hero-interior.jpg and replace the contents of app/page.tsx with the code below.
What is Sofoximmo Gets Right
Credit where it’s due. The orange-on-navy palette is recognisable, and that matters in a niche where most sites default to grey-on-grey. The top bar gives a phone number and email above the fold, which is the right call for a service business in Belgium. The four-field search (statut, type, localité, référence) covers the basic property hunt.
Where it falls down is in the details: the hero photo is blurred so heavily that it could be any living room anywhere, the search form lacks price and surface filters that buyers actually use, and the entire page weight on mobile pushes well past what Core Web Vitals will reward.
Why Next.js is The Right Base for Property Sites
Real estate has a specific shape that Next.js handles cleanly. You have a few dozen marketing pages that rarely change, and thousands of property listings that change daily. The framework supports both without forcing a choice.
Static generation handles the marketing routes homepage, about, contact, locality landing pages. Incremental Static Regeneration handles listings: each property gets a pre-rendered HTML page that rebuilds in the background when prices change or the property sells. Google crawls fully-rendered HTML, the user gets a fast paint, and the agent doesn’t need to redeploy when they edit a listing in the CMS.
Add next/image for automatic WebP conversion and lazy loading (real estate photos are heavy), the Metadata API for per-page SEO, and JSON-LD RealEstateListing schema on every property route, and you’ve leapfrogged 90% of the agency sites in this market.
Recreating the Sofoximmo Hero in Next.js
// app/page.tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import {
Phone,
Mail,
Facebook,
Instagram,
Youtube,
MessageCircle,
Search,
ChevronDown,
Menu,
X,
} from "lucide-react";
type NavItem = {
label: string;
href: string;
active?: boolean;
};
const NAV: NavItem[] = [
{ label: "ACCUEIL", href: "/", active: true },
{ label: "A VENDRE", href: "/a-vendre" },
{ label: "A LOUER", href: "/a-louer" },
{ label: "A PROPOS", href: "/a-propos" },
{ label: "CONTACT", href: "/contact" },
{ label: "ESTIMATION", href: "/estimation" },
];
export default function HomePage() {
const [menuOpen, setMenuOpen] = useState(false);
return (
<main className="min-h-screen bg-[#0d1b2a] text-white">
{/* ===== Top contact bar ===== */}
<div className="border-b border-white/10 text-xs md:text-sm">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-2">
<div className="flex flex-wrap items-center gap-4 md:gap-6">
<a
href="tel:+32471853679"
className="flex items-center gap-2 transition hover:text-orange-400"
>
<Phone size={14} /> 0471/85.36.79
</a>
<a
href="mailto:info@sofoximmo.be"
className="flex items-center gap-2 transition hover:text-orange-400"
>
<Mail size={14} /> info@sofoximmo.be
</a>
</div>
<div className="flex items-center gap-4">
<a href="#" aria-label="Facebook"><Facebook size={15} /></a>
<a href="#" aria-label="Instagram"><Instagram size={15} /></a>
<a href="#" aria-label="YouTube"><Youtube size={15} /></a>
<a href="#" aria-label="WhatsApp"><MessageCircle size={15} /></a>
</div>
</div>
</div>
{/* ===== Main header / nav ===== */}
<header className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/" className="block rounded-sm bg-white px-4 py-3">
<span className="text-2xl font-bold italic text-orange-500">Sofox</span>
<span className="align-super text-xs text-gray-700">immo</span>
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-8 text-sm tracking-widest md:flex">
{NAV.map((item) => (
<Link
key={item.label}
href={item.href}
className={`pb-1 transition hover:text-orange-400 ${
item.active ? "border-b-2 border-orange-500 text-white" : ""
}`}
>
{item.label}
</Link>
))}
</nav>
{/* Mobile menu button */}
<button
type="button"
className="md:hidden"
aria-label="Toggle menu"
aria-expanded={menuOpen}
onClick={() => setMenuOpen((v) => !v)}
>
{menuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</header>
{/* Mobile nav drawer */}
{menuOpen && (
<nav className="border-t border-white/10 md:hidden">
<div className="mx-auto flex max-w-7xl flex-col px-4 py-3">
{NAV.map((item) => (
<Link
key={item.label}
href={item.href}
className={`py-3 text-sm tracking-widest ${
item.active ? "text-orange-500" : "text-white"
}`}
onClick={() => setMenuOpen(false)}
>
{item.label}
</Link>
))}
</div>
</nav>
)}
{/* ===== Hero ===== */}
<section className="relative flex min-h-[500px] items-center justify-center overflow-hidden md:min-h-[600px]">
<Image
src="/hero-interior.jpg"
alt=""
fill
priority
sizes="100vw"
className="scale-105 object-cover blur-sm"
/>
<div className="absolute inset-0 bg-[#0d1b2a]/70" aria-hidden />
<div className="relative z-10 w-full max-w-5xl px-4 text-center">
<h1 className="mb-3 text-4xl font-light tracking-[0.3em] md:text-6xl">
SOFOXIMMO
</h1>
<p className="mb-10 text-sm tracking-widest text-orange-500 md:text-lg">
L'IMMOBILIER RAPIDE, EFFICACE ET RENTABLE
</p>
<form
action="/recherche"
method="GET"
className="rounded-lg bg-white/10 p-3 backdrop-blur-md md:p-4"
>
<div className="flex flex-col gap-3 md:flex-row">
<SelectField name="statut" placeholder="À vendre">
<option value="vente">À vendre</option>
<option value="location">À louer</option>
</SelectField>
<SelectField name="type" placeholder="Type de biens">
<option value="maison">Maison</option>
<option value="appartement">Appartement</option>
<option value="terrain">Terrain</option>
<option value="commerce">Commerce</option>
</SelectField>
<SelectField name="localite" placeholder="Localité">
<option value="charleroi">Charleroi</option>
<option value="mons">Mons</option>
<option value="namur">Namur</option>
<option value="liege">Liège</option>
</SelectField>
<input
type="text"
name="reference"
placeholder="Référence"
className="flex-1 rounded-md border border-orange-500 bg-white/95 px-4 py-3 text-gray-700 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
<button
type="submit"
aria-label="Rechercher"
className="flex items-center justify-center rounded-md bg-orange-500 px-6 py-3 text-white transition hover:bg-orange-600"
>
<Search size={20} />
</button>
</div>
</form>
</div>
</section>
</main>
);
}
// Reusable select with custom chevron
function SelectField({
name,
placeholder,
children,
}: {
name: string;
placeholder: string;
children: React.ReactNode;
}) {
return (
<div className="relative flex-1">
<select
name={name}
defaultValue=""
className="w-full appearance-none rounded-md border border-orange-500 bg-white/95 px-4 py-3 pr-10 text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="" disabled>
{placeholder}
</option>
{children}
</select>
<ChevronDown
size={18}
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500"
/>
</div>
);
}
What Changed in The Build Review
The NAV array is now typed as NavItem[] with active marked optional, so TypeScript stops complaining when accessing .active on items that don’t define it. Every <option> has an explicit value so form submissions produce clean URL parameters like ?statut=vente&type=maison instead of ?statut=%C3%80%20vendre. The hero is min-h-[500px] on mobile and min-h-[600px] from md: up, which keeps small phones from scrolling a wall of dark space before they see anything else.
The component is now "use client" because the mobile menu uses useState. If you want to keep the page as a server component for SEO, lift the header into its own client component (components/Header.tsx) and import it that way only the small interactive piece ships JavaScript to the browser, and the hero remains server-rendered.
A few things that quietly matter in this component:
The top bar phone number uses tel:+32... instead of the local format, so it works internationally on mobile dialers. The hero <Image> is marked priority because it’s the LCP element without that flag, Lighthouse will dock you several points. The decorative overlay has aria-hidden so screen readers don’t announce it. The custom SelectField swaps the browser’s native dropdown arrow for a styled chevron that matches the orange theme, because the default arrow on Chrome looks like Windows 95.
Where to Push Further Than Sofoximmo
The recreation above is faithful, but the original design has limits worth fixing before you ship something to a real client.
- Stop blurring the hero. Whatever the original designer was thinking, blurring the only photo on the page above the fold is a wasted slot. A modern build uses a rotating set of actual property photos sharp, full-bleed, optimized with a subtle dark gradient at the bottom for text contrast. That alone signals “real listings” instead of “stock interior.”
- Expand the search. Four fields is fine for a quick filter, but anyone serious about buying wants price range, surface in m², number of bedrooms, and PEB rating (the Belgian energy certificate is legally required and buyers filter on it). Build the advanced form on a secondary
/rechercheroute and keep the hero version as a quick-entry, but don’t pretend four dropdowns is enough. - Add locality landing pages. This is where you outrank competitors over twelve months. Build dedicated routes for
/maisons-a-vendre/charleroi,/appartements-a-louer/mons, and so on each with unique market commentary, a price/m² trend chart, and the live listings filtered for that area. These are SEO compounders, and most Belgian agencies don’t bother. - Mark every property page with structured data. A
RealEstateListingJSON-LD block with price, surface, number of rooms, address, and main photo lets Google show rich results in search. It costs you fifteen lines of code per page and lifts CTR measurably.
A Note on the Rest of the Build
The hero is the visible part, but it’s not where the real engineering lives. A production real estate site needs a database (PostgreSQL with PostGIS for radius searches), an image pipeline (Cloudinary or Bunny CDN), an instant search layer (Meilisearch or Typesense), a map component (Mapbox is cheaper and more flexible than Google Maps), and a CMS interface so the agent can add listings without touching code (Directus or Payload work well alongside Next.js).
Pull those pieces together and the result isn’t just “a faster sofoximmo.” It’s a property search experience that ranks for the queries buyers actually type, loads in under two seconds on a mid-range Android, and gives the agent a back office they don’t need to call you about every Tuesday.
