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_page_records p ON p.id = pr.page_id WHERE pr.status = 'online' AND pr.access_mode = 'public' 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_page_records 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 ` ${escapeXml(loc)} ${escapeXml(lastmod)} `; }) .filter(Boolean) .join('\n'); return ` ${urls} `; } 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', '# Public online 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, }; }