Only a page we own gets its direction set

An RTL locale reflected onto document.documentElement whenever it
resolved — so a reader following a ?lang=ar link to a PARTNER'S page
flipped the partner's whole <html>: their headings, their nav, their
footer. The existing comment already worried about the host owning that
decision, but it only guarded against CLEARING dir, never against
setting it.

A generated leaf is the only thing that knows a document belongs to the
plugin, so it says so: window.MAP_OWNS_PAGE, written into all three leaf
templates. library/i18n reads it before touching the document. Nothing
else sets it, so the DEFAULT IS "not ours" — a partner page that
declares nothing is never touched. Absence has to mean restraint for a
flag like this.

The element's own direction was never this code's business: ProfileLoader
binds dir on .profile-loader from effectiveLocale (0f6d5049), so the
plugin mirrors correctly whether or not it owns the page.

★ THE OLD GATE COULD NOT HAVE SEEN THIS, which is why it needed a new
case rather than a re-run. check-embedded-rtl drove the locale through
profile-config — the channel a HOST uses. This defect fires on the URL
channel, which is what a READER uses, and detectLocale() reads the URL
FIRST. Added a ?lang=ar case on the partner page:

  before  profile-config cases  partner html unset / body ltr   pass
          ?lang=ar              partner html RTL / body RTL     FAIL
  after   ?lang=ar              partner html unset / body ltr   pass
          plugin root stays rtl throughout — it still mirrors

Standalone leaves are unchanged and verified: ownsPage true, html
dir="rtl", plugin root rtl. The page we own still mirrors completely.
This commit is contained in:
Doxa Maps 2026-09-25 23:29:22 +00:00
parent d2a643d0aa
commit 57bf76422f
2 changed files with 33 additions and 1 deletions

View file

@ -613,6 +613,12 @@ function forgeHtml(o) {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>${esc(o.title)}</title>
<link rel="icon" href="data:,">
<style>${FORGE_CSS}</style>
@ -704,6 +710,12 @@ function studioRootHtml(bakedToken, integrity = '') {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>${esc(DISPLAY_TITLE)} plugin studio</title>
<link rel="icon" href="data:,">
${bakedToken ? ` <script>window.MAP_TOKEN=${JSON.stringify(bakedToken)};</script>\n` : ''}${DEST_CONFIG.apiBaseUrl ? ` <script>window.MAP_APP_API_URL = ${JSON.stringify(DEST_CONFIG.apiBaseUrl)};</script>\n` : ''} <style>
@ -810,6 +822,12 @@ function mapPageHtml(opts) {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>${esc(title)}</title>
<link rel="icon" href="data:,">
${peerBlock}

View file

@ -135,8 +135,22 @@ export function createAppI18n() {
// Only ever SET it, never clear it: a host page that already declares dir (a
// WordPress RTL theme) owns that decision, and stomping it to "ltr" for an
// English map embedded in an Arabic page would break the page around us.
// ★ ONLY WHERE WE OWN THE PAGE. This used to run whenever the locale resolved to an RTL
// language, which meant an EMBEDDED plugin on a partner's page flipped the PARTNER'S whole
// <html> — their headings, their nav, their footer — because a reader followed a ?lang=ar link.
// The comment below already worried about the host owning that decision, but it only guarded
// against CLEARING dir, never against setting it.
//
// A GENERATED LEAF owns its document and says so with window.MAP_OWNS_PAGE. Nothing else sets
// it, so the DEFAULT IS "not ours" — a partner page that declares nothing is never touched,
// which is the safe direction for a flag whose absence must mean restraint.
//
// The element's OWN direction is not this code's business and never was: ProfileLoader binds
// dir on .profile-loader from effectiveLocale (0f6d5049), so the plugin mirrors correctly
// whether or not it owns the document. This is only about the page around it.
try {
if (RTL_LOCALES.has(locale) && document?.documentElement) {
const ownsPage = typeof window !== 'undefined' && window.MAP_OWNS_PAGE === true
if (ownsPage && RTL_LOCALES.has(locale) && document?.documentElement) {
document.documentElement.dir = 'rtl'
}
} catch { /* no document (SSR/tests) — nothing to reflect */ }