From 4e789663a1257fc56d245fec84941b25ee4de8fb Mon Sep 17 00:00:00 2001 From: john Date: Thu, 13 Aug 2026 15:51:48 +0800 Subject: [PATCH] feat(seo): index public online pages without confirmation 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 --- docs/local-dev.md | 8 ++--- docs/regression-guards/mindspace-seo-geo.md | 8 ++--- mindspace-config.mjs | 16 ++++----- mindspace-config.test.mjs | 39 ++++++++++++++++++++- mindspace-index-policy.mjs | 3 +- mindspace-index-policy.test.mjs | 9 ++++- mindspace-seo-discovery-service.mjs | 3 +- mindspace-seo-geo-admin-catalog.test.mjs | 2 +- mindspace-seo-geo-delivery.test.mjs | 4 +-- skills/static-page-publish/SKILL.md | 2 +- 10 files changed, 68 insertions(+), 26 deletions(-) diff --git a/docs/local-dev.md b/docs/local-dev.md index e262c51..637f34c 100644 --- a/docs/local-dev.md +++ b/docs/local-dev.md @@ -205,15 +205,15 @@ Plaza 本地开发说明见 [plaza-local.md](./plaza-local.md)。生产发布、 ## MindSpace SEO / GEO(本地联调) -默认关闭,与生产一致。在 **memind_adm**(见上表)打开 **MindSpace 配置**,保存 **SEO / GEO** 卡片中的总开关与子开关。 +默认开启。在 **memind_adm**(见上表)打开 **MindSpace 配置**,可按需关闭 **SEO / GEO** 卡片中的总开关与子开关。 | 开关 | 作用 | |------|------| -| 总开关 `seoGeo.enabled` | 关闭时交付链与改前一致;开启后按收录策略注入 meta / JSON-LD | +| 总开关 `seoGeo.enabled` | 关闭时不注入 meta / JSON-LD;开启后按收录策略注入 | | `seo.sitemap` / `seo.robots` / `geo.llms` | 控制 `GET /sitemap.xml`、`/robots.txt`、`/llms.txt` | -| `seo.baiduPush` | 用户确认公开 MindSpace 页或 Plaza 发帖后推送百度 | +| `seo.baiduPush` | 公开 MindSpace 页或 Plaza 发帖后推送百度 | -可索引页条件:`public` + `online` + `user_confirmed_at` 非空。私有页、未确认公开、embed 预览一律 `noindex`。 +可索引页条件:`public` + `online` + 未过期。密码、登录可见、owner_only、已过期、embed 预览一律 `noindex`。 本地验证: diff --git a/docs/regression-guards/mindspace-seo-geo.md b/docs/regression-guards/mindspace-seo-geo.md index 81b5ca0..49f5821 100644 --- a/docs/regression-guards/mindspace-seo-geo.md +++ b/docs/regression-guards/mindspace-seo-geo.md @@ -4,11 +4,11 @@ ## 保护内容 -1. **收录策略硬规则**:仅 `access_mode=public` 且 `status=online` 且 `user_confirmed_at` 非空的发布页允许 SEO/GEO 注入与 sitemap/llms 收录。 -2. **私有页强制 noindex**:密码、登录可见、owner_only、未确认公开、工作区预览直链等必须注入 `noindex, nofollow` 并设置 `X-Robots-Tag`。 -3. **总开关默认关闭**:`mindspace_config.seo_geo_config.enabled=false` 时,交付链行为与未上线前一致。 +1. **收录策略硬规则**:仅 `access_mode=public` 且 `status=online` 且未过期的发布页允许 SEO/GEO 注入与 sitemap/llms 收录。不再要求 `user_confirmed_at`。 +2. **私有页强制 noindex**:密码、登录可见、owner_only、已过期、工作区预览直链等必须注入 `noindex, nofollow` 并设置 `X-Robots-Tag`。 +3. **总开关默认开启**:`mindspace_config.seo_geo_config` 缺省为全开;库内已保存的旧值仍以数据库为准,需在 memind_adm MindSpace 配置页保存后才会改写生产。 4. **配置来源**:memind_adm MindSpace 配置页 → `PATCH /admin-api/mindspace/config` → Portal `loadMindSpaceConfigCached()`。 -5. **百度推送**:仅在 admin 开启 `seo.baiduPush` 且页面可索引时触发;MindSpace 确认公开与 Plaza 发帖共用该开关。 +5. **百度推送**:仅在 admin 开启 `seo.baiduPush` 且页面可索引时触发;公开页发布与 Plaza 发帖共用该开关。 ## 必跑验证 diff --git a/mindspace-config.mjs b/mindspace-config.mjs index 4a5a102..8ff14a4 100644 --- a/mindspace-config.mjs +++ b/mindspace-config.mjs @@ -59,18 +59,18 @@ function parseJsonObject(value, fallback = {}) { export function defaultSeoGeoConfig() { return { - enabled: false, + enabled: true, seo: { - enabled: false, + enabled: true, canonical: true, - sitemap: false, - robotsTxt: false, - baiduPush: false, + sitemap: true, + robotsTxt: true, + baiduPush: true, }, geo: { - enabled: false, - jsonLd: false, - llmsTxt: false, + enabled: true, + jsonLd: true, + llmsTxt: true, }, }; } diff --git a/mindspace-config.test.mjs b/mindspace-config.test.mjs index 2fead2a..9a750b1 100644 --- a/mindspace-config.test.mjs +++ b/mindspace-config.test.mjs @@ -9,6 +9,18 @@ import { updateMindSpaceConfig, } from './mindspace-config.mjs'; +test('defaultSeoGeoConfig enables all discovery switches', () => { + const config = defaultSeoGeoConfig(); + assert.equal(config.enabled, true); + assert.equal(config.seo.enabled, true); + assert.equal(config.seo.sitemap, true); + assert.equal(config.seo.robotsTxt, true); + assert.equal(config.seo.baiduPush, true); + assert.equal(config.geo.enabled, true); + assert.equal(config.geo.jsonLd, true); + assert.equal(config.geo.llmsTxt, true); +}); + test('defaultMindSpaceConfig falls back to env or 5', () => { assert.equal(defaultMindSpaceConfig({ MINDSPACE_FREE_PUBLIC_PAGE_LIMIT: '8' }).publicPageLimit, 8); assert.equal(defaultMindSpaceConfig({ MINDSPACE_FREE_PUBLIC_PAGE_LIMIT: '0' }).publicPageLimit, 5); @@ -24,7 +36,7 @@ test('normalizeSeoGeoConfig merges nested switches', () => { assert.equal(config.enabled, true); assert.equal(config.seo.sitemap, true); assert.equal(config.geo.llmsTxt, true); - assert.equal(config.seo.baiduPush, false); + assert.equal(config.seo.baiduPush, true); }); test('ensureMindSpaceConfig seeds the default row only when empty', async () => { @@ -45,6 +57,31 @@ test('ensureMindSpaceConfig seeds the default row only when empty', async () => assert.deepEqual(calls[2].params.slice(0, 3), ['public_page_limit', '9', '公开页面数量上限']); }); +test('loadMindSpaceConfig keeps stored all-off seoGeo instead of new defaults', async () => { + const pool = { + async query(sql) { + if (sql.includes('FROM mindspace_config')) { + return [[ + { + key: 'seo_geo_config', + value: JSON.stringify({ + enabled: false, + seo: { enabled: false, canonical: true, sitemap: false, robotsTxt: false, baiduPush: false }, + geo: { enabled: false, jsonLd: false, llmsTxt: false }, + }), + }, + ]]; + } + return [[]]; + }, + }; + + const config = await loadMindSpaceConfig(pool); + assert.equal(config.seoGeo.enabled, false); + assert.equal(config.seoGeo.seo.sitemap, false); + assert.equal(config.seoGeo.geo.jsonLd, false); +}); + test('loadMindSpaceConfig merges stored config with fallback', async () => { const pool = { async query(sql) { diff --git a/mindspace-index-policy.mjs b/mindspace-index-policy.mjs index a046afc..4d5f397 100644 --- a/mindspace-index-policy.mjs +++ b/mindspace-index-policy.mjs @@ -40,7 +40,6 @@ export function isPublicationIndexable(publication, { now = Date.now() } = {}) { if (!snapshot) return false; if (snapshot.status !== 'online') return false; if (snapshot.accessMode !== INDEXABLE_ACCESS_MODE) return false; - if (snapshot.userConfirmedAt == null) return false; if (snapshot.expiresAt != null && snapshot.expiresAt <= now) return false; return true; } @@ -74,7 +73,7 @@ export function resolveIndexPolicy({ return { mode: 'indexable', injectNoindex: false, - reason: 'public_confirmed', + reason: 'public_online', seoEnabled: Boolean(seoGeoConfig.seo?.enabled), geoEnabled: Boolean(seoGeoConfig.geo?.enabled), canonicalEnabled: seoGeoConfig.seo?.canonical !== false, diff --git a/mindspace-index-policy.test.mjs b/mindspace-index-policy.test.mjs index d5f1c87..e1dd30f 100644 --- a/mindspace-index-policy.test.mjs +++ b/mindspace-index-policy.test.mjs @@ -17,6 +17,13 @@ test('isPublicationIndexable accepts confirmed public online publications', () = assert.equal(isPublicationIndexable(indexablePublication), true); }); +test('isPublicationIndexable accepts public online pages without confirmation', () => { + assert.equal( + isPublicationIndexable({ ...indexablePublication, userConfirmedAt: null }), + true, + ); +}); + test('isPublicationIndexable rejects private access modes', () => { for (const accessMode of ['password', 'private_link', 'login_required', 'owner_only']) { assert.equal( @@ -44,7 +51,7 @@ test('resolveIndexPolicy noindexes non-public publications when enabled', () => assert.equal(policy.injectNoindex, true); }); -test('resolveIndexPolicy marks confirmed public pages indexable', () => { +test('resolveIndexPolicy marks public online pages indexable', () => { const policy = resolveIndexPolicy({ seoGeoConfig: { enabled: true, diff --git a/mindspace-seo-discovery-service.mjs b/mindspace-seo-discovery-service.mjs index 7103b93..6ffa3e1 100644 --- a/mindspace-seo-discovery-service.mjs +++ b/mindspace-seo-discovery-service.mjs @@ -32,7 +32,6 @@ export async function listIndexablePublications( LEFT JOIN h5_page_records 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 ?`, @@ -153,7 +152,7 @@ export function renderRobotsTxt({ export function renderLlmsTxt(entries, { origin = '' } = {}) { const header = [ '# TKMind MindSpace public pages', - '# Only confirmed public publications are listed here.', + '# Public online publications are listed here.', '', ]; const body = entries diff --git a/mindspace-seo-geo-admin-catalog.test.mjs b/mindspace-seo-geo-admin-catalog.test.mjs index bd982cf..2811e4a 100644 --- a/mindspace-seo-geo-admin-catalog.test.mjs +++ b/mindspace-seo-geo-admin-catalog.test.mjs @@ -108,6 +108,6 @@ test('listAdminSeoGeoPublicationCatalog merges zero-traffic pages with view stat assert.equal(result.data[0].indexable, true); assert.equal(result.data[1].publicationId, 'pub-2'); assert.equal(result.data[1].totalViews, 0); - assert.equal(result.data[1].indexable, false); + assert.equal(result.data[1].indexable, true); assert.match(result.data[1].publicUrl, /^https:\/\/m\.tkmind\.cn\//); }); diff --git a/mindspace-seo-geo-delivery.test.mjs b/mindspace-seo-geo-delivery.test.mjs index 3d2a85e..26b1cf6 100644 --- a/mindspace-seo-geo-delivery.test.mjs +++ b/mindspace-seo-geo-delivery.test.mjs @@ -24,7 +24,7 @@ test('decorateMindSpaceSeoGeoHtml noindexes private publications', () => { assert.match(result.html, /noindex, nofollow/); }); -test('decorateMindSpaceSeoGeoHtml injects seo and geo for public pages', () => { +test('decorateMindSpaceSeoGeoHtml injects seo and geo for unconfirmed public pages', () => { const result = decorateMindSpaceSeoGeoHtml( 'Demo', { @@ -32,7 +32,7 @@ test('decorateMindSpaceSeoGeoHtml injects seo and geo for public pages', () => { publication: { status: 'online', accessMode: 'public', - userConfirmedAt: Date.now(), + userConfirmedAt: null, publicUrl: '/u/john/pages/demo', }, context: { diff --git a/skills/static-page-publish/SKILL.md b/skills/static-page-publish/SKILL.md index 33874f7..9677ce5 100644 --- a/skills/static-page-publish/SKILL.md +++ b/skills/static-page-publish/SKILL.md @@ -146,7 +146,7 @@ npm run check:mindspace-cover ## SEO / GEO 元数据(推荐) -平台在 memind_adm **MindSpace 配置 → SEO / GEO** 开启后,会对**已确认公开**(`access_mode=public` 且用户已确认)的发布页自动注入 canonical、结构化数据等。Agent 仍应写好基础 meta,以提升搜索与 AI 引用质量: +平台在 memind_adm **MindSpace 配置 → SEO / GEO** 开启后,会对**公开在线**(`access_mode=public` 且 `status=online`)的发布页自动注入 canonical、结构化数据等。Agent 仍应写好基础 meta,以提升搜索与 AI 引用质量: ```html 页面真实主题