Real estate agency
An editorial website for a property agency in Morocco, with a back office where the team publishes listings, photographs and filmed visits on its own.
- Role
- Design and full-stack development
- Status
- In development
- Category
- Agency website and back office
- Place and year
- Morocco, 2026
- Stack
- Next.js, React, TypeScript, Tailwind CSS, GSAP, Lenis, SQLite, Zod
- Links
- Not publicly launched yet, and the source code isn’t public. The agency’s name and logo are left out.





The story
The challenge
A property agency selling and letting villas, riads and apartments in Morocco wanted every property presented like a feature in a magazine, and a back office simple enough to publish listings, photographs and filmed visits without help.
The approach
An editorial site in French: numbered chapters, captioned full-bleed photographs and an asymmetric property grid, with GSAP motion on Lenis. Behind it, a protected back office on SQLite for listings, photo uploads, vertical video tours and enquiries.
The result
The site and the back office are built and seeded with nine demo listings. The agency’s own photography, its real listings and email notifications are what remain before launch.
Public site
- Property catalogue with search and filters for transaction, type, price, bedrooms and surface.
- Property pages with a gallery you can browse with the arrow keys, key figures and a technical sheet.
- Filmed visits: an uploaded vertical video played in 9:16, or a link to the property’s TikTok post.
- A seller journey: a free estimate, a four-step mandate and an enquiry form already set to “sell”.
- Motion that respects reduced motion: the grain freezes, movement stops and content stays visible.
Back office
- Sign-in enforced by middleware on every admin page, with signed session cookies and bcrypt-hashed passwords.
- A dashboard with counters, the latest enquiries and recent listings.
- Listings: create, edit, delete, feature and set their status.
- Photo uploads re-encoded to JPEG at 2000 px at most, with metadata stripped and every file proven to be an image.
- Enquiries tracked from new to handled to archived.
Under the hood
real excerpts from the codebaseNever trust an upload
A declared MIME type is only the browser’s claim. Every photo is decoded and re-encoded on the server, which strips its metadata and rejects anything that isn’t really an image.
src/app/admin/actions.ts
if (!TYPES_IMAGE.includes(f.type)) {
return { ok: false, message: `Format refusé : ${f.name}. JPEG, PNG, WebP ou AVIF.` };
}
if (f.size > TAILLE_MAX) {
return { ok: false, message: `Fichier trop lourd : ${f.name} (12 Mo maximum).` };
}
const nom = `${crypto.randomUUID()}.jpg`;
try {
// Re-encoding strips metadata and proves the bytes really are an image —
// a declared MIME type is only a claim by the client.
await sharp(Buffer.from(await f.arrayBuffer()))
.rotate()
.resize({ width: 2000, withoutEnlargement: true })
.jpeg({ quality: 82, mozjpeg: true })
.toFile(path.join(dossier, nom));
} catch {
return { ok: false, message: `Fichier illisible : ${f.name}. Ce n'est pas une image valide.` };
}One gate for the back office
Every admin route goes through the same check before it renders; without a valid session the visitor lands on the sign-in page and returns where they were going.
src/middleware.ts
export async function middleware(req: NextRequest) {
const session = await lireJeton(req.cookies.get(NOM_COOKIE)?.value);
if (!session) {
const url = new URL("/admin/connexion", req.url);
url.searchParams.set("suite", req.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
// Everything under /admin except the login page itself.
export const config = {
matcher: ["/admin/((?!connexion).*)", "/admin"],
};Decisions
A reference, not a copy
An editorial reference site set the direction. Its type scale and layout grammar were adopted; the copy, the photography grading and every line of markup were written for this project.
Honest structured data
Real estate listing data for search engines stays off until the listings are real, because publishing example offers to Google would mislead.
Video where the audience is
The agency films its properties vertically, so each listing takes a 9:16 file or simply links to the TikTok post that already carries the views.













Status
Photography in the build is licensed stock, graded to match, and the nine listings and enquiries are demo content. The agency’s name, logo, phone number and social accounts are left out of this case study.