← back to Costa Rica
yolo tick 2: dedicated region landing page (/r/:slug) with hero image, headline variety, by-vertical breakdown, nearby-in-province cross-links, region map
cfab471d192b39e705075888b208bd27b753aa96 · 2026-05-08 07:57:20 -0700 · Steve
Files touched
A public/region.htmlM server.js
Diff
commit cfab471d192b39e705075888b208bd27b753aa96
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 07:57:20 2026 -0700
yolo tick 2: dedicated region landing page (/r/:slug) with hero image, headline variety, by-vertical breakdown, nearby-in-province cross-links, region map
---
public/region.html | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 43 +++++++-
2 files changed, 353 insertions(+), 4 deletions(-)
diff --git a/public/region.html b/public/region.html
new file mode 100644
index 0000000..1cf5cac
--- /dev/null
+++ b/public/region.html
@@ -0,0 +1,314 @@
+<!doctype html>
+<html lang="en" data-theme="light">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Loading… — Costa Rica Directory</title>
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
+ <script>(function(){try{var t=localStorage.getItem('cr-theme');if(t)document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
+ <style>
+ :root{--bg:#fafaf7;--fg:#0e1015;--muted:#5a6270;--line:#e3e0d6;--card:#fff;--card-line:#ebe7da;--accent:#0a7a3a;--accent-2:#c2a85a;--cols:4}
+ [data-theme="dark"]{--bg:#0e1015;--fg:#f3f1ea;--muted:#9aa3b2;--line:#23262d;--card:#15181f;--card-line:#262932;--accent:#3ec07a;--accent-2:#d8c074}
+ *{box-sizing:border-box}
+ body{margin:0;background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',Roboto,sans-serif;line-height:1.55}
+ a{color:inherit;text-decoration:none} a:hover{color:var(--accent)}
+ .nav{position:sticky;top:0;z-index:30;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:saturate(160%) blur(10px);border-bottom:1px solid var(--line)}
+ .nav-in{max-width:1320px;margin:0 auto;padding:14px 22px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
+ .brand{font-weight:700;font-size:18px} .brand .pin{color:var(--accent)}
+ .nav-links{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:14px}
+ .nav-links a.active{color:var(--fg);font-weight:600}
+ .ml-auto{margin-left:auto} .icon-btn{appearance:none;background:transparent;color:inherit;border:1px solid var(--line);border-radius:999px;width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
+
+ .hero-wrap{position:relative;width:100%;height:42vh;min-height:340px;max-height:520px;overflow:hidden;background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 35%, var(--card)), color-mix(in srgb, var(--accent-2) 30%, var(--card)))}
+ .hero-wrap img{width:100%;height:100%;object-fit:cover;display:block;filter:saturate(1.05)}
+ .hero-overlay{position:absolute;inset:0;background:linear-gradient(to bottom, rgba(0,0,0,.05) 0%, rgba(0,0,0,.7) 100%);display:flex;align-items:flex-end;padding:36px 22px}
+ .hero-inner{max-width:1320px;margin:0 auto;width:100%;color:#fff}
+ .hero-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.85;margin-bottom:6px}
+ .hero-title{font-size:clamp(36px,6vw,68px);letter-spacing:-.025em;line-height:1.05;margin:0 0 6px;font-weight:800}
+ .hero-sub{font-size:16px;opacity:.92;max-width:760px}
+ .hero-credit{position:absolute;bottom:8px;right:14px;background:rgba(0,0,0,.5);color:#fff;font-size:11px;padding:4px 10px;border-radius:999px}
+ .hero-credit a{color:#fff;text-decoration:underline;text-decoration-color:rgba(255,255,255,.5)}
+
+ main{max-width:1320px;margin:0 auto;padding:24px 22px 80px}
+ .crumbs{font-size:13px;color:var(--muted);margin-bottom:16px}
+ .stats-row{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:18px}
+ .stat{padding:10px 14px;border:1px solid var(--card-line);border-radius:12px;background:var(--card);font-size:13px;color:var(--muted)}
+ .stat .n{color:var(--fg);font-weight:600;font-size:18px;margin-right:6px;font-variant-numeric:tabular-nums}
+
+ .layout{display:grid;grid-template-columns:1fr 360px;gap:32px;margin-top:8px}
+ @media(max-width:980px){.layout{grid-template-columns:1fr}}
+
+ .filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
+ .pill{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border:1px solid var(--line);border-radius:999px;background:var(--card);color:var(--fg);font-size:13px;cursor:pointer}
+ .pill.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
+ .pill .n{font-variant-numeric:tabular-nums;color:var(--muted);font-size:12px;margin-left:4px}
+ .pill.active .n{color:color-mix(in srgb,var(--bg) 70%, transparent)}
+
+ .controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:6px 0 14px}
+ .search{flex:1;min-width:240px;display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--line);border-radius:12px;background:var(--card)}
+ .search input{flex:1;border:0;outline:0;background:transparent;color:inherit;font-size:15px}
+ .ctrl{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}
+ .ctrl select{appearance:none;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:6px 10px;font-size:13px}
+ .ctrl input[type=range]{padding:0;width:120px}
+
+ .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:16px}
+ @media(max-width:760px){.grid{grid-template-columns:repeat(2,1fr)}}
+ .card{background:var(--card);border:1px solid var(--card-line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;transition:transform .15s,box-shadow .15s}
+ .card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(20,20,20,.08)}
+ .card .img{aspect-ratio:16/10;background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 35%, var(--card)), color-mix(in srgb, var(--accent-2) 30%, var(--card)));display:flex;align-items:center;justify-content:center;font-size:24px;color:color-mix(in srgb,var(--fg) 35%,transparent)}
+ .card .img img{width:100%;height:100%;object-fit:cover;display:block}
+ .card .body{padding:12px 14px 14px}
+ .cat{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-weight:600}
+ .name{font-weight:600;letter-spacing:-.01em;margin-top:4px}
+ .where{font-size:12px;color:var(--muted);margin-top:2px}
+
+ .side .card{padding:18px;margin-bottom:14px}
+ .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;margin-bottom:10px}
+ #region-map{aspect-ratio:1/1;border-radius:12px;border:1px solid var(--card-line);overflow:hidden;background:color-mix(in srgb,var(--fg) 4%,transparent)}
+ .vert-list{display:flex;flex-direction:column;gap:6px}
+ .vert-list a{display:flex;justify-content:space-between;padding:7px 10px;border-radius:8px;font-size:13px}
+ .vert-list a:hover{background:color-mix(in srgb,var(--fg) 5%,transparent)}
+ .vert-list .n{color:var(--muted);font-variant-numeric:tabular-nums}
+ .nearby{display:grid;grid-template-columns:1fr 1fr;gap:8px}
+ .nearby a{display:flex;align-items:center;gap:8px;padding:6px;border:1px solid var(--card-line);border-radius:10px;font-size:12px}
+ .nearby a:hover{border-color:var(--accent)}
+ .nearby .pic{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 35%,var(--card)),color-mix(in srgb,var(--accent-2) 30%,var(--card)));background-size:cover;background-position:center;flex-shrink:0}
+ .meta{font-size:13px;color:var(--muted);margin:6px 0 14px}
+ footer{max-width:1320px;margin:0 auto;padding:30px 22px 60px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);margin-top:30px}
+ .leaflet-container{background:color-mix(in srgb,var(--fg) 4%,transparent)!important}
+ </style>
+</head>
+<body>
+ <header class="nav">
+ <div class="nav-in">
+ <a class="brand" href="/"><span class="pin">●</span> Costa Rica Directory</a>
+ <nav class="nav-links">
+ <a href="/?category=tourism">Tourism</a>
+ <a href="/?category=rentals">Rentals</a>
+ <a href="/?category=service">Local services</a>
+ </nav>
+ <div class="ml-auto" style="display:flex;gap:8px;align-items:center">
+ <button class="icon-btn" id="theme-toggle" title="Toggle theme">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
+ </button>
+ </div>
+ </div>
+ </header>
+
+ <section class="hero-wrap" id="hero-wrap">
+ <img id="hero-img" alt="" />
+ <div class="hero-overlay">
+ <div class="hero-inner">
+ <div class="hero-eyebrow" id="hero-eyebrow">Costa Rica</div>
+ <h1 class="hero-title" id="hero-title">Loading…</h1>
+ <p class="hero-sub" id="hero-sub"></p>
+ </div>
+ </div>
+ <div class="hero-credit" id="hero-credit" style="display:none"></div>
+ </section>
+
+ <main>
+ <div class="crumbs"><a href="/">Directory</a> · <span id="crumb-region">Region</span></div>
+
+ <div class="stats-row">
+ <div class="stat"><span class="n" id="s-count">—</span> listings</div>
+ <div class="stat"><span class="n" id="s-tourism">—</span> tourism</div>
+ <div class="stat"><span class="n" id="s-rentals">—</span> rentals</div>
+ <div class="stat"><span class="n" id="s-service">—</span> local services</div>
+ </div>
+
+ <div class="layout">
+ <section>
+ <div class="filter-bar" id="vertical-bar"></div>
+
+ <div class="controls">
+ <div class="search">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
+ <input id="q" placeholder="Search businesses in this region…" autocomplete="off" />
+ </div>
+ <div class="ctrl">
+ <label for="sort">Sort</label>
+ <select id="sort">
+ <option value="newest">Newest</option>
+ <option value="name">Name A→Z</option>
+ <option value="rating">Rating</option>
+ <option value="verified">Verified first</option>
+ </select>
+ </div>
+ <div class="ctrl">
+ <label for="cols">Density</label>
+ <input id="cols" type="range" min="2" max="6" step="1" value="4" />
+ </div>
+ </div>
+
+ <div class="meta" id="meta">Loading places…</div>
+ <div class="grid" id="grid"></div>
+ </section>
+
+ <aside class="side">
+ <div class="card">
+ <div class="label">On the map</div>
+ <div id="region-map"></div>
+ </div>
+
+ <div class="card">
+ <div class="label">By vertical</div>
+ <div class="vert-list" id="vert-list"></div>
+ </div>
+
+ <div class="card" id="nearby-card" style="display:none">
+ <div class="label">Nearby in province</div>
+ <div class="nearby" id="nearby"></div>
+ </div>
+ </aside>
+ </div>
+ </main>
+
+ <footer>© Costa Rica Directory. Aggregated from public records (MEIC, ICT, municipal patentes) and local web directories. Hero photos via Wikipedia/Wikimedia Commons. Map data © OpenStreetMap contributors. Owners can correct or claim a listing at <a href="mailto:info@agentabrams.com">info@agentabrams.com</a>.</footer>
+
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
+ <script>
+ const $ = (s,r=document)=>r.querySelector(s);
+ const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
+ document.documentElement.style.setProperty('--cols', localStorage.getItem('cr-cols') || 4);
+
+ $('#theme-toggle').addEventListener('click', () => {
+ const cur = document.documentElement.getAttribute('data-theme') || 'light';
+ const nxt = cur === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', nxt);
+ try { localStorage.setItem('cr-theme', nxt); } catch(e){}
+ });
+
+ const slug = location.pathname.replace(/^\/r\//,'');
+ const state = { vertical: '', q: '', sort: 'newest' };
+
+ function pluralize(n, s) { return n === 1 ? s : s + 's'; }
+ function headline(region, count) {
+ const variants = [
+ `${count.toLocaleString()} ${pluralize(count,'business')} in ${region.name}`,
+ `${region.name} — every business, in one place`,
+ `Find ${pluralize(count,'business')} across ${region.name}`,
+ `The directory of ${region.name}`,
+ ];
+ return variants[Math.abs(region.id) % variants.length] || variants[0];
+ }
+ function subline(region, byVert) {
+ const top = (byVert||[]).slice(0,3).map(v => v.vertical.replace(/_/g,' ')).join(' · ');
+ const where = region.province ? `${region.province} province` : 'Costa Rica';
+ return `${region.region_type === 'city' ? 'City of' : ''} ${region.name}, ${where}. ${top || 'Tourism, rentals, and local services'}.`.replace(/^\s+/,'');
+ }
+
+ Promise.all([
+ fetch('/api/regions/' + encodeURIComponent(slug)).then(r => r.ok ? r.json() : Promise.reject(r)),
+ ]).then(([region]) => {
+ document.title = region.name + ' — Costa Rica Directory';
+ $('#crumb-region').textContent = region.name;
+
+ // Hero
+ if (region.image_url) { $('#hero-img').src = region.image_url; $('#hero-img').alt = region.name; }
+ else { $('#hero-img').style.display = 'none'; }
+ $('#hero-eyebrow').textContent = region.province ? region.province + ' · Costa Rica' : 'Costa Rica';
+ $('#hero-title').textContent = headline(region, region.place_count);
+ $('#hero-sub').textContent = subline(region, region.by_vertical);
+ if (region.image_credit && region.image_source_url) {
+ $('#hero-credit').innerHTML = '📷 <a href="'+region.image_source_url+'" target="_blank" rel="noopener nofollow">'+region.image_credit+'</a>';
+ $('#hero-credit').style.display = '';
+ }
+
+ // Stats
+ const byCat = {};
+ for (const v of (region.by_vertical||[])) {
+ const cat = v.vertical.startsWith('tourism_') ? 'tourism' : v.vertical.startsWith('rentals_') ? 'rentals' : 'service';
+ byCat[cat] = (byCat[cat] || 0) + v.n;
+ }
+ $('#s-count').textContent = region.place_count.toLocaleString();
+ $('#s-tourism').textContent = (byCat.tourism||0).toLocaleString();
+ $('#s-rentals').textContent = (byCat.rentals||0).toLocaleString();
+ $('#s-service').textContent = (byCat.service||0).toLocaleString();
+
+ // Vertical pill bar (top 8) + sidebar list (all)
+ const vbar = $('#vertical-bar');
+ const allPill = document.createElement('button');
+ allPill.className = 'pill active'; allPill.dataset.vert='';
+ allPill.innerHTML = `All<span class="n">${region.place_count}</span>`;
+ vbar.appendChild(allPill);
+ (region.by_vertical||[]).slice(0,8).forEach(v => {
+ const p = document.createElement('button');
+ p.className = 'pill'; p.dataset.vert = v.vertical;
+ p.innerHTML = `${v.vertical.replace(/_/g,' ')}<span class="n">${v.n}</span>`;
+ vbar.appendChild(p);
+ });
+ vbar.addEventListener('click', e => {
+ const t = e.target.closest('.pill'); if (!t) return;
+ $$('.pill', vbar).forEach(x => x.classList.remove('active'));
+ t.classList.add('active');
+ state.vertical = t.dataset.vert;
+ loadGrid();
+ });
+
+ const vlist = $('#vert-list');
+ (region.by_vertical||[]).forEach(v => {
+ const a = document.createElement('a');
+ a.href = '#'; a.dataset.vert = v.vertical;
+ a.innerHTML = `<span>${v.vertical.replace(/_/g,' ')}</span><span class="n">${v.n}</span>`;
+ a.addEventListener('click', e => { e.preventDefault(); state.vertical=v.vertical; $$('.pill', vbar).forEach(x=>x.classList.remove('active')); loadGrid(); window.scrollTo({top:0,behavior:'smooth'}); });
+ vlist.appendChild(a);
+ });
+
+ // Nearby in province
+ if (region.nearby_in_province && region.nearby_in_province.length) {
+ $('#nearby-card').style.display = '';
+ const wrap = $('#nearby');
+ region.nearby_in_province.forEach(n => {
+ const a = document.createElement('a');
+ a.href = '/r/' + n.slug;
+ a.innerHTML = `<div class="pic"${n.image_url ? ' style="background-image:url('+n.image_url+')"' : ''}></div><div><div>${n.name}</div><div style="color:var(--muted);font-size:11px">${n.n} listings</div></div>`;
+ wrap.appendChild(a);
+ });
+ }
+
+ // Region map (Leaflet) — pin region center, fetch place markers async
+ if (region.lat && region.lng) {
+ const map = L.map('region-map', { zoomControl: true, scrollWheelZoom: false }).setView([+region.lat, +region.lng], 12);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a>' }).addTo(map);
+ L.circleMarker([+region.lat, +region.lng], { radius: 8, color: '#0a7a3a', fillColor:'#0a7a3a', fillOpacity:.7 }).addTo(map).bindPopup('<strong>'+region.name+'</strong><br>'+region.place_count+' listings');
+ } else {
+ $('#region-map').textContent = 'No coordinates on file.';
+ $('#region-map').style.cssText = 'aspect-ratio:auto;padding:20px;color:var(--muted);font-size:13px;display:flex;align-items:center;justify-content:center';
+ }
+
+ loadGrid();
+ }).catch(() => {
+ $('#hero-title').textContent = 'Region not found';
+ });
+
+ function card(p){
+ const wrap = document.createElement('a');
+ wrap.className = 'card'; wrap.href = '/p/' + encodeURIComponent(p.slug);
+ const heroSrc = p.effective_image_url || p.image_url;
+ wrap.innerHTML = `${heroSrc ? '<div class="img"><img loading="lazy" src="'+heroSrc+'" alt=""></div>' : '<div class="img">⛰</div>'}<div class="body"><div class="cat">${(p.vertical||'').replace(/_/g,' ')}</div><div class="name">${p.name}</div><div class="where">${p.address || p.region_name || ''}</div></div>`;
+ return wrap;
+ }
+
+ let qDebounce;
+ $('#q').addEventListener('input', e => { clearTimeout(qDebounce); qDebounce=setTimeout(()=>{ state.q=e.target.value.trim(); loadGrid(); },220); });
+ $('#sort').addEventListener('change', e => { state.sort=e.target.value; loadGrid(); });
+ $('#cols').addEventListener('input', e => { document.documentElement.style.setProperty('--cols', e.target.value); try{localStorage.setItem('cr-cols',e.target.value)}catch(e){} });
+
+ function loadGrid() {
+ const params = new URLSearchParams();
+ params.set('region', slug);
+ if (state.vertical) params.set('vertical', state.vertical);
+ if (state.q) params.set('q', state.q);
+ if (state.sort) params.set('sort', state.sort);
+ params.set('limit', '120');
+ $('#meta').textContent = 'Loading…';
+ $('#grid').innerHTML = '';
+ fetch('/api/places?' + params).then(r => r.json()).then(j => {
+ $('#meta').textContent = `${(j.total||0).toLocaleString()} matches` + (state.vertical ? ` · ${state.vertical.replace(/_/g,' ')}` : '');
+ (j.places||[]).forEach(p => $('#grid').appendChild(card(p)));
+ });
+ }
+ </script>
+</body>
+</html>
diff --git a/server.js b/server.js
index d01cf37..862bbac 100644
--- a/server.js
+++ b/server.js
@@ -329,15 +329,50 @@ app.get('/api/places/:slug/jsonld', async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); }
});
-// Region landing page — same shell, deep-link a region pre-selected
+// Region landing page — dedicated layout with hero image + region map.
app.get('/r/:slug', async (req, res) => {
try {
- const { rows } = await pool.query('SELECT slug, name FROM regions WHERE slug = $1', [req.params.slug]);
- if (!rows.length) return res.status(404).sendFile(path.join(__dirname, 'public', 'index.html'));
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
+ const { rows } = await pool.query('SELECT slug FROM regions WHERE slug = $1', [req.params.slug]);
+ if (!rows.length) return res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
+ res.sendFile(path.join(__dirname, 'public', 'region.html'));
} catch (e) { res.status(500).json({ error: e.message }); }
});
+// Region details endpoint (one region + count + sibling regions in same province)
+app.get('/api/regions/:slug', async (req, res) => {
+ try {
+ const { rows } = await pool.query(`
+ SELECT r.id, r.slug, r.name, r.province, r.region_type, r.lat, r.lng,
+ r.image_url, r.image_credit, r.image_license, r.image_source_url,
+ r.description,
+ COUNT(p.id)::int AS place_count
+ FROM regions r
+ LEFT JOIN places p ON p.region_id = r.id AND p.status = 'active'
+ WHERE r.slug = $1
+ GROUP BY r.id`, [req.params.slug]);
+ if (!rows.length) return res.status(404).json({ error: 'not_found' });
+ const region = rows[0];
+
+ const { rows: byVert } = await pool.query(
+ `SELECT vertical, COUNT(*)::int AS n FROM places WHERE region_id=$1 AND status='active' GROUP BY vertical ORDER BY n DESC`,
+ [region.id]
+ );
+ const { rows: provincial } = await pool.query(
+ `SELECT r.slug, r.name, r.image_url, COUNT(p.id)::int AS n
+ FROM regions r LEFT JOIN places p ON p.region_id=r.id AND p.status='active'
+ WHERE r.province = $1 AND r.id != $2 AND r.region_type IN ('city','town','canton')
+ GROUP BY r.id ORDER BY n DESC NULLS LAST, r.name LIMIT 12`,
+ [region.province, region.id]
+ );
+
+ region.by_vertical = byVert;
+ region.nearby_in_province = provincial;
+ res.json(region);
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// Place detail page
app.get('/p/:slug', async (req, res) => {
try {
← 1670949 VCL features + image attribution: per-region Wikipedia hero
·
back to Costa Rica
·
yolo tick 3: /stats dashboard with KPIs, by-vertical/categor 72b08a1 →