Files
memind/mindspace-seo-discovery-service.mjs
T
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 08:06:12 +08:00

180 lines
5.2 KiB
JavaScript

import { isPublicationIndexable, normalizePublicationSnapshot } from './mindspace-index-policy.mjs';
function escapeXml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
function toAbsoluteUrl(origin, value) {
const raw = String(value ?? '').trim();
if (!raw) return '';
if (/^https?:\/\//i.test(raw)) return raw.split('#')[0];
if (raw.startsWith('/') && origin) return `${origin}${raw}`.split('#')[0];
return raw.split('#')[0];
}
export async function listIndexablePublications(
pool,
{ limit = 5000, offset = 0 } = {},
) {
if (!pool) return [];
const safeLimit = Math.min(Math.max(Number(limit) || 5000, 1), 10000);
const safeOffset = Math.max(Number(offset) || 0, 0);
const [rows] = await pool.query(
`SELECT pr.id, pr.page_id, pr.public_url, pr.access_mode, pr.status,
pr.user_confirmed_at, pr.expires_at, pr.updated_at, pr.published_at,
p.title, p.summary
FROM h5_publish_records pr
LEFT JOIN h5_pages p ON p.id = pr.page_id
WHERE pr.status = 'online'
AND pr.access_mode = 'public'
AND pr.user_confirmed_at IS NOT NULL
AND (pr.expires_at IS NULL OR pr.expires_at > ?)
ORDER BY pr.published_at DESC
LIMIT ? OFFSET ?`,
[Date.now(), safeLimit, safeOffset],
);
return rows
.map((row) =>
normalizePublicationSnapshot({
id: row.id,
pageId: row.page_id,
publicUrl: row.public_url,
accessMode: row.access_mode,
status: row.status,
userConfirmedAt: row.user_confirmed_at,
expiresAt: row.expires_at,
updatedAt: row.updated_at ?? row.published_at,
title: row.title,
summary: row.summary,
}),
)
.filter((entry) => isPublicationIndexable(entry));
}
export async function resolvePublicationIndexSnapshot(
pool,
{
publicationId = null,
pageId = null,
userId = null,
} = {},
) {
if (!pool) return null;
const id = String(publicationId ?? '').trim();
const page = String(pageId ?? '').trim();
const owner = String(userId ?? '').trim();
if (!id && !page) return null;
const params = [];
let sql = `SELECT pr.id, pr.page_id, pr.user_id, pr.public_url, pr.access_mode, pr.status,
pr.user_confirmed_at, pr.expires_at, pr.updated_at, pr.published_at,
p.title, p.summary
FROM h5_publish_records pr
LEFT JOIN h5_pages p ON p.id = pr.page_id
WHERE pr.status = 'online'`;
if (id) {
sql += ' AND pr.id = ?';
params.push(id);
} else {
sql += ' AND pr.page_id = ?';
params.push(page);
if (owner) {
sql += ' AND pr.user_id = ?';
params.push(owner);
}
}
sql += ' ORDER BY pr.published_at DESC LIMIT 1';
const [rows] = await pool.query(sql, params);
const row = rows[0];
if (!row) return null;
return normalizePublicationSnapshot({
id: row.id,
pageId: row.page_id,
publicUrl: row.public_url,
accessMode: row.access_mode,
status: row.status,
userConfirmedAt: row.user_confirmed_at,
expiresAt: row.expires_at,
updatedAt: row.updated_at ?? row.published_at,
title: row.title,
summary: row.summary,
});
}
export function renderSitemapXml(entries, { origin = '' } = {}) {
const urls = entries
.map((entry) => {
const loc = toAbsoluteUrl(origin, entry.publicUrl);
if (!loc) return '';
const lastmod = entry.updatedAt
? new Date(Number(entry.updatedAt)).toISOString()
: new Date().toISOString();
return ` <url>
<loc>${escapeXml(loc)}</loc>
<lastmod>${escapeXml(lastmod)}</lastmod>
</url>`;
})
.filter(Boolean)
.join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>
`;
}
export function renderRobotsTxt({
origin = '',
sitemapEnabled = false,
llmsTxtEnabled = false,
} = {}) {
const lines = [
'User-agent: *',
'Allow: /u/',
'Disallow: /api/',
'Disallow: /admin-api/',
'Disallow: /mindspace/',
];
if (sitemapEnabled && origin) {
lines.push(`Sitemap: ${origin}/sitemap.xml`);
}
if (llmsTxtEnabled && origin) {
lines.push(`# AI discovery: ${origin}/llms.txt`);
}
lines.push('');
return `${lines.join('\n')}`;
}
export function renderLlmsTxt(entries, { origin = '' } = {}) {
const header = [
'# TKMind MindSpace public pages',
'# Only confirmed public publications are listed here.',
'',
];
const body = entries
.map((entry) => {
const url = toAbsoluteUrl(origin, entry.publicUrl);
if (!url) return '';
const summary = String(entry.summary ?? entry.title ?? '').trim();
return summary ? `- [${summary}](${url})` : `- ${url}`;
})
.filter(Boolean);
return `${header.concat(body).join('\n')}\n`;
}
export function createMindspaceSeoDiscoveryService(pool) {
return {
listIndexablePublications: (options) => listIndexablePublications(pool, options),
resolvePublicationIndexSnapshot: (options) =>
resolvePublicationIndexSnapshot(pool, options),
renderSitemapXml,
renderRobotsTxt,
renderLlmsTxt,
};
}