Skip to content

Commit dde6ecf

Browse files
authored
fix(compare): correct four stale counts and four wrong prices (#21)
Every comparison entry was audited against the vendor's own site, with the JavaScript-rendered pricing pages driven through a real browser rather than guessed at. Counts left over from when TablePro shipped 18 databases. Postico promised "Postgres plus 17 other databases" and named five engines "and 13 more"; HeidiSQL named five "plus 13 more". Both read as 18, on pages whose own comparison table said 25 two rows below. postico.description five named + 13 -> + 20 postico.verdict Postgres + 17 -> + 24 postico.ogPunchline Postgres + 17 -> + 24 heidisql five named + 13 -> + 20 Prices that contradicted their own entry. The OG card is generated from its own fields, so it drifts silently from the page it advertises — and nobody sees an OG image until it is already being shared. postico.ogCompetitorMetaHtml $40 -> $69 (entry said $69/$99 throughout) datagrip.ogCompetitorMetaHtml $100 -> $99 (entry said $99 throughout) Prices now stale against the vendor, both read off the rendered page: datagrip $9.90/mo -> $10.90/mo jetbrains.com/datagrip/buy navicat $1,299 perpetual -> $1,599 navicat.com/en/store/navicat-premium Postico's $69 personal (3 devices) and $99 per device commercial were checked against eggerapps.at and are correct as written. Beekeeper's $108/yr Indie is correct: their page renders $9, $14 and $18 per month. DBeaver's desktop tiers are NOT verified. Their buy page renders prices only behind an async quantity selector, and the figures that do render belong to Team Edition roles. "Free / from $99/yr PRO" is left exactly as it was rather than replaced with a guess. Two guards so this cannot rot back: - Relative counts must total 25. "plus N more" has to be re-derived every time the driver count moves and nobody remembers to, which is how four of them ended up describing an app that no longer exists. Scoped to the sentence, because Postico's verdict names Postgres twice and counting the whole string turns a correct 25 into a false 26. - Any price on an OG card must appear elsewhere in the same entry, which is exactly the check that $40 and $100 would have failed. The four affected OG images are regenerated.
1 parent bc795f1 commit dde6ecf

6 files changed

Lines changed: 99 additions & 13 deletions

File tree

public/og/compare/datagrip.png

-65 KB
Loading

public/og/compare/heidisql.png

-70.3 KB
Loading

public/og/compare/navicat.png

-63.4 KB
Loading

public/og/compare/postico.png

-72.1 KB
Loading

resources/data/comparisons.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,12 @@
300300
"name": "DataGrip",
301301
"tagline": "Free, Native DataGrip Alternative for Mac.",
302302
"description": "Native DataGrip alternative for Mac. Free, open source, fast. JetBrains-quality editor without the IntelliJ weight.",
303-
"longDescription": "DataGrip is JetBrains' database IDE, built on the IntelliJ platform in Java. It costs $99 the first year, $79 the second, $59 from year three; or $9.90 per month. TablePro is free, native macOS, and starts in 1 second. Same SQL editor power for daily work, without the IDE weight.",
303+
"longDescription": "DataGrip is JetBrains' database IDE, built on the IntelliJ platform in Java. It costs $99 the first year, $79 the second, $59 from year three; or $10.90 per month. TablePro is free, native macOS, and starts in 1 second. Same SQL editor power for daily work, without the IDE weight.",
304304
"rows": [
305305
{
306306
"label": "Price",
307307
"tablePro": "Free",
308-
"competitor": "$99/yr individual (first year), $9.90/mo"
308+
"competitor": "$99/yr individual (first year), $10.90/mo"
309309
},
310310
{
311311
"label": "Open Source",
@@ -364,7 +364,7 @@
364364
}
365365
],
366366
"prosTablePro": [
367-
"Free vs $99/yr (DataGrip Personal) or $9.90/mo",
367+
"Free vs $99/yr (DataGrip Personal) or $10.90/mo",
368368
"Native macOS, not Java",
369369
"1 second start vs ~10 seconds",
370370
"80 MB RAM vs 1-2 GB",
@@ -387,7 +387,7 @@
387387
"datagrip alternative mac"
388388
],
389389
"ogTableProMetaHtml": "<strong>$0 Forever</strong><br>SwiftUI · 80 MB RAM<br>One-click connect",
390-
"ogCompetitorMetaHtml": "<strong>$100/yr individual</strong><br>IntelliJ . 1-2 GB RAM<br>1.1 GB download",
390+
"ogCompetitorMetaHtml": "<strong>$99/yr individual</strong><br>IntelliJ . 1-2 GB RAM<br>1.1 GB download",
391391
"ogPunchline": "Free. Native. No JetBrains tax.",
392392
"ogStatValue": "10x",
393393
"ogStatLabel": "LESS RAM",
@@ -445,12 +445,12 @@
445445
"name": "Navicat",
446446
"tagline": "Free, Native Navicat Alternative for Mac.",
447447
"description": "Native Navicat alternative for Mac. Free, open source, fast. 25 databases, AI assistant, iOS app.",
448-
"longDescription": "Navicat Premium covers 7+ databases in one app. It costs $399 per year subscription or $1,299 for a perpetual license per user. TablePro covers 25 databases and is free, open source under AGPLv3. Native macOS, no per-database editions.",
448+
"longDescription": "Navicat Premium covers 7+ databases in one app. It costs $399 per year subscription or $1,599 for a perpetual license per user. TablePro covers 25 databases and is free, open source under AGPLv3. Native macOS, no per-database editions.",
449449
"rows": [
450450
{
451451
"label": "Price",
452452
"tablePro": "Free",
453-
"competitor": "$399/yr or $1,299 perpetual"
453+
"competitor": "$399/yr or $1,599 perpetual"
454454
},
455455
{
456456
"label": "Open Source",
@@ -514,7 +514,7 @@
514514
}
515515
],
516516
"prosTablePro": [
517-
"Free vs $399/yr or $1,299 perpetual",
517+
"Free vs $399/yr or $1,599 perpetual",
518518
"25 databases in one app",
519519
"Native macOS",
520520
"AI SQL assistant in the free tier",
@@ -538,7 +538,7 @@
538538
"navicat too expensive"
539539
],
540540
"ogTableProMetaHtml": "<strong>$0 Lifetime</strong><br>SwiftUI · open source<br>One app, every DB",
541-
"ogCompetitorMetaHtml": "<strong>$1,299+ bundle</strong><br>Per-DB licenses<br>Closed source",
541+
"ogCompetitorMetaHtml": "<strong>$1,599+ perpetual</strong><br>Per-DB licenses<br>Closed source",
542542
"ogPunchline": "Free vs $30/month. One app for every database.",
543543
"ogStatValue": "100%",
544544
"ogStatLabel": "CHEAPER",
@@ -906,7 +906,7 @@
906906
"slug": "postico",
907907
"name": "Postico",
908908
"tagline": "Multi-Database Postico Alternative for Mac.",
909-
"description": "Native, free, open-source Postico alternative for Mac. Postgres plus MySQL, MongoDB, Redis, SQLite, and 13 more.",
909+
"description": "Native, free, open-source Postico alternative for Mac. Postgres plus MySQL, MongoDB, Redis, SQLite, and 20 more.",
910910
"longDescription": "Postico 2 is a polished native Mac client for PostgreSQL, Redshift, CockroachDB, and Greenplum. It costs $69 for a personal license (3 devices) or $99 per device for commercial use. TablePro is free and open source under AGPLv3, and covers 25 databases beyond Postgres.",
911911
"rows": [
912912
{
@@ -1001,7 +1001,7 @@
10011001
],
10021002
"chooseTablePro": "you work with multiple databases or want AI and iOS support in the free tier.",
10031003
"chooseCompetitor": "Postgres, Redshift, CockroachDB, or Greenplum is your only database and you prefer paying once for a focused tool.",
1004-
"verdict": "Postico is excellent for Postgres alone. TablePro covers the same ground for Postgres plus 17 other databases, free.",
1004+
"verdict": "Postico is excellent for Postgres alone. TablePro covers the same ground for Postgres plus 24 other databases, free.",
10051005
"keywords": [
10061006
"postico alternative",
10071007
"tablepro vs postico",
@@ -1010,8 +1010,8 @@
10101010
"multi-database mac client"
10111011
],
10121012
"ogTableProMetaHtml": "<strong>FREE</strong> · 25 databases",
1013-
"ogCompetitorMetaHtml": "<strong>$40</strong> · Postgres only",
1014-
"ogPunchline": "Postgres plus 17 more. Free. Native.",
1013+
"ogCompetitorMetaHtml": "<strong>$69</strong> · Postgres only",
1014+
"ogPunchline": "Postgres plus 24 more. Free. Native.",
10151015
"ogStatValue": "25",
10161016
"ogStatLabel": "DATABASES",
10171017
"benchmarks": {
@@ -1229,7 +1229,7 @@
12291229
"name": "HeidiSQL",
12301230
"tagline": "Native HeidiSQL Alternative for Mac.",
12311231
"description": "Native HeidiSQL alternative for Mac. No Wine, no VM. Free and open source. 25 databases, AI assistant, iCloud sync.",
1232-
"longDescription": "HeidiSQL is a free GPL-2.0 database client built with Delphi for Windows. As of 2026 there is a separate Lazarus-based macOS preview build, but it remains a Windows-first app. TablePro is built natively for macOS in Swift and AppKit and covers MySQL, MariaDB, PostgreSQL, SQL Server, SQLite (the same databases HeidiSQL handles) plus 13 more.",
1232+
"longDescription": "HeidiSQL is a free GPL-2.0 database client built with Delphi for Windows. As of 2026 there is a separate Lazarus-based macOS preview build, but it remains a Windows-first app. TablePro is built natively for macOS in Swift and AppKit and covers MySQL, MariaDB, PostgreSQL, SQL Server, SQLite (the same databases HeidiSQL handles) plus 20 more.",
12331233
"rows": [
12341234
{
12351235
"label": "Price",

tests/Feature/Seo/StaleClaimsTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,89 @@
219219

220220
expect($routed)->toBe($authored);
221221
});
222+
223+
it('never implies a database count other than 25', function (): void {
224+
/*
225+
* Four entries carried counts from the era when TablePro shipped 18 —
226+
* Postico promised "Postgres plus 17 other databases" and named five
227+
* engines "and 13 more", HeidiSQL the same. Both read as 18, on pages whose
228+
* own table said 25 two rows below.
229+
*
230+
* Relative phrasing is what rots: "plus N more" has to be re-derived every
231+
* time the driver count moves, and nobody remembers to. This catches the
232+
* ones that no longer add up.
233+
*/
234+
$entries = json_decode(file_get_contents(base_path('resources/data/comparisons.json')), true);
235+
236+
$offenders = [];
237+
238+
foreach ($entries as $entry) {
239+
array_walk_recursive($entry, function ($value) use (&$offenders, $entry): void {
240+
if (! is_string($value)) {
241+
return;
242+
}
243+
244+
// "Postgres plus 24 other databases" / "... and 20 more"
245+
if (! preg_match('/\b(?:plus|and)\s+(\d{1,3})\s+(?:other|more)\b/i', $value, $m)) {
246+
return;
247+
}
248+
249+
/*
250+
* Only the engines named in the same sentence count. Postico's
251+
* verdict reads "excellent for Postgres alone. TablePro covers the
252+
* same ground for Postgres plus 24 other databases" — counting the
253+
* whole string sees Postgres twice and makes 25 look like 26.
254+
*/
255+
$before = substr($value, 0, strpos($value, $m[0]));
256+
$sentence = preg_split('/(?<=[.!?])\s+/', $before);
257+
258+
$named = preg_match_all(
259+
'/\b(PostgreSQL|Postgres|MySQL|MariaDB|MongoDB|Redis|SQLite|SQL Server|Redshift|CockroachDB|Greenplum|Snowflake|Cassandra|DuckDB|Oracle|ClickHouse)\b/i',
260+
(string) end($sentence),
261+
);
262+
263+
if ($named + (int) $m[1] !== 25) {
264+
$offenders[] = "{$entry['slug']}: \"{$m[0]}\" after {$named} named engines = " . ($named + (int) $m[1]);
265+
}
266+
});
267+
}
268+
269+
expect($offenders)->toBe([], "Database counts that do not total 25:\n " . implode("\n ", $offenders));
270+
});
271+
272+
it('quotes one price per competitor claim, consistently within an entry', function (): void {
273+
/*
274+
* The OG card is generated from its own fields, so it drifts silently from
275+
* the page it advertises: Postico's said $40 against $69/$99 in the entry,
276+
* DataGrip's said $100 against $99. Nobody sees an OG image until it is
277+
* already being shared.
278+
*/
279+
$entries = json_decode(file_get_contents(base_path('resources/data/comparisons.json')), true);
280+
281+
foreach ($entries as $entry) {
282+
if (! isset($entry['ogCompetitorMetaHtml'])) {
283+
continue;
284+
}
285+
286+
preg_match_all('/\$[\d,]+(?:\.\d{2})?/', $entry['ogCompetitorMetaHtml'], $onCard);
287+
288+
if ($onCard[0] === []) {
289+
continue;
290+
}
291+
292+
$inEntry = [];
293+
array_walk_recursive($entry, function ($value) use (&$inEntry): void {
294+
if (is_string($value)) {
295+
preg_match_all('/\$[\d,]+(?:\.\d{2})?/', $value, $found);
296+
$inEntry = array_merge($inEntry, $found[0]);
297+
}
298+
});
299+
300+
foreach ($onCard[0] as $price) {
301+
expect(array_count_values($inEntry)[$price] ?? 0)->toBeGreaterThan(
302+
1,
303+
"{$entry['slug']}: the OG card quotes {$price}, which appears nowhere else in the entry",
304+
);
305+
}
306+
}
307+
});

0 commit comments

Comments
 (0)