Posterverse: a hidden full-page app for every poster plugin, like Deep Research

/posterverse mounts the Posterverse shell (<posterverse-app>) from the site's own /dx-maps tree as a
full-page app with no site header, the same way /deep-research mounts the Research Gallery. It is in no
menu and not indexed; ?tab=<row id> opens one plugin. The site's language drives the shell's, and the
shell's "Back to homepage" leads to this site's home in the reader's language.
This commit is contained in:
Doxa Life 2026-09-30 23:25:27 +00:00
parent 072e0d740e
commit fd771272f7
12 changed files with 152 additions and 0 deletions

View file

@ -0,0 +1,141 @@
<script setup lang="ts">
// Posterverse: every poster plugin (UPG 100 List Posters, Country Posters, Poster Design and the poster
// shelf of experimental plugins) in one full-page app, the poster twin of Deep Research. Like Deep
// Research, this page is HIDDEN: it is in no menu and links nowhere, and robots are told not to index
// it. It is reached by its address, and ?tab=<row id> opens one plugin directly (the shell reads it).
//
// The shell is a web component served from this site's OWN /dx-maps/ tree, the same copy the Deep
// Research page uses (see DX-MAPS-GALLERY.md): one script, one element, <posterverse-app>. A custom
// element's name needs a hyphen, which is why the tag is not just <posterverse>.
//
// 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
// without any nuxt.config change.
import { mapboxTokenIfChosen } from '~/utils/mapProvider'
// A full-page app on every screen: no layout, so no site header and no footer. The way back to the site
// is the shell's own "Back to homepage" button, directly above its Collapse button (home-href below),
// so the shell fills the whole screen. The page itself never scrolls (the shell's own panes scroll).
definePageMeta({ layout: false })
const { t, locale, locales } = useI18n()
const localePath = useLocalePath()
const runtimeConfig = useRuntimeConfig()
// 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 POSTERVERSE_SCRIPT = `${DX_MAPS_BASE || '/dx-maps'}/posterverse/posterverse.js`
useHead({
title: () => t('Posterverse'),
meta: [{ name: 'robots', content: 'noindex' }],
script: [{ src: POSTERVERSE_SCRIPT, defer: true, key: 'posterverse' }]
})
// With no layout, this page sets <html lang dir> itself, as the public layout does for every other
// page: in Arabic the shell reads right to left.
const dir = computed(() => {
const loc = locales.value.find(l => typeof l === 'object' && 'code' in l && l.code === locale.value)
return (typeof loc === 'object' && loc && 'dir' in loc && loc.dir) ? loc.dir : 'ltr'
})
useHead(() => ({
htmlAttrs: {
lang: locale.value,
dir: dir.value
}
}))
const host = ref<HTMLElement | null>(null)
let shell: HTMLElement | null = null
onMounted(() => {
shell = document.createElement('posterverse-app')
// 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.
shell.setAttribute('tree-base', DX_MAPS_BASE || `${window.location.origin}/dx-maps`)
// The site's language drives the shell's (and every plugin's); it follows the language switcher.
shell.setAttribute('language', locale.value)
// The way back to the site: the shell draws it above its Collapse button, in the site's own words.
shell.setAttribute('home-href', localePath('/'))
shell.setAttribute('home-label', t('Back to homepage'))
// MapLibre (keyless) by default; Mapbox only when NUXT_PUBLIC_MAP_PROVIDER=mapbox and a token is set.
// The provider is always explicit: a token alone never selects Mapbox in dx-maps.
const shellConfig: Record<string, unknown> = {}
const token = mapboxTokenIfChosen(runtimeConfig.public)
const provider = token ? 'mapbox' : 'maplibre'
shellConfig.provider = provider
if (token) shellConfig.tk = token
shell.setAttribute('provider', provider)
shell.setAttribute('profile-config', JSON.stringify(shellConfig))
host.value?.appendChild(shell)
})
watch(locale, (lang) => {
shell?.setAttribute('language', lang)
shell?.setAttribute('home-href', localePath('/'))
shell?.setAttribute('home-label', t('Back to homepage'))
})
onBeforeUnmount(() => {
shell?.remove()
shell = null
})
</script>
<template>
<div class="posterverse-page">
<h1 class="visually-hidden">
{{ t('Posterverse') }}
</h1>
<div
ref="host"
class="posterverse-host"
/>
</div>
</template>
<style scoped>
.posterverse-page {
/* 100svh: the phone's SMALL viewport, so the browser's own toolbars never cover the shell. */
height: 100svh;
}
/* The shell takes the whole screen, so there is no page scrollbar. */
.posterverse-host {
display: block;
width: 100%;
height: 100%;
}
/* 100%, not the element's own 85vh default: here the shell is the whole page. */
.posterverse-host :deep(posterverse-app) {
display: block;
width: 100%;
height: 100%;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
</style>
<style>
/* Not scoped, and keyed to this page's own element, so it applies only while this page is shown and
no other page changes. With no layout, the site's stylesheet may not be loaded here: the page sets
the two things it needs itself, no body margin and no page scroll. */
html:has(.posterverse-page),
html:has(.posterverse-page) body {
margin: 0;
overflow: hidden;
}
/* The site keeps room for a scrollbar on every page (scrollbar-gutter: stable, base/_globals.scss).
This page never scrolls, so on a desktop that room would only be an empty strip at the right edge. */
html:has(.posterverse-page) {
scrollbar-gutter: auto;
}
</style>

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} مجموعة شعبية",
"{0} unengaged people groups": "{0} مجموعة شعبية غير مَخْدُومَة",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "هل تريد بحثًا أعمق؟",
"Deep Research app": "تطبيق Deep Research",
"Back to homepage": "العودة إلى الصفحة الرئيسية"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} Volksgruppen",
"{0} unengaged people groups": "{0} unberührte Volksgruppen",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Möchten Sie tiefer recherchieren?",
"Deep Research app": "Deep-Research-App",
"Back to homepage": "Zurück zur Startseite"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} People Groups",
"{0} unengaged people groups": "{0} unengaged people groups",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Want Deeper Research?",
"Deep Research app": "Deep Research app",
"Back to homepage": "Back to homepage"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} grupos étnicos",
"{0} unengaged people groups": "{0} grupos étnicos no comprometidos",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "¿Quiere investigar más a fondo?",
"Deep Research app": "App Deep Research",
"Back to homepage": "Volver a la página de inicio"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} peuples",
"{0} unengaged people groups": "{0} peuples non engagés",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Vous voulez aller plus loin dans vos recherches ?",
"Deep Research app": "Application Deep Research",
"Back to homepage": "Retour à l'accueil"

View file

@ -327,6 +327,7 @@
"{0} People Groups": "{0} जनसमूह",
"{0} unengaged people groups": "{0} अनसंपर्कित जनसमूह",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "क्या आप और गहन शोध चाहते हैं?",
"Deep Research app": "Deep Research ऐप",
"Back to homepage": "होमपेज पर वापस जाएँ"

View file

@ -299,6 +299,7 @@
"{0} People Groups": "{0} gruppi etnici",
"{0} unengaged people groups": "{0} gruppi etnici non coinvolti",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Vuoi approfondire la ricerca?",
"Deep Research app": "App Deep Research",
"Back to homepage": "Torna alla home page"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} Grupos étnicos",
"{0} unengaged people groups": "{0} grupos étnicos não engajados",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Quer uma pesquisa mais aprofundada?",
"Deep Research app": "App Deep Research",
"Back to homepage": "Voltar à página inicial"

View file

@ -299,6 +299,7 @@
"{0} People Groups": "{0} grupuri etnolingvistice",
"{0} unengaged people groups": "{0} grupuri etnolingvistice neangajate misionar",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Vrei o cercetare mai aprofundată?",
"Deep Research app": "Aplicația Deep Research",
"Back to homepage": "Înapoi la pagina principală"

View file

@ -313,6 +313,7 @@
"{0} People Groups": "{0} народов",
"{0} unengaged people groups": "{0} невовлечённых народов",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "Хотите исследовать глубже?",
"Deep Research app": "Приложение Deep Research",
"Back to homepage": "Вернуться на главную"

View file

@ -299,6 +299,7 @@
"{0} People Groups": "{0} 个族群",
"{0} unengaged people groups": "{0} 个未接触族群",
"Deep Research": "Deep Research",
"Posterverse": "Posterverse",
"Want Deeper Research?": "想进行更深入的研究?",
"Deep Research app": "Deep Research 应用",
"Back to homepage": "返回首页"