Skip to content

Commit d9624bc

Browse files
datlechinclaude
andauthored
fix(seo): stop publishing a rating nobody gave, on 26 pages (#11)
DatabaseClient.tsx derived an aggregateRating from the GitHub star count — a constant ratingValue of 4.9 with ratingCount set to the stars — and shipped it on every one of the 26 database pages. Compare.tsx already refuses to do this, and says why in a docblock. There is even a test named "never publishes a rating nobody gave". Its file list was Home.tsx and Compare.tsx, so it passed for as long as the violation shipped. The guard now enumerates the pages that emit a SoftwareApplication node from the filesystem and fails when that set grows, rather than trusting a list somebody has to remember to update. Running it that way immediately turned up Download.tsx, a fourth emitter that had never been covered either. Two smaller repairs to the same test: - It matched only the quoted 'aggregateRating'. The property-assignment form this bug actually used, data.aggregateRating = {...}, was never matched. - Matching the bare word made it fail on the docblocks that explain why the rating is absent, so comments are stripped before the needles run. A structured-data manual action for fabricated reviews applies to the whole site, so this does not wait behind the landing-page work. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c964396 commit d9624bc

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

resources/js/pages/DatabaseClient.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ interface Props {
1919
githubStars?: number | null;
2020
}
2121

22-
function buildAppJsonLd(name: string, description: string, slug: string, stars?: number | null): object {
22+
/**
23+
* No `aggregateRating`. This page used to derive one from the GitHub star count
24+
* — a constant 4.9 with `ratingCount` set to the stars — on all 26 database
25+
* pages. A star is not a review, so that published a rating nobody gave, and
26+
* `Compare.tsx` already refuses to do it for the same reason. The guard in
27+
* `StaleClaimsTest` was written to catch exactly this and did not list this
28+
* file; it does now.
29+
*/
30+
function buildAppJsonLd(name: string, description: string, slug: string): object {
2331
const data: Record<string, unknown> = {
2432
'@context': 'https://schema.org',
2533
'@type': 'SoftwareApplication',
@@ -36,16 +44,6 @@ function buildAppJsonLd(name: string, description: string, slug: string, stars?:
3644
downloadUrl: 'https://tablepro.app/download',
3745
};
3846

39-
if (stars && stars > 0) {
40-
data.aggregateRating = {
41-
'@type': 'AggregateRating',
42-
ratingValue: '4.9',
43-
ratingCount: stars,
44-
bestRating: '5',
45-
worstRating: '1',
46-
};
47-
}
48-
4947
return data;
5048
}
5149

@@ -55,7 +53,7 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop
5553
if (!db) return null;
5654

5755
const title = `${db.name} GUI Client for Mac - Free & Native | TablePro`;
58-
const jsonLd: object[] = [buildAppJsonLd(db.name, db.description, db.slug, githubStars)];
56+
const jsonLd: object[] = [buildAppJsonLd(db.name, db.description, db.slug)];
5957

6058
if (db.faqs && db.faqs.length > 0) {
6159
jsonLd.push({

tests/Feature/Seo/StaleClaimsTest.php

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,51 @@
6060
});
6161

6262
it('never publishes a rating nobody gave', function () use ($readSource): void {
63-
foreach (['resources/js/pages/Home.tsx', 'resources/js/pages/Compare.tsx'] as $source) {
64-
$contents = $readSource($source);
63+
/*
64+
* Every page that emits a SoftwareApplication node, not just the two that
65+
* were listed. DatabaseClient.tsx was absent for as long as it shipped
66+
* `ratingValue: '4.9'` with `ratingCount` set to the GitHub star count, so
67+
* this test passed while 26 indexed URLs published a fabricated rating.
68+
*
69+
* The list is asserted against the filesystem rather than hardcoded alone:
70+
* a new page that builds a SoftwareApplication must be added here, and the
71+
* count below is what forces that.
72+
*/
73+
$sources = [
74+
'resources/js/pages/Home.tsx',
75+
'resources/js/pages/Compare.tsx',
76+
'resources/js/pages/DatabaseClient.tsx',
77+
'resources/js/pages/Download.tsx',
78+
];
6579

66-
// A rating derived from star counts, and a constant score applied to
67-
// every competitor, are both inventions. Neither may come back.
68-
expect($contents)
69-
->not->toContain("'aggregateRating'")
80+
$emitters = array_values(array_filter(
81+
glob(base_path('resources/js/pages/*.tsx')),
82+
static fn(string $path): bool => str_contains(file_get_contents($path), "'SoftwareApplication'"),
83+
));
84+
85+
expect($emitters)->toHaveCount(
86+
count($sources),
87+
'A page started emitting a SoftwareApplication node. Add it to $sources above.',
88+
);
89+
90+
foreach ($sources as $source) {
91+
/*
92+
* Comments stripped first. Two of these files carry a docblock saying
93+
* why they do not publish a rating, and a bare `aggregateRating` needle
94+
* matches the explanation as readily as the offence.
95+
*/
96+
$code = preg_replace(['#/\*[\s\S]*?\*/#', '#//.*$#m'], '', $readSource($source));
97+
98+
/*
99+
* A rating derived from star counts, and a constant score applied to
100+
* every competitor, are both inventions. Neither may come back.
101+
*
102+
* `aggregateRating` is matched unquoted because the property-assignment
103+
* form — `data.aggregateRating = {...}` — is how it actually shipped,
104+
* and the quoted needle this test used to carry never matched it.
105+
*/
106+
expect($code)
107+
->not->toContain('aggregateRating')
70108
->not->toContain('AggregateRating')
71109
->not->toContain('ratingValue');
72110
}

0 commit comments

Comments
 (0)