feat(hub): landing page with product cards (hides empty products)

This commit is contained in:
2026-05-17 10:27:50 -07:00
parent d4970ef408
commit 84e1318b33

184
src/pages/index.astro Normal file
View File

@@ -0,0 +1,184 @@
---
import { getCollection } from 'astro:content';
import '~/styles/anhh-tokens.css';
import '@fontsource-variable/inter';
interface Product {
slug: string;
title: string;
blurb: string;
href: string;
count: number;
}
// Hardcoded product metadata. Adding a new product = adding to this map.
const PRODUCT_META: Record<string, { title: string; blurb: string; firstSection: string }> = {
whp: {
title: 'WHP',
blurb: 'Our hosting control panel. Add domains, create sites, manage email, set up backups.',
firstSection: 'getting-started/welcome',
},
wordpress: {
title: 'WordPress',
blurb: 'Tips and tricks for getting the most out of WordPress on WHP.',
firstSection: 'index',
},
'email-clients': {
title: 'Email clients',
blurb: 'Configure Outlook, Apple Mail, Thunderbird, and mobile clients.',
firstSection: 'index',
},
};
const docs = await getCollection('docs');
// Group articles by top-level product folder
const byProduct = new Map<string, number>();
for (const entry of docs) {
const product = entry.id.split('/')[0];
byProduct.set(product, (byProduct.get(product) ?? 0) + 1);
}
const visibleProducts: Product[] = Array.from(byProduct.entries())
.filter(([slug]) => PRODUCT_META[slug] !== undefined)
.filter(([, count]) => count > 0)
.map(([slug, count]) => ({
slug,
title: PRODUCT_META[slug].title,
blurb: PRODUCT_META[slug].blurb,
href: `/${slug}/${PRODUCT_META[slug].firstSection}/`,
count,
}))
.sort((a, b) => a.title.localeCompare(b.title));
---
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>An Honest Host Knowledge Base</title>
<meta
name="description"
content="Customer documentation for WHP and other An Honest Host services."
/>
<link rel="canonical" href="https://kb.anhonesthost.com/" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<style>
body {
background: var(--anhh-bg, #0a1628);
color: var(--anhh-text-primary, #f0f4ff);
font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header,
footer {
padding: 1.5rem 2rem;
}
header {
border-bottom: 1px solid var(--anhh-border-color, #334155);
}
footer {
border-top: 1px solid var(--anhh-border-color, #334155);
font-size: 0.875rem;
color: var(--anhh-text-secondary, #94a3b8);
}
main {
flex: 1;
max-width: 64rem;
margin: 0 auto;
padding: 4rem 2rem;
width: 100%;
box-sizing: border-box;
}
h1 {
font-size: 2.5rem;
margin: 0 0 0.5rem;
}
.lede {
color: var(--anhh-text-secondary, #94a3b8);
font-size: 1.125rem;
margin: 0 0 3rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}
a.card {
display: block;
background: var(--anhh-bg-surface, #1e293b);
border: 1px solid var(--anhh-border-color, #334155);
border-radius: 0.75rem;
padding: 1.5rem;
text-decoration: none;
color: inherit;
transition:
border-color 120ms ease,
transform 120ms ease;
}
a.card:hover,
a.card:focus {
border-color: var(--anhh-accent, #00d4aa);
transform: translateY(-2px);
outline: none;
}
a.card h2 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
color: var(--anhh-accent, #00d4aa);
}
a.card p {
margin: 0 0 0.75rem;
color: var(--anhh-text-secondary, #94a3b8);
}
a.card .count {
font-size: 0.875rem;
color: var(--anhh-text-muted, #64748b);
}
.empty {
background: var(--anhh-bg-surface, #1e293b);
border: 1px dashed var(--anhh-border-color, #334155);
border-radius: 0.75rem;
padding: 2rem;
color: var(--anhh-text-muted, #64748b);
text-align: center;
}
footer a {
color: var(--anhh-accent, #00d4aa);
}
</style>
</head>
<body>
<header><strong>An Honest Host</strong> · Knowledge Base</header>
<main>
<h1>Knowledge Base</h1>
<p class="lede">Pick a product to get started.</p>
{
visibleProducts.length === 0 ? (
<div class="empty">No documentation has been published yet.</div>
) : (
<div class="grid">
{visibleProducts.map((p) => (
<a class="card" href={p.href}>
<h2>{p.title}</h2>
<p>{p.blurb}</p>
<span class="count">
{p.count} {p.count === 1 ? 'article' : 'articles'}
</span>
</a>
))}
</div>
)
}
</main>
<footer>
Need help?{' '}
<a href="https://secure.anhonesthost.com/submitticket.php">Open a support ticket</a>. ·{' '}
<a href="https://repo.anhonesthost.net/cloud-hosting-platform/kb-anhonesthost">Repo on Gitea</a>
</footer>
</body>
</html>