← back to Costa Rica
Localize all region/place images (Wikimedia burst-429 fix) + curated province scenic fallback (locator-SVG/Mikulov cleanup)
4ad9f203f52391d9919b93112469a592b44775eb · 2026-07-22 21:06:56 -0700 · Steve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
A scripts/cr-fix-fallback.jsA scripts/cr-localize-images.js
Diff
commit 4ad9f203f52391d9919b93112469a592b44775eb
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 21:06:56 2026 -0700
Localize all region/place images (Wikimedia burst-429 fix) + curated province scenic fallback (locator-SVG/Mikulov cleanup)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
scripts/cr-fix-fallback.js | 42 +++++++++++++++++++++++++++
scripts/cr-localize-images.js | 67 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+)
diff --git a/scripts/cr-fix-fallback.js b/scripts/cr-fix-fallback.js
new file mode 100644
index 0000000..a5e696c
--- /dev/null
+++ b/scripts/cr-fix-fallback.js
@@ -0,0 +1,42 @@
+'use strict';
+// Replace bad fallback region images (province locator-map SVGs + one wrong
+// Mikulov/Czech photo) with a real scenic Wikipedia lead image per province.
+// Curated pages: tourism-famous, real photos, attributable. Run on Kamatera.
+const { Pool } = require('pg');
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+const UA = { 'User-Agent': 'CR-Directory/1.0 (https://costarica.agentabrams.com; info@agentabrams.com)' };
+
+const PROVINCE_SCENIC = {
+ 'San José': 'San_José,_Costa_Rica',
+ 'Alajuela': 'Arenal_Volcano',
+ 'Heredia': 'Barva_Volcano',
+ 'Cartago': 'Irazú_Volcano',
+ 'Limón': 'Cahuita_National_Park',
+ 'Puntarenas': 'Manuel_Antonio_National_Park',
+ 'Guanacaste': 'Rincón_de_la_Vieja_Volcano',
+};
+
+(async () => {
+ for (const [prov, page] of Object.entries(PROVINCE_SCENIC)) {
+ const r = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(page)}`, { headers: UA });
+ if (!r.ok) { console.log(`${prov}: summary HTTP ${r.status}`); continue; }
+ const j = await r.json();
+ const img = j.originalimage || j.thumbnail;
+ if (!img || img.source.endsWith('.svg') || img.source.includes('.svg/')) {
+ console.log(`${prov}: no usable photo on ${page}`); continue;
+ }
+ // Only rows still carrying a BAD remote image: locator-map SVG renders or
+ // the known-wrong Mikulov photo. Already-localized and real photos untouched.
+ const res = await pool.query(
+ `UPDATE regions SET image_url=$1,
+ image_credit=$2, image_license='CC via Wikimedia Commons',
+ image_source_url=$3, image_fetched_at=NOW()
+ WHERE province=$4 AND image_url LIKE 'http%'
+ AND (image_url LIKE '%_in_Costa_Rica.svg%' OR image_url LIKE '%Mikulov%')`,
+ [img.source, `Wikipedia: ${j.title}`,
+ j.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${page}`, prov]);
+ console.log(`${prov}: ${res.rowCount} regions -> ${page}`);
+ await new Promise(s => setTimeout(s, 500));
+ }
+ await pool.end();
+})();
diff --git a/scripts/cr-localize-images.js b/scripts/cr-localize-images.js
new file mode 100644
index 0000000..11cf079
--- /dev/null
+++ b/scripts/cr-localize-images.js
@@ -0,0 +1,67 @@
+'use strict';
+// costa-rica: localize region + place images. Wikimedia hotlinks 429 on
+// burst-loads (grids fire ~50 at once), so download each once (~900px thumb)
+// into public/img/{regions,places}/ and repoint the DB, preserving the
+// original in image_remote_url. Idempotent: skips rows already local.
+// Run ON Kamatera from /root/Projects/costa-rica with DATABASE_URL exported.
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+const UA = 'CR-Directory/1.0 (https://costarica.agentabrams.com; info@agentabrams.com) image-localizer';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+function thumbUrl(remote) {
+ const clean = remote.split('?')[0];
+ if (clean.includes('upload.wikimedia.org')) {
+ // /commons/thumb/<h>/<hh>/<File>/<Npx-File> → real filename is 2nd-to-last
+ const segs = clean.split('/');
+ const file = decodeURIComponent(clean.includes('/thumb/') ? segs[segs.length - 2] : segs[segs.length - 1]);
+ return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(file)}?width=900`;
+ }
+ return remote; // non-wikimedia: fetch as-is
+}
+
+async function fetchWithRetry(url) {
+ for (let a = 1; a <= 4; a++) {
+ const res = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow' });
+ if (res.status === 429) { console.log(` 429 — backoff 20s (${a})`); await sleep(20000); continue; }
+ return res;
+ }
+ return null;
+}
+
+async function localize(table, dir) {
+ await pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS image_remote_url TEXT`);
+ const { rows } = await pool.query(
+ `SELECT id, slug, image_url FROM ${table} WHERE image_url LIKE 'http%' ORDER BY slug`);
+ console.log(`${table}: ${rows.length} remote images`);
+ const out = path.join(__dirname, 'public', 'img', dir);
+ fs.mkdirSync(out, { recursive: true });
+ let ok = 0, fail = 0;
+ for (const r of rows) {
+ try {
+ const res = await fetchWithRetry(thumbUrl(r.image_url));
+ if (!res || !res.ok) { console.log(`FAIL ${r.slug}: HTTP ${res && res.status}`); fail++; continue; }
+ const ct = res.headers.get('content-type') || '';
+ const ext = ct.includes('png') ? 'png' : 'jpg';
+ const buf = Buffer.from(await res.arrayBuffer());
+ if (buf.length < 4096) { console.log(`FAIL ${r.slug}: tiny (${buf.length}B)`); fail++; continue; }
+ fs.writeFileSync(path.join(out, `${r.slug}.${ext}`), buf);
+ await pool.query(
+ `UPDATE ${table} SET image_remote_url = image_url, image_url = $1 WHERE id = $2`,
+ [`/img/${dir}/${r.slug}.${ext}`, r.id]);
+ ok++;
+ console.log(`ok ${r.slug} (${Math.round(buf.length / 1024)}KB)`);
+ } catch (e) { console.log(`FAIL ${r.slug}: ${e.message}`); fail++; }
+ await sleep(1100);
+ }
+ console.log(`${table} DONE ok=${ok} fail=${fail}`);
+}
+
+(async () => {
+ await localize('regions', 'regions');
+ await localize('places', 'places');
+ await pool.end();
+})();
← 31e910d guard against serving .bak / .pre- snapshot files
·
back to Costa Rica
·
Free website enrichment via OpenStreetMap/Overpass: 89 place 2f8854c →