How I kept 62 of 80 programmatic pages alive while hiding them from Google
How I kept 62 of 80 programmatic pages alive while hiding them from Google
在第二次因“规模化内容”(scaled content)被 AdSense 拒绝后,我面临两个选择:要么删除 Open Alternative To 网站上那些内容单薄的页面,接受外部链接失效导致的 404 错误;要么保留这些页面,但将它们从 Google 的质量评估中隐藏起来。我选择了后者。理由是:我的一些 URL 有外部链接指向它们——来自本系列之前的文章、社交媒体帖子以及网站内部导航。404 会导致所有这些链接失效。这些页面本身没有错,只是内容不够丰富。给 Google 正确的信号应该是“不要评估这些页面”,而不是“这些页面不存在”。
The isCurated gate (筛选门控)
这个门控逻辑位于 apps/oss-alternatives/src/lib/curation.ts:
export const CURATION = {
MIN_ALTERNATIVES: 4,
MIN_TOP_STARS: 1000,
MIN_INTRO_LEN: 80,
} as const;
export function isCurated(s: SaasEntry): boolean {
if (!s.intro || s.intro.length < CURATION.MIN_INTRO_LEN) return false;
const alts = s.alternatives ?? [];
if (alts.length < CURATION.MIN_ALTERNATIVES) return false;
const topStars = alts.reduce((m, a) => Math.max(m, a.stars ?? 0), 0);
if (topStars < CURATION.MIN_TOP_STARS) return false;
return true;
}
这里有三个必须同时满足的条件:
- 至少列出 4 个开源替代品 —— 条目过少的对比页面几乎算不上对比。
- 顶级替代品拥有 1,000+ GitHub 星标 —— 过滤掉那些无法体现类别深度的冷门或无人维护的项目。
- 简介文本至少 80 个字符 —— 排除掉当 Claude 不可用时,ETL 质量阶梯(quality ladder)生成的备用模板内容。
这些是客观阈值,而非人工挑选。该门控在每次 Astro 构建时自动运行。在下一次 ETL 运行中获得更多替代品或更长简介的条目,将自动跨越阈值并变得可被索引,无需任何人工干预。目前,80 个条目中有 18 个通过了筛选。这是真实的数据状态,而非目标。每晚的 ETL 会逐步升级条目;随着内容质量的提升,被筛选出的条目数量也会增加。
Why the gate lives in its own module (为什么门控需要独立模块)
saas.ts(主要数据访问代码所在处)导入了 @libsql/client 以查询 Turso。任何在值层面(value level)导入 saas.ts 的模块都会引入该依赖。Astro 的静态页面包无法包含仅限服务器端的数据库依赖,因此会导致构建失败。
解决方案是:curation.ts 仅从 saas.ts 导入类型:
import type { SaasEntry } from "./saas.ts";
TypeScript 会在编译时擦除类型导入。在运行时,curation.ts 没有外部依赖——它是一个纯计算模块,Astro 可以安全地将其包含在静态页面包中。saas.ts 保持仅限服务器端,仅在预期有数据库依赖的 getStaticPaths 中导入。这种“按依赖类型拆分”的模式在 Astro monorepo 中很常见。任何涉及运行时外部依赖的代码都放在服务器端;而两端都需要用到的纯逻辑则拥有自己的独立模块。
Four discovery surfaces gated on the same function (基于同一函数的四个发现入口)
一个页面被“隐藏”意味着同时发生四件事:
- noindex 元标签:
Base.astro会检查isCurated(entry),并为未通过筛选的条目添加<meta name="robots" content="noindex, nofollow">。 - 站点地图排除:
astro.config.mjs拥有一个应用相同阈值逻辑的站点地图过滤器。这是唯一尴尬的部分:astro.config.mjs无法从src/导入,因此阈值被重复定义了。我在这两处都加上了// KEEP IN SYNC: curation.ts的注释。如果只修改一处而不更新另一处,会导致站点地图与 noindex 标签冲突——即某些页面会被提交给 Google,同时又被声明为 noindex。 - RSS 订阅源:订阅源仅包含已筛选的条目。未筛选的页面不会作为新内容出现在订阅阅读器中。
- 内部导航:主页类别卡片、页脚类别链接、面包屑路径和“相关替代品”小部件全部通过
isCurated进行过滤。来自站外的直接链接仍然可以访问该页面,但在网站内进行自然浏览时,不会出现未筛选的条目。
The category layer (类别层)
类别遵循相同的逻辑。一个类别只有在至少有两个已筛选条目时(CATEGORY_MIN_CURATED = 2)才是可索引的。低于该阈值的类别仍然会生成页面——以保留指向类别 URL 的外部链接——但它们会被标记为 noindex,并从站点地图、主页和页脚导航中排除。目前,只有一个类别(customer-support)达到了阈值。这是数据的真实状态:网站覆盖面广,但在大多数类别中编辑深度不足。随着 ETL 运行和更多条目跨越筛选阈值,更多的类别将自动变得可索引。
What changes automatically (自动更新的内容)
该门控是确定性的,并在构建时根据实时数据库数据进行评估。当 foss-alternative-to-figma 获得第四个替代品,且 Claude Haiku 在下一次夜间运行中生成了 90 个字符的简介时,下一次 Astro 构建将自动将其包含在站点地图中,移除其 noindex 标签,并将其添加到相关的类别卡片和页脚链接中。唯一不会自动更新的是 astro.config.mjs 中的重复阈值。最终我会将这些常量提取到一个共享的 JSON 文件中,供 curation.ts 和 astro.config.mjs 同时读取,从而消除同步风险。目前,注释就是我的防线。
这是正在进行的为期 6 个月的实验的一部分,涉及运营三个 AI 策划的目录网站。文中的技术主张均为真实情况;本文由 AI 辅助撰写。