Compare-cluster audit + registry
Created 2026-07-19. Which HiveJournal products should have a competitor "vs"
SEO cluster (/compare/<product> + /compare/<product>/<competitor>, honesty-first),
and the registry service + quarterly cron that keeps them fresh.
Prompted by QuickSites building the same pattern (their vs pages mirror ours). This
doc is the analysis; services/compare-registry.ts
is the machine-readable source of truth (one entry per product) that the cron reads.
The pattern (what a cluster is)
A lib/<product>-compare.ts file holds a competitor array (slug, name, oneLiner,
pricing, freeTier, strengths, tradeoffs, pickThemIf, pickUsIf, sources) +
a PRICES_VERIFIED date. Routes /compare/<product> (hub) + /compare/<product>/<slug>
(per competitor) render it; sitemap.ts imports
the slug array so a new competitor is auto-indexed. Honesty-first is the brand rule:
every competitor gets a fair one-liner + real strengths, and we only claim the trade-off
where it genuinely runs our way. Two clusters exist today (Family Wall, journaling).
Shared SEO enrichment (2026-07-19) — both clusters are set up identically via
lib/compare-seo.tsx: a canonical tag on
every hub + per-competitor page, BreadcrumbList JSON-LD (Home → Compare → cluster →
competitor), and an auto-generated OG share card per page (opengraph-image.tsx, one
design parameterized per cluster palette, pre-rendered statically per slug). Adding a new
cluster → reuse the same three helpers so it launches at parity; a new competitor needs
nothing extra (its OG image + breadcrumb + canonical all derive from the slug).
Audit findings (2026-07-19)
LIVE — keep fresh
| Product | Cluster | Covers | Expansion candidates |
|---|---|---|---|
| Family Wall | /compare/family-wall | Skylight, Hearth, Cozi | Google/Nest Hub, Echo Show, FamilyWall app, Jam |
| Journaling | /compare/journaling (+ /best-journaling-apps-2026) | Rosebud, Day One, Stoic | Reflectly, Journey, Finch, Daylio |
CANDIDATES — real named competitors + a public surface; build a cluster
| Product | Competitors | Why / notes |
|---|---|---|
| EmberKiln / Graphene story studio ⭐ | Novelcrafter, Sudowrite, World Anvil, Campfire, Novarrium (+ LivingWriter, Scrivener, Plottr, Dabble…) | Highest-value, lowest-effort gap. The research is already written + weekly-refreshed in COMPETITIVE_LANDSCAPE_STORY_SOFTWARE.md — scorecards, pricing, positioning. Angle: "one canon → five formats (audiobook/screenplay/video/print/reader)", a category no competitor spans. Build lib/story-compare.ts mirroring the two live libs. |
| Workout Window (accountability) | StickK, Habitica, Focusmate, Strava clubs, 75 Hard apps | Beta; coaching skin (John Rowley). Defensible. |
| write.cafe | 750 Words, NaNoWriMo, Reedsy Prompts, writing-sprint tools | Daily writing ritual + weekly contest. Viable. |
| JQ Forms | Jotform, Google Forms, Typeform, DocuSign | Niche but viable — form creation + on-device autofill. |
SKIP — deliberately NO public compare page
Kept in the registry (status skip) so the "why not" is documented + the audit stays exhaustive.
- Sensitive / counsel-gated (never compare these): Family Photo Frame (minors, COPPA/BIPA/GDPR), Legacy Channel / eldercare voice (memorial voice cloning; ELVIS-Act/AB-1836/BIPA), Speech Mirror (clinical-adjacent, "a mirror, not a therapist"), Throughline / for-therapists (clinical/consent). Minor-facing education front doors stay informational, never "vs."
- Category-of-one / niche (a vs-page would be forced): Odessa (journal → graphic novel), Downstream (procrastination-curve framing), Looking Glass (weak direct competition). DreamPro/Open-Energy citizen science competes with institutions (Zooniverse), not SaaS — no shopper intent.
The registry service + quarterly cron
services/compare-registry.ts holds
COMPARE_REGISTRY (the table above, machine-readable) and two functions:
auditCompareRegistry(now?)(pure, unit-tested) → classifies every entry: a live cluster is stale when itsPRICES_VERIFIEDdate is >90 days old (or unparseable — fail toward refreshing); a candidate with no cluster is a gap.runCompareRegistryAuditTick()→ the quarterly job. Files aproduct_tasksrefresh task per stale cluster + a build task per gap (idempotent — one open task per product, category "Compare clusters" ⚖️). It does NOT rewrite competitor claims — pricing/features change and every claim is human-verified honesty-first, so it prompts a refresh rather than fabricating one (mirrors the auto-demo "generate into a review queue, never auto-publish" rule).
Cadence mechanics: the cron (index.ts compare_registry_audit)
fires daily but only does work once ≥90 days have elapsed since the last audit (timestamp in
site_settings.compare_registry_last_audit). A 90-day setInterval would never survive a process
restart; the daily-fire + due-check does. OFF by default (kill switch at
/dashboard/admin/crons, key
compare_registry_audit; heartbeat compare_registry_audit_tick). Super-admin can view the live
audit or force a run: GET /api/compare-registry/audit, POST /api/compare-registry/run.
How to add or refresh a cluster
- Refresh (a "Refresh compare cluster: X" task fired): re-verify each competitor's
pricing/features/free-tier, update
lib/<x>-compare.ts, and bumpPRICES_VERIFIEDthere AND the matchingpricesVerifiedincompare-registry.ts. Mark the task done. - Build (a "Build compare cluster: X" task fired): create
lib/<x>-compare.tsmirroring the two live libs, add the/compare/<x>+/compare/<x>/[competitor]routes, wire the slug array intositemap.ts, then flip the registry entry fromcandidate→livewith itsclusterPath,libFile, andpricesVerified. - New product: add one
CompareProductentry (statuscandidate/skipwith a rationale).