doxa-maps-cdn/app/pages/regions/index.vue
doxa-maps 5dea900183 Plugin Studio branch: the gallery and maps addresses are read per request, never baked
This branch sends readers to the Research Gallery (the public Plugin Studio
on its own subdomain) and loads the site's maps from that subdomain's /cdn.
Both addresses belong to the deployment:
  NUXT_PUBLIC_RESEARCH_GALLERY_URL  https://research.<host>/
  NUXT_PUBLIC_DX_MAPS_BASE          https://research.<host>/cdn/dx-maps

About 4,000 pages are prerendered, and a prerendered page carries the
build's runtime config, so an address read there is baked into the build.
One build could then serve only one deployment; the CDN branch failed that
way. So neither address is in nuxt.config any more:
- the server reads both from its environment per request
  (server/utils/dx-runtime.ts);
- GET /api/dx-runtime answers them, no-store. Every map (DoxaMapSlot via
  useDoxaMap, /research, /embed-test, and the static pages under public/)
  asks it first, then loads its bundle from that tree. The map's element is
  added only once the tree is known, and the map bundle's preload is added
  then, still while Mapbox downloads;
- every gallery link, the Research menu item, and the /regions and
  /countries index rules go to this site's /research-gallery?tab=&lang=,
  which 302s (no-store) to the gallery. It works without JavaScript, and a
  prerendered page names no host;
- old /js/doxa-maps and /embed/doxa-maps-build links are redirected per
  request by server/middleware/legacy-map-links.ts. They were build-time
  route rules.

No fallback host: the maps-dev and research defaults are gone. When a
setting is unset, /research-gallery answers 503 and each map says which
setting to set. Changing an address needs a restart, not a rebuild.
2026-09-29 05:09:37 +00:00

196 lines
6 KiB
Vue

<script setup lang="ts">
// Index of every WAGF region with the countries that have people groups in it.
// A region heading opens that region's dashboard (/regions/<region>); a country
// opens its own page (/regions/<country>). Data comes from /api/regions
// (derived from the prayer API).
import type { RegionSummary } from '~~/config/countries-meta'
// Regions is a view of the Research Gallery now, so /regions (in every language) REDIRECTS there with
// ?tab=regions-dashboard and old links keep working. The region and country detail pages (/regions/<slug>)
// stay. Set USE_LEGACY_REGIONS_PAGE to true (inside the middleware) to bring this index page back.
// The redirect goes through this site's /research-gallery, which resolves the deployment's gallery
// address on the server (server/routes/research-gallery.get.ts), so no address is read here.
definePageMeta({
middleware: [() => {
const USE_LEGACY_REGIONS_PAGE = false
if (!USE_LEGACY_REGIONS_PAGE) {
const lang = (useNuxtApp().$i18n as { locale: { value: string } })?.locale?.value
const q = new URLSearchParams({ tab: 'regions-dashboard' })
if (lang && lang !== 'en') q.set('lang', lang)
return navigateTo(`/research-gallery?${q}`, { external: true, redirectCode: 301 })
}
}]
})
const { t, locale } = useI18n()
const localePath = useLocalePath()
const { data: regions } = await useAsyncData(
`regions-index-${locale.value}`,
async () => {
const res = await $fetch<{ regions: RegionSummary[] }>('/api/regions', {
query: { lang: locale.value }
})
return res.regions
},
{ watch: [locale], default: () => [] as RegionSummary[] }
)
// Locale grouping separators with Latin digits, matching the rest of the site.
const formatNumber = (n: number) => n.toLocaleString(`${locale.value}-u-nu-latn`)
// Placeholders are %tokens% substituted manually (see the map note on the
// country page for why vue-i18n's {0} slots aren't used here).
function summaryLine(region: RegionSummary): string {
return t('%count% people groups in %countries% countries')
.replace('%count%', formatNumber(region.count))
.replace('%countries%', formatNumber(region.countryCount))
}
function dashboardLink(region: RegionSummary): string {
return t('See %region% dashboard').replace('%region%', region.label || t('Other'))
}
useSeoMeta({
title: () => `${t('Regions')} — Doxa`,
description: () => t('Find unreached people groups by region and country.')
})
useTextHighlight()
</script>
<template>
<div class="container page-content stack stack--3xl">
<div class="stack stack--xs text-center">
<h1
class="highlight"
data-highlight-last
>
{{ t('Regions') }}
</h1>
<p class="color-brand-lighter">
{{ t('Find unreached people groups by region and country.') }}
</p>
</div>
<div class="stack stack--lg">
<div
v-for="region in regions"
:key="region.slug"
class="region-card"
>
<div class="region-card__header">
<h2 class="region-card__title">
<NuxtLink
:to="localePath(`/regions/${region.slug}`)"
class="region-card__link | with-icon"
>
{{ region.label || t('Other') }}
<svg
class="icon | rotate-90 right"
aria-hidden="true"
>
<use href="/assets/icons/arrow-chevron.svg#chevron-up" />
</svg>
</NuxtLink>
</h2>
<p class="region-card__summary">
{{ summaryLine(region) }}
</p>
</div>
<div class="region-card__body">
<ul class="countries-grid">
<li
v-for="c in region.countries"
:key="c.slug"
>
<NuxtLink :to="localePath(`/regions/${c.slug}`)">
{{ c.name }} <span class="color-brand-lighter">({{ c.count }})</span>
</NuxtLink>
</li>
</ul>
<p class="region-card__footer">
<NuxtLink
:to="localePath(`/regions/${region.slug}`)"
class="with-icon"
>
{{ dashboardLink(region) }}
<svg
class="icon | rotate-90 right"
aria-hidden="true"
>
<use href="/assets/icons/arrow-chevron.svg#chevron-up" />
</svg>
</NuxtLink>
</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Card with a brand-coloured header band (region name + count) above the
white country list, following the site's two-tone card pattern. Plain divs
rather than <section>, which carries the page-level section padding. */
.region-card {
border-radius: var(--border-radius-xlg);
overflow: hidden;
background-color: var(--color-surface-white);
}
.region-card__header {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
padding: var(--spacing-md) clamp(var(--spacing-md), 4vw, var(--spacing-2xl));
background-color: var(--color-surface-brand);
color: var(--color-text-on-brand);
}
.region-card__title {
font-size: clamp(var(--font-size-xl), 4vw, var(--font-size-3xl));
}
.region-card__link {
color: inherit;
text-decoration: none;
}
.region-card__link:hover {
color: var(--color-brand-primary);
}
.region-card__summary {
font-size: var(--font-size-md);
opacity: 0.85;
}
.region-card__body {
padding: var(--spacing-lg) clamp(var(--spacing-md), 4vw, var(--spacing-2xl)) var(--spacing-xl);
}
/* Keeps the site's list bullets (global ul padding + ::before dot) and lays
the countries out in responsive columns. */
.countries-grid {
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--spacing-xs) var(--spacing-xl);
}
.countries-grid a {
text-decoration: none;
}
.countries-grid a:hover {
text-decoration: underline;
}
.region-card__footer {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-lg);
}
</style>