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.
This commit is contained in:
parent
8770903270
commit
5dea900183
16 changed files with 318 additions and 113 deletions
23
.env.example
23
.env.example
|
|
@ -37,13 +37,22 @@ MOBILE_APP_ANDROID_PACKAGE=life.doxa.pray
|
|||
# Mapbox (used by the prayer map on /pray)
|
||||
NUXT_PUBLIC_MAPBOX_TOKEN=your-mapbox-public-token
|
||||
|
||||
# Research page: where the dx-maps tree loads from. Unset = the maps CDN below (this branch's default).
|
||||
# Set it empty (NUXT_PUBLIC_DX_MAPS_BASE=) to use this site's own public/dx-maps copy instead.
|
||||
# The CDN must send Access-Control-Allow-Origin for the tree.
|
||||
# NUXT_PUBLIC_DX_MAPS_BASE=https://maps-dev.nuxtbase.app/cdn/dx-maps
|
||||
|
||||
# The Research Gallery's subdomain (the hamburger menu's Research item links here).
|
||||
# NUXT_PUBLIC_RESEARCH_GALLERY_URL=https://research.nuxtbase.app
|
||||
# The plugin-studio branch's two addresses, per deployment. Both are REQUIRED, and both are read by the
|
||||
# server at REQUEST time (server/utils/dx-runtime.ts). They are never baked into a build, so one build
|
||||
# serves any deployment. Changing them needs a restart, not a rebuild. There is no default host: when
|
||||
# one is unset, the pages that need it say which setting is missing.
|
||||
#
|
||||
# The Research Gallery: the public Plugin Studio on its own subdomain. The menu's Research item, every
|
||||
# in-page gallery link and the /regions index go here (through this site's /research-gallery).
|
||||
# VPS: NUXT_PUBLIC_RESEARCH_GALLERY_URL=https://research.<host>/
|
||||
# laptop: NUXT_PUBLIC_RESEARCH_GALLERY_URL=http://research.localhost:4616/
|
||||
NUXT_PUBLIC_RESEARCH_GALLERY_URL=
|
||||
#
|
||||
# The dx-maps tree this site's maps load from: that subdomain's published /cdn. It must send
|
||||
# Access-Control-Allow-Origin for the tree (the container does).
|
||||
# VPS: NUXT_PUBLIC_DX_MAPS_BASE=https://research.<host>/cdn/dx-maps
|
||||
# laptop: NUXT_PUBLIC_DX_MAPS_BASE=http://research.localhost:4616/cdn/dx-maps
|
||||
NUXT_PUBLIC_DX_MAPS_BASE=
|
||||
|
||||
# Cloudflare Turnstile
|
||||
NUXT_TURNSTILE_SITE_KEY=your-turnstile-site-key
|
||||
|
|
|
|||
|
|
@ -54,14 +54,36 @@ or `.env` is needed for this page. Without `DATABASE_URL`, the dev server only l
|
|||
- To run it against a **local** dx-maps, and to try all three ways a site can take the gallery (drop-in,
|
||||
CDN, subdomain) with the exact ports and settings: [documentation/LAPTOP-6WAY.md](documentation/LAPTOP-6WAY.md).
|
||||
|
||||
## Deployment method: the Research Gallery on its own subdomain (this branch, research-gallery-subdomain)
|
||||
## Deployment method: the Research Gallery as the public Plugin Studio (this branch, research-gallery-plugin-studio)
|
||||
|
||||
The hamburger menu's **Research** item links to the Research Gallery on its own subdomain, served by the
|
||||
Plugin Studio container: `NUXT_PUBLIC_RESEARCH_GALLERY_URL`, default `https://research.nuxtbase.app`. The
|
||||
menu no longer lists this site's Research page or its **Regions** page (the Regions item is switched off
|
||||
with `off: true` in `app/utils/nav.ts`, not deleted; both pages still exist at `/research` and `/regions`,
|
||||
and in-page links to them, such as the home map's research button, are unchanged). The site's other maps
|
||||
and `/embed-test` load as JavaScript from the maps CDN, as on research-gallery-js. No iframe is used.
|
||||
The research subdomain runs the dx-maps container in its public mode: the **Plugin Studio**, open to
|
||||
everyone, with every research map, dashboard and list, and its published `/cdn` of production maps.
|
||||
This site sends readers there and loads its own maps from there. Each deployment sets two addresses,
|
||||
read by the server at **request time** and never baked into a build:
|
||||
|
||||
| Setting | What it is | VPS | Laptop |
|
||||
|---|---|---|---|
|
||||
| `NUXT_PUBLIC_RESEARCH_GALLERY_URL` | the public Plugin Studio | `https://research.<host>/` | `http://research.localhost:4616/` |
|
||||
| `NUXT_PUBLIC_DX_MAPS_BASE` | its published tree, where this site's maps load from | `https://research.<host>/cdn/dx-maps` | `http://research.localhost:4616/cdn/dx-maps` |
|
||||
|
||||
**Why request time.** About 4,000 pages are prerendered, and a prerendered page carries the build's
|
||||
runtime config. An address read there would be baked into the build, which is how the CDN branch
|
||||
failed. So neither setting is in `nuxt.config.ts`:
|
||||
- **Links:** the menu's **Research** item, every in-page gallery link and the `/regions` and
|
||||
`/countries` index pages go to this site's own `/research-gallery?tab=…&lang=…`. The server
|
||||
redirects that (302) to the deployment's gallery (`server/routes/research-gallery.get.ts`).
|
||||
- **Maps:** every map (the home, pray and adopt maps, `/embed-test`, `/research`, and the static pages
|
||||
under `public/`) first asks this site's server for the tree (`GET /api/dx-runtime`), then loads its
|
||||
bundle from there.
|
||||
- **Old links:** old `/js/doxa-maps/…` links are redirected per request to the same path in the tree
|
||||
(`server/middleware/legacy-map-links.ts`).
|
||||
|
||||
When a setting is unset there is no fallback host. `/research-gallery` answers 503 and the maps say
|
||||
which setting to set. Changing an address needs a restart, not a rebuild.
|
||||
|
||||
The menu no longer lists this site's Research page or its **Regions** page. The Regions item is
|
||||
switched off with `off: true` in `app/utils/nav.ts`, not deleted. `/research` is kept, unlinked, and
|
||||
the region and country detail pages stay. No iframe is used.
|
||||
|
||||
## /embed-test
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ interface Props {
|
|||
|
||||
const props = withDefaults(defineProps<Props>(), { bundle: 'simple-map' })
|
||||
|
||||
useDoxaMap(props.bundle)
|
||||
// base: the deployment's dx-maps tree, known once this site's server answers (''); error: why not, in words.
|
||||
const { base, error } = useDoxaMap(props.bundle)
|
||||
|
||||
/* Each bundle registers a DIFFERENT custom element name so both IIFEs can
|
||||
* coexist on the same SPA. customElements.define() is one-shot per tag,
|
||||
|
|
@ -35,16 +36,16 @@ onMounted(() => {
|
|||
* dx-maps bundle for its keyless MapLibre engine instead, as the Research
|
||||
* page does. With a token, the config passes through unchanged. */
|
||||
/* The map's own files (styles, boundaries) live in the dx-maps tree it was loaded from, not on this
|
||||
* site: without assetsBase the bundle would look for them under this page's ./assets. */
|
||||
* site: without assetsBase the bundle would look for them under this page's ./assets. The tree's
|
||||
* address comes from the server at runtime (useDoxaMap), so the element is added only once it is known. */
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const dxMapsBase = ((runtimeConfig.public as { dxMapsBase?: string }).dxMapsBase || '').replace(/\/+$/, '') || '/dx-maps'
|
||||
/* The site's language (it lives in the URL path, /ar/pray, which the map cannot read) goes to the map as
|
||||
* its locale, so its text, direction and data follow DX-life's language and its switcher. */
|
||||
const { locale } = useI18n()
|
||||
const effectiveConfig = computed(() => {
|
||||
try {
|
||||
// The page's own tk is ignored: the site's provider setting decides (MapLibre by default).
|
||||
const { tk: _pageToken, ...cfg } = { assetsBase: `${dxMapsBase}/assets`, locale: locale.value, ...JSON.parse(props.profileConfig) }
|
||||
const { tk: _pageToken, ...cfg } = { assetsBase: `${base.value}/assets`, locale: locale.value, ...JSON.parse(props.profileConfig) }
|
||||
const tk = mapboxTokenIfChosen(runtimeConfig.public)
|
||||
return JSON.stringify(tk ? { ...cfg, tk } : { ...cfg, provider: 'maplibre' })
|
||||
} catch {
|
||||
|
|
@ -55,14 +56,24 @@ const effectiveConfig = computed(() => {
|
|||
|
||||
<template>
|
||||
<div class="doxa-map-slot" :aria-busy="ready ? undefined : 'true'">
|
||||
<div v-if="!ready" class="doxa-map-loading" aria-hidden="true"><span class="doxa-map-spinner" /></div>
|
||||
<component :is="tagName" :id="mapId" :profile-config="effectiveConfig" />
|
||||
<div v-if="!ready" class="doxa-map-loading" :aria-hidden="error ? undefined : 'true'">
|
||||
<p v-if="error" class="doxa-map-error" role="status">{{ error }}</p>
|
||||
<span v-else class="doxa-map-spinner" />
|
||||
</div>
|
||||
<component :is="tagName" v-if="base" :id="mapId" :profile-config="effectiveConfig" />
|
||||
<div v-if="isSimple" class="doxa-map-scroll-pad doxa-map-scroll-pad--top" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.doxa-map-error {
|
||||
max-width: 32rem;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: rgba(59, 70, 61, 0.9);
|
||||
}
|
||||
.doxa-map-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
const MAPBOX_JS = 'https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js'
|
||||
const MAPBOX_GEOCODER_JS = 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'
|
||||
|
||||
// Map of bundle keys → bundles in the dx-maps build tree, the same tree the Research page's gallery
|
||||
// is served from: NUXT_PUBLIC_DX_MAPS_BASE (the maps CDN, the default) or, when that is set empty,
|
||||
// this site's own /dx-maps/. Each bundle registers its own custom element (<doxa-map>,
|
||||
// Map of bundle keys → bundles in the dx-maps build tree: the deployment's NUXT_PUBLIC_DX_MAPS_BASE (the
|
||||
// public Plugin Studio's /cdn), asked from this site's server at runtime (loadDxRuntime) because a
|
||||
// prerendered page cannot carry it. Each bundle registers its own custom element (<doxa-map>,
|
||||
// <doxa-research-map>).
|
||||
const BUNDLES = {
|
||||
'simple-map': 'doxa-simple-map/doxa-simple-map.js',
|
||||
|
|
@ -27,6 +27,16 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
// Start downloading a script now, to run later (loadScript), without blocking anything.
|
||||
function preloadScript(href: string) {
|
||||
if (document.querySelector(`link[rel="preload"][href="${href}"]`)) return
|
||||
const l = document.createElement('link')
|
||||
l.rel = 'preload'
|
||||
l.as = 'script'
|
||||
l.href = href
|
||||
document.head.appendChild(l)
|
||||
}
|
||||
|
||||
function loadScript(src: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector<HTMLScriptElement>(`script[data-doxa-map="${src}"]`)
|
||||
|
|
@ -52,12 +62,16 @@ function loadScript(src: string): Promise<void> {
|
|||
* callers (home, pray, adopt, contact-us) don't break. The
|
||||
* research/index.vue page passes 'research-map' to load the
|
||||
* 5-tab research bundle.
|
||||
* @returns base - the dx-maps tree's address once known ('' until then): the
|
||||
* map's element is added only when it is set, since the
|
||||
* element needs it for its styles and boundaries.
|
||||
* error - why the map cannot load, in words (a missing setting names
|
||||
* itself), or ''.
|
||||
*/
|
||||
export function useDoxaMap(bundle: BundleKey = 'simple-map') {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const DX_MAPS_BASE = ((config.public as { dxMapsBase?: string }).dxMapsBase || '').replace(/\/+$/, '')
|
||||
const MAP_APP_JS = `${DX_MAPS_BASE || '/dx-maps'}/${BUNDLES[bundle]}`
|
||||
const base = ref('')
|
||||
const error = ref('')
|
||||
|
||||
useHead({
|
||||
link: [
|
||||
|
|
@ -66,9 +80,10 @@ export function useDoxaMap(bundle: BundleKey = 'simple-map') {
|
|||
// The three scripts below still RUN one after another (the map bundle must find
|
||||
// window.mapboxgl), but nothing stops them DOWNLOADING together: without these hints a cold
|
||||
// first visit fetched ~1.8 MB of Mapbox GL, then the geocoder, then the map bundle, in series.
|
||||
// The map bundle's address is known only once /api/dx-runtime answers, so its preload is added
|
||||
// then (onMounted), still while Mapbox downloads.
|
||||
{ rel: 'preload', as: 'script', href: MAPBOX_JS },
|
||||
{ rel: 'preload', as: 'script', href: MAPBOX_GEOCODER_JS },
|
||||
{ rel: 'preload', as: 'script', href: MAP_APP_JS }
|
||||
{ rel: 'preload', as: 'script', href: MAPBOX_GEOCODER_JS }
|
||||
]
|
||||
})
|
||||
|
||||
|
|
@ -80,11 +95,22 @@ export function useDoxaMap(bundle: BundleKey = 'simple-map') {
|
|||
if (config.public.prayBaseUrl) {
|
||||
window.MAP_APP_API_URL = config.public.prayBaseUrl as string
|
||||
}
|
||||
const mapAppJs = dxMapsBaseOrThrow().then((tree) => {
|
||||
base.value = tree
|
||||
const src = `${tree}/${BUNDLES[bundle]}`
|
||||
preloadScript(src)
|
||||
return src
|
||||
})
|
||||
// A missing address is shown at once, not after Mapbox has downloaded (the await below throws too).
|
||||
mapAppJs.catch((err) => { error.value = String((err as Error)?.message || err) })
|
||||
await loadScript(MAPBOX_JS)
|
||||
await loadScript(MAPBOX_GEOCODER_JS)
|
||||
await loadScript(MAP_APP_JS)
|
||||
await loadScript(await mapAppJs)
|
||||
} catch (err) {
|
||||
error.value = String((err as Error)?.message || err)
|
||||
console.error('[useDoxaMap] script load failed', err)
|
||||
}
|
||||
})
|
||||
|
||||
return { base, error }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
// The Research Gallery lives on its own subdomain (the Plugin Studio container), so every link that
|
||||
// went to this site's /research or /regions page goes there instead (NUXT_PUBLIC_RESEARCH_GALLERY_URL).
|
||||
// A view is opened with ?tab=<view id> (for example 'regions-dashboard'), and the reader's language
|
||||
// travels as ?lang=<locale> (the gallery reads it, and lays Arabic out right-to-left), so an Arabic reader
|
||||
// does not land on the English gallery. The /research and /regions pages themselves are kept; nothing
|
||||
// links to them any more.
|
||||
// The Research Gallery lives on its own subdomain (the public Plugin Studio), so every link that
|
||||
// went to this site's /research or /regions page goes there instead. The link is this site's own
|
||||
// /research-gallery, which the server redirects to the deployment's gallery
|
||||
// (NUXT_PUBLIC_RESEARCH_GALLERY_URL, server/routes/research-gallery.get.ts). A prerendered page
|
||||
// therefore names no deployment's host. A view opens with ?tab=<view id> (for example
|
||||
// 'regions-dashboard'). The reader's language travels as ?lang=<locale>, since the gallery lays
|
||||
// Arabic out right-to-left and an Arabic reader should not land on the English gallery. The
|
||||
// /research and /regions detail pages are kept; nothing links to them any more.
|
||||
export function useResearchGalleryUrl() {
|
||||
const config = useRuntimeConfig()
|
||||
const { locale } = useI18n()
|
||||
const base = String((config.public as { researchGalleryUrl?: string }).researchGalleryUrl || '')
|
||||
return (tab?: string): string => {
|
||||
try {
|
||||
const u = new URL(base)
|
||||
if (tab) u.searchParams.set('tab', tab)
|
||||
if (locale.value && locale.value !== 'en') u.searchParams.set('lang', locale.value)
|
||||
return u.toString()
|
||||
} catch {
|
||||
return base
|
||||
}
|
||||
const q = new URLSearchParams()
|
||||
if (tab) q.set('tab', tab)
|
||||
if (locale.value && locale.value !== 'en') q.set('lang', locale.value)
|
||||
const s = q.toString()
|
||||
return `/research-gallery${s ? `?${s}` : ''}`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ const status = ref('Reading the manifest…')
|
|||
const hosts = new Map<string, HTMLElement>()
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
// The dx-maps tree: NUXT_PUBLIC_DX_MAPS_BASE when set, otherwise this site's own /dx-maps/ copy.
|
||||
// The dx-maps tree: the deployment's NUXT_PUBLIC_DX_MAPS_BASE, asked from this site's server when the page
|
||||
// mounts (loadDxRuntime), never read from the page's own config.
|
||||
let tree = ''
|
||||
function treeBase(): string {
|
||||
const base = ((runtimeConfig.public as { dxMapsBase?: string }).dxMapsBase || '').replace(/\/+$/, '')
|
||||
return base || `${window.location.origin}/dx-maps`
|
||||
return tree
|
||||
}
|
||||
const resolveUrl = (base: string, ref: string) =>
|
||||
/^(https?:)?\/\//.test(ref) ? ref : ref.startsWith('/') ? new URL(ref, base).href : `${base}/${ref.replace(/^\.\//, '')}`
|
||||
|
|
@ -105,6 +106,12 @@ function unmount(card: Card) {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
tree = await dxMapsBaseOrThrow()
|
||||
} catch (e) {
|
||||
status.value = String((e as Error)?.message || e)
|
||||
return
|
||||
}
|
||||
const base = treeBase()
|
||||
// Must be set before any bundle runs so the maps fetch from the configured prayer API.
|
||||
const pray = (runtimeConfig.public as { prayBaseUrl?: string }).prayBaseUrl
|
||||
|
|
|
|||
|
|
@ -9,20 +9,16 @@ 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: [(to) => {
|
||||
middleware: [() => {
|
||||
const USE_LEGACY_REGIONS_PAGE = false
|
||||
if (!USE_LEGACY_REGIONS_PAGE) {
|
||||
const base = String((useRuntimeConfig().public as { researchGalleryUrl?: string }).researchGalleryUrl || '')
|
||||
const lang = (useNuxtApp().$i18n as { locale: { value: string } })?.locale?.value
|
||||
try {
|
||||
const u = new URL(base)
|
||||
u.searchParams.set('tab', 'regions-dashboard')
|
||||
if (lang && lang !== 'en') u.searchParams.set('lang', lang)
|
||||
return navigateTo(u.toString(), { external: true, redirectCode: 301 })
|
||||
} catch {
|
||||
return navigateTo(to.path.replace(/\/regions\/?$/, '/research'), { redirectCode: 301 })
|
||||
}
|
||||
const q = new URLSearchParams({ tab: 'regions-dashboard' })
|
||||
if (lang && lang !== 'en') q.set('lang', lang)
|
||||
return navigateTo(`/research-gallery?${q}`, { external: true, redirectCode: 301 })
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
// "By Region" link that were here. Those three are NOT deleted: they are kept below, switched off by
|
||||
// SHOW_LEGACY_RESEARCH, so the previous page returns by flipping one constant.
|
||||
//
|
||||
// The gallery is a web component served from DX-life's OWN origin (public/dx-maps/, a copy of
|
||||
// the dx-maps build tree), so this site is its CDN: one script, one element, no third-party host.
|
||||
// It reads its manifest and every child map from the same /dx-maps/ tree. See
|
||||
// DX-MAPS-GALLERY.md at the repo root.
|
||||
// The gallery is a web component served from the deployment's dx-maps tree (NUXT_PUBLIC_DX_MAPS_BASE,
|
||||
// the public Plugin Studio's /cdn), asked from this site's server at runtime: this page is prerendered,
|
||||
// and a prerendered page cannot carry a deployment's address. It reads its manifest and every child map
|
||||
// from the same tree. The menu's Research item goes to the gallery's own subdomain instead; this page
|
||||
// is kept, unlinked. See DX-MAPS-GALLERY.md at the repo root.
|
||||
//
|
||||
// The element is created in onMounted rather than written in the template: it is a custom element
|
||||
// that only exists in the browser, so this keeps it out of SSR and out of Vue's component resolver
|
||||
|
|
@ -39,15 +40,7 @@ const researchProfileConfig = computed(() => JSON.stringify({
|
|||
}))
|
||||
if (SHOW_LEGACY_RESEARCH) useTextHighlight()
|
||||
|
||||
// The dx-maps tree: NUXT_PUBLIC_DX_MAPS_BASE when set (a maps CDN), otherwise this site's own
|
||||
// /dx-maps/ copy. The script and every embed URL come from the same tree.
|
||||
const DX_MAPS_BASE = ((runtimeConfig.public as { dxMapsBase?: string }).dxMapsBase || '').replace(/\/+$/, '')
|
||||
const GALLERY_SCRIPT = `${DX_MAPS_BASE || '/dx-maps'}/doxa-maps-spa/doxa-maps-spa.js`
|
||||
|
||||
useHead({
|
||||
title: () => t('Research'),
|
||||
script: [{ src: GALLERY_SCRIPT, defer: true, key: 'research-gallery' }]
|
||||
})
|
||||
useHead({ title: () => t('Research') })
|
||||
|
||||
const host = ref<HTMLElement | null>(null)
|
||||
let gallery: HTMLElement | null = null
|
||||
|
|
@ -73,17 +66,31 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
gallery = document.createElement('doxa-maps-spa')
|
||||
// Every embed URL is built at RUNTIME: from NUXT_PUBLIC_DX_MAPS_BASE when set, otherwise from the
|
||||
// origin serving this page, so the same build works on any host (laptop, staging, production) with
|
||||
// nothing hardcoded.
|
||||
gallery.setAttribute('tree-base', DX_MAPS_BASE || `${window.location.origin}/dx-maps`)
|
||||
// The site's language drives the gallery's (and every map's); it follows the language switcher.
|
||||
gallery.setAttribute('language', locale.value)
|
||||
// MapLibre (keyless) by default; Mapbox only when NUXT_PUBLIC_MAP_PROVIDER=mapbox and a token is set.
|
||||
const token = mapboxTokenIfChosen(runtimeConfig.public)
|
||||
if (token) gallery.setAttribute('profile-config', JSON.stringify({ tk: token }))
|
||||
else gallery.setAttribute('provider', 'maplibre')
|
||||
host.value?.appendChild(gallery)
|
||||
// The tree, its script and every embed URL come from the deployment's address, asked at RUNTIME, so
|
||||
// the same build works on any host (laptop, staging, production) with nothing hardcoded.
|
||||
dxMapsBaseOrThrow().then((tree) => {
|
||||
if (!gallery) return
|
||||
gallery.setAttribute('tree-base', tree)
|
||||
host.value?.appendChild(gallery)
|
||||
if (!document.querySelector('script[data-research-gallery]')) {
|
||||
const s = document.createElement('script')
|
||||
s.src = `${tree}/doxa-maps-spa/doxa-maps-spa.js`
|
||||
s.dataset.researchGallery = '1'
|
||||
document.head.appendChild(s)
|
||||
}
|
||||
}).catch((err) => {
|
||||
const p = document.createElement('p')
|
||||
p.className = 'research-gallery-error'
|
||||
p.setAttribute('role', 'status')
|
||||
p.textContent = String((err as Error)?.message || err)
|
||||
host.value?.replaceChildren(p)
|
||||
})
|
||||
})
|
||||
|
||||
watch(locale, (lang) => { gallery?.setAttribute('language', lang) })
|
||||
|
|
@ -137,6 +144,12 @@ onBeforeUnmount(() => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.research-gallery-host :deep(.research-gallery-error) {
|
||||
max-width: 32rem;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
|
|
|||
29
app/utils/dxRuntime.ts
Normal file
29
app/utils/dxRuntime.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// This deployment's Research Gallery and dx-maps tree, asked from this site's own server at RUNTIME
|
||||
// (/api/dx-runtime). Never read from useRuntimeConfig(): about 4,000 pages are prerendered, and a
|
||||
// prerendered page carries the build's values, so one build could not serve two deployments. See
|
||||
// server/utils/dx-runtime.ts. Browser only: call it from onMounted or later. One request per page load.
|
||||
export interface DxRuntime {
|
||||
researchGalleryUrl: string
|
||||
dxMapsBase: string
|
||||
}
|
||||
|
||||
let pending: Promise<DxRuntime> | null = null
|
||||
|
||||
export function loadDxRuntime(): Promise<DxRuntime> {
|
||||
if (!pending) {
|
||||
pending = fetch('/api/dx-runtime', { cache: 'no-store' })
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`/api/dx-runtime answered ${r.status}`)
|
||||
return r.json() as Promise<DxRuntime>
|
||||
})
|
||||
.catch((e) => { pending = null; throw e })
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
// The dx-maps tree's address, or an Error that names the missing setting.
|
||||
export async function dxMapsBaseOrThrow(): Promise<string> {
|
||||
const { dxMapsBase } = await loadDxRuntime()
|
||||
if (!dxMapsBase) throw new Error('This deployment has no maps address: set NUXT_PUBLIC_DX_MAPS_BASE.')
|
||||
return dxMapsBase
|
||||
}
|
||||
|
|
@ -49,16 +49,12 @@ const marketingRouteRules = Object.fromEntries(
|
|||
// restores these rules and its prerender entry (modules/region-routes.ts). The page's own middleware covers
|
||||
// in-app navigation.
|
||||
const LEGACY_REGIONS_PAGE = false
|
||||
// The subdomain branch: the Regions view lives on the gallery's own subdomain; ?lang carries the locale.
|
||||
const REGIONS_GALLERY_URL = (process.env.NUXT_PUBLIC_RESEARCH_GALLERY_URL || 'https://research.nuxtbase.app').replace(/\/+$/, '')
|
||||
// Built with URL, not by joining strings: the base can be a bare host (https://research.<host>) or a page
|
||||
// address (…/doxa-maps-spa/index.html, as on a laptop), and a joined "/?tab=" breaks the second.
|
||||
const regionsTarget = (prefix: string) => {
|
||||
const u = new URL(REGIONS_GALLERY_URL)
|
||||
u.searchParams.set('tab', 'regions-dashboard')
|
||||
if (prefix) u.searchParams.set('lang', prefix.replace(/^\//, ''))
|
||||
return u.toString()
|
||||
}
|
||||
// The Regions view lives on the gallery's own subdomain; ?lang carries the locale. The target is this
|
||||
// site's /research-gallery, which the server resolves to the deployment's gallery per request
|
||||
// (server/routes/research-gallery.get.ts). A route rule's target is fixed at build time, so it names
|
||||
// no host.
|
||||
const regionsTarget = (prefix: string) =>
|
||||
`/research-gallery?tab=regions-dashboard${prefix ? `&lang=${prefix.replace(/^\//, '')}` : ''}`
|
||||
const regionRouteRules = Object.fromEntries(
|
||||
MARKETING_LOCALE_PREFIXES.flatMap(prefix => [
|
||||
LEGACY_REGIONS_PAGE
|
||||
|
|
@ -80,10 +76,6 @@ const termsRouteRules = Object.fromEntries(
|
|||
])
|
||||
)
|
||||
|
||||
// The dx-maps tree the site loads maps from: the same value as runtimeConfig.public.dxMapsBase below, read
|
||||
// at build time for the redirect rules.
|
||||
const DX_MAPS_TREE = ((process.env.NUXT_PUBLIC_DX_MAPS_BASE ?? 'https://maps-dev.nuxtbase.app/cdn/dx-maps').replace(/\/+$/, '')) || '/dx-maps'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
// Embeddables (micro-frontends + web-components) have their own Vite builds —
|
||||
// Nuxt must not watch or scan their source trees.
|
||||
|
|
@ -185,16 +177,8 @@ export default defineNuxtConfig({
|
|||
},
|
||||
|
||||
routeRules: {
|
||||
// OLD MAP LINKS HEAL THEMSELVES (carried from doxa-maps-build): the dx-maps tree used to be served from
|
||||
// this site at /js/doxa-maps/, then /js/doxa-maps-build/ and /embed/doxa-maps-build/. Bookmarks and older
|
||||
// partner embeds 301 to the same path in the tree the site uses now (NUXT_PUBLIC_DX_MAPS_BASE, the maps
|
||||
// CDN by default; this site's own /dx-maps when that is set empty). Bundles that no longer exist in the
|
||||
// tree 404 there, as they would here.
|
||||
'/js/doxa-maps/**': { redirect: { to: `${DX_MAPS_TREE}/**`, statusCode: 301 } },
|
||||
'/js/doxa-maps-build/doxa-maps/**': { redirect: { to: `${DX_MAPS_TREE}/**`, statusCode: 301 } },
|
||||
'/js/doxa-maps-build/assets/**': { redirect: { to: `${DX_MAPS_TREE}/assets/**`, statusCode: 301 } },
|
||||
'/embed/doxa-maps-build/doxa-maps/**': { redirect: { to: `${DX_MAPS_TREE}/**`, statusCode: 301 } },
|
||||
'/embed/doxa-maps-build/assets/**': { redirect: { to: `${DX_MAPS_TREE}/assets/**`, statusCode: 301 } },
|
||||
// Old map links (/js/doxa-maps/**, /js/doxa-maps-build/**, /embed/doxa-maps-build/**) are redirected
|
||||
// per request by server/middleware/legacy-map-links.ts: their target is the deployment's maps tree.
|
||||
// Content-hashed build assets never change under the same URL — cache forever.
|
||||
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
|
||||
// Fonts/images use stable filenames, so avoid `immutable`: a 7-day cache
|
||||
|
|
@ -321,13 +305,9 @@ export default defineNuxtConfig({
|
|||
mapboxToken: process.env.NUXT_PUBLIC_MAPBOX_TOKEN || '',
|
||||
// The map engine: 'maplibre' (keyless, the default) or 'mapbox' (uses mapboxToken). See app/utils/mapProvider.ts.
|
||||
mapProvider: process.env.NUXT_PUBLIC_MAP_PROVIDER || 'maplibre',
|
||||
// Where the Research page loads the dx-maps tree from. This branch defaults to the maps CDN (the
|
||||
// Plugin Studio container's /cdn). Set NUXT_PUBLIC_DX_MAPS_BASE= (empty) to use this site's own
|
||||
// /dx-maps/ copy (public/dx-maps) instead. A foreign host must send CORS for the tree.
|
||||
dxMapsBase: process.env.NUXT_PUBLIC_DX_MAPS_BASE ?? 'https://maps-dev.nuxtbase.app/cdn/dx-maps',
|
||||
// The Research Gallery's own subdomain, served by the Plugin Studio container. The hamburger
|
||||
// menu's Research item links here.
|
||||
researchGalleryUrl: process.env.NUXT_PUBLIC_RESEARCH_GALLERY_URL || 'https://research.nuxtbase.app',
|
||||
// NOT HERE, on purpose: NUXT_PUBLIC_DX_MAPS_BASE and NUXT_PUBLIC_RESEARCH_GALLERY_URL. They differ
|
||||
// per deployment, and anything in this block is baked into every prerendered page. The server reads
|
||||
// them per request (server/utils/dx-runtime.ts); the client asks /api/dx-runtime.
|
||||
statinatorUrl: process.env.NUXT_PUBLIC_STATINATOR_URL || 'https://statinator.doxa.life',
|
||||
statinatorProjectId: process.env.NUXT_PUBLIC_STATINATOR_PROJECT_ID || 'doxa',
|
||||
statinatorEnabled: process.env.NUXT_PUBLIC_STATINATOR_ENABLED === 'true',
|
||||
|
|
|
|||
|
|
@ -151,9 +151,18 @@
|
|||
}
|
||||
|
||||
// ── Render the custom element ────────────────────────────────────────
|
||||
// The dx-maps tree this static page loads from (it cannot read Nuxt's runtime config): the maps CDN,
|
||||
// the same default as NUXT_PUBLIC_DX_MAPS_BASE. The drop-in branch uses '/dx-maps' here.
|
||||
var DX_MAPS_BASE = 'https://maps-dev.nuxtbase.app/cdn/dx-maps';
|
||||
// The dx-maps tree this static page loads from: this deployment's NUXT_PUBLIC_DX_MAPS_BASE, asked from
|
||||
// this site's server at load time. A static page cannot read Nuxt's runtime config, and an address
|
||||
// typed here would belong to one deployment.
|
||||
let DX_MAPS_BASE = '';
|
||||
try {
|
||||
const r = await fetch('/api/dx-runtime', { cache: 'no-store' });
|
||||
if (r.ok) DX_MAPS_BASE = (await r.json()).dxMapsBase || '';
|
||||
} catch (_) { /* reported just below */ }
|
||||
if (!DX_MAPS_BASE) {
|
||||
showError('<strong>This deployment has no maps address.</strong><br><br>Set <code>NUXT_PUBLIC_DX_MAPS_BASE</code> in the host environment.');
|
||||
return;
|
||||
}
|
||||
const config = isResearch
|
||||
? { profile: 'research-map', tk: token, dataSource: 'pray-tools', instanceId: 'embed-research' }
|
||||
: { profile: 'doxa-simple-map', tk: token, dataSource: 'pray-tools', instanceId: 'embed-map', tabs };
|
||||
|
|
|
|||
|
|
@ -70,9 +70,19 @@
|
|||
}
|
||||
|
||||
// ── Render the custom element ────────────────────────────────────────
|
||||
// The dx-maps tree (static page, no Nuxt runtime config): the maps CDN, the same default as
|
||||
// NUXT_PUBLIC_DX_MAPS_BASE. The drop-in branch uses '/dx-maps' here.
|
||||
var DX_MAPS_BASE = 'https://maps-dev.nuxtbase.app/cdn/dx-maps';
|
||||
// The dx-maps tree: this deployment's NUXT_PUBLIC_DX_MAPS_BASE, asked from this site's server at load
|
||||
// time (a static page cannot read Nuxt's runtime config, and a typed address would be one deployment's).
|
||||
let DX_MAPS_BASE = '';
|
||||
try {
|
||||
const r = await fetch('/api/dx-runtime', { cache: 'no-store' });
|
||||
if (r.ok) DX_MAPS_BASE = (await r.json()).dxMapsBase || '';
|
||||
} catch (_) { /* reported just below */ }
|
||||
if (!DX_MAPS_BASE) {
|
||||
document.body.innerHTML =
|
||||
'<div class="token-error"><strong>This deployment has no maps address.</strong><br><br>' +
|
||||
'Set <code>NUXT_PUBLIC_DX_MAPS_BASE</code> in the host environment.</div>';
|
||||
return;
|
||||
}
|
||||
const config = JSON.stringify({ profile: 'research-map', tk: token, dataSource: 'pray-tools', instanceId: 'embed-research', assetsBase: DX_MAPS_BASE + '/assets' });
|
||||
const el = document.createElement('doxa-research-map');
|
||||
el.setAttribute('profile-config', config);
|
||||
|
|
|
|||
11
server/api/dx-runtime.get.ts
Normal file
11
server/api/dx-runtime.get.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* GET /api/dx-runtime → { researchGalleryUrl, dxMapsBase }
|
||||
*
|
||||
* This deployment's two addresses, read at request time (server/utils/dx-runtime.ts explains why they
|
||||
* are never in a page). Every client-side map load, and the static pages under public/, ask here first.
|
||||
* Never cached: the answer belongs to the deployment, not to a build.
|
||||
*/
|
||||
export default defineEventHandler((event) => {
|
||||
setResponseHeader(event, 'cache-control', 'no-store')
|
||||
return dxRuntime()
|
||||
})
|
||||
29
server/middleware/legacy-map-links.ts
Normal file
29
server/middleware/legacy-map-links.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* OLD MAP LINKS HEAL THEMSELVES (carried from doxa-maps-build). The dx-maps tree used to be served from
|
||||
* this site at /js/doxa-maps/, then at /js/doxa-maps-build/ and /embed/doxa-maps-build/. Bookmarks and
|
||||
* older partner embeds go to the same path in the tree this deployment uses (NUXT_PUBLIC_DX_MAPS_BASE),
|
||||
* resolved per request. These were route rules, but a route rule's target is fixed at build time, and
|
||||
* the tree's address belongs to the deployment. 302, since that address can change; bundles that no
|
||||
* longer exist 404 there, as they would have here.
|
||||
*/
|
||||
const LEGACY: Array<[from: string, to: string]> = [
|
||||
['/js/doxa-maps/', ''],
|
||||
['/js/doxa-maps-build/doxa-maps/', ''],
|
||||
['/js/doxa-maps-build/assets/', 'assets/'],
|
||||
['/embed/doxa-maps-build/doxa-maps/', ''],
|
||||
['/embed/doxa-maps-build/assets/', 'assets/']
|
||||
]
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const path = event.path
|
||||
const hit = LEGACY.find(([from]) => path.startsWith(from))
|
||||
if (!hit) return
|
||||
const { dxMapsBase } = dxRuntime()
|
||||
setResponseHeader(event, 'cache-control', 'no-store')
|
||||
if (!dxMapsBase) {
|
||||
setResponseStatus(event, 404)
|
||||
setResponseHeader(event, 'content-type', 'text/plain; charset=utf-8')
|
||||
return 'This map moved to the maps tree, and this deployment has no maps address: set NUXT_PUBLIC_DX_MAPS_BASE.'
|
||||
}
|
||||
return sendRedirect(event, `${dxMapsBase}/${hit[1]}${path.slice(hit[0].length)}`, 302)
|
||||
})
|
||||
26
server/routes/research-gallery.get.ts
Normal file
26
server/routes/research-gallery.get.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* GET /research-gallery?tab=<view>&lang=<locale> → 302 to the Research Gallery
|
||||
*
|
||||
* Pages link here, never to the gallery's own address, so a prerendered page names no deployment's
|
||||
* host: this server resolves NUXT_PUBLIC_RESEARCH_GALLERY_URL per request and redirects. It works
|
||||
* without JavaScript. 302 and no-store, because the target differs per deployment; a 301 would be
|
||||
* cached by browsers for good. `tab` opens a view (regions-dashboard), and `lang` carries the
|
||||
* reader's language, since the gallery lays Arabic out right-to-left.
|
||||
*/
|
||||
export default defineEventHandler((event) => {
|
||||
setResponseHeader(event, 'cache-control', 'no-store')
|
||||
let target: URL
|
||||
try {
|
||||
target = new URL(dxRuntime().researchGalleryUrl)
|
||||
} catch {
|
||||
setResponseStatus(event, 503)
|
||||
setResponseHeader(event, 'content-type', 'text/plain; charset=utf-8')
|
||||
return 'The Research Gallery address is not configured on this deployment: set NUXT_PUBLIC_RESEARCH_GALLERY_URL.'
|
||||
}
|
||||
const query = getQuery(event)
|
||||
const tab = typeof query.tab === 'string' && /^[a-z0-9-]{1,64}$/.test(query.tab) ? query.tab : ''
|
||||
const lang = typeof query.lang === 'string' && /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/.test(query.lang) ? query.lang : ''
|
||||
if (tab) target.searchParams.set('tab', tab)
|
||||
if (lang) target.searchParams.set('lang', lang)
|
||||
return sendRedirect(event, target.toString(), 302)
|
||||
})
|
||||
30
server/utils/dx-runtime.ts
Normal file
30
server/utils/dx-runtime.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* The two per-deployment addresses of the plugin-studio branch, read from the server's environment
|
||||
* at REQUEST time:
|
||||
*
|
||||
* NUXT_PUBLIC_RESEARCH_GALLERY_URL the Research Gallery: the public Plugin Studio on its own
|
||||
* subdomain (https://research.<host>/). The Research menu item,
|
||||
* every in-page gallery link and the /regions index go there.
|
||||
* NUXT_PUBLIC_DX_MAPS_BASE the dx-maps tree this site's maps load from: that subdomain's
|
||||
* published /cdn (https://research.<host>/cdn/dx-maps).
|
||||
*
|
||||
* Why not runtimeConfig.public: about 4,000 pages are PRERENDERED at build time, and a prerendered
|
||||
* page carries the build's runtime config in its payload. An address read there is baked into the
|
||||
* build, and one build could not serve two deployments. The CDN branch died of exactly that. So
|
||||
* neither value is declared in nuxt.config, neither is ever in a page, and the client asks this
|
||||
* server for them (/api/dx-runtime), the way the static pages already ask for the map token
|
||||
* (/api/maps/token). Unset means unset: there is no fallback host, and every caller says which
|
||||
* setting is missing.
|
||||
*/
|
||||
export interface DxRuntime {
|
||||
researchGalleryUrl: string
|
||||
dxMapsBase: string
|
||||
}
|
||||
|
||||
export function dxRuntime(): DxRuntime {
|
||||
const read = (name: string) => String(process.env[name] || '').trim()
|
||||
return {
|
||||
researchGalleryUrl: read('NUXT_PUBLIC_RESEARCH_GALLERY_URL'),
|
||||
dxMapsBase: read('NUXT_PUBLIC_DX_MAPS_BASE').replace(/\/+$/, '')
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue