diff --git a/AGENTS.md b/AGENTS.md index 540b2ed4..3c547bef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,10 @@ Do not fabricate personal experiences, tests, expert credentials, prices, produc Affiliate disclosure must remain intact. +### AdSense content-value gate + +When the repository is under an AdSense low-value-content remediation, treat publisher value as a first-class acceptance gate. Editorial content must be the focal user value, not a wrapper around eBay inventory or affiliate conversion. Do not solve a low-value decision by mass-producing generic articles. Audit originality, evidence, repetition, factual risk, and commercial intent before publishing or expanding content. Keep AdSense blocks and sponsorship surfaces out of editorial content when they materially compete with the content's focal purpose. See `docs/ADSENSE_APPROVAL_GATE.md`. + ## SEO / AI visibility rules SEO work must preserve: diff --git a/__tests__/adsense/editorial-gate.test.ts b/__tests__/adsense/editorial-gate.test.ts new file mode 100644 index 00000000..fd314245 --- /dev/null +++ b/__tests__/adsense/editorial-gate.test.ts @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { blogArticles } from '../../lib/blog-data'; + +describe('AdSense editorial gate', () => { + it('keeps the public editorial corpus intentionally small and explicit', () => { + expect(blogArticles.filter((article) => article.indexable !== false).map((article) => article.id)) + .toEqual([31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 61]); + }); + + it('keeps the indexable corpus above the minimum content threshold', () => { + expect(blogArticles.filter((article) => article.indexable !== false).length).toBe(11); + }); + + it('does not expose duplicate article slugs', () => { + const slugs = blogArticles.map((article) => article.slug); + expect(new Set(slugs).size).toBe(slugs.length); + }); + + it('keeps selected guides free of fabricated first-person testing claims', () => { + const selected = blogArticles.filter((article) => article.indexable !== false); + const articleText = selected + .flatMap((article) => article.content) + .map((block) => [block.text ?? '', ...(block.items ?? [])].join(' ')) + .join(' '); + + expect(articleText).not.toMatch(/\b(I\s+(?:tested|personally|owned|used)|my\s+(?:testing|experience)|we\s+(?:tested|personally))\b/i); + }); + + it('keeps commercial ad/sponsorship components out of editorial and research pages', () => { + const routes = [ + 'app/page.tsx', + 'app/blog/[slug]/page.tsx', + 'app/blog/page.tsx', + 'app/categories/page.tsx', + 'app/tools/deal-comparison/page.tsx', + 'app/research/ebay-deal-comparison-methodology/page.tsx', + ]; + + for (const route of routes) { + const source = fs.readFileSync(path.join(process.cwd(), route), 'utf8'); + expect(source).not.toMatch(/AdSenseBlock|SponsorSlot/); + } + }); +}); diff --git a/__tests__/components/Footer.test.tsx b/__tests__/components/Footer.test.tsx index e1d583a9..7e7637d1 100644 --- a/__tests__/components/Footer.test.tsx +++ b/__tests__/components/Footer.test.tsx @@ -2,11 +2,13 @@ import { render, screen } from '@testing-library/react'; import Footer from '@/components/Footer'; describe('Footer', () => { - it('renders accessible footer content without a custom visitor counter', () => { + it('renders accessible footer content without advertising or placeholder social links', () => { render(