fix(learn): skip plaza share widget on learning assistant pages

Wire delivery path detection through portal static routes and strip baked share chrome from learn parent/child HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-11 18:50:01 +08:00
parent 6e7a75a79e
commit ee8677cc6d
10 changed files with 154 additions and 16 deletions
+30 -4
View File
@@ -9,6 +9,7 @@ import { stripPublicationHtmlCspMeta } from './plaza-embed.mjs';
import {
decorateMindSpaceSeoGeoHtml,
} from './mindspace-seo-geo-delivery.mjs';
import { stripPublicFileShareWidget } from './mindspace-public-share-widget.mjs';
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
@@ -24,6 +25,22 @@ export function collectInlineScriptHashes(html) {
return hashes;
}
export function shouldSkipPublicShareWidget({
htmlFilePath = '',
pageUrl = '',
relativePath = '',
requestPath = '',
} = {}) {
const haystack = `${htmlFilePath}\n${pageUrl}\n${relativePath}\n${requestPath}`
.replace(/\\/g, '/')
.toLowerCase();
return (
haystack.includes('learning-assistant') ||
haystack.includes('/public/learn/') ||
/\/learn(?:\/|$|[?#])/.test(haystack)
);
}
export async function handleMindSpaceLongImageDownload({
query,
filePath,
@@ -59,6 +76,8 @@ export function decorateMindSpacePublishedHtml({
pageDataContext = null,
context,
htmlFilePath = '',
relativePath = '',
requestPath = '',
fileExists = fs.existsSync,
userAgent = '',
preparePublicationHtmlForEmbed,
@@ -97,10 +116,17 @@ export function decorateMindSpacePublishedHtml({
if (wechatShare) {
nextHtml = injectWechatShareBridge(nextHtml, { pageUrl: context.pageUrl });
}
const shareInjection = !embed
? injectPublicFileShareButton(nextHtml, { isOwner })
: { html: nextHtml, scriptHashes: [] };
nextHtml = shareInjection.html;
const skipShareWidget = !embed && shouldSkipPublicShareWidget({
htmlFilePath,
pageUrl: context?.pageUrl ?? '',
relativePath,
requestPath,
});
if (skipShareWidget) {
nextHtml = stripPublicFileShareWidget(nextHtml);
} else if (!embed) {
nextHtml = injectPublicFileShareButton(nextHtml, { isOwner }).html;
}
// Retries transient asset-download failures client-side before a page's own onerror
// permanently swaps the <img> for a placeholder. See mindspace-public-image-retry.mjs.
if (!embed) {
+62
View File
@@ -4,6 +4,7 @@ import {
collectInlineScriptHashes,
decorateMindSpacePublishedHtml,
handleMindSpaceLongImageDownload,
shouldSkipPublicShareWidget,
} from './mindspace-public-delivery.mjs';
test('collectInlineScriptHashes returns sha256 hashes for inline scripts only', () => {
@@ -173,6 +174,67 @@ test('decorateMindSpacePublishedHtml forwards isOwner=false to the share button
assert.equal(sharedIsOwner, false);
});
test('shouldSkipPublicShareWidget matches learning assistant delivery paths', () => {
assert.equal(
shouldSkipPublicShareWidget({
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/learning-assistant.html',
}),
true,
);
assert.equal(
shouldSkipPublicShareWidget({
htmlFilePath: '/Users/john/Project/Memind/public/learn/child.html',
}),
true,
);
assert.equal(
shouldSkipPublicShareWidget({
relativePath: 'public/learning-assistant.html',
}),
true,
);
assert.equal(
shouldSkipPublicShareWidget({
requestPath: '/MindSpace/u/public/learning-assistant-parent.html',
}),
true,
);
assert.equal(
shouldSkipPublicShareWidget({
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/report.html',
}),
false,
);
});
test('decorateMindSpacePublishedHtml skips plaza share widget on learning assistant pages', () => {
let shareInjected = false;
const baked = `<html><body><h1>Learn</h1><style id="mindspace-public-share-style">x</style><script>x</script></body></html>`;
const result = decorateMindSpacePublishedHtml({
html: baked,
embed: false,
htmlFilePath: '/tmp/MindSpace/u/public/learning-assistant.html',
context: {
origin: 'https://m.tkmind.cn',
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/learning-assistant.html',
pageDirUrl: 'https://m.tkmind.cn/MindSpace/u/public/',
fallbackImageUrl: '',
},
preparePublicationHtmlForEmbed: (value) => value,
injectOgTags: (value) => value,
injectWechatShareBridge: (value) => value,
injectPublicFileShareButton: (value) => {
shareInjected = true;
return { html: `${value}<!--share-->`, scriptHashes: [] };
},
publishedPageCsp: (value) => value,
isWechatUserAgent: () => false,
});
assert.equal(shareInjected, false);
assert.doesNotMatch(result.html, /mindspace-public-share/);
});
test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => {
const result = decorateMindSpacePublishedHtml({
html: '<html></html>',
+4 -1
View File
@@ -202,7 +202,10 @@ const PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN =
/<style id="mindspace-public-share-style">[\s\S]*?<\/script>\s*/i;
export function stripPublicFileShareWidget(html) {
return String(html ?? '').replace(PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN, '');
let next = String(html ?? '').replace(PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN, '');
next = next.replace(/<div[^>]*data-mindspace-wechat-dialog[\s\S]*?<\/div>\s*/gi, '');
next = next.replace(/<div[^>]*data-mindspace-public-share[\s\S]*?<\/div>\s*/gi, '');
return next;
}
export function injectPublicFileShareButton(html, { isOwner = true } = {}) {
+12 -1
View File
@@ -1,6 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { injectPublicFileShareButton } from './mindspace-public-share-widget.mjs';
import {
injectPublicFileShareButton,
stripPublicFileShareWidget,
} from './mindspace-public-share-widget.mjs';
test('injectPublicFileShareButton adds plaza entry and confirm dialog', () => {
const result = injectPublicFileShareButton('<!doctype html><html><body><h1>Hi</h1></body></html>');
@@ -63,3 +66,11 @@ test('injectPublicFileShareButton keeps Plaza entry when isOwner explicitly true
assert.match(result.html, /data-action="plaza"/);
assert.match(result.html, /data-mindspace-public-share-dialog/);
});
test('stripPublicFileShareWidget removes injected share chrome', () => {
const injected = injectPublicFileShareButton('<!doctype html><html><body><h1>Hi</h1></body></html>');
const stripped = stripPublicFileShareWidget(injected.html);
assert.doesNotMatch(stripped, /data-mindspace-public-share/);
assert.doesNotMatch(stripped, /公众号/);
assert.doesNotMatch(stripped, /保存长图/);
});
+2 -1
View File
@@ -67,7 +67,8 @@ th{font-size:11px;color:#6b7280}
.tabs{display:flex;gap:8px;margin-top:14px}
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
.tabs button.on{background:#3b5bfd;color:#fff}
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}</style></head><body>
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}</style></head><body>
<div class="wrap" id="app"></div><div class="tabbar" id="tabbar"></div>
<div style="text-align:center;font-size:10px;color:#c3c9d6;padding:6px 0 76px">v2</div>
<script>
+2 -1
View File
@@ -67,7 +67,8 @@ th{font-size:11px;color:#6b7280}
.tabs{display:flex;gap:8px;margin-top:14px}
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
.tabs button.on{background:#3b5bfd;color:#fff}
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style></head><body>
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style></head><body>
<div class="wrap pl" id="app"></div>
<script>
var FAMILY_SLUG=new URLSearchParams(location.search).get('f')||'';
@@ -79,7 +79,8 @@ th{font-size:11px;color:#6b7280}
.tabs{display:flex;gap:8px;margin-top:14px}
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
.tabs button.on{background:#3b5bfd;color:#fff}
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}`;
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}`;
function childScript() {
return `
+10 -1
View File
@@ -4,6 +4,7 @@ import {
} from '../mindspace-public-page-context.mjs';
import {
collectInlineScriptHashes,
shouldSkipPublicShareWidget,
} from '../mindspace-public-delivery.mjs';
import {
buildViewerAnalyticsIdentity,
@@ -336,7 +337,15 @@ export function createPortalPublishedPageDelivery({
!raw &&
isFullHtml &&
result.publication?.accessMode !==
'password';
'password' &&
!shouldSkipPublicShareWidget({
pageUrl,
requestPath: originalPath,
relativePath:
result.relativePath ??
result.pageSource?.relativePath ??
'',
});
if (canWrapWithShell) {
const title =
detectPublishedPageTitle(html);
+27 -6
View File
@@ -6,6 +6,7 @@ import {
injectOgTags,
renderWechatSharePreviewHtml,
} from '../mindspace-og-tags.mjs';
import { stripPublicFileShareWidget } from '../mindspace-public-share-widget.mjs';
export function attachPortalStaticDeliveryRoutes({
app,
@@ -161,12 +162,32 @@ export function attachPortalStaticDeliveryRoutes({
{ maxAge: '1d' },
),
);
app.use(
'/learn',
staticMiddleware(pathApi.join(h5Root, 'public/learn'), {
maxAge: '0',
}),
);
const learnRoot = pathApi.join(h5Root, 'public/learn');
const learnStatic = staticMiddleware(learnRoot, { maxAge: '0' });
app.use('/learn', (req, res, next) => {
const pathname = String(req.path ?? '').split('?')[0];
if (!/\.html$/i.test(pathname)) {
return learnStatic(req, res, next);
}
const filePath = pathApi.join(learnRoot, pathname);
const resolvedRoot = pathApi.resolve(learnRoot);
const resolvedPath = pathApi.resolve(filePath);
if (
!resolvedPath.startsWith(`${resolvedRoot}${pathApi.sep}`) &&
resolvedPath !== resolvedRoot
) {
return res.status(403).json({ message: '禁止访问' });
}
if (!fsApi.existsSync(resolvedPath) || !fsApi.statSync(resolvedPath).isFile()) {
return learnStatic(req, res, next);
}
const html = stripPublicFileShareWidget(
fsApi.readFileSync(resolvedPath, 'utf8'),
);
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', 'no-store');
return res.send(html);
});
app.get(
'/dev/wechat-share-preview',
@@ -288,6 +288,9 @@ export function createPortalWorkspacePublicationDelivery({
isOwner,
pageDataContext,
context,
htmlFilePath: delivery.filePath ?? '',
relativePath: delivery.relativePath ?? '',
requestPath: req.originalUrl || req.url || '',
userAgent:
req.get('user-agent') || '',
preparePublicationHtmlForEmbed,