Files
memind/wechat-draft-article-layout.mjs
T
john b6d85d487a
Memind CI / Test, build, and release guards (push) Failing after 2m51s
Update WeChat draft follow template
2026-09-14 15:36:40 +08:00

161 lines
6.4 KiB
JavaScript

/**
* 公众号草稿统一 layout:阅读原文 + 底部关注区。
* 标准定义见 wechat-draft-publication-standard.mjs 与 docs/wechat-draft-publication-standard.md
*/
import fs from 'node:fs';
import path from 'node:path';
import { fetch as undiciFetch } from 'undici';
import { PLATFORM_BRAND_ICON_PATH } from './mindspace-og-tags.mjs';
import {
resolveMpFollowQrcodePath,
uploadWechatArticleContentImage,
} from './wechat-news-morning-draft.mjs';
import { WECHAT_DRAFT_PUBLICATION_STANDARD } from './wechat-draft-publication-standard.mjs';
const WECHAT_DRAFT_CONTENT_CHAR_LIMIT =
WECHAT_DRAFT_PUBLICATION_STANDARD.limits.maxContentChars;
const FOLLOW_QRCODE_SIZE_PX = 120;
const MP_FOLLOW_QRCODE_PUBLIC_PATH = '/assets/mp-follow-qrcode.png';
export function resolveMpFollowQrcodePublicUrl(env = process.env) {
const base = String(env.H5_PUBLIC_BASE_URL ?? 'https://m.tkmind.cn').replace(/\/$/, '');
return `${base}${MP_FOLLOW_QRCODE_PUBLIC_PATH}`;
}
export function clipWechatDraftBodyToFitFooter(
body,
footer,
limit = WECHAT_DRAFT_CONTENT_CHAR_LIMIT,
) {
const source = String(body ?? '');
const suffix = String(footer ?? '');
const separator = source && suffix ? '\n' : '';
const maxBody = Math.max(0, Number(limit) - suffix.length - separator.length);
if (source.length <= maxBody) return source;
let clipped = source.slice(0, maxBody);
const lastLt = clipped.lastIndexOf('<');
const lastGt = clipped.lastIndexOf('>');
if (lastLt > lastGt) clipped = clipped.slice(0, lastLt);
return clipped.trimEnd();
}
export function renderReadOriginalLink(publicUrl) {
if (!publicUrl) return '';
return [
'<section style="text-align:center;margin:26px 0 14px;padding:20px 16px 18px;background:linear-gradient(180deg,#eef4ff 0%,#fff 100%);border:2px dashed #576b95;border-radius:16px;box-shadow:0 6px 18px rgba(87,107,149,.12);">',
'<p style="margin:0;font-size:14px;color:#4a5568;line-height:1.6;font-weight:600;">✨ 完整网页版更精彩</p>',
'<p style="margin:6px 0 0;font-size:12px;color:#718096;line-height:1.5;">动画 · 互动 · 高清大图 · 完整内容</p>',
'<p style="margin:14px 0 0;font-size:26px;line-height:1;color:#576b95;letter-spacing:6px;font-weight:700;">⬇ ⬇ ⬇</p>',
'<p style="margin:12px 0 0;">',
`<a href="${publicUrl}" style="display:inline-block;padding:12px 32px;border-radius:999px;background:linear-gradient(135deg,#576b95,#3d5a80);color:#fff;font-size:16px;font-weight:700;text-decoration:none;box-shadow:0 6px 16px rgba(87,107,149,.35);">📖 阅读原文</a>`,
'</p>',
'<p style="margin:12px 0 0;font-size:13px;color:#576b95;font-weight:700;line-height:1.5;">👆 点击上方按钮,查看完整精彩页面</p>',
'<p style="margin:8px 0 0;font-size:22px;line-height:1;color:#576b95;letter-spacing:4px;">➡️ ➡️ ➡️</p>',
'</section>',
].join('');
}
/** @deprecated 保留别名,避免外部引用断裂 */
export const renderReadOriginalTop = renderReadOriginalLink;
export function renderFollowTkMindSection({
qrcodeImageUrl = '',
} = {}) {
const qrcodeBlock = qrcodeImageUrl
? `<img src="${qrcodeImageUrl}" alt="TKMind 服务号二维码" style="width:${FOLLOW_QRCODE_SIZE_PX}px;height:${FOLLOW_QRCODE_SIZE_PX}px;display:block;margin:12px auto 0;border-radius:10px;border:1px solid #eee;" />`
: '';
return [
'<section style="text-align:center;padding:22px 16px 18px;margin:18px 0 8px;background:linear-gradient(180deg,#fff8f8,#fff);border:1px solid #ffcdd2;border-radius:12px;">',
'<p style="margin:0 0 6px;font-size:16px;font-weight:700;color:#b71c1c;line-height:1.5;">欢迎关注 TKMind 智趣</p>',
qrcodeBlock,
qrcodeImageUrl
? '<p style="margin:12px 0 0;font-size:12px;color:#a0aec0;">长按扫描关注</p>'
: '<p style="margin:12px 0 0;font-size:12px;color:#a0aec0;">关注 TKMind 服务号</p>',
'</section>',
].filter(Boolean).join('');
}
export function applyWechatDraftArticleLayout(
content,
{ publicUrl = '', qrcodeImageUrl = '' } = {},
) {
const footerParts = [];
if (publicUrl) {
footerParts.push(renderReadOriginalLink(publicUrl));
}
footerParts.push(renderFollowTkMindSection({ qrcodeImageUrl }));
const footer = footerParts.filter(Boolean).join('\n');
const body = clipWechatDraftBodyToFitFooter(String(content ?? '').trim(), footer);
return [body, footer].filter(Boolean).join(body && footer ? '\n' : '');
}
export function resolveTkMindBrandIconPath(memindLibRoot = process.cwd()) {
const candidates = [
path.join(memindLibRoot, 'public', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
path.join(memindLibRoot, 'dist', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
path.join(process.cwd(), 'public', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
];
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
}
export function resolveMpFollowQrcodeAssetPath(memindLibRoot = process.cwd()) {
const fromHelper = resolveMpFollowQrcodePath(memindLibRoot);
if (fromHelper) return fromHelper;
const publicAsset = path.join(memindLibRoot, 'public', MP_FOLLOW_QRCODE_PUBLIC_PATH.replace(/^\//, ''));
if (fs.existsSync(publicAsset)) return publicAsset;
const cwdPublicAsset = path.join(process.cwd(), 'public', MP_FOLLOW_QRCODE_PUBLIC_PATH.replace(/^\//, ''));
return fs.existsSync(cwdPublicAsset) ? cwdPublicAsset : null;
}
export async function uploadWechatDraftBrandAssets({
accessToken,
wechatFetch = undiciFetch,
memindLibRoot = process.cwd(),
env = process.env,
} = {}) {
const qrcodePath = resolveMpFollowQrcodeAssetPath(memindLibRoot);
const publicFallbackUrl = resolveMpFollowQrcodePublicUrl(env);
let qrcodeImageUrl = '';
if (!qrcodePath) {
return { qrcodeImageUrl };
}
if (!accessToken) {
return { qrcodeImageUrl: publicFallbackUrl };
}
try {
qrcodeImageUrl = await uploadWechatArticleContentImage(
accessToken,
fs.readFileSync(qrcodePath),
{ wechatFetch, filename: 'mp-follow-qrcode.png' },
);
} catch {
qrcodeImageUrl = publicFallbackUrl;
}
return { qrcodeImageUrl };
}
export async function finalizeWechatDraftArticleForPush({
content,
publicUrl = '',
accessToken = null,
wechatFetch = undiciFetch,
memindLibRoot = process.cwd(),
} = {}) {
const brandAssets = await uploadWechatDraftBrandAssets({
accessToken,
wechatFetch,
memindLibRoot,
});
return {
content: applyWechatDraftArticleLayout(content, {
publicUrl,
...brandAssets,
}),
...brandAssets,
};
}