diff --git a/src/landings/components/CategoryLanding.tsx b/src/landings/components/CategoryLanding.tsx
index 7e62560830..e2d12bafeb 100644
--- a/src/landings/components/CategoryLanding.tsx
+++ b/src/landings/components/CategoryLanding.tsx
@@ -35,7 +35,10 @@ export const CategoryLanding = () => {
if (typeof value === 'string') {
return value.toLowerCase().includes(searchQuery.toLowerCase())
} else if (Array.isArray(value)) {
- return value.some((item) => item.toLowerCase().includes(searchQuery.toLowerCase()))
+ return value.some(
+ (item) =>
+ typeof item === 'string' && item.toLowerCase().includes(searchQuery.toLowerCase()),
+ )
}
return false
})
diff --git a/src/landings/components/bespoke/BespokeLanding.tsx b/src/landings/components/bespoke/BespokeLanding.tsx
index cd97570b5b..5dc38b3589 100644
--- a/src/landings/components/bespoke/BespokeLanding.tsx
+++ b/src/landings/components/bespoke/BespokeLanding.tsx
@@ -1,5 +1,3 @@
-import { useMemo } from 'react'
-
import { DefaultLayout } from '@/frame/components/DefaultLayout'
import { useLandingContext } from '@/landings/context/LandingContext'
import { LandingHero } from '@/landings/components/shared/LandingHero'
@@ -7,16 +5,9 @@ import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWith
import { UtmPreserver } from '@/frame/components/UtmPreserver'
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
-import type { ArticleCardItems } from '@/landings/types'
-
export const BespokeLanding = () => {
const { title, intro, heroImage, introLinks, tocItems, recommended } = useLandingContext()
- const flatArticles: ArticleCardItems = useMemo(
- () => tocItems.flatMap((item) => item.childTocItems || []),
- [tocItems],
- )
-
return (
@@ -25,7 +16,7 @@ export const BespokeLanding = () => {
diff --git a/src/landings/components/discovery/DiscoveryLanding.tsx b/src/landings/components/discovery/DiscoveryLanding.tsx
index 105b671b81..c8fe5a2d59 100644
--- a/src/landings/components/discovery/DiscoveryLanding.tsx
+++ b/src/landings/components/discovery/DiscoveryLanding.tsx
@@ -1,5 +1,3 @@
-import { useMemo } from 'react'
-
import { DefaultLayout } from '@/frame/components/DefaultLayout'
import { useLandingContext } from '@/landings/context/LandingContext'
import { LandingHero } from '@/landings/components/shared/LandingHero'
@@ -7,16 +5,9 @@ import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWith
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
import { UtmPreserver } from '@/frame/components/UtmPreserver'
-import type { ArticleCardItems } from '@/landings/types'
-
export const DiscoveryLanding = () => {
const { title, intro, heroImage, introLinks, tocItems, recommended } = useLandingContext()
- const flatArticles: ArticleCardItems = useMemo(
- () => tocItems.flatMap((item) => item.childTocItems || []),
- [tocItems],
- )
-
return (
@@ -24,7 +15,7 @@ export const DiscoveryLanding = () => {
diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx
index abc3d20205..841f86bd81 100644
--- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx
+++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx
@@ -1,20 +1,44 @@
-import { useState, useRef, useEffect } from 'react'
+import { useState, useRef, useEffect, useMemo } from 'react'
import { TextInput, ActionMenu, ActionList, Token, Pagination } from '@primer/react'
import { SearchIcon } from '@primer/octicons-react'
import cx from 'classnames'
import { Link } from '@/frame/components/Link'
import { useTranslation } from '@/languages/components/useTranslation'
-import { ArticleCardItems, ChildTocItem } from '@/landings/types'
+import { ArticleCardItems, ChildTocItem, TocItem } from '@/landings/types'
import styles from './LandingArticleGridWithFilter.module.scss'
type ArticleGridProps = {
- flatArticles: ArticleCardItems
+ tocItems: TocItem[]
}
const ALL_CATEGORIES = 'all_categories'
+// Helper function to recursively flatten nested articles
+// Excludes index pages (pages with childTocItems)
+const flattenArticlesRecursive = (articles: ArticleCardItems): ArticleCardItems => {
+ const flattened: ArticleCardItems = []
+
+ for (const article of articles) {
+ // If the article has children, recursively process them but don't include the parent (index page)
+ if (article.childTocItems && article.childTocItems.length > 0) {
+ flattened.push(...flattenArticlesRecursive(article.childTocItems))
+ } else {
+ // Only add articles that don't have children (actual article pages, not index pages)
+ flattened.push(article)
+ }
+ }
+
+ return flattened
+}
+
+// Wrapper function that flattens and sorts alphabetically by title (only once)
+const flattenArticles = (articles: ArticleCardItems): ArticleCardItems => {
+ const flattened = flattenArticlesRecursive(articles)
+ return flattened.sort((a, b) => a.title.localeCompare(b.title))
+}
+
// Hook to get current articles per page based on screen size
const useResponsiveArticlesPerPage = () => {
const [articlesPerPage, setArticlesPerPage] = useState(9) // Default to desktop
@@ -42,7 +66,7 @@ const useResponsiveArticlesPerPage = () => {
return articlesPerPage
}
-export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
+export const ArticleGrid = ({ tocItems }: ArticleGridProps) => {
const { t } = useTranslation('product_landing')
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORIES)
@@ -53,6 +77,12 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
const inputRef = useRef(null)
const headingRef = useRef(null)
+ // Extract child items from tocItems and recursively flatten nested articles to ensure we get all articles with categories
+ const allArticles = useMemo(
+ () => flattenArticles(tocItems.flatMap((item) => item.childTocItems || [])),
+ [tocItems],
+ )
+
// Reset to first page when articlesPerPage changes (screen size changes)
useEffect(() => {
setCurrentPage(1)
@@ -61,13 +91,13 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
// Extract unique categories from the articles
const categories: string[] = [
ALL_CATEGORIES,
- ...Array.from(new Set(flatArticles.flatMap((item) => item.category || []))).sort((a, b) =>
+ ...Array.from(new Set(allArticles.flatMap((item) => item.category || []))).sort((a, b) =>
a.localeCompare(b),
),
]
const applyFilters = () => {
- let results = flatArticles
+ let results = allArticles
if (searchQuery) {
results = results.filter((token) => {
diff --git a/src/landings/types.ts b/src/landings/types.ts
index e468cc8911..f6e0ddcfb7 100644
--- a/src/landings/types.ts
+++ b/src/landings/types.ts
@@ -12,11 +12,13 @@ export type BaseTocItem = {
}
// Extended type for child TOC items with additional metadata
+// This is recursive - children can also have their own children
export type ChildTocItem = BaseTocItem & {
octicon?: ValidOcticon | null
category?: string[] | null
complexity?: string[] | null
industry?: string[] | null
+ childTocItems?: ChildTocItem[]
}
// Main TOC item type that can contain children