Skip to content

Commit 7594c76

Browse files
authored
Add support for defining spotlight in category landing pages (#56343)
1 parent 04f0efd commit 7594c76

File tree

6 files changed

+247
-33
lines changed

6 files changed

+247
-33
lines changed

content/copilot/tutorials/copilot-chat-cookbook/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ versions:
1010
topics:
1111
- Copilot
1212
layout: category-landing
13+
spotlight:
14+
- article: /testing-code/generate-unit-tests
15+
image: /assets/images/copilot-landing/generating_unit_tests.png
16+
- article: /refactoring-code/improving-code-readability-and-maintainability
17+
image: /assets/images/copilot-landing/improving_code_readability.png
18+
- article: /debugging-errors/debugging-invalid-json
19+
image: /assets/images/copilot-landing/debugging_invalid_json.png
1320
children:
1421
- /debugging-errors
1522
- /functionality-analysis-and-feature-suggestions
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
// Mock data to simulate tocItems and spotlight configurations
4+
const mockTocItems = [
5+
{
6+
title: 'Test Debug Article',
7+
intro: 'A test article for debugging functionality.',
8+
fullPath: '/en/category/debugging-errors/test-debug-article',
9+
},
10+
{
11+
title: 'Test Refactor Article',
12+
intro: 'A test article for refactoring functionality.',
13+
fullPath: '/en/category/refactoring-code/test-refactor-article',
14+
},
15+
{
16+
title: 'Test Unit Article',
17+
intro: 'A test article for unit testing functionality.',
18+
fullPath: '/en/category/testing-code/test-unit-article',
19+
},
20+
]
21+
22+
// Helper function to simulate the spotlight processing logic from CategoryLanding
23+
function processSpotlight(spotlight, tocItems) {
24+
const findArticleData = (articlePath) => {
25+
const cleanPath = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath
26+
return tocItems.find(
27+
(item) =>
28+
item.fullPath?.endsWith(cleanPath) ||
29+
item.fullPath?.includes(cleanPath.split('/').pop() || ''),
30+
)
31+
}
32+
33+
return (
34+
spotlight?.map((spotlightItem) => {
35+
const articleData = findArticleData(spotlightItem.article)
36+
return {
37+
article: spotlightItem.article,
38+
title: articleData?.title || 'Unknown Article',
39+
description: articleData?.intro || '',
40+
url: articleData?.fullPath || spotlightItem.article,
41+
image: spotlightItem.image,
42+
}
43+
}) || []
44+
)
45+
}
46+
47+
describe('spotlight processing logic', () => {
48+
test('processes spotlight object items correctly', () => {
49+
const spotlight = [
50+
{
51+
article: '/debugging-errors/test-debug-article',
52+
image: '/assets/images/test-debugging.png',
53+
},
54+
{
55+
article: '/refactoring-code/test-refactor-article',
56+
image: '/assets/images/test-refactoring.png',
57+
},
58+
]
59+
60+
const result = processSpotlight(spotlight, mockTocItems)
61+
62+
expect(result).toHaveLength(2)
63+
expect(result[0]).toEqual({
64+
article: '/debugging-errors/test-debug-article',
65+
title: 'Test Debug Article',
66+
description: 'A test article for debugging functionality.',
67+
url: '/en/category/debugging-errors/test-debug-article',
68+
image: '/assets/images/test-debugging.png',
69+
})
70+
expect(result[1]).toEqual({
71+
article: '/refactoring-code/test-refactor-article',
72+
title: 'Test Refactor Article',
73+
description: 'A test article for refactoring functionality.',
74+
url: '/en/category/refactoring-code/test-refactor-article',
75+
image: '/assets/images/test-refactoring.png',
76+
})
77+
})
78+
79+
test('processes multiple spotlight items with different images', () => {
80+
const spotlight = [
81+
{
82+
article: '/debugging-errors/test-debug-article',
83+
image: '/assets/images/debugging.png',
84+
},
85+
{
86+
article: '/refactoring-code/test-refactor-article',
87+
image: '/assets/images/refactoring.png',
88+
},
89+
{
90+
article: '/testing-code/test-unit-article',
91+
image: '/assets/images/testing.png',
92+
},
93+
]
94+
95+
const result = processSpotlight(spotlight, mockTocItems)
96+
97+
expect(result).toHaveLength(3)
98+
expect(result[0].image).toBe('/assets/images/debugging.png')
99+
expect(result[1].image).toBe('/assets/images/refactoring.png')
100+
expect(result[2].image).toBe('/assets/images/testing.png')
101+
expect(result[2].title).toBe('Test Unit Article')
102+
})
103+
104+
test('finds articles by filename when full path does not match', () => {
105+
const spotlight = [
106+
{
107+
article: 'test-debug-article',
108+
image: '/assets/images/debug.png',
109+
},
110+
]
111+
const result = processSpotlight(spotlight, mockTocItems)
112+
113+
expect(result[0].title).toBe('Test Debug Article')
114+
expect(result[0].url).toBe('/en/category/debugging-errors/test-debug-article')
115+
expect(result[0].image).toBe('/assets/images/debug.png')
116+
})
117+
118+
test('handles articles not found in tocItems', () => {
119+
const spotlight = [
120+
{
121+
article: '/completely/nonexistent/path',
122+
image: '/assets/images/missing1.png',
123+
},
124+
{
125+
article: '/another/totally-missing-article',
126+
image: '/assets/images/missing2.png',
127+
},
128+
]
129+
130+
const result = processSpotlight(spotlight, mockTocItems)
131+
132+
expect(result).toHaveLength(2)
133+
expect(result[0]).toEqual({
134+
article: '/completely/nonexistent/path',
135+
title: 'Unknown Article',
136+
description: '',
137+
url: '/completely/nonexistent/path',
138+
image: '/assets/images/missing1.png',
139+
})
140+
expect(result[1]).toEqual({
141+
article: '/another/totally-missing-article',
142+
title: 'Unknown Article',
143+
description: '',
144+
url: '/another/totally-missing-article',
145+
image: '/assets/images/missing2.png',
146+
})
147+
})
148+
149+
test('handles empty spotlight array', () => {
150+
const spotlight = []
151+
const result = processSpotlight(spotlight, mockTocItems)
152+
expect(result).toEqual([])
153+
})
154+
155+
test('handles undefined spotlight', () => {
156+
const result = processSpotlight(undefined, mockTocItems)
157+
expect(result).toEqual([])
158+
})
159+
})

src/frame/components/context/CategoryLandingContext.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LearningTrack } from './ArticleContext'
33
import { FeaturedLink, getFeaturedLinksFromReq } from '@/landings/components/ProductLandingContext'
44
import type { TocItem } from '@/landings/types'
55
import { mapRawTocItemToTocItem } from '@/landings/types'
6+
import type { SpotlightItem } from '@/types'
67

78
export type CategoryLandingContextT = {
89
title: string
@@ -15,6 +16,7 @@ export type CategoryLandingContextT = {
1516
renderedPage: string
1617
currentLearningTrack?: LearningTrack
1718
currentLayout: string
19+
spotlight?: SpotlightItem[]
1820
}
1921

2022
export const CategoryLandingContext = createContext<CategoryLandingContextT | null>(null)
@@ -45,5 +47,6 @@ export const getCategoryLandingContextFromRequest = (req: any): CategoryLandingC
4547
renderedPage: req.context.renderedPage,
4648
currentLearningTrack: req.context.currentLearningTrack,
4749
currentLayout: req.context.currentLayoutName,
50+
spotlight: req.context.page.spotlight,
4851
}
4952
}

src/frame/lib/frontmatter.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,26 @@ export const schema = {
291291
type: 'string',
292292
},
293293
// END category landing tags
294+
// Spotlight configuration for category landing pages
295+
spotlight: {
296+
type: 'array',
297+
items: {
298+
type: 'object',
299+
required: ['article', 'image'],
300+
properties: {
301+
article: {
302+
type: 'string',
303+
description: 'Path to the article to spotlight',
304+
},
305+
image: {
306+
type: 'string',
307+
description: 'Path to image for the spotlight card',
308+
},
309+
},
310+
additionalProperties: false,
311+
},
312+
description: 'Array of articles to feature in the spotlight section',
313+
},
294314
},
295315
}
296316

src/landings/components/CategoryLanding.tsx

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ArticleCardItems } from '@/landings/types'
1616
export const CategoryLanding = () => {
1717
const { t } = useTranslation('cookbook_landing')
1818
const router = useRouter()
19-
const { title, intro, tocItems } = useCategoryLandingContext()
19+
const { title, intro, tocItems, spotlight } = useCategoryLandingContext()
2020

2121
// tocItems contains directories and its children, we only want the child articles
2222
const onlyFlatItems: ArticleCardItems = tocItems.flatMap((item) => item.childTocItems || [])
@@ -71,6 +71,30 @@ export const CategoryLanding = () => {
7171
setSelectedCategory('All')
7272
setSelectedComplexity('All')
7373
}
74+
75+
// Helper function to find article data from tocItems
76+
const findArticleData = (articlePath: string) => {
77+
const cleanPath = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath
78+
return onlyFlatItems.find(
79+
(item) =>
80+
item.fullPath?.endsWith(cleanPath) ||
81+
item.fullPath?.includes(cleanPath.split('/').pop() || ''),
82+
)
83+
}
84+
85+
// Process spotlight items to get complete data
86+
const processedSpotlight =
87+
spotlight?.map((spotlightItem) => {
88+
const articleData = findArticleData(spotlightItem.article)
89+
return {
90+
article: spotlightItem.article,
91+
title: articleData?.title || 'Unknown Article',
92+
description: articleData?.intro || '',
93+
url: articleData?.fullPath || spotlightItem.article,
94+
image: spotlightItem.image,
95+
}
96+
}) || []
97+
7498
return (
7599
<DefaultLayout>
76100
{router.route === '/[versionId]/rest/[category]' && <RestRedirect />}
@@ -85,39 +109,34 @@ export const CategoryLanding = () => {
85109
<ArticleTitle>{title}</ArticleTitle>
86110
{intro && <Lead data-search="lead">{intro}</Lead>}
87111

88-
<h2 className="py-5">{t('spotlight')}</h2>
89-
<div className="d-md-flex d-sm-block col-md-12">
90-
<div className="col-md-4">
91-
<CookBookArticleCard
92-
image={'/assets/images/copilot-landing/generating_unit_tests.png'}
93-
title="Generate unit tests"
94-
description="Copilot Chat can help with generating unit tests for a function."
95-
spotlight={true}
96-
url="/copilot/copilot-chat-cookbook/testing-code/generate-unit-tests"
97-
tags={[]}
98-
/>
112+
{!spotlight || spotlight.length === 0 ? (
113+
<div className="p-4 border border-danger rounded">
114+
<h3 className="text-danger">Configuration Error</h3>
115+
<p>
116+
Category landing pages with <code>layout: category-landing</code> must define a{' '}
117+
<code>spotlight</code> property in the frontmatter. Each spotlight item requires both
118+
an <code>article</code> path and an <code>image</code> path.
119+
</p>
99120
</div>
100-
<div className="col-md-4">
101-
<CookBookArticleCard
102-
image={'/assets/images/copilot-landing/improving_code_readability.png'}
103-
title="Improving code readability and maintainability"
104-
description="Learn how to improve your code readability and maintainability."
105-
spotlight={true}
106-
url="/copilot/copilot-chat-cookbook/refactoring-code/improving-code-readability-and-maintainability"
107-
tags={[]}
108-
/>
109-
</div>
110-
<div className="col-md-4">
111-
<CookBookArticleCard
112-
image={'/assets/images/copilot-landing/debugging_invalid_json.png'}
113-
title="Debugging invalid JSON"
114-
description="Copilot can identify and resolve syntax errors or structural issues in JSON data."
115-
spotlight={true}
116-
tags={[]}
117-
url="/copilot/copilot-chat-cookbook/debugging-errors/debugging-invalid-json"
118-
/>
119-
</div>
120-
</div>
121+
) : (
122+
<>
123+
<h2 className="py-5">{t('spotlight')}</h2>
124+
<div className="d-md-flex d-sm-block col-md-12">
125+
{processedSpotlight.slice(0, 3).map((item) => (
126+
<div key={item.article} className="col-md-4">
127+
<CookBookArticleCard
128+
image={item.image}
129+
title={item.title}
130+
description={item.description}
131+
spotlight={true}
132+
url={item.url}
133+
tags={[]}
134+
/>
135+
</div>
136+
))}
137+
</div>
138+
</>
139+
)}
121140

122141
<div className="pt-8">
123142
<div className="py-5 border-bottom">

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export type PageFrontmatter = {
5555
defaultPlatform?: 'mac' | 'windows' | 'linux'
5656
defaultTool?: string
5757
childGroups?: ChildGroup[]
58+
spotlight?: SpotlightItem[]
5859
}
5960

6061
type FeaturedLinks = {
@@ -77,6 +78,11 @@ export type ChildGroup = {
7778
icon?: string
7879
}
7980

81+
export type SpotlightItem = {
82+
article: string
83+
image: string
84+
}
85+
8086
export type Product = {
8187
id: string
8288
name: string

0 commit comments

Comments
 (0)