CLIENT SITE · NEXT.JS + SANITY · LIVE
VagabondandoTravel journals, written by the client.
Vagabondando is the website of a guide who organises hikes and small-group trips. Visitors read how a trip went and, if they feel like joining, find a simple way to get in touch right next to the story. I designed and shipped it with Next.js 16 and Sanity, developing it with Claude Code: I led the product, architecture, review and infrastructure.
- Role
- Client project, AI-assisted: product, architecture, review and infrastructure
- Stack
- Next.js 16 · React 19 · TypeScript · Tailwind 4 · Sanity · next-intl · Resend · Upstash Redis
- Quality
- Vitest, 102 tests · GitHub Actions: types, lint, tests and build
- Status
- Live at vagabondando.site · non-indexed demo awaiting launch
The goal
The client had a WordPress site and wanted to share her trips and receive enquiries without depending on someone for every change. That set three constraints: anything that changes often must be editable through a visual interface; no failure may silently lose an enquiry, because to her it is a customer; and costs must stay at zero until the site brings in work.
How it works
Static pages regenerated every minute: stories live in Sanity and the client writes them in the Studio built into the site. The contact form is a Server Action that sends a single email to the client, with the visitor as the reply-to address: no database and no archive of personal data. Domain rules live in pure, tested TypeScript modules; pages and actions just call them.
Features
Travel journal
A list with the latest story featured; each story has a drop cap, date, place, a link to the Instagram post it came from, and the previous and next stories. A newly published journal appears on the site within a minute, with no deploy.
Content in the client’s hands
Sanity Studio is built in at /studio: journals, the home photo, the Instagram profile and the privacy policy change without touching code. Every field has an Italian and English version; if the English is missing, the story stays readable in Italian.
Protected contact form
Four checks before sending, cheapest first: Vercel BotID, a honeypot field, field validation and a limit of 3 submissions every 10 minutes on Upstash Redis. The logic is covered by 28 tests, and a failure is never shown as “sent”.
Two languages, two themes
Italian and English with next-intl and the language in the URL, translated plurals, localised dates and error pages in both languages. Light and dark themes, applied before React loads to avoid a flash of the wrong colours.
SEO and images
Canonical URLs, hreflang, Open Graph and a localised sitemap come from the same tested logic. Images are served by Sanity’s CDN at the right width for the screen, with space reserved in advance so the text does not jump.
WordPress migration
A script moves the old site’s 8 journals and privacy policy into Sanity, converting HTML to Portable Text. By default it only shows what it would do, it can be rerun without creating duplicates, and it never overwrites text the client has already edited.
Screens



Screenshots of the live site, October 2026. Photos and stories belong to the client.
Technical decisions
The site never emails the visitor
No confirmation emails and no archive: a single email reaches the client’s inbox, and she replies straight to the sender. That removes a database, an archive of personal data to protect, and the risk of the form being abused to send mail to third parties.
Pure logic, thin adapters
The function that handles the form receives BotID, the rate limiter, CMS reads and email sending as parameters. Tests replace them with fakes, so every edge case runs without network access or keys.
Block visibly
If BotID mistakes a person for a bot, the page says so and suggests Instagram instead of faking a successful send: an enquiry lost in silence is a lost customer. A fake success is kept only for the honeypot, which a person cannot fill in.
“Missing” is not “unavailable”
A temporary Sanity outage produces an error page with “Try again”, not a 404. A 404 would tell Google to drop the page from its index over a half-hour problem.
From e-commerce to showcase in a day
The first version sold trips with Stripe Checkout, deposits and balances, PostgreSQL with SELECT … FOR UPDATE against overbooking, and idempotent webhooks. When the client chose not to take payments online, that version went into the v1-stripe git tag and the payment subsystem was removed in a day, without rewriting content, languages or design.