4e789663a1
Memind CI / Test, build, and release guards (push) Failing after 5m19s
Drop the user_confirmed_at gate so public, online, unexpired pages can enter sitemap/llms and receive SEO/GEO tags. Defaults now enable all discovery switches; stored all-off config is still preserved until admin saves. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.1 KiB
JavaScript
179 lines
5.1 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_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 ` <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',
|
|
'# 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,
|
|
};
|
|
}
|