From 896775903ba237d83d8dcbee6a78859731195a7e Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 08:10:31 +0000 Subject: [PATCH] Share > Embed: three codes for THIS plugin (URL, Iframe, JavaScript), from the tree it is served from (shareable pages P3b) The Embed tab now offers the plugin's own page (URL), that page in an iframe (with the sizes), and its own bundle + element (JavaScript), each with one plain sentence and one Copy button. They embed the plugin whose Share was pressed, even nested in Plugin Studio, Deep Research or Posterverse. Where the codes point: a new build-config key shareBaseUrl, baked as __DOXA_SHARE_BASE__ and overridable per embed (profile-config.shareBaseUrl) or per page (window.MAP_SHARE_BASE_URL), https only (or a local test server). It is committed EMPTY: the codes then point at the tree the bundle is served from, so the stable site gives its own public URLs by construction. embedBaseUrl stays empty, so the manifest's snippets and a dropped tree are unchanged. The JavaScript code is the plugin's embed.script from that tree's manifest (library/utils/publicEmbed.js, fetched once when Embed opens, default credentials), made absolute, with integrity dropped from our unversioned bundle URL (a pinned hash would break every partner's embed at the next release). A plugin that tree does not carry gets a plain note instead of codes that would 404. Share this page stays first; the explanation and the 5 steps stay. English only (parity debt 19 per locale). The library index lists publicEmbed.js. --- documentation/reference-library-index.md | 2 + internal/_tree-name.mjs | 13 ++ internal/check-i18n-parity.mjs | 3 +- .../components/map-controls/ShareButton.vue | 123 ++++++++++++------ library/i18n/locales/en/share.json | 11 +- library/utils/embedBase.js | 18 +++ library/utils/publicEmbed.js | 73 +++++++++++ .../scripts/check-no-free-identifiers.mjs | 2 +- vite.config.js | 4 +- vite.doxa-maps-build-config.json | 2 + 10 files changed, 207 insertions(+), 44 deletions(-) create mode 100644 library/utils/publicEmbed.js diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index 7c33c5c3d..7c6bd82f7 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -280,6 +280,8 @@ and a change here can break other maps. `import … from '@map/utils/peopleGroupName.js'` - **peopleGroupPoints** — peopleGroupPoints.js — SHARED (@map/utils/peopleGroupPoints.js). `import … from '@map/utils/peopleGroupPoints.js'` +- **publicEmbed** — publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address. + `import … from '@map/utils/publicEmbed.js'` - **scrollport** — findScrollportAncestor — the nearest ancestor that actually scrolls. `import … from '@map/utils/scrollport.js'` - **viewportScrim** — viewportScrim.js — make a position:fixed full-screen SCRIM actually cover the VIEWPORT, diff --git a/internal/_tree-name.mjs b/internal/_tree-name.mjs index 9f4a6d352..f51d447b1 100644 --- a/internal/_tree-name.mjs +++ b/internal/_tree-name.mjs @@ -50,6 +50,19 @@ function readGated() { } export const GATED_PROFILES = readGated() +// The PUBLIC address of the tree for the Share menu's Embed tab (the config's shareBaseUrl): an https URL with no +// trailing slash, or '' (unset). vite.config.js bakes it as __DOXA_SHARE_BASE__; library/utils/embedBase.js reads it. +function readShareBase() { + let v = '' + try { + const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8')) + v = typeof cfg.shareBaseUrl === 'string' ? cfg.shareBaseUrl.trim().replace(/\/+$/, '') : '' + } catch { return '' } + if (v && !/^https:\/\/[^\s"'<>]+$/i.test(v)) throw new Error(`[_tree-name] build config "shareBaseUrl" must be an https URL, got ${JSON.stringify(v)}`) + return v +} +export const SHARE_BASE = readShareBase() + // The name escaped for use inside a RegExp source (a '.' in a future name must not match any char). export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') diff --git a/internal/check-i18n-parity.mjs b/internal/check-i18n-parity.mjs index 32f6e2493..7e7759908 100644 --- a/internal/check-i18n-parity.mjs +++ b/internal/check-i18n-parity.mjs @@ -71,7 +71,8 @@ const DEBT = { // 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now. // tStatic falls back to the English text, so these locales show English until the translations arrive; // lower each to 0 then, and empty the map again. - ar: 10, de: 10, es: 10, fr: 10, hi: 10, it: 10, pt: 10, ro: 10, ru: 10, zh: 10, + // + the Embed tab's 9 keys (URL / Iframe / JavaScript, P3b), the same English-only rule. + ar: 19, de: 19, es: 19, fr: 19, hi: 19, it: 19, pt: 19, ro: 19, ru: 19, zh: 19, } const fails = [] diff --git a/library/components/map-controls/ShareButton.vue b/library/components/map-controls/ShareButton.vue index c5bb319bb..72037713e 100644 --- a/library/components/map-controls/ShareButton.vue +++ b/library/components/map-controls/ShareButton.vue @@ -33,7 +33,8 @@ import { tStatic } from '@map/i18n/index.js' const T = (key, en) => tStatic(key) || en import { useI18n } from 'vue-i18n' import { useShadowStyles } from '../../composables/useShadowStyles.js' -import { resolveShareUrl } from '../../utils/embedBase.js' +import { resolveShareUrl, resolvePublicBase, resolveEmbedBase } from '../../utils/embedBase.js' +import { publicLeafUrl, publicScriptCode } from '../../utils/publicEmbed.js' import { urlOwnerOf } from '../../composables/useViewAddress.js' import MapControlButton from './MapControlButton.vue' import MapControlMenu from './MapControlMenu.vue' @@ -357,6 +358,8 @@ const pageUrl = ref('') // "Share this page": read when the menu const linkCopied = ref(false) const embedCopied = ref(false) const embedSize = ref('medium') // 'small' | 'medium' | 'large' +const embedKind = ref('url') // 'url' | 'iframe' | 'script': the Embed tab's three codes +const scriptState = ref({ loading: false }) // the JavaScript code from the public manifest: { code } | { missing } | { failed } const sizePresets = { small: { label: 'Small', height: 400 }, @@ -390,10 +393,35 @@ const shareUrl = computed(() => resolveShareUrl({ profileConfig: unref(profileConfig) || null, })) +/* ★ EMBED THIS PLUGIN, FROM THE PUBLIC SITE (shareable pages P3b). The Embed tab's three codes are for the plugin + whose Share was pressed (its own page; its own bundle + element), even nested in Plugin Studio, Deep Research or + Posterverse, and they point at the tree's PUBLIC address (resolvePublicBase: the build's shareBaseUrl), never a dev + server or the page's own host. With no public address configured, the tree this bundle loaded from stands in. */ +const embedBase = computed(() => resolvePublicBase(unref(profileConfig) || null) || resolveEmbedBase(unref(profileConfig) || null)) +const embedPathOf = () => props.embedPath || (unref(profileConfig) || {}).shareEmbedPath || '' +// URL: the plugin's own generated page on the public site; an absolute or root path keeps the old resolution. +const leafUrl = computed(() => publicLeafUrl(embedBase.value, embedPathOf()) || shareUrl.value) const embedSnippet = computed(() => { const h = sizePresets[embedSize.value].height - return '' + return '' }) +// JavaScript: the plugin's embed.script from the public manifest, read once the Embed tab opens. "missing" means the +// public site does not carry this plugin yet, so the tab shows a plain note instead of codes that would 404. +function loadScriptCode() { + if (scriptState.value.code || scriptState.value.loading) return + scriptState.value = { loading: true } + publicScriptCode(embedBase.value, embedPathOf()).then((r) => { scriptState.value = r }) +} +const embedText = computed(() => embedKind.value === 'url' ? leafUrl.value + : embedKind.value === 'iframe' ? embedSnippet.value : (scriptState.value.code || '')) +const EMBED_KINDS = ['url', 'iframe', 'script'] +const kindLabel = (k) => ({ url: T('share.kind_url', 'URL'), iframe: T('share.kind_iframe', 'Iframe'), script: T('share.kind_script', 'JavaScript') })[k] +// One plain sentence per code, for a reader who has never embedded anything. +const kindHint = (k) => ({ + url: T('share.url_hint', 'A link to open the map on its own page.'), + iframe: T('share.iframe_hint', 'Put the map in your page, in a frame.'), + script: T('share.script_hint', 'Load the map straight into your page, with no frame.'), +})[k] /* ★ SHARE THIS PAGE. Where the page OWNS its address (useViewAddress: urlOwner 'app' on our own pages, 'host' on DX Life), the address bar already names the exact view (the tab, and the filters), so the @@ -433,6 +461,7 @@ function toggle() { pageUrl.value = pageLink() linkCopied.value = false embedCopied.value = false + embedKind.value = 'url' } } @@ -472,7 +501,7 @@ async function copyLink() { } async function copyEmbed() { - if (await copyText(embedSnippet.value)) { + if (embedText.value && await copyText(embedText.value)) { embedCopied.value = true setTimeout(() => { embedCopied.value = false }, 2000) } @@ -578,7 +607,7 @@ function bindAnchor(el) { @@ -612,7 +641,7 @@ function bindAnchor(el) { - +