diff --git a/app/src/pages/CategoryPage.jsx b/app/src/pages/CategoryPage.jsx
new file mode 100644
index 0000000..cf831dc
--- /dev/null
+++ b/app/src/pages/CategoryPage.jsx
@@ -0,0 +1,62 @@
+import { useState, useEffect } from 'react';
+import { TeknavData } from '../../teknav-data.js';
+import { useNav, EmptyState } from '../../teknav-ui.jsx';
+import { LazyDiagram } from '../../src/components/lazy-diagram.jsx';
+import { ArticleRow } from '../../teknav-articles.jsx';
+import { contentApi } from '../../src/lib/content-api.js';
+
+export function CategoryPage({ slug }) {
+ const { navigate } = useNav();
+ const [cat, setCat] = useState(() => TeknavData.categories.find(c => c.slug === slug));
+ const [articles, setArticles] = useState(() => contentApi.fallbackArticles().filter(a => a.category === slug));
+
+ useEffect(() => {
+ let cancelled = false;
+ setCat(TeknavData.categories.find(c => c.slug === slug));
+ setArticles(contentApi.fallbackArticles().filter(a => a.category === slug));
+ Promise.all([
+ contentApi.getCategory(slug).catch(() => null),
+ contentApi.listArticles({ category: slug, limit: 100 }).catch(() => null),
+ ]).then(([category, result]) => {
+ if (cancelled) return;
+ if (category) setCat({ ...(TeknavData.categories.find(c => c.slug === slug) ?? {}), ...category, id: category.slug });
+ if (result?.items) setArticles(result.items);
+ });
+ return () => { cancelled = true; };
+ }, [slug]);
+
+ if (!cat) return
{cat.description}
+