← back to Costa Rica
yolo tick 1: place detail page (/p/:slug) + region landing route (/r/:slug) + 404; cards now link through; cleaned 11 dead portal URLs from local-portals
1c4ce3453917a73d70219a5833905baea20256cf · 2026-05-08 00:31:54 -0700 · Steve
Files touched
A public/404.htmlM public/index.htmlA public/place.htmlM scripts/ingest/local-portals.jsM server.js
Diff
commit 1c4ce3453917a73d70219a5833905baea20256cf
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 00:31:54 2026 -0700
yolo tick 1: place detail page (/p/:slug) + region landing route (/r/:slug) + 404; cards now link through; cleaned 11 dead portal URLs from local-portals
---
public/404.html | 4 ++
public/index.html | 2 +-
public/place.html | 130 ++++++++++++++++++++++++++++++++++++++++
scripts/ingest/local-portals.js | 6 ++
server.js | 23 +++++++
5 files changed, 164 insertions(+), 1 deletion(-)
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..72f8bec
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<html lang="en"><head><meta charset="utf-8"><title>Not found — CR Directory</title>
+<style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#fafaf7;color:#0e1015;margin:0;display:grid;place-items:center;min-height:100vh}main{text-align:center;padding:40px}h1{font-size:48px;margin:0}p{color:#5a6270}a{color:#0a7a3a}</style></head>
+<body><main><h1>404</h1><p>That listing does not exist.</p><p><a href="/">Back to directory</a></p></main></body></html>
diff --git a/public/index.html b/public/index.html
index 735387a..6661c9e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -206,7 +206,7 @@
function card(p){
const wrap = document.createElement('a');
wrap.className = 'card';
- wrap.href = '#';
+ wrap.href = '/p/' + encodeURIComponent(p.slug);
const verticalLabel = (p.vertical||'').replace(/_/g,' ');
const img = p.image_url
? `<div class="img"><img loading="lazy" src="${p.image_url}" alt=""></div>`
diff --git a/public/place.html b/public/place.html
new file mode 100644
index 0000000..97cd89d
--- /dev/null
+++ b/public/place.html
@@ -0,0 +1,130 @@
+<!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>
+ <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}
+ [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} 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:980px;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)}
+ .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}
+ main{max-width:980px;margin:0 auto;padding:34px 22px 80px}
+ .crumbs{font-size:13px;color:var(--muted);margin-bottom:8px}
+ h1{font-size:clamp(28px,4vw,40px);letter-spacing:-.02em;margin:0 0 6px}
+ .where{color:var(--muted);margin-bottom:24px;font-size:14px}
+ .hero{aspect-ratio:21/9;border-radius:14px;background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 35%, var(--card)), color-mix(in srgb, var(--accent-2) 30%, var(--card)));margin-bottom:24px;display:flex;align-items:center;justify-content:center;font-size:64px;color:color-mix(in srgb,var(--fg) 30%,transparent)}
+ .hero img{width:100%;height:100%;object-fit:cover;border-radius:14px}
+ .grid{display:grid;grid-template-columns:2fr 1fr;gap:32px}
+ @media(max-width:760px){.grid{grid-template-columns:1fr}}
+ .card{background:var(--card);border:1px solid var(--card-line);border-radius:14px;padding:18px 20px}
+ .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;margin-bottom:4px}
+ .row{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--line)}
+ .row:last-child{border-bottom:0}
+ .row .k{color:var(--muted);font-size:13px} .row .v{font-size:14px;text-align:right;max-width:60%;word-break:break-word}
+ .badges{display:flex;gap:6px;flex-wrap:wrap;margin:10px 0 6px}
+ .badge{font-size:11px;padding:3px 9px;border-radius:999px;background:color-mix(in srgb,var(--fg) 6%,transparent);color:var(--muted)}
+ .badge.verified{background:color-mix(in srgb,var(--accent) 18%,transparent);color:var(--accent)}
+ .desc{font-size:15px;line-height:1.7;color:color-mix(in srgb,var(--fg) 92%,transparent)}
+ .cta{display:inline-block;padding:9px 16px;border:1px solid var(--accent);color:var(--accent);border-radius:999px;font-size:13px;text-decoration:none;margin-top:10px}
+ .cta:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}
+ footer{max-width:980px;margin:0 auto;padding:30px 22px 60px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);margin-top:40px}
+ </style>
+</head>
+<body>
+ <header class="nav">
+ <div class="nav-in">
+ <a class="brand" href="/"><span class="pin">●</span> Costa Rica Directory</a>
+ <button class="icon-btn ml-auto" 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>
+ </header>
+
+ <main>
+ <div class="crumbs"><a href="/">Directory</a> · <a href="" id="region-link">Region</a> · <span id="vertical-crumb">…</span></div>
+ <h1 id="name">Loading…</h1>
+ <div class="where" id="where">—</div>
+
+ <div class="hero" id="hero">⛰</div>
+
+ <div class="grid">
+ <section>
+ <div class="card">
+ <div class="label">About</div>
+ <p class="desc" id="desc">—</p>
+ <div class="badges" id="badges"></div>
+ <a class="cta" id="cta-website" target="_blank" rel="noopener" style="display:none">Visit website ↗</a>
+ </div>
+ </section>
+
+ <aside>
+ <div class="card">
+ <div class="label">Details</div>
+ <div class="row"><span class="k">Vertical</span><span class="v" id="d-vertical">—</span></div>
+ <div class="row"><span class="k">Address</span><span class="v" id="d-address">—</span></div>
+ <div class="row"><span class="k">Phone</span><span class="v" id="d-phone">—</span></div>
+ <div class="row"><span class="k">Email</span><span class="v" id="d-email">—</span></div>
+ <div class="row"><span class="k">Cédula jurídica</span><span class="v" id="d-cedula">—</span></div>
+ <div class="row"><span class="k">Source</span><span class="v" id="d-source">—</span></div>
+ </div>
+ </aside>
+ </div>
+ </main>
+
+ <footer>
+ <div>© Costa Rica Directory. Aggregated public-record data — owners can correct or claim a listing at <a href="mailto:info@agentabrams.com">info@agentabrams.com</a>.</div>
+ </footer>
+
+ <script>
+ document.getElementById('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(/^\/p\//,'');
+ fetch('/api/places/' + encodeURIComponent(slug)).then(r => r.ok ? r.json() : Promise.reject(r)).then(p => {
+ document.title = p.name + ' — Costa Rica Directory';
+ document.getElementById('name').textContent = p.name;
+ const where = (p.region_name ? p.region_name + (p.province ? ', ' + p.province : '') : (p.address || ''));
+ document.getElementById('where').textContent = where;
+ const vertical = (p.vertical||'').replace(/_/g,' ');
+ document.getElementById('vertical-crumb').textContent = vertical;
+ const regionLink = document.getElementById('region-link');
+ if (p.region_slug){ regionLink.href = '/r/' + p.region_slug; regionLink.textContent = p.region_name; }
+
+ const hero = document.getElementById('hero');
+ if (p.image_url){ hero.innerHTML = '<img src="'+p.image_url+'" alt="">'; }
+
+ document.getElementById('desc').textContent = p.description || '—';
+ const badges = document.getElementById('badges');
+ const tags = [vertical, p.verified ? 'verified' : null, p.rating ? '★ ' + (+p.rating).toFixed(1) : null].filter(Boolean);
+ tags.forEach(t => { const span=document.createElement('span'); span.className='badge'+(t==='verified'?' verified':''); span.textContent=t; badges.appendChild(span); });
+
+ if (p.website){ const a=document.getElementById('cta-website'); a.href=p.website; a.style.display=''; }
+
+ const set = (id,v) => document.getElementById(id).textContent = v || '—';
+ set('d-vertical', vertical);
+ set('d-address', p.address);
+ set('d-phone', p.phone);
+ set('d-email', p.email);
+ set('d-cedula', p.cedula_juridica);
+ const src = p.source ? (p.source_url ? '' : p.source) : '—';
+ const dSrc = document.getElementById('d-source');
+ if (p.source_url){ dSrc.innerHTML = '<a href="'+p.source_url+'" target="_blank" rel="noopener">'+(p.source||'link')+' ↗</a>'; }
+ else { dSrc.textContent = p.source || '—'; }
+ }).catch(() => {
+ document.getElementById('name').textContent = 'Listing not found';
+ document.getElementById('desc').textContent = 'This listing does not exist or was removed.';
+ });
+ </script>
+</body>
+</html>
diff --git a/scripts/ingest/local-portals.js b/scripts/ingest/local-portals.js
index 7d0fca7..250274e 100644
--- a/scripts/ingest/local-portals.js
+++ b/scripts/ingest/local-portals.js
@@ -19,6 +19,12 @@ const PORTALS = [
{ url: 'https://www.searchsamara.com/copy-of-leisure-services-1', region: 'samara', vertical: 'service_retail', label: 'searchsamara' },
{ url: 'https://nosaranow.com/business-directory/', region: 'nosara', vertical: 'service_retail', label: 'nosaranow' },
];
+// Candidate URLs that 404'd or had DNS errors — kept here for posterity / future verification:
+// quepolandia.com/category/businesses, arenal.net/business-directory, santateresacostarica.com/businesses,
+// thecostaricanews.com/business-directory, govisitcostarica.com/region/business-directory,
+// nosaratravel.com/local-businesses, manuelantonioestates.com/businesses, uvitainformationcenter.com,
+// cocosbeachcostarica.com/business-directory, dominicaldays.com/business-directory, escazunews.com/category/local-business
+
const KEYWORD_BLOCK = /(^#|mailto:|tel:|javascript:|^\/?$|\bhome\b|\bcontact\b|\babout\b|\blogin\b|\bregister\b|\bcart\b|\bcheckout\b|\bsearch\b|\bsubscribe\b|\bprivacy\b|\bterms\b|\bsitemap\b|\bblog\b|\bnews\b|\barchive\b)/i;
const KEYWORD_KEEP = /(business|directory|listing|company|empresa|negocio|hotel|tour|restaurant|restaurante|salon|spa|fitness|surf|yoga|store|tienda|cabin|villa|rental|alquiler|service|servicio|gym|cafe|bar)/i;
diff --git a/server.js b/server.js
index d67e685..74e6dd3 100644
--- a/server.js
+++ b/server.js
@@ -190,6 +190,29 @@ app.get('/api/ingest/runs', async (_req, res) => {
}
});
+// Region landing page — same shell, deep-link a region pre-selected
+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'));
+ } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Place detail page
+app.get('/p/:slug', async (req, res) => {
+ try {
+ const { rows } = await pool.query(`
+ SELECT p.*, r.slug AS region_slug, r.name AS region_name, r.province
+ FROM places p
+ LEFT JOIN regions r ON p.region_id = r.id
+ WHERE p.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', 'place.html'));
+ } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
app.use(express.static(path.join(__dirname, 'public')));
app.listen(PORT, '0.0.0.0', () => {
← c91cf7b MEIC PYMEs: full XLSX import (25,661 rows) + auto-region cre
·
back to Costa Rica
·
VCL features + image attribution: per-region Wikipedia hero 1670949 →