Portfolio

An honest local guide,
built with AI.

azoresbyrui.com is a local-first travel platform for the nine islands of the Azores. Brutally honest, opinionated, unsponsored. Built and operated by one resident, with AI as a collaborator on research, code, and content.

This document is the real, unpolished record of how that product was designed, built, and shipped.

Role
Product strategy · Research · UX · UI · Design system · Front-end · SEO
Timeline
~10 weeks · research → live · solo
Stack
Lovable · Supabase · React · TS · ChatGPT · Claude · UX Pilot
Azores by Rui homepage
01 · What this actually is

A local travel platform.
Not a blog.
Not a directory.
An opinion engine.

Every place on the site has been personally visited, eaten at, or hiked by a single resident of São Miguel. No affiliate links. No sponsorship. No "top 10" lists generated from Tripadvisor scrapes.

The product carries opinion as a first-class object. Each entry surfaces a clear verdict (Top Pick, Worth It, Meh, Skip It) alongside a numeric score and a written explanation of why.

This project was also a real-world experiment: how far can a senior product designer take a full live product using AI as the primary building tool? The honest answer is in this case study.

Local-first

Recommendations come from one resident. No editorial team chasing SEO.

Opinionated

Every place gets a verdict. 'Skip It' is a feature, not a bug.

Unsponsored

Zero affiliate links. Zero paid placements. Zero sponsored reviews.

AI-assisted

Built with Lovable, ChatGPT, Claude, and UX Pilot. All decisions human.

02 · The problem

Most Azores travel guides are SEO farms with affiliate links wearing the costume of advice.SEO

01
Optimised for Google, not for tourists

The top-ranking pages are written to match keyword intent, not to be useful. They list 25 places. They've been to none.

02
Affiliate incentives bend the recommendation

Many 'best of' posts surface what pays them most: booking widgets, tours, hotels. All buried under editorial language.

03
Places loved by locals barely show up online

The best tasca on the island has 12 Google reviews and no website. The mediocre tourist restaurant has 4,000 and a marketing team.

04
'Hidden gems' became a content category

The phrase is used so often it now signals the opposite. Real gems aren't advertised; they're mentioned in a Reddit reply.

rr/Azores· Posted by u/throwaway_traveller · 8mo ago
Every blog recommends the exact same 10 places. Where do locals actually eat?
↑ 412 · 💬 3 comments · Share · Save
u/ponta_visitor · 1.2k karma · 8mo

Half the 'top 10' places I tried were tourist traps. The food was fine but nothing special. Felt scammed by every guide I read.

↑ 47 · Reply
u/miguel_azores LOCAL · 8.4k karma · 8mo

Honestly? Skip the lists. The places locals actually go are the ones with 30 Google reviews, no English menu, and a queue of regulars at 1pm. Nobody writes about them because there's no affiliate link.

↑ 284 · Reply
u/throwaway_2024 · 320 karma · 8mo

I asked an AI for restaurant recs and it gave me the same names as the first Google result. We're in a loop.

↑ 112 · Reply
Hypothesis

The trust gap in travel content is not a writing problem. It's an incentive problem.

03 · Research

Reading 700+ angry comments
before opening Figma.

734
Reddit comments tagged
r/Azores · r/travel · r/Portugal
42
SEO competitor pages analysed
SEMrush keyword overlap
23
Informal tourist interviews
Ponta Delgada airport + Airbnb hosts
12
Content pillars identified
From clustering, not assumption
Hypothesis

I scraped every "what should I do in the Azores" thread from the last two years. Tagged each answer by category, sentiment, and whether the recommender actually lived here.

Locals over-index on a handful of unglamorous things. Tourists ask for those exact things and get sent to the photogenic, mid ones instead. By Google. By Tripadvisor. By AI assistants now too.

sKeyword Gap · azores travel competitorsexport · oct 2025
KeywordVolKDABC
things to do são miguel18,10072124azoresbyrui
best restaurants azores9,90064317
azores itinerary 7 days6,60058241
hidden gems são miguel2,40041159
where to eat ponta delgada1,90038426
best viewpoint sete cidades1,30032GAP
tasca são miguel local88024GAP
free bathing area furnas59019GAP
3 competitors share 90% of high-intent keywords · 47 long-tail gaps identified
Key finding

Three competitors owned 90% of high-intent keywords with content none of them had personally verified.

The gap between ranking authority and lived authority became the entire positioning of the product.

04 · Design System

The chips do the
emotional heavy lifting.

This is not a corporate token sheet. The Azores by Rui design system exists to do one job: communicate opinion clearly so a tourist skimming dozens of places knows, in 300ms, whether I'd send a friend there. Every chip below is rendered live from the production codebase.

Verdict chips · semantic confidence system
My Top Pick4.8

Top tier. I'd recommend this without hesitation.

Worth it3.5

A solid choice. I'd recommend it.

Meh2.1

Average. Nothing special about it.

Skip it1.7

Disappointing. I'd save your time.

Rating anatomy

Every score is explainable.

Each criterion can be tapped to read why the number is what it is, in plain words. Trust requires showing the seams.

Rui's rating
My Top Pick4.8
Quality
5.0
Value for money
4.5
Atmosphere
4.0
Service
4.5
Typography
Fraunces
H1 · editorial voice · serif
Montserrat
H2–H6 · UI headings

Inter for body text. Set in #36443B for warmth, never pure black. Long-form descriptions, reviews, captions.

Color tokens
#008562
Brand · primary green
#314158
Foreground · slate
#36443B
Body text · warm
#467BFF
Top Pick · trust blue
#2EA27A
Worth It · confidence
#EF9E01
Meh · honest amber
#EC4641
Skip It · refusal red
#1A1A1A
Unrated · neutral
05 · AI Workflow

Four tools.
Four very different jobs.

ChatGPT logo
ChatGPT
Research synthesis & SEO scaffolding

Clustering 734 Reddit comments into themes. Drafting interview prompts. Translating Portuguese sources. Generating SEO-friendly outline structures. Never used for final copy. Its travel voice is too generic.

CChatGPT
Cluster these 734 r/Azores comments into 8–12 thematic pillars by tourist intent. Strip the marketing-style ones.
Done. 12 clusters: Restaurants (142), Itineraries (112), Viewpoints (98), Weather (97)… 'Hidden gems' was the most-used phrase but the most-mocked one.
Tag each by sentiment toward existing guides. I want the negative ones.
71% negative on Restaurants, 73% negative on Weather guidance. Locals dominate the corrections.
Claude logo
Claude
Long-form reasoning & editorial drafts

First-person review drafts in Rui's voice from raw notes. Better at restraint and tone than ChatGPT. Used for debugging dense problems (RLS, sitemap, canonicals). Everything still rewritten by hand before shipping.

CClaude
Draft a Worth It review for Terras Brunch. My notes: surprised by price/quality, modern but not trying too hard, risotto ~10€, no Instagrammable BS. Voice: first-person, no clichés.
'Terras was a huge surprise. I walked in expecting one more trendy brunch place with overpriced food and tiny portions. Nope. Completely wrong.'
Closer. Cut the second sentence, too long. Keep the 'Nope. Completely wrong.'
Trimmed. Want me to add a Pros/Cons block in the same voice?
Lovable logo
Lovable
Production build & ship

The entire production codebase. React + Supabase backend, edge functions, sitemap, OG meta, RLS policies, image pipeline. Iterated on real components, not throwaway prototypes.

CLovable
Rating chip should never read 0/10 when unrated. Show OnMyRadarBadge. Unrated bypasses 45min trip default → use 0.
Updated RatingBadge, OnMyRadarBadge, tripGenerator.ts. Added guard so radar badge renders when rating === null.
Admin table now hides unrated rows. Default filter is rating > 0. Add an Unrated segment.
Fixed. Added a unit test against the regression.
UX Pilot logo
UX Pilot
Visual exploration

Burn through 20 layout directions in an afternoon. Picked none of them. Still learned what NOT to do. Used as a thinking tool, not an output tool.

CUX Pilot
Generate 6 hero variations for a local-first Azores guide. Editorial, anti-affiliate. No travel-clichés.
Returning 6 directions: magazine split, sticker-board, dossier, brochure, postcard wall, brutalist grid.
Dossier and editorial-magazine directions are interesting. Sticker-board feels too cute. Discard the rest.
Refining dossier with serif H1, mono eyebrow, asymmetric grid…
06 · Component Evolution

The rating chip went through
eight versions.

V1KILLED
★ ★ ★ ★ ☆ 4.0

Generic 5-star. Looked like Tripadvisor. Conveyed nothing.

V2KILLED
4.0

Just the number. Cleaner, but no semantic signal at all.

V3KILLED
Recommended 4.0

Added a verbal label. Colour was wrong. Green for everything felt dishonest.

V4KILLED
Top Pick 4.7

Four-tier semantic colour. Closer. Outline felt too quiet on listing cards.

V5KILLED
Top Pick 4.7

Filled pill. Worked. But all chips looked identical on a long page.

V6KILLED
Top Pick 4.7

Added icons. Too many shapes competing. Score got lost.

V7KILLED
Top Pick 4.7

Split score visually. Solved hierarchy. But 'unrated' broke completely.

V8FINAL
My Top Pick4.8

Rebalanced weights and shadow. Community rating was about to be split into a separate chip, but the verdict alone finally felt finished.

Global search results
Global search

Bilingual, FTS-backed, grouped by what the tourist actually typed.

Search resolves both Portuguese and English queries against the same index. Results group by Places, Experiences, and Fun Facts, so the answer feels structured instead of infinite.

07 · The boring real problems

The unglamorous bugs
that actually shipped the product.

People keep asking what "the AI" did. There is no the AI. Each model is good at one slice of the problem and unusable for the others. The skill is knowing which one to reach for, and when to override it.

01Sitemap vs. preview domain

Search Console kept comparing canonical URLs against the Lovable preview subdomain. Locked the production host list in seo.ts; emitted noindex/nofollow on every non-canonical host.

GSearch Console · Coverage
⚠ Alternate page with proper canonical tag
217 URLs · preview subdomain referenced as canonical
https://id-preview--2bd02ae9...lovable.app/places/...
217
EXCLUDED
1,482
VALID
3
ERRORS
02AI hallucinated a restaurant

An AI-generated draft confidently described a place that does not exist. Added a publishing gate: nothing ships without is_published=true and a verified visit_log entry.

supabase/migrations/publishing_guard.sql

+ create policy "must_be_verified"
+ on public.places for select
+ using (
+   is_published = true
+   and exists (
+     select 1 from visit_log
+     where place_id = places.id
+   )
+ );
03Images murdering LCP

Original 4MB uploads were being served on mobile. Built a strict fallback chain (Medium → Large → Thumbnail → Transformation) so the smallest acceptable variant ships first.

Lighthouse · /places/terras before → after
PERFORMANCE
58 96
LCP
4.2s 1.1s
TOTAL BYTES
3.8MB 612KB
ACCESSIBILITY
91 100
04Supabase 1000-row ceiling

Public listing pages silently truncated at 1,000 rows. Replaced every fetch with a paginated .range() loop and a unit test that fails if a helper forgets it.

▶ DevTools · Network · XHR
namestatus / rows
supabase/rest/v1/places?...200 · 1000 rows ⚠
supabase/rest/v1/places?...range=1000-1999200 · 1000
supabase/rest/v1/places?...range=2000-2999200 · 412
⚠ default ceiling silently truncates at 1000. fixed via .range() loop.
05Canonical drift on Cloudflare

lovablehtml.com → Cloudflare → Lovable origin. Three different URLs were claiming to be canonical. Single source of truth in canonical.ts; everything else derives.

CANONICAL CHAIN · BEFORE
azoresbyrui.com → CF proxy
lovablehtml.com → origin
id-preview…lovable.app
3 URLs claim canonical · Google indexed all 3
AFTER · CANONICAL.TS
azoresbyrui.com, only
non-canonical hosts → noindex,nofollow
06RLS that almost shipped

Draft experience_items were SELECT-able by the anon role. Caught in security scan. Restricted to is_published=true OR admin role before launch.

Security Scan · RLS AuditHIGH
experience_items, anon role can SELECT drafts
places, is_published guard verified
comments_public, view strips email/token
1 finding · resolved before launch
07Mobile CLS from late-loading chips

The verdict chip rendered after the place title settled, pushing the entire card down 36px on first paint. Reserved height with a min-h skeleton so layout locks before async data lands. CLS dropped from 0.21 → 0.02.

lighthouse · mobile · cumulative layout shift
0.21
before · 3 card pushes / fold
0.02
after · min-h on chip slot
08Image compression vs. crispness

First pass shipped every photo as 60% WebP. Looked fine on hero, looked plasticky on food close-ups. Switched to a per-variant quality curve: thumbnail 0.80, medium 0.82, large 0.85. Capped source width at 2400px so phone uploads stop being 6MB.

IMAGEPROCESSING.TS · VARIANT CURVE
thumbnail200w · q 0.80
medium600w · q 0.82
large1400w · q 0.85
source cap2400w max
avg page weight: 3.1MB → 740KB · no visible quality loss above the fold
09Google indexed the duplicates

Legacy /place/<slug> URLs co-existed with the new /places/restaurants/<slug>. Google indexed both. 301 redirects with a custom-selected canonical fixed it in one crawl cycle.

Search Console · Duplicate without user-selected canonical
/places/restaurants/tasca-da-lota
/place/tasca-da-lota
/place/tasca-da-lota · 301 from legacy

None of this is glamorous.
But it's part of the job.

08 · The product, live

Live. Indexed.
Used by real travelers.

Azores by Rui homepage live
Search experience
Place detail page
Blog index
09 · What this actually taught me

What ten weeks of shipping
actually taught me.

Lesson 01

AI made execution faster. Judgement was still the hard part.

Ten weeks. One product, live in production. Models in every part of the loop. A handful of lessons I didn't expect, and none of them are about prompts.

Lesson 02

Taste became more important, not less.

When the model generates ten viable directions in an hour, the bottleneck moves to knowing which one is right. The cost of generating dropped to almost zero. The cost of choosing went up.

Lesson 03

Systems outranked visuals.

The chip I shipped on launch day looks almost identical to v4. What changed wasn't the pixels. It was the system underneath. What unrated means. When community shows. Why two ratings never blend. Visuals followed the logic.

Lesson 04

Shipping exposed problems prototypes hid.

Every interesting bug in this case study only appeared in production. Canonical drift. CLS from late chips. The hallucinated restaurant. Real users, real Search Console, real Lighthouse runs. Figma never warned me about any of it.

Lesson 05

Authenticity beat optimised content.

I wrote 'skip it' on places real tourists drive an hour to see. That one editorial decision did more for trust than any SEO page. The product's voice is the product. AI cannot fake having actually been there.

Lesson 06

Iteration got cheaper. Decisions got more expensive.

AI made eleven chip versions almost free. It also made it tempting to keep going forever. The skill became noticing the difference between iterating because the answer is unclear, and iterating because I was avoiding the call.

Lesson 07

Product thinking became the real bottleneck.

Design, code, and copy all got faster. The only thing that didn't was the work of figuring out what the product should refuse to do. That work scales with the designer, not with the model.

Lesson 08

Senior work didn't get easier. It got more concentrated.

The boring parts compressed. What was left was almost entirely the hard part. Framing. Taste. Judgement. Restraint. AI made the job feel more senior, not less.

10 · Reflection
"AI helped me move faster. It did not replace product thinking.
If anything, it made taste the bottleneck again."

The model can ship a homepage in twenty minutes. It cannot decide whether the homepage should exist. That decision is still the most expensive thing on the page.

Every interesting choice in this project was a judgement call. What to publish. What to leave unrated. What to refuse to recommend. AI gave me leverage on everything around those calls. It never made one for me.