Put the list on the same index as everything else, and shrink the bundle 60%

resources-list was the last surface still reading the old 862-language file, and the only
one that BAKED its data in: `import data from './data/resources.json'`, 695KB of JSON
compiled into the shipped JS. Every other profile in this bundle carried that weight,
because they all share one .js — the map paid for the list's dataset on every load.

Fetched at runtime from the pin index instead: 5,067 languages rather than 862, and the
bundle drops 798KB to 317KB, gzip 148KB to 106KB.

★ AND IT RENDERED NOTHING, SILENTLY. I emitted `countries` as a COUNT on the strength of a
comment I wrote in the same edit — "rendered as a count elsewhere in this file" — without
reading the template, which does `l.countries.slice(0, 3).join(', ')`. Result:
"x.countries.slice is not a function", a render-time TypeError Vue logs to the CONSOLE and
does not raise as a pageerror. My probe reported an empty page AND "no page errors", which
is the most misleading pair of signals available here. Only listening for console errors
found it. countries is an array of country NAMES again, resolved through
gospel-by-country with an ISO3 fallback.

★ THE HEADLINE NUMBER MOVED TO 2.99 BILLION and it is true, verified against an
independent count — languages 4,031, groups 7,105, people 2,989,037,834, all three
matching exactly. It is also dangerously misreadable. The largest entries are Javanese
113M, Western Panjabi 101M, Shanghainese 92M, Egyptian Arabic 81M, Cantonese 77M: major
world languages that plainly DO have Scripture, which eBible does not host. Read as "3
billion people have no Bible" it is false; read as "not found in the pools we can reach"
it is exact. The page now names those languages directly under the number rather than
leaving the reader to find the caveat further down.

That figure is also the clearest argument yet for the credentials this build keeps asking
for: the pools that DO carry Javanese and Cantonese Scripture are the ones behind a key.

Verified: no console errors, 1,365 real anchors on ebible/jesusfilm/door43/arc.gt hosts,
and the coverage gate, map gate, dashboard and charts probes all pass.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 09:10:06 +00:00
parent 48bc645c67
commit 22a216a207
9 changed files with 151 additions and 38 deletions

View file

@ -32,15 +32,105 @@
* `primary_language` as ISO 639-3, so no fuzzy name matching is involved; name
* matching across scripts is exactly where scripture gets mis-attributed.
*/
import { computed, inject, ref } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import data from './data/resources.json'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
const profileConfig = inject('profileConfig', ref(null))
const isDark = computed(() => profileConfig.value?.isDark === true)
const languages = computed(() => data.languages || [])
const stats = computed(() => data.stats || {})
// ── FETCHED, NOT BUNDLED, AND FROM THE INDEX THE MAP DRAWS ──────────────────────
// This profile used to `import data from './data/resources.json'` — 862 languages baked
// into the JS at build time, while every other surface in this bundle now reads the pin
// index and its 5,067. It was the last one still describing a sixth of the corpus.
//
// Fetching also takes 695KB of JSON out of the shipped bundle. A list view is not worth
// making every OTHER profile in the bundle carry its dataset, which is what a build-time
// import does: they all share one .js.
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
const pins = ref(null)
const loadErr = ref(null)
const countryNames = ref({})
async function load() {
if (pins.value) return
try {
const r = await fetch(`${assetsBase.value}/tables/people-group-pins.json`)
if (!r.ok) throw new Error(`pin index not found (${r.status})`)
pins.value = await r.json()
} catch (e) { loadErr.value = String(e.message || e) }
// Country NAMES, so a row reads "India, Nepal" rather than "IND, NPL". Allowed to fail:
// without it the rows fall back to ISO3, which is worse-looking but not wrong.
try {
const r = await fetch(`${assetsBase.value}/tables/gospel-by-country.json`)
if (r.ok) {
const m = {}
for (const c of (await r.json()).countries || []) if (c.iso3) m[c.iso3] = c.country
countryNames.value = m
}
} catch (_) { /* ISO3 fallback */ }
}
watch(profileConfig, load, { immediate: true })
// Same shape the list has always rendered, rebuilt per LANGUAGE from codes[] + pins[].
const languages = computed(() => {
const d = pins.value
if (!d?.codes) return []
const { codes, pins: rows } = d
const by = new Map()
for (const [, , , iso3, code, pop] of rows) {
if (!code) continue
let e = by.get(code)
if (!e) {
const c = codes[code] || {}
const scope = c.ss || ''
// resources[] is what the UI turns into links; Door43 entries keep their own
// subject so Open Bible Stories is never presented as a translation.
const res = []
if (c.s) res.push({ url: c.s, title: c.sv || 'Scripture in this language' })
for (const dd of c.d || []) res.push({ url: dd.u, title: dd.s })
e = {
iso: code,
language: c.l || 'unknown',
people_group_count: 0,
population: 0,
countries: new Set(),
has_scripture: !!c.s,
full_bible: scope === 'Full Bible',
nt: scope === 'Full Bible' || /New Testament|both Testaments/.test(scope),
film_url: c.f || '',
resources: res,
}
by.set(code, e)
}
e.people_group_count += 1
e.population += Number(pop) || 0
if (iso3) e.countries.add(iso3)
}
// ★ AN ARRAY OF NAMES, because that is what the template consumes:
// `l.countries.slice(0, 3).join(', ')`. I first emitted a COUNT here on the strength of a
// comment I wrote rather than the template I was feeding, and the whole profile rendered
// NOTHING — "x.countries.slice is not a function", a render-time TypeError that Vue logs
// to the console and does not raise as a page error. The probe saw an empty page and
// "no page errors", which is the most misleading pair of signals available.
const nm = countryNames.value
return Array.from(by.values()).map((e) => ({
...e,
countries: [...e.countries].map((c) => nm[c] || c).sort(),
}))
})
// Derived, not read from a baked stats block — the numbers have to move with the data.
const stats = computed(() => {
const l = languages.value
const without = l.filter((x) => !x.has_scripture)
return {
languages_without: without.length,
people_groups_without_scripture: without.reduce((n, x) => n + x.people_group_count, 0),
population_without_scripture: without.reduce((n, x) => n + x.population, 0),
ebible_languages: l.filter((x) => x.has_scripture).length,
ebible_translations: l.reduce((n, x) => n + x.resources.length, 0),
}
})
// 'none' first — the whole app exists to make the gap visible.
const filter = ref('none')
@ -57,6 +147,24 @@ const shown = computed(() => {
return l
})
// ★ THE HEADLINE NUMBER IS TRUE AND DANGEROUSLY MISREADABLE, so the page names its own
// limit beside it. Widening the corpus from 862 to 5,067 languages moved this figure to
// 2.99 BILLION people — verified against an independent count, all three totals matching
// exactly. But the largest entries are Javanese (113M), Western Panjabi (101M),
// Shanghainese (92M), Egyptian Arabic (81M) and Cantonese (77M): major world languages
// that plainly DO have Scripture, which eBible simply does not host. Read as "3 billion
// people have no Bible" the number is false; read as "no Scripture found in the pools we
// can reach" it is exactly right. The gap between those two readings is the whole reason
// this list names its examples.
const biggestUnfound = computed(() =>
languages.value.filter((l) => !l.has_scripture)
.sort((a, b) => b.population - a.population).slice(0, 4)
.map((l) => `${l.language} (${fmtShort(l.population)})`).join(', '))
const fmtShort = (n) => {
const v = Number(n) || 0
return v >= 1e6 ? `${Math.round(v / 1e5) / 10}M` : v.toLocaleString()
}
const fmt = (n) => (Number(n) || 0).toLocaleString()
// ── EVERY LINK THE ROW ACTUALLY HAS, not just the scripture ones ─────────────────────────────
@ -146,6 +254,11 @@ useShadowStyles(
people groups across {{ fmt(stats.languages_without) }} languages
— no scripture found in the pools checked
<br>≈ {{ fmt(stats.population_without_scripture) }} people
<template v-if="biggestUnfound">
<br><small>The largest are {{ biggestUnfound }} — major languages that DO have
Scripture, which the pools reachable here simply do not host. This counts what was
not FOUND in eBible, JESUS Film and Door43, not what does not exist.</small>
</template>
</div>
</div>

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -556,7 +556,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3",
"integrity": "sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz",
"elements": [
"gospel-resources-map"
],
@ -576,7 +576,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -590,7 +590,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -604,7 +604,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -618,7 +618,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -632,7 +632,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-iZfkYQAqPPbbGZjfVRkboOX+xWDdyqqQEwDAQLk3iqyE4wMmrpMoDocigcm0Xyd3\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-w0mZyUWVPLIZqK9qIJzxTqdT5PETy/KJhjTKxFHayHxCD3zjN/Ah+BGaQ52x1dAz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]