From 9e93f91309ae930e56af6b8e9f2b4c53c7bd58f2 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 07:55:35 +0000 Subject: [PATCH] Share > Embed: three codes for THIS plugin from the public site (URL, Iframe, JavaScript) (shareable pages P3b) The Embed tab now offers the plugin's own public 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, and they point at the tree's PUBLIC address: a new build-config key shareBaseUrl (https://research.nuxtbase.app/dx-maps), baked as __DOXA_SHARE_BASE__ and overridable per embed (profile-config.shareBaseUrl) or per page (window.MAP_SHARE_BASE_URL). embedBaseUrl stays empty, so the manifest's snippets and a dropped tree are unchanged. The JavaScript code is the plugin's embed.script from the public manifest (library/utils/publicEmbed.js, fetched once when Embed opens), 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 the public site does not carry yet 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). --- 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 | 19 +++ library/utils/publicEmbed.js | 71 ++++++++++ .../scripts/check-no-free-identifiers.mjs | 2 +- vite.config.js | 4 +- vite.doxa-maps-build-config.json | 2 + 9 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 library/utils/publicEmbed.js 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) { - +