fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
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>
180 lines
5.2 KiB
JavaScript
180 lines
5.2 KiB
JavaScript
import { isPublicationIndexable, normalizePublicationSnapshot } from './mindspace-index-policy.mjs';
|
|
|
|
function escapeXml(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|