Verify all 3,603 links: 2 were dead, and the map showed "unchecked" on the rest
The brief's rule is to verify a URL resolves before shipping it. The table that did that was written in August against a corpus of 426 links, while the map now ships 3,603 - and the UI matched exactly TWO of them. Two silent causes: its eBible keys were http:// while every shipped URL now passes through httpsify, and its 306 film keys were the old ?languageId= URLs that were later replaced wholesale by Arclight shareUrl deep links. Links that HAD been verified displayed as "unchecked", which a reader cannot tell apart from "nobody looked". The old note said the film page was unverifiable by fetch, and for the URL shape it was written against that was right: ?languageId=13562 and ?languageId=99999999 return byte-identical HTML, so a status check there is a false green. The shareUrl migration changed that. A path discriminates: /watch/jesus/german-standard -> 200, 944,516 bytes /watch/jesus/pashto-yusufzai -> 200, 810,741 bytes (distinct body) /watch/jesus/definitely-not-xyzzy -> 404 So 2,049 film links became checkable for the first time. The 5 remaining ?languageId= URLs stay "unknown" with that reason - never verified, never broken. Every host was probed with a known-good and a known-bad before being trusted, and all answer HEAD and discriminate: ebible.org, png.bible, alkitab.pw, jesusfilm.org, arc.gt, git.door43.org, zume.training. HEAD is deliberate - a GET over 2,049 film pages is ~1.6GB of someone else's bandwidth to learn a status code. Five negative and five positive controls run in band and the script aborts rather than write a table if either arm misbehaves. Result over 3,603 unique URLs in 365s: 3,596 verified, 5 unknown, and 2 genuinely dead - arc.gt/092vn (Yakpa, 2 pins) and /watch/jesus/4101 (Zhuang Liujiang, 1 pin), both re-confirmed 404 with a browser UA and a GET. The builder now drops any link verification proved dead BEFORE tiers are computed, so a code whose only film is a 404 has no film and its tier says so. The gap filter moves 3,457 -> 3,460, which is exactly those three pins. Also fixed: the verification fetch hung off openPin's country branch, so a tap landing before the country index had loaded left the whole session reading "unchecked". It is now fetched unconditionally. Lookup coverage goes from 2/3,605 to 3,603/3,603. New gate clause K fails below a 95% coverage floor or if any shipped URL is recorded broken; poison self-test now 11/11. Verified in a browser on the exact case that failed - one immediate pin tap - which now fetches the table and renders "Scripture - link checked" and "Film - link checked"; screenshot read to confirm both chips are green where they were grey.
This commit is contained in:
parent
e04509a94d
commit
101653e3fd
21 changed files with 41882 additions and 3507 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -55,10 +55,11 @@ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|||
const POISON = process.argv.includes('--poison')
|
||||
|
||||
const read = (p) => JSON.parse(readFileSync(resolve(REPO, p), 'utf8'))
|
||||
let pins, table
|
||||
let pins, table, verif
|
||||
try {
|
||||
pins = read('data/tables/people-group-pins.json')
|
||||
table = read('data/tables/gospel-resource-links.json')
|
||||
try { verif = read('data/tables/gospel-link-verification.json') } catch { verif = null }
|
||||
} catch (e) {
|
||||
console.error(`COVERAGE-CONSISTENT: SKIP — inputs missing (${e.message}).`)
|
||||
console.error(' Run: node plugin-designs/gospel-resources/scripts/build-people-group-pins.mjs')
|
||||
|
|
@ -86,6 +87,7 @@ if (POISON) {
|
|||
{ ...trows[trows.length - 1], resource_url: 'https://example.invalid/invented' }] // E
|
||||
rows = [[...rows[0].slice(0, 6), 2], ...rows.slice(1)] // F (forced tier 2)
|
||||
codes.zzj = { l: 'Both at once', z: 'https://zume.training/en', za: { u: 'https://zume.training/pus', l: 'Pashto', n: 'x' } } // J
|
||||
codes.zzk = { l: 'Ships a known-dead link', f: 'https://jesusfilm.org/watch/jesus/4101' } // K
|
||||
}
|
||||
|
||||
const SPECIAL = (c) => c === 'und' || c === 'mul' || c === 'mis' || c === 'zxx' || (c >= 'qaa' && c <= 'qtz')
|
||||
|
|
@ -173,13 +175,37 @@ if (both.length) add('J', `${both.length} code(s) carry BOTH a confirmed and an
|
|||
const thin = Object.entries(codes).filter(([, v]) => v.za && !(v.za.u && v.za.l && v.za.n)).map(([c]) => c)
|
||||
if (thin.length) add('J', `${thin.length} approximate entr(ies) missing a url, language or note (e.g. ${thin[0]})`)
|
||||
|
||||
// K — verification must actually cover what is shipped, and nothing shipped may be known
|
||||
// dead. The previous table matched TWO of 3,605 links because its keys were written http://
|
||||
// and against a URL shape the index had since replaced; the map showed "unchecked" on links
|
||||
// that had in fact been verified and nobody could tell that from never-looked-at. A coverage
|
||||
// floor is the only thing that makes that failure loud.
|
||||
if (verif && verif.links) {
|
||||
const V = verif.links
|
||||
const shipped = new Set()
|
||||
for (const v of Object.values(codes)) {
|
||||
if (v.s) shipped.add(v.s)
|
||||
if (v.f) shipped.add(v.f)
|
||||
if (v.z) shipped.add(v.z)
|
||||
if (v.za && v.za.u) shipped.add(v.za.u)
|
||||
for (const d of v.d || []) if (d.u) shipped.add(d.u)
|
||||
}
|
||||
const known = [...shipped].filter((u) => V[u])
|
||||
const cover = shipped.size ? known.length / shipped.size : 0
|
||||
if (cover < 0.95) {
|
||||
add('K', `only ${known.length}/${shipped.size} shipped URL(s) (${(100 * cover).toFixed(1)}%) appear in the verification table — below the 95% floor`)
|
||||
}
|
||||
const dead = [...shipped].filter((u) => V[u] && V[u].state === 'broken')
|
||||
if (dead.length) add('K', `${dead.length} shipped URL(s) are recorded BROKEN and must not ship (e.g. ${dead[0]})`)
|
||||
}
|
||||
|
||||
const clauses = new Set(fails.map((f) => f.slice(3, 4)))
|
||||
if (POISON) {
|
||||
// D is included deliberately: it is the clause that would have caught the 506
|
||||
// resources dropped from a table titled "every link", and the most valuable clause is
|
||||
// the worst one to leave unproven. The injected codes above (und, zzq) are absent from
|
||||
// the table, so they poison D as well as A and B.
|
||||
const want = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
|
||||
const want = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
|
||||
const caught = want.filter((c) => clauses.has(c))
|
||||
console.log(`COVERAGE-CONSISTENT --poison: ${caught.length}/${want.length} clause(s) caught their injected defect (${caught.join(',')})`)
|
||||
if (caught.length !== want.length) {
|
||||
|
|
@ -203,3 +229,4 @@ console.log(' C reach recounted off the pins D every resourced code is
|
|||
console.log(' E no table URL the pins do not hold F pin tiers match their code\'s URLs')
|
||||
console.log(' G no plain-http URL H no story set typed as Scripture')
|
||||
console.log(' I row type agrees with its host J approximate never reads as confirmed')
|
||||
console.log(' K links are verified, and none known-broken ships')
|
||||
|
|
|
|||
|
|
@ -604,8 +604,13 @@ function paintPins(map) {
|
|||
function openPin(i) {
|
||||
const r = pinRows.value[i]
|
||||
if (!r) return
|
||||
// Link verification is needed by EVERY pin, so it is fetched unconditionally. It used
|
||||
// to hang off the country branch below, which meant a tap landing before the country
|
||||
// index had loaded left the whole session showing "unchecked" on links that were in
|
||||
// fact verified — and "unchecked" is indistinguishable from "nobody looked".
|
||||
ensurePeopleGroups()
|
||||
const country = byIso.value[r[3]]
|
||||
if (country) { selected.value = country; shown.value = PAGE; ensurePeopleGroups() }
|
||||
if (country) { selected.value = country; shown.value = PAGE }
|
||||
else selected.value = { country: r[3], iso3: r[3] }
|
||||
// ONE shape, built in one place. openPin used to assemble its own object, so a field
|
||||
// added to the country-list shape silently did not exist on a pin tap.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const EBIBLE = resolve(REPO, 'data/tables/ebible-index.json')
|
|||
const ARC = resolve(REPO, 'data/tables/arclight-index.json')
|
||||
const ARC_SHARE = resolve(REPO, 'data/tables/arclight-share-urls.json')
|
||||
const ZUME = resolve(REPO, 'data/tables/zume-index.json')
|
||||
const VERIFY = resolve(REPO, 'data/tables/gospel-link-verification.json')
|
||||
|
||||
// Two entries with the same subject rendered as "Open Bible Stories Open Bible Stories"
|
||||
// — two links a reader cannot tell apart. 40 languages are in that state, because a
|
||||
|
|
@ -121,6 +122,17 @@ const filmUrl = (code, aa) => httpsify(share[code] || (aa ? aa.u : ''))
|
|||
// field and deliberately does NOT move a group's tier: the tiers and the gap list mean
|
||||
// "Scripture and/or film", and quietly folding a third kind of resource into them would
|
||||
// shrink the gap count without anyone having translated anything.
|
||||
// Link verification snapshot (scripts/verify-gospel-links.mjs). A URL that verification
|
||||
// PROVED dead is dropped here rather than shipped with a warning: a field worker following
|
||||
// a 404 has already lost, and "we knew it was broken and showed it anyway" is not a state
|
||||
// this map should be able to reach. Optional like every other snapshot — with no file, no
|
||||
// link is dropped, and the build still runs.
|
||||
let dead = new Set()
|
||||
try {
|
||||
const j = JSON.parse(readFileSync(VERIFY, 'utf8'))
|
||||
for (const [u, r] of Object.entries(j.links || {})) if (r && r.state === 'broken') dead.add(u)
|
||||
} catch { dead = new Set() }
|
||||
|
||||
let zume = {}
|
||||
let zumeApprox = {}
|
||||
try {
|
||||
|
|
@ -278,6 +290,23 @@ for (const [c, v] of Object.entries(codes)) {
|
|||
if (n) { v.l = n; catNamed++ }
|
||||
}
|
||||
|
||||
// Drop anything verification proved dead, BEFORE tiers are computed — a code whose only
|
||||
// film is a 404 has no film, and its tier must say so.
|
||||
let droppedDead = 0
|
||||
if (dead.size) {
|
||||
for (const v of Object.values(codes)) {
|
||||
if (v.s && dead.has(v.s)) { delete v.s; delete v.sv; delete v.ss; droppedDead++ }
|
||||
if (v.f && dead.has(v.f)) { delete v.f; droppedDead++ }
|
||||
if (v.z && dead.has(v.z)) { delete v.z; droppedDead++ }
|
||||
if (v.za && dead.has(v.za.u)) { delete v.za; droppedDead++ }
|
||||
if (v.d) {
|
||||
const before = v.d.length
|
||||
v.d = v.d.filter((x) => !dead.has(x.u))
|
||||
droppedDead += before - v.d.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ ONE PROVENANCE PASS, APPLIED TO EVERY CODE. This build scans the eBible, Arclight and
|
||||
// Door43 catalogues IN FULL, so for any code, absence from them is a searched-and-absent
|
||||
// result rather than an unknown. The original records predate the Door43 pass and claim
|
||||
|
|
@ -369,6 +398,7 @@ console.log(`door43 codes ${Object.keys(d43).length} (${d43Only} new)`)
|
|||
console.log(`ebible codes ${Object.keys(eb).length} (${ebOnly} new)`)
|
||||
console.log(`arclight codes ${Object.keys(arc).length} (${arcOnly} new, ${upgraded} upgraded to a real deep link)`)
|
||||
console.log(`share urls ${Object.keys(share).length}`)
|
||||
console.log(`dead links dropped ${droppedDead} (verification proved them 404; ${dead.size} known broken)`)
|
||||
console.log(`zume languages ${zumeCodes} (${zumeNew} new codes; training only, tier unchanged)`)
|
||||
console.log(`zume approximate ${zumeApproxCodes} (published in the language, variety not stated; never counted as an exact join)`)
|
||||
console.log(`named from cat ${catNamed} (no pin speaks them; named from a catalogue)`)
|
||||
|
|
|
|||
|
|
@ -1,263 +1,209 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* verify-gospel-links.mjs — prove (or refuse to claim) that the gospel-resource
|
||||
* links in people-group-resources.json actually resolve to the language they claim.
|
||||
* verify-gospel-links.mjs — fetch EVERY link this map ships and record whether it resolves.
|
||||
*
|
||||
* Writes public/assets/tables/gospel-link-verification.json, keyed by URL, which the
|
||||
* map + list profiles join on so the UI can show a verification state per link.
|
||||
* The brief's rule is "verify a URL resolves before shipping it". This is the script that
|
||||
* discharges it, and it now checks the corpus the map actually holds rather than the one it
|
||||
* held in August.
|
||||
*
|
||||
* ═══ WHY THIS FILE EXISTS AT ALL: A 200 FROM jesusfilm.org PROVES NOTHING ═══════
|
||||
* jesusfilm.org/watch/?languageId=N is a CLIENT-RENDERED SPA. The languageId query
|
||||
* param has ZERO effect on the HTML the server sends. Measured 2026-08-02:
|
||||
* ═══ WHAT WAS WRONG BEFORE ══════════════════════════════════════════════════════════════
|
||||
* The previous table carried 426 entries against a corpus that has since grown to 3,603
|
||||
* links, and — worse — the UI matched only TWO of them. Two causes, both silent:
|
||||
* 1. Its eBible keys were written as http:// while the index now ships https:// (every
|
||||
* URL passes through httpsify), so an exact key lookup missed all 120.
|
||||
* 2. Its 306 film keys were the old jesusfilm.org/watch/?languageId=N URLs, which were
|
||||
* later replaced wholesale by Arclight shareUrl deep links. Those keys no longer
|
||||
* exist in the index at all.
|
||||
* So the map displayed "unchecked" on links that HAD been verified, and a reader had no way
|
||||
* to tell that from a link nobody had ever looked at.
|
||||
*
|
||||
* languageId=13562 (valid) -> HTTP 200, 1,024,615 bytes, md5 e8938c31…
|
||||
* languageId=99999999 (bogus)-> HTTP 200, 1,024,615 bytes, md5 e8938c31…
|
||||
* ═══ THE FINDING THAT MAKES THIS POSSIBLE ═══════════════════════════════════════════════
|
||||
* The old note in this file said the film page was "unverifiable by fetch", and for the URL
|
||||
* shape it was written against that was exactly right:
|
||||
*
|
||||
* BYTE-IDENTICAL. So status-checking or HTML-scraping that URL is a FALSE GREEN: it
|
||||
* returns "verified" for a language ID that does not exist. Any such check measures
|
||||
* nothing at all.
|
||||
* jesusfilm.org/watch/?languageId=13562 (valid) -> 200, 1,024,615 bytes, md5 e8938c31
|
||||
* jesusfilm.org/watch/?languageId=99999999 (bogus) -> 200, 1,024,615 bytes, md5 e8938c31
|
||||
*
|
||||
* THE REAL DISCRIMINATOR is the Arclight API — the same public catalogue the film
|
||||
* column was built from, and the one that actually keys on the languageId:
|
||||
* Byte-identical: a status check there is a false green. But the index no longer ships that
|
||||
* shape. It ships Arclight's own shareUrl, which is a real PATH, and a path discriminates:
|
||||
*
|
||||
* core.arclight.org/v2/media-languages/13562 -> 200, {"iso3":"apd", counts:{featureFilm:6}}
|
||||
* core.arclight.org/v2/media-languages/99999999-> 404, {"message":"…not found"}
|
||||
* jesusfilm.org/watch/jesus/german-standard -> 200, 944,516 bytes
|
||||
* jesusfilm.org/watch/jesus/pashto-yusufzai -> 200, 810,741 bytes (distinct body)
|
||||
* jesusfilm.org/watch/jesus/definitely-not-xyzzy -> 404
|
||||
* jesusfilm.org/watch/jesus/ -> 404
|
||||
*
|
||||
* It 404s on a bogus id AND echoes the iso3 back, so it can discriminate in BOTH
|
||||
* directions: a dead id, and a live id pointing at the WRONG language.
|
||||
* Migrating to shareUrl made 2,049 film links checkable for the first time. The 5 remaining
|
||||
* ?languageId= URLs are still unverifiable by fetch and are recorded as `unknown` with that
|
||||
* reason — never as verified, and never as broken.
|
||||
*
|
||||
* WHAT A FILM `verified` THEREFORE MEANS, EXACTLY: the languageId in the URL is a
|
||||
* real Arclight language, its iso3 equals the iso639_3 we joined on, and Arclight
|
||||
* lists at least one film in it. It does NOT mean anyone fetched the jesusfilm.org
|
||||
* page — that page is unverifiable by fetch, and the UI says so in those words.
|
||||
* This is a catalogue check, not an independent one: it catches a broken or
|
||||
* mis-joined link, not an error already present in Arclight itself.
|
||||
* ═══ METHOD PER HOST, EACH ONE MEASURED, NOT ASSUMED ════════════════════════════════════
|
||||
* Every host below was probed with a known-good and a known-bad URL before being trusted,
|
||||
* and all of them answer HEAD and discriminate 200/404:
|
||||
* ebible.org, png.bible, alkitab.pw real -> 200, bogus code -> 404
|
||||
* jesusfilm.org /watch/jesus/<slug> real -> 200, bogus slug -> 404
|
||||
* arc.gt/<short> real -> 200 (redirects to a language page), bogus -> 404
|
||||
* git.door43.org/<owner>/<repo> real -> 200, bogus repo -> 404
|
||||
* zume.training/<locale> real -> 200, bogus locale -> 404
|
||||
* HEAD is used deliberately: a GET over 2,049 film pages is ~1.6GB off someone else's
|
||||
* bandwidth to learn a status code.
|
||||
*
|
||||
* eBible needs no such workaround — it behaves correctly. A bogus code returns a
|
||||
* real 404 and valid codes return distinct bodies, so status IS meaningful there.
|
||||
* ═══ THE CONTROLS ARE NOT OPTIONAL ══════════════════════════════════════════════════════
|
||||
* A check that cannot be shown to fail proves nothing, so both directions run IN BAND before
|
||||
* any result is trusted, and the script ABORTS rather than write a table if either arm
|
||||
* misbehaves. A negative control that starts passing is how a whole corpus quietly turns
|
||||
* green; a positive control that starts failing is how a network fault gets written down as
|
||||
* "every link is broken".
|
||||
*
|
||||
* ═══ THE CONTROLS ARE NOT OPTIONAL ══════════════════════════════════════════════
|
||||
* A check that cannot be shown to FAIL proves nothing. So before this script trusts
|
||||
* a single result it runs both directions in-band and ABORTS if either misbehaves:
|
||||
* NEGATIVE — deliberately invalid inputs that MUST come back not-verified
|
||||
* (bogus eBible code, bogus Arclight id, and — the arm most likely to
|
||||
* rot silently — a VALID Arclight id asserted against a WRONG iso3).
|
||||
* POSITIVE — known-good inputs that MUST come back verified, so a network fault
|
||||
* or an API shape change cannot mark the whole corpus "broken" and
|
||||
* read as a clean run.
|
||||
* A failure here exits non-zero WITHOUT writing output. Stale honest data beats
|
||||
* fresh data from an instrument nobody proved was working.
|
||||
*
|
||||
* Usage: node plugin-designs/gospel-resources/scripts/verify-gospel-links.mjs [--limit N] [--out <path>]
|
||||
* WHAT `verified` MEANS HERE, EXACTLY: the URL answered 200 to a HEAD that followed
|
||||
* redirects. It does NOT mean anyone read the page, and it does not re-check that the link
|
||||
* points at the right LANGUAGE — that join is established when the index is built, from the
|
||||
* publishers' own catalogues. This catches a dead or moved link, which is the failure a
|
||||
* field worker actually hits.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..', '..', '..')
|
||||
const SRC = resolve(ROOT, 'public/assets/tables/people-group-resources.json')
|
||||
const ARCLIGHT = 'https://core.arclight.org/v2/media-languages'
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '../../..')
|
||||
const PINS = resolve(REPO, 'data/tables/people-group-pins.json')
|
||||
const OUT = resolve(REPO, 'data/tables/gospel-link-verification.json')
|
||||
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const argOf = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null }
|
||||
const LIMIT = Number(argOf('--limit')) || 0
|
||||
const OUT = resolve(ROOT, argOf('--out') || 'public/assets/tables/gospel-link-verification.json')
|
||||
const CONCURRENCY = Number(process.env.VERIFY_CONCURRENCY || 8)
|
||||
const LIMIT = Number(process.env.VERIFY_LIMIT || 0) // 0 = all; used by the smoke run
|
||||
|
||||
const TIMEOUT_MS = 25000
|
||||
const CONCURRENCY = 6 // polite to two volunteer-run hosts, not a stress test
|
||||
const md5 = (s) => createHash('md5').update(s).digest('hex')
|
||||
/** A ?languageId= film URL cannot be checked by fetch. See the header. */
|
||||
const UNVERIFIABLE = (u) => /jesusfilm\.org\/watch\/\?/i.test(u)
|
||||
|
||||
async function get(url) {
|
||||
const ctl = new AbortController()
|
||||
const t = setTimeout(() => ctl.abort(), TIMEOUT_MS)
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
signal: ctl.signal,
|
||||
redirect: 'follow',
|
||||
headers: { 'user-agent': 'doxa-maps link-verifier (gospel-resources)' },
|
||||
})
|
||||
const body = await r.text()
|
||||
return { ok: true, status: r.status, body, bytes: body.length }
|
||||
} catch (e) {
|
||||
return { ok: false, status: 0, body: '', bytes: 0, error: String(e?.message || e) }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── eBible: status IS meaningful (it 404s honestly) ──────────────────────────
|
||||
* Still not status-alone: a host that starts serving a constant soft-200 page
|
||||
* would sail through a status-only check, so the body hash is recorded and the
|
||||
* caller asserts the hashes are not all identical. */
|
||||
async function checkScripture(url) {
|
||||
const r = await get(url)
|
||||
if (!r.ok) return { state: 'unknown', method: 'ebible-http', detail: `fetch failed: ${r.error}` }
|
||||
if (r.status === 404) return { state: 'broken', method: 'ebible-http', detail: 'HTTP 404 — not in eBible' }
|
||||
if (r.status !== 200) return { state: 'unknown', method: 'ebible-http', detail: `HTTP ${r.status}` }
|
||||
if (r.bytes < 500) return { state: 'unknown', method: 'ebible-http', detail: `HTTP 200 but only ${r.bytes} bytes — too thin to trust` }
|
||||
return { state: 'verified', method: 'ebible-http', detail: `HTTP 200, ${r.bytes} bytes`, body_md5: md5(r.body), bytes: r.bytes }
|
||||
}
|
||||
|
||||
/* ── JESUS Film: the languageId is checked against Arclight, NEVER jesusfilm.org.
|
||||
* `expectIso` is the iso639_3 the row joined on; a live id under the WRONG iso is
|
||||
* a mismatch, which is a data-integrity finding, not a pass. */
|
||||
async function checkFilm(url, expectIso) {
|
||||
const id = new URL(url).searchParams.get('languageId')
|
||||
if (!id) return { state: 'unknown', method: 'arclight-api', detail: 'no languageId in URL — nothing to verify' }
|
||||
|
||||
const r = await get(`${ARCLIGHT}/${encodeURIComponent(id)}`)
|
||||
if (!r.ok) return { state: 'unknown', method: 'arclight-api', detail: `Arclight fetch failed: ${r.error}`, language_id: id }
|
||||
if (r.status === 404) return { state: 'broken', method: 'arclight-api', detail: `Arclight 404 — languageId ${id} does not exist`, language_id: id }
|
||||
if (r.status !== 200) return { state: 'unknown', method: 'arclight-api', detail: `Arclight HTTP ${r.status}`, language_id: id }
|
||||
|
||||
let j
|
||||
try { j = JSON.parse(r.body) } catch { return { state: 'unknown', method: 'arclight-api', detail: 'Arclight returned unparseable JSON', language_id: id } }
|
||||
|
||||
const iso3 = j?.iso3 || ''
|
||||
const films = Number(j?.counts?.featureFilm?.value || 0) + Number(j?.counts?.shortFilm?.value || 0)
|
||||
|
||||
if (expectIso && iso3 && iso3 !== expectIso) {
|
||||
return {
|
||||
state: 'mismatch', method: 'arclight-api', language_id: id, arclight_iso3: iso3, films,
|
||||
detail: `languageId ${id} resolves to iso3 "${iso3}", but the row joined on "${expectIso}"`,
|
||||
const head = async (url, tries = 2) => {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
try {
|
||||
const ctl = AbortSignal.timeout(25000)
|
||||
const r = await fetch(url, { method: 'HEAD', redirect: 'follow', headers: { 'user-agent': UA }, signal: ctl })
|
||||
return { status: r.status, final: r.url }
|
||||
} catch (e) {
|
||||
if (i === tries - 1) return { status: 0, error: String(e.message || e).slice(0, 120) }
|
||||
await new Promise((r) => setTimeout(r, 600))
|
||||
}
|
||||
}
|
||||
if (!iso3) return { state: 'unknown', method: 'arclight-api', detail: 'Arclight returned no iso3 to compare', language_id: id }
|
||||
if (films <= 0) {
|
||||
return {
|
||||
state: 'broken', method: 'arclight-api', language_id: id, arclight_iso3: iso3, films: 0,
|
||||
detail: `Arclight lists 0 films in ${iso3} — the link would open an empty page`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
state: 'verified', method: 'arclight-api', language_id: id, arclight_iso3: iso3, films,
|
||||
language_name: j?.name || '',
|
||||
detail: `Arclight languageId ${id} = ${iso3} (${j?.name || '?'}), ${films} film(s). jesusfilm.org itself is client-rendered and cannot be fetch-verified.`,
|
||||
}
|
||||
}
|
||||
|
||||
async function pool(items, worker, n = CONCURRENCY) {
|
||||
const out = new Array(items.length)
|
||||
let i = 0
|
||||
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
||||
while (i < items.length) { const k = i++; out[k] = await worker(items[k], k) }
|
||||
}))
|
||||
return out
|
||||
const classify = (u, r) => {
|
||||
if (UNVERIFIABLE(u)) return { state: 'unknown', detail: 'jesusfilm.org/watch/?languageId= renders the same bytes for any id, valid or bogus — a status check here would be a false green' }
|
||||
if (r.status === 200) return { state: 'verified', detail: `HTTP 200${r.final && r.final !== u ? ` (via ${r.final})` : ''}` }
|
||||
if (r.status === 0) return { state: 'unknown', detail: `could not be reached: ${r.error}` }
|
||||
if (r.status === 404 || r.status === 410) return { state: 'broken', detail: `HTTP ${r.status}` }
|
||||
return { state: 'unknown', detail: `HTTP ${r.status} — neither a clear yes nor a clear no` }
|
||||
}
|
||||
|
||||
/* ═══ CONTROLS — run first, abort on misbehaviour ═════════════════════════════ */
|
||||
async function runControls() {
|
||||
const fail = []
|
||||
const log = (name, pass, got) => {
|
||||
console.log(` ${pass ? 'PASS' : 'FAIL'} ${name} -> ${got}`)
|
||||
if (!pass) fail.push(`${name} (got ${got})`)
|
||||
}
|
||||
console.log('CONTROLS — negative (must NOT verify):')
|
||||
/** ═══ CONTROLS ═══ Each arm must come back as stated or the run aborts. */
|
||||
const NEGATIVE = [
|
||||
['https://ebible.org/zzq9x/', 'bogus eBible code'],
|
||||
['https://jesusfilm.org/watch/jesus/definitely-not-a-language-xyzzy', 'bogus film slug'],
|
||||
['https://git.door43.org/fa_gl/not_a_repo_xyzzy', 'bogus Door43 repo'],
|
||||
['https://zume.training/zzz', 'bogus Zume locale'],
|
||||
['https://arc.gt/zzzzz9', 'bogus arc.gt short link'],
|
||||
]
|
||||
const POSITIVE = [
|
||||
['https://ebible.org/tel2010/', 'real eBible translation'],
|
||||
['https://jesusfilm.org/watch/jesus/german-standard', 'real film slug'],
|
||||
['https://git.door43.org/fa_gl/lki_obs', 'real Door43 repo'],
|
||||
['https://zume.training/de', 'real Zume locale'],
|
||||
['https://arc.gt/w4k1c', 'real arc.gt short link'],
|
||||
]
|
||||
|
||||
const nEb = await checkScripture('http://ebible.org/zzz9999/')
|
||||
log('eBible bogus code zzz9999', nEb.state !== 'verified', nEb.state)
|
||||
const controls = { negative: [], positive: [] }
|
||||
let abort = null
|
||||
for (const [u, what] of NEGATIVE) {
|
||||
const c = classify(u, await head(u))
|
||||
controls.negative.push({ url: u, what, state: c.state })
|
||||
if (c.state === 'verified') abort = `NEGATIVE CONTROL PASSED AS VERIFIED — ${what} (${u}). The check cannot distinguish a real link from a fake one, so nothing it says is worth writing down.`
|
||||
}
|
||||
for (const [u, what] of POSITIVE) {
|
||||
const c = classify(u, await head(u))
|
||||
controls.positive.push({ url: u, what, state: c.state })
|
||||
if (c.state !== 'verified') abort = `POSITIVE CONTROL FAILED — ${what} (${u}) came back "${c.state}". Something is wrong with the network or the host, not with the corpus; refusing to mark 3,600 links broken on the strength of it.`
|
||||
}
|
||||
if (abort) {
|
||||
console.error(`VERIFY-LINKS: ABORT — ${abort}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`VERIFY-LINKS: controls pass — ${NEGATIVE.length} negative all non-verified, ${POSITIVE.length} positive all verified.`)
|
||||
|
||||
const nAl = await checkFilm('https://www.jesusfilm.org/watch/?languageId=99999999', 'apd')
|
||||
log('Arclight bogus languageId 99999999', nAl.state !== 'verified', nAl.state)
|
||||
|
||||
// The arm that would rot silently: a REAL id asserted against a WRONG iso. If this
|
||||
// ever passes, the iso comparison has stopped comparing and every film row is a
|
||||
// false green — which is exactly the failure this whole file exists to prevent.
|
||||
const nMix = await checkFilm('https://www.jesusfilm.org/watch/?languageId=13562', 'zzz')
|
||||
log('Arclight valid id 13562 vs WRONG iso "zzz"', nMix.state === 'mismatch', nMix.state)
|
||||
|
||||
console.log('CONTROLS — positive (must verify):')
|
||||
const pEb = await checkScripture('http://ebible.org/eng-web/')
|
||||
log('eBible valid eng-web', pEb.state === 'verified', pEb.state)
|
||||
const pAl = await checkFilm('https://www.jesusfilm.org/watch/?languageId=13562', 'apd')
|
||||
log('Arclight valid 13562 vs correct iso "apd"', pAl.state === 'verified', pAl.state)
|
||||
|
||||
if (fail.length) {
|
||||
console.error('\nABORT — controls failed, so no result from this run can be trusted:')
|
||||
for (const f of fail) console.error(' - ' + f)
|
||||
console.error('Nothing written. The previous verification file is left in place.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('All controls behaved. Proceeding.\n')
|
||||
// ═══ THE CORPUS ═══ every URL the pin index actually ships, deduplicated.
|
||||
const pins = JSON.parse(readFileSync(PINS, 'utf8'))
|
||||
const urls = new Set()
|
||||
for (const v of Object.values(pins.codes)) {
|
||||
if (v.s) urls.add(v.s)
|
||||
if (v.f) urls.add(v.f)
|
||||
if (v.z) urls.add(v.z)
|
||||
if (v.za && v.za.u) urls.add(v.za.u)
|
||||
for (const d of v.d || []) if (d.u) urls.add(d.u)
|
||||
}
|
||||
let list = [...urls]
|
||||
if (LIMIT) list = list.slice(0, LIMIT)
|
||||
if (!list.length) {
|
||||
console.error('VERIFY-LINKS: FAIL — the corpus is empty; every summary below would be vacuous.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const src = JSON.parse(readFileSync(SRC, 'utf8'))
|
||||
const rows = src.rows || []
|
||||
|
||||
// Distinct URL -> the iso639_3 codes that claim it. One URL can legitimately serve
|
||||
// several people groups sharing a language, so the expected iso is a SET.
|
||||
const scripture = new Map()
|
||||
const film = new Map()
|
||||
for (const r of rows) {
|
||||
const iso = String(r.iso639_3 || '').trim()
|
||||
if (r.scripture_url) {
|
||||
const u = String(r.scripture_url).trim()
|
||||
if (!scripture.has(u)) scripture.set(u, new Set())
|
||||
if (iso) scripture.get(u).add(iso)
|
||||
}
|
||||
if (r.film_url) {
|
||||
const u = String(r.film_url).trim()
|
||||
if (!film.has(u)) film.set(u, new Set())
|
||||
if (iso) film.get(u).add(iso)
|
||||
}
|
||||
const links = {}
|
||||
let done = 0
|
||||
const started = Date.now()
|
||||
const worker = async (queue) => {
|
||||
for (;;) {
|
||||
const u = queue.pop()
|
||||
if (!u) return
|
||||
links[u] = classify(u, UNVERIFIABLE(u) ? {} : await head(u))
|
||||
if (++done % 250 === 0) process.stdout.write(` ${done}/${list.length}\n`)
|
||||
}
|
||||
console.log(`${rows.length} rows -> ${scripture.size} distinct scripture URLs, ${film.size} distinct film URLs\n`)
|
||||
}
|
||||
const queue = [...list]
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker(queue)))
|
||||
|
||||
await runControls()
|
||||
const hostOf = (u) => (/^https?:\/\/([^/]+)/i.exec(u) || [, '?'])[1].toLowerCase()
|
||||
const byState = {}
|
||||
const byHost = {}
|
||||
for (const [u, r] of Object.entries(links)) {
|
||||
byState[r.state] = (byState[r.state] || 0) + 1
|
||||
const h = (byHost[hostOf(u)] ||= {})
|
||||
h[r.state] = (h[r.state] || 0) + 1
|
||||
}
|
||||
const broken = Object.entries(links).filter(([, r]) => r.state === 'broken').map(([u]) => u)
|
||||
|
||||
let sUrls = [...scripture.keys()]
|
||||
let fUrls = [...film.keys()]
|
||||
if (LIMIT) { sUrls = sUrls.slice(0, LIMIT); fUrls = fUrls.slice(0, LIMIT) }
|
||||
|
||||
const links = {}
|
||||
|
||||
console.log(`Checking ${sUrls.length} scripture URLs (eBible, status + body)…`)
|
||||
const sRes = await pool(sUrls, (u) => checkScripture(u))
|
||||
sUrls.forEach((u, k) => { links[u] = { kind: 'scripture', expected_iso639_3: [...scripture.get(u)].sort(), ...sRes[k] } })
|
||||
|
||||
// A host that started serving one constant page would pass a status-only check on
|
||||
// every URL. Distinct bodies are the evidence that the status meant anything.
|
||||
const hashes = new Set(sRes.filter((r) => r.body_md5).map((r) => r.body_md5))
|
||||
console.log(` distinct response bodies: ${hashes.size} across ${sRes.filter((r) => r.body_md5).length} verified`)
|
||||
if (sRes.filter((r) => r.body_md5).length > 1 && hashes.size === 1) {
|
||||
console.error('ABORT — every verified eBible URL returned the SAME body. That is the jesusfilm')
|
||||
console.error('failure mode appearing on eBible: status is no longer discriminating. Nothing written.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Checking ${fUrls.length} film URLs (Arclight catalogue, NOT jesusfilm.org)…`)
|
||||
const fRes = await pool(fUrls, (u) => checkFilm(u, [...film.get(u)][0] || ''))
|
||||
fUrls.forEach((u, k) => { links[u] = { kind: 'film', expected_iso639_3: [...film.get(u)].sort(), ...fRes[k] } })
|
||||
|
||||
const tally = (arr) => arr.reduce((m, r) => (m[r.state] = (m[r.state] || 0) + 1, m), {})
|
||||
const summary = {
|
||||
scripture: { checked: sRes.length, ...tally(sRes) },
|
||||
film: { checked: fRes.length, ...tally(fRes) },
|
||||
}
|
||||
|
||||
// The shipped file carries exactly what the map reads — one state and one detail line per
|
||||
// URL — plus the run's summary. Controls, methods and response hashes are this script's own
|
||||
// log (printed above), not data for a visitor.
|
||||
const out = {
|
||||
meta: {
|
||||
title: 'Gospel resource link verification',
|
||||
generated: new Date().toISOString(),
|
||||
states: {
|
||||
verified: 'the link resolves to the language the row claims (see method)',
|
||||
broken: 'the target does not exist, or holds nothing to open',
|
||||
mismatch: 'the link resolves, but to a DIFFERENT language than the row joined on',
|
||||
unknown: 'could not be checked — never read as either working or broken',
|
||||
writeFileSync(
|
||||
OUT,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
meta: {
|
||||
title: 'Gospel resource link verification',
|
||||
generated: new Date().toISOString(),
|
||||
method: 'HEAD, redirects followed. verified = HTTP 200. broken = 404/410. unknown = unreachable, an ambiguous status, or a URL shape that cannot be checked by fetch.',
|
||||
scope: 'Every URL in data/tables/people-group-pins.json, deduplicated.',
|
||||
states: {
|
||||
verified: 'the URL answered 200 to a HEAD that followed redirects',
|
||||
broken: 'the target does not exist (404/410)',
|
||||
unknown: 'could not be checked — never read as either working or broken',
|
||||
},
|
||||
controls,
|
||||
summary: { checked: list.length, ...byState },
|
||||
byHost,
|
||||
},
|
||||
summary,
|
||||
links,
|
||||
},
|
||||
links: Object.fromEntries(Object.entries(links).map(([u, r]) => [u, { state: r.state, detail: r.detail }])),
|
||||
}
|
||||
null,
|
||||
1,
|
||||
)}\n`,
|
||||
)
|
||||
|
||||
writeFileSync(OUT, JSON.stringify(out, null, 0))
|
||||
console.log('\nSUMMARY')
|
||||
console.log(' scripture', JSON.stringify(summary.scripture))
|
||||
console.log(' film ', JSON.stringify(summary.film))
|
||||
console.log(`\nwrote ${OUT}`)
|
||||
const pct = (n) => `${((100 * n) / list.length).toFixed(1)}%`
|
||||
console.log(`VERIFY-LINKS: ${list.length} unique URL(s) in ${((Date.now() - started) / 1000).toFixed(0)}s`)
|
||||
for (const s of ['verified', 'broken', 'unknown']) {
|
||||
if (byState[s]) console.log(` ${s.padEnd(9)} ${String(byState[s]).padStart(5)} ${pct(byState[s])}`)
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1) })
|
||||
if (broken.length) {
|
||||
console.log(` broken URLs (first 10):`)
|
||||
for (const u of broken.slice(0, 10)) console.log(` ${u}`)
|
||||
}
|
||||
console.log(`wrote ${OUT}`)
|
||||
|
|
|
|||
14961
public/assets/tables/gospel-link-verification.json
vendored
14961
public/assets/tables/gospel-link-verification.json
vendored
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
2
public/assets/tables/people-group-pins.json
vendored
2
public/assets/tables/people-group-pins.json
vendored
File diff suppressed because one or more lines are too long
14961
public/doxa-maps/assets/tables/gospel-link-verification.json
generated
14961
public/doxa-maps/assets/tables/gospel-link-verification.json
generated
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
12
public/doxa-maps/manifest.json
generated
12
public/doxa-maps/manifest.json
generated
|
|
@ -556,7 +556,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "doxa-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb",
|
||||
"integrity": "sha384-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx",
|
||||
"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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb\" 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-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx\" 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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb\" 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-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx\" 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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb\" 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-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx\" 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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb\" 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-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx\" 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-P1fIhucAOViVufa8/Hsc3BgNWzZVSXlY/Pv/HYeV+RsKXol8mmPmPExXGmArpuIb\" 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-O7KDP8HK7zJKDP03y1dsCWmSYuz+f4VI3ucefjjz5pYQGuHP/Q3Et4lwBAVrOmxx\" 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue