From e5bdb75d3d3ccc5e8ea6c84809624c4b6ecd5981 Mon Sep 17 00:00:00 2001 From: cverorg <292680828+cverorg@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:48:23 +0900 Subject: [PATCH] =?UTF-8?q?inbox-bubble=20=E9=A0=90=E8=A8=AD=E5=85=A8?= =?UTF-8?q?=E7=AB=99=E6=8E=9B=E8=BC=89=EF=BC=9A=5Fsite.md=20=E7=9A=84=20in?= =?UTF-8?q?box-bubble=20on|off=20=E8=88=87=20inbox-bubble-except=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=91=E6=A8=A3=E5=BC=8F=E3=80=81=E5=96=AE=E9=A0=81?= =?UTF-8?q?=20frontmatter=20=E5=8F=AF=E8=A6=86=E5=AF=AB=E3=80=81=E6=89=8B?= =?UTF-8?q?=E6=8E=9B=E9=A0=81=E4=B8=8D=E9=87=8D=E8=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 站主決策(2026-09-03):泡泡預設每頁都有,站主決定哪裡沒有。版面層在 body 尾端掛一顆(data-kind=site、data-id=SITE_ID、data-api-base=PLATFORM_ORIGIN、有 kaito-corpus 才 data-kaito=1);except 樣式去掉語系前綴後比對;頁面已有手掛 inbox-bubble 就不再掛;兩把鑰匙匯出到 site-core 的 site-layer key 清單供 reef-mcp 文法採用。smoke 78/78、sitetile node tests 全綠。 by KITT --- packages/sitetile/astro/content/_site.md | 4 +++ packages/sitetile/astro/content/forms.md | 4 +++ .../sitetile/astro/content/ko-kr/_site.md | 4 +++ packages/sitetile/astro/content/markers.md | 1 + packages/sitetile/astro/smoke-build.mjs | 17 ++++++++++-- .../astro/src/layouts/SiteLayout.astro | 19 +++++++++++-- .../sitetile/astro/src/lib/inbox-bubble.mjs | 27 +++++++++++++++++++ .../sitetile/astro/src/pages/[...path].astro | 3 +++ packages/sitetile/inbox-bubble.test.mjs | 19 +++++++++++++ packages/sitetile/site-core.js | 8 +++++- 10 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/sitetile/astro/content/_site.md create mode 100644 packages/sitetile/astro/content/ko-kr/_site.md create mode 100644 packages/sitetile/astro/src/lib/inbox-bubble.mjs create mode 100644 packages/sitetile/inbox-bubble.test.mjs diff --git a/packages/sitetile/astro/content/_site.md b/packages/sitetile/astro/content/_site.md new file mode 100644 index 0000000..d40ae52 --- /dev/null +++ b/packages/sitetile/astro/content/_site.md @@ -0,0 +1,4 @@ +--- +inbox-bubble-except: /blocks, /markers +--- + diff --git a/packages/sitetile/astro/content/forms.md b/packages/sitetile/astro/content/forms.md index 22cba64..1abf2ef 100644 --- a/packages/sitetile/astro/content/forms.md +++ b/packages/sitetile/astro/content/forms.md @@ -45,3 +45,7 @@ supposed to mean. ### Your name ### Tell us more {textarea} + +## Existing inbox bubble +%% sitetile: embed %% +
diff --git a/packages/sitetile/astro/content/ko-kr/_site.md b/packages/sitetile/astro/content/ko-kr/_site.md new file mode 100644 index 0000000..43aa2c3 --- /dev/null +++ b/packages/sitetile/astro/content/ko-kr/_site.md @@ -0,0 +1,4 @@ +--- +inbox-bubble: off +--- + diff --git a/packages/sitetile/astro/content/markers.md b/packages/sitetile/astro/content/markers.md index 7ce9855..dc0de99 100644 --- a/packages/sitetile/astro/content/markers.md +++ b/packages/sitetile/astro/content/markers.md @@ -3,6 +3,7 @@ sitetile-page: markers title: Marker coverage — hero variants + linked cells lang: en-US locales: en-US, zh-TW +inbox-bubble: on --- ## Logo hero, uncropped diff --git a/packages/sitetile/astro/smoke-build.mjs b/packages/sitetile/astro/smoke-build.mjs index 87b2cb2..acd5e1a 100644 --- a/packages/sitetile/astro/smoke-build.mjs +++ b/packages/sitetile/astro/smoke-build.mjs @@ -35,7 +35,10 @@ console.log('▸ astro build → dist-smoke/ (themeless + staged Lilac custom-th if (existsSync(LILAC_STAGED)) throw new Error(`refusing to shadow an existing staged theme: ${LILAC_STAGED}`); copyFileSync(LILAC_SOURCE, LILAC_STAGED); try { - execFileSync('npx', ['astro', 'build', '--outDir', DIST], { cwd: HERE, stdio: 'inherit' }); + execFileSync('npx', ['astro', 'build', '--outDir', DIST], { + cwd: HERE, stdio: 'inherit', + env: { ...process.env, SITE_ID: 'smoke-site', PLATFORM_ORIGIN: 'https://feelreef.com' }, + }); } finally { rmSync(LILAC_STAGED, { force: true }); } @@ -63,6 +66,8 @@ const signedPost = findPost('a-signed-post'); const unsignedPost = findPost('an-unsigned-post'); const markers = readFileSync(join(DIST, 'markers/index.html'), 'utf8'); const customThemeBuilt = readFileSync(join(DIST, 'custom-theme/index.html'), 'utf8'); +const localeExcept = readFileSync(join(DIST, 'zh-tw/blocks/index.html'), 'utf8'); +const siteOff = readFileSync(join(DIST, 'ko-kr/index.html'), 'utf8'); // Safe negative-control seam: mutate only the HTML held by this test process, never source/output. const customTheme = process.env.SITETILE_SMOKE_REMOVE_CUSTOM_MARKER === '1' ? customThemeBuilt.replace(/\sdata-theme-custom(?:="")?/, '') @@ -83,6 +88,7 @@ const occ = (s) => occIn(html, s); // pkg-runtimes SiteLayout body-end conditional imports (bleedblend et al) — Astro emits the // chunk even when no fixture page opts in; it must stay UNREFERENCED by fixtures. const ALLOWED_INLINE = [ + ['site inbox bubble module', '/corals/inbox-bubble/v0/inbox-bubble.js'], ['header-overlay', 'rf-header--overlay'], ['parallax', 'st-hero-layered[data-parallax]'], ['pv-gate', 'pv-notice-ok'], @@ -156,7 +162,7 @@ function auditScripts() { while ((m = re.exec(h))) { if (!executesAsScript(m[1])) { dataBlocks.push(m[1].trim()); continue; } const src = /src="([^"]+)"/.exec(m[1]); - const body = src ? readFileSync(join(DIST, src[1].replace(/^\//, '')), 'utf8') : m[2]; + const body = src ? (/^https?:\/\//.test(src[1]) ? src[1] : readFileSync(join(DIST, src[1].replace(/^\//, '')), 'utf8')) : m[2]; const hit = [...ALLOWED_INLINE, ...ALLOWED_CHUNKS].find(([, sig]) => body.includes(sig)); if (!hit) unaccounted.push(`${page.slice(DIST.length + 1)}: ${(src ? 'src ' + src[1] + ' → ' : '') + body.trim().slice(0, 70)}`); } @@ -208,6 +214,13 @@ function claimedIcons() { } const checks = [ + // -- born-on site inbox bubble: site/page resolution + legacy embed dedupe -- + ['inbox bubble: default on with the build site key, platform origin, and site title', () => + /
<\/div>\s* + + )} {/* package runtimes (body-end, module). Astro bundles } diff --git a/packages/sitetile/astro/src/lib/inbox-bubble.mjs b/packages/sitetile/astro/src/lib/inbox-bubble.mjs new file mode 100644 index 0000000..1c7997e --- /dev/null +++ b/packages/sitetile/astro/src/lib/inbox-bubble.mjs @@ -0,0 +1,27 @@ +const OFF = /^(off|false|no|0)$/i; +const ON = /^(on|true|yes|1)$/i; + +export function localeAgnosticPath(pathname, locales = []) { + const path = `/${String(pathname || '/').split(/[?#]/)[0].replace(/^\/+|\/+$/g, '')}`; + const first = path.split('/')[1].toLowerCase(); + const localeSet = new Set(locales.map((v) => String(v).trim().toLowerCase())); + return localeSet.has(first) ? (path.slice(first.length + 1) || '/') : path; +} + +export function pathMatches(pattern, pathname) { + const p = String(pattern || '').trim(); + if (!p) return false; + return p.endsWith('*') ? pathname.startsWith(p.slice(0, -1)) : pathname === p; +} + +export function inboxBubbleOn(meta = {}, pathname = '/', renderedBody = '') { + if (renderedBody.includes('data-dynamic-coral="inbox-bubble"')) return false; + const raw = String(meta['inbox-bubble'] ?? 'on').trim(); + if (OFF.test(raw)) return false; + if (ON.test(raw) && Object.prototype.hasOwnProperty.call(meta, 'inbox-bubble-page-override')) return true; + const locales = String(meta.locales || '').split(',').map((v) => v.trim()).filter(Boolean); + const path = localeAgnosticPath(pathname, locales); + const excluded = String(meta['inbox-bubble-except'] || '').split(',').some((p) => pathMatches(p, path)); + return !excluded; +} + diff --git a/packages/sitetile/astro/src/pages/[...path].astro b/packages/sitetile/astro/src/pages/[...path].astro index b03a113..cb9208f 100644 --- a/packages/sitetile/astro/src/pages/[...path].astro +++ b/packages/sitetile/astro/src/pages/[...path].astro @@ -78,6 +78,9 @@ export function getStaticPaths() { // blog-search etc. consistent across ALL pages instead of each page duplicating the chrome. const site = parseSite(raw); const siteConfig = loadSite(files, rel); + // Reserved resolution marker: after Site ⊕ page both values look identical, but the layout + // must know that an explicit page `on` wins over the site's exclusion patterns. + if (Object.prototype.hasOwnProperty.call(site.meta, 'inbox-bubble')) site.meta['inbox-bubble-page-override'] = true; // 🩸 `site-title` is stashed BEFORE the merge because the merge destroys the distinction the // head needs. og:site_name must be a constant per site, but it was reading headerBrand, which // falls back to meta.title — and after this merge meta.title is the PAGE's title on any page diff --git a/packages/sitetile/inbox-bubble.test.mjs b/packages/sitetile/inbox-bubble.test.mjs new file mode 100644 index 0000000..381b23e --- /dev/null +++ b/packages/sitetile/inbox-bubble.test.mjs @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { inboxBubbleOn, localeAgnosticPath, pathMatches } from './astro/src/lib/inbox-bubble.mjs'; + +test('inbox bubble defaults on and obeys site off', () => { + assert.equal(inboxBubbleOn({}, '/'), true); + assert.equal(inboxBubbleOn({ 'inbox-bubble': 'off' }, '/'), false); +}); + +test('suffix patterns are locale agnostic', () => { + assert.equal(localeAgnosticPath('/zh-tw/shop/item', ['en-US', 'zh-TW']), '/shop/item'); + assert.equal(pathMatches('/shop/*', '/shop/item'), true); + assert.equal(inboxBubbleOn({ locales: 'en-US, zh-TW', 'inbox-bubble-except': '/shop/*' }, '/zh-tw/shop/item'), false); +}); + +test('a hand-mounted bubble is deduped', () => { + assert.equal(inboxBubbleOn({}, '/', '
'), false); +}); + diff --git a/packages/sitetile/site-core.js b/packages/sitetile/site-core.js index dfcf746..c7e8608 100644 --- a/packages/sitetile/site-core.js +++ b/packages/sitetile/site-core.js @@ -43,6 +43,12 @@ import { highlightCode, knowsLanguage } from '../cssmd/highlight.js'; const FRONTMATTER_KEY = 'sitetile-page'; const KNOWN_TYPES = ['prose', 'hero', 'grid', 'gallery', 'carousel', 'cta', 'embed', 'collection', 'timeline', 'social', 'tagcloud', 'faq', 'form', 'people']; +// Site-layer vocabulary exported beside KNOWN_TYPES so grammar vendors have one renderer-owned +// source of truth for chrome keys that are otherwise invisible to the section model. +const SITE_LAYER_KEYS = [ + { key: 'inbox-bubble', syntax: 'inbox-bubble: on | off', purpose: 'Show the site visitor Q&A/message bubble on every page by default; a page value overrides the site value.' }, + { key: 'inbox-bubble-except', syntax: 'inbox-bubble-except: /shop/*, /checkout', purpose: 'Comma-separated locale-agnostic paths to suppress the bubble; a trailing * matches any suffix.' }, +]; // tagcloudLinks: a tagcloud section body (a markdown list of `- [Label](/href)` items) → an // ordered [{label, href}]. General — a weighted category/tag cloud is a near-universal WP/Blogger @@ -1500,6 +1506,6 @@ export { heroParts, socialParts, linkButtonsHtml, firstImage, imgTag, tagcloudLinks, // sidebar layout helpers — exported so the Astro layer can reuse the same parser. parseSidebarNav, - FRONTMATTER_KEY, KNOWN_TYPES, + FRONTMATTER_KEY, KNOWN_TYPES, SITE_LAYER_KEYS, deriveDescription, DESC_MAX, };