[object Object]

← 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

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>&nbsp;Costa&nbsp;Rica&nbsp;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 →