From 711d02c2eaf15d37cf5d1c1e0445729eefd73add Mon Sep 17 00:00:00 2001
From: Bissbert <43237892+Bissbert@users.noreply.github.com>
Date: Mon, 4 May 2026 18:17:08 +0700
Subject: [PATCH 3/8] fix(content): single source of truth for mineral counts
Three pages displayed three different (and all stale) mineral counts:
- / claimed "50+"
- /gallery claimed "90+"
- /404 claimed "94+"
The live database has 159 families. New src/lib/stats.ts queries
mineral_families and mineral_expressions at build time and exposes the
counts via getSiteStats(); the three pages now consume that helper.
Resolves P0-7 from the 2026-05-04 UX review.
---
src/lib/stats.ts | 38 +++++++++++++++++++++++++++++++++++
src/pages/404.astro | 5 ++++-
src/pages/gallery/index.astro | 5 ++++-
src/pages/index.astro | 7 +++++--
4 files changed, 51 insertions(+), 4 deletions(-)
create mode 100644 src/lib/stats.ts
diff --git a/src/lib/stats.ts b/src/lib/stats.ts
new file mode 100644
index 0000000..e1df828
--- /dev/null
+++ b/src/lib/stats.ts
@@ -0,0 +1,38 @@
+/**
+ * Build-time mineral / expression counts.
+ *
+ * Single source of truth for the number of mineral families and expressions
+ * surfaced in homepage / gallery / 404 copy. Reads directly from the SQLite
+ * database used for SSG via db-server.ts.
+ */
+import { getDB } from './db-server';
+
+export interface SiteStats {
+ /** Number of mineral families (e.g. Diamond, Ruby, Emerald). */
+ familyCount: number;
+ /** Number of distinct crystal expressions across all families. */
+ expressionCount: number;
+}
+
+let cached: SiteStats | null = null;
+
+export async function getSiteStats(): Promise {
+ if (cached) return cached;
+
+ const db = await getDB();
+
+ const familyResult = db.exec('SELECT COUNT(*) FROM mineral_families');
+ const familyCount =
+ familyResult.length > 0 && familyResult[0].values.length > 0
+ ? Number(familyResult[0].values[0][0])
+ : 0;
+
+ const exprResult = db.exec('SELECT COUNT(*) FROM mineral_expressions');
+ const expressionCount =
+ exprResult.length > 0 && exprResult[0].values.length > 0
+ ? Number(exprResult[0].values[0][0])
+ : 0;
+
+ cached = { familyCount, expressionCount };
+ return cached;
+}
diff --git a/src/pages/404.astro b/src/pages/404.astro
index d7f5fd4..ad7fb74 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -5,9 +5,12 @@
*/
import BaseLayout from '../layouts/BaseLayout.astro';
import { Container, Button, Card } from '../components/ui-astro';
+import { getSiteStats } from '../lib/stats';
+
+const { familyCount } = await getSiteStats();
const popularPages = [
- { title: 'Crystal Gallery', href: '/gallery', description: 'Explore 94+ mineral crystal structures' },
+ { title: 'Crystal Gallery', href: '/gallery', description: `Explore ${familyCount} mineral crystal structures` },
{ title: 'Learn Gemmology', href: '/learn', description: 'FGA curriculum and educational content' },
{ title: 'CDL Playground', href: '/playground', description: 'Interactive crystal visualization' },
{ title: 'Documentation', href: '/docs', description: 'API reference and guides' },
diff --git a/src/pages/gallery/index.astro b/src/pages/gallery/index.astro
index 2a016aa..aecf325 100644
--- a/src/pages/gallery/index.astro
+++ b/src/pages/gallery/index.astro
@@ -1,6 +1,7 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { ProtectedGallery } from '../../components/auth/ProtectedGallery';
+import { getSiteStats } from '../../lib/stats';
const url = Astro.url;
const initialSystem = url.searchParams.get('system') || '';
@@ -9,11 +10,13 @@ const initialOrigin = url.searchParams.get('origin') || '';
// Always point to base gallery URL as canonical to prevent duplicate content from filters
const canonicalURL = 'https://gemmology.dev/gallery';
+
+const { familyCount } = await getSiteStats();
---
= {
Ready to visualize crystals?
- Start with the interactive playground or explore our gallery of 50+ mineral presets.
+ Start with the interactive playground or explore our gallery of {familyCount} mineral families.