Skip to content

Commit b358bac

Browse files
authored
add multiple carousel pattern (#59184)
1 parent da3cdfc commit b358bac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+807
-463
lines changed

content/copilot/tutorials/index.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ layout: bespoke-landing
3939
sidebarLink:
4040
text: All tutorials
4141
href: /copilot/tutorials
42-
recommended:
43-
- /copilot/tutorials/copilot-chat-cookbook
44-
- /copilot/tutorials/customization-library
45-
- /copilot/tutorials/roll-out-at-scale
42+
carousels:
43+
recommended:
44+
- /copilot/tutorials/copilot-chat-cookbook
45+
- /copilot/tutorials/customization-library
46+
- /copilot/tutorials/roll-out-at-scale
4647
includedCategories:
4748
- Accelerate PR velocity
4849
- Automate simple user stories

data/reusables/contributing/content-linter-rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
| GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | error | tables, accessibility, formatting |
5858
| GHD051 | frontmatter-versions-whitespace | Versions frontmatter should not contain unnecessary whitespace | error | frontmatter, versions |
5959
| GHD054 | third-party-actions-reusable | Code examples with third-party actions must include disclaimer reusable | error | actions, reusable, third-party |
60-
| GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended |
60+
| GHD056 | frontmatter-landing-carousels | Only landing pages can have carousels, there should be no duplicate articles, and all articles must exist | error | frontmatter, landing, carousels |
6161
| GHD057 | ctas-schema | CTA URLs must conform to the schema | error | ctas, schema, urls |
6262
| GHD058 | journey-tracks-liquid | Journey track properties must use valid Liquid syntax | error | frontmatter, journey-tracks, liquid |
6363
| GHD059 | journey-tracks-guide-path-exists | Journey track guide paths must reference existing content files | error | frontmatter, journey-tracks |

data/ui.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@ cookbook_landing:
346346
category: Category
347347
complexity: Complexity
348348

349+
carousels:
350+
recommended: Recommended
351+
349352
not_found:
350353
title: Ooops!
351354
message: It looks like this page doesn't exist.

src/article-api/transformers/bespoke-landing-transformer.ts

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
1-
import type { Context, Page } from '@/types'
1+
import type { Context, Page, ResolvedArticle } from '@/types'
22
import type { PageTransformer, TemplateData, Section, LinkData } from './types'
33
import { renderContent } from '@/content-render/index'
44
import { loadTemplate } from '@/article-api/lib/load-template'
55
import { getAllTocItems, flattenTocItems } from '@/article-api/lib/get-all-toc-items'
66

7-
interface RecommendedItem {
8-
href: string
9-
title?: string
10-
intro?: string
11-
}
12-
137
interface BespokeLandingPage extends Omit<Page, 'featuredLinks'> {
148
featuredLinks?: Record<string, Array<string | { href: string; title: string; intro?: string }>>
159
children?: string[]
16-
recommended?: RecommendedItem[]
17-
rawRecommended?: string[]
10+
carousels?: Record<string, ResolvedArticle[]>
11+
rawCarousels?: Record<string, string[]>
1812
includedCategories?: string[]
1913
}
2014

2115
/**
2216
* Transforms bespoke-landing pages into markdown format.
23-
* Handles recommended carousel and full article listings.
17+
* Handles carousels and full article listings.
2418
* Note: Unlike discovery-landing, bespoke-landing shows ALL articles
2519
* regardless of includedCategories.
2620
*/
@@ -53,38 +47,48 @@ export class BespokeLandingTransformer implements PageTransformer {
5347
const bespokePage = page as BespokeLandingPage
5448
const sections: Section[] = []
5549

56-
// Recommended carousel
57-
const recommended = bespokePage.recommended ?? bespokePage.rawRecommended
58-
if (recommended && recommended.length > 0) {
50+
// Process carousels (each carousel becomes a section)
51+
const carousels = bespokePage.carousels ?? bespokePage.rawCarousels
52+
if (carousels && typeof carousels === 'object') {
5953
const { default: getLearningTrackLinkData } = await import(
6054
'@/learning-track/lib/get-link-data'
6155
)
6256

63-
let links: LinkData[]
64-
if (typeof recommended[0] === 'object' && 'title' in recommended[0]) {
65-
links = recommended.map((item) => ({
66-
href: typeof item === 'string' ? item : item.href,
67-
title: (typeof item === 'object' && item.title) || '',
68-
intro: (typeof item === 'object' && item.intro) || '',
69-
}))
70-
} else {
71-
const linkData = await getLearningTrackLinkData(recommended as string[], context, {
72-
title: true,
73-
intro: true,
74-
})
75-
links = (linkData || []).map((item: { href: string; title?: string; intro?: string }) => ({
76-
href: item.href,
77-
title: item.title || '',
78-
intro: item.intro || '',
79-
}))
80-
}
81-
82-
const validLinks = links.filter((l) => l.href && l.title)
83-
if (validLinks.length > 0) {
84-
sections.push({
85-
title: 'Recommended',
86-
groups: [{ title: null, links: validLinks }],
87-
})
57+
for (const [carouselKey, articles] of Object.entries(carousels)) {
58+
if (!Array.isArray(articles) || articles.length === 0) continue
59+
60+
let links: LinkData[]
61+
if (typeof articles[0] === 'object' && 'title' in articles[0]) {
62+
// Already resolved articles
63+
links = articles.map((item) => ({
64+
href: typeof item === 'string' ? item : item.href,
65+
title: (typeof item === 'object' && item.title) || '',
66+
intro: (typeof item === 'object' && item.intro) || '',
67+
}))
68+
} else {
69+
// Raw paths that need resolution
70+
const linkData = await getLearningTrackLinkData(articles as string[], context, {
71+
title: true,
72+
intro: true,
73+
})
74+
links = (linkData || []).map(
75+
(item: { href: string; title?: string; intro?: string }) => ({
76+
href: item.href,
77+
title: item.title || '',
78+
intro: item.intro || '',
79+
}),
80+
)
81+
}
82+
83+
const validLinks = links.filter((l) => l.href && l.title)
84+
if (validLinks.length > 0) {
85+
// Use carousel key as title (capitalize first letter)
86+
const sectionTitle = carouselKey.charAt(0).toUpperCase() + carouselKey.slice(1)
87+
sections.push({
88+
title: sectionTitle,
89+
groups: [{ title: null, links: validLinks }],
90+
})
91+
}
8892
}
8993
}
9094

src/article-api/transformers/discovery-landing-transformer.ts

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import type { Context, Page } from '@/types'
1+
import type { Context, Page, ResolvedArticle } from '@/types'
22
import type { PageTransformer, TemplateData, Section, LinkData } from './types'
33
import { renderContent } from '@/content-render/index'
44
import { loadTemplate } from '@/article-api/lib/load-template'
55
import { getAllTocItems, flattenTocItems } from '@/article-api/lib/get-all-toc-items'
66

7-
interface RecommendedItem {
8-
href: string
9-
title?: string
10-
intro?: string
11-
}
12-
137
interface DiscoveryPage extends Page {
148
rawIntroLinks?: Record<string, string>
159
introLinks?: Record<string, string>
16-
recommended?: RecommendedItem[]
17-
rawRecommended?: string[]
10+
carousels?: Record<string, ResolvedArticle[]>
11+
rawCarousels?: Record<string, string[]>
1812
includedCategories?: string[]
1913
children?: string[]
2014
}
@@ -53,38 +47,48 @@ export class DiscoveryLandingTransformer implements PageTransformer {
5347
const discoveryPage = page as DiscoveryPage
5448
const sections: Section[] = []
5549

56-
// Recommended carousel
57-
const recommended = discoveryPage.recommended ?? discoveryPage.rawRecommended
58-
if (recommended && recommended.length > 0) {
50+
// Process carousels (each carousel becomes a section)
51+
const carousels = discoveryPage.carousels ?? discoveryPage.rawCarousels
52+
if (carousels && typeof carousels === 'object') {
5953
const { default: getLearningTrackLinkData } = await import(
6054
'@/learning-track/lib/get-link-data'
6155
)
6256

63-
let links: LinkData[]
64-
if (typeof recommended[0] === 'object' && 'title' in recommended[0]) {
65-
links = recommended.map((item) => ({
66-
href: typeof item === 'string' ? item : item.href,
67-
title: (typeof item === 'object' && item.title) || '',
68-
intro: (typeof item === 'object' && item.intro) || '',
69-
}))
70-
} else {
71-
const linkData = await getLearningTrackLinkData(recommended as string[], context, {
72-
title: true,
73-
intro: true,
74-
})
75-
links = (linkData || []).map((item: { href: string; title?: string; intro?: string }) => ({
76-
href: item.href,
77-
title: item.title || '',
78-
intro: item.intro || '',
79-
}))
80-
}
57+
for (const [carouselKey, articles] of Object.entries(carousels)) {
58+
if (!Array.isArray(articles) || articles.length === 0) continue
59+
60+
let links: LinkData[]
61+
if (typeof articles[0] === 'object' && 'title' in articles[0]) {
62+
// Already resolved articles
63+
links = articles.map((item) => ({
64+
href: typeof item === 'string' ? item : item.href,
65+
title: (typeof item === 'object' && item.title) || '',
66+
intro: (typeof item === 'object' && item.intro) || '',
67+
}))
68+
} else {
69+
// Raw paths that need resolution
70+
const linkData = await getLearningTrackLinkData(articles as string[], context, {
71+
title: true,
72+
intro: true,
73+
})
74+
links = (linkData || []).map(
75+
(item: { href: string; title?: string; intro?: string }) => ({
76+
href: item.href,
77+
title: item.title || '',
78+
intro: item.intro || '',
79+
}),
80+
)
81+
}
8182

82-
const validLinks = links.filter((l) => l.href && l.title)
83-
if (validLinks.length > 0) {
84-
sections.push({
85-
title: 'Recommended',
86-
groups: [{ title: null, links: validLinks }],
87-
})
83+
const validLinks = links.filter((l) => l.href && l.title)
84+
if (validLinks.length > 0) {
85+
// Use carousel key as title (capitalize first letter)
86+
const sectionTitle = carouselKey.charAt(0).toUpperCase() + carouselKey.slice(1)
87+
sections.push({
88+
title: sectionTitle,
89+
groups: [{ title: null, links: validLinks }],
90+
})
91+
}
8892
}
8993
}
9094

src/article-api/transformers/product-landing-transformer.ts

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import type { Context, Page } from '@/types'
1+
import type { Context, Page, ResolvedArticle } from '@/types'
22
import type { PageTransformer, TemplateData, Section, LinkGroup, LinkData } from './types'
33
import { renderContent } from '@/content-render/index'
44
import { loadTemplate } from '@/article-api/lib/load-template'
55
import { resolvePath } from '@/article-api/lib/resolve-path'
66
import { getLinkData } from '@/article-api/lib/get-link-data'
77

8-
interface RecommendedItem {
9-
href: string
10-
title?: string
11-
intro?: string
12-
}
13-
148
interface ProductPage extends Omit<Page, 'featuredLinks'> {
159
featuredLinks?: Record<string, Array<string | { href: string; title: string; intro?: string }>>
1610
children?: string[]
17-
recommended?: RecommendedItem[]
18-
rawRecommended?: string[]
11+
carousels?: Record<string, ResolvedArticle[]>
12+
rawCarousels?: Record<string, string[]>
1913
includedCategories?: string[]
2014
}
2115

@@ -59,38 +53,48 @@ export class ProductLandingTransformer implements PageTransformer {
5953
const languageCode = page.languageCode || 'en'
6054
const sections: Section[] = []
6155

62-
// Recommended carousel
63-
const recommended = productPage.recommended ?? productPage.rawRecommended
64-
if (recommended && recommended.length > 0) {
56+
// Process carousels (each carousel becomes a section)
57+
const carousels = productPage.carousels ?? productPage.rawCarousels
58+
if (carousels && typeof carousels === 'object') {
6559
const { default: getLearningTrackLinkData } = await import(
6660
'@/learning-track/lib/get-link-data'
6761
)
6862

69-
let links: LinkData[]
70-
if (typeof recommended[0] === 'object' && 'title' in recommended[0]) {
71-
links = recommended.map((item) => ({
72-
href: typeof item === 'string' ? item : item.href,
73-
title: (typeof item === 'object' && item.title) || '',
74-
intro: (typeof item === 'object' && item.intro) || '',
75-
}))
76-
} else {
77-
const linkData = await getLearningTrackLinkData(recommended as string[], context, {
78-
title: true,
79-
intro: true,
80-
})
81-
links = (linkData || []).map((item: { href: string; title?: string; intro?: string }) => ({
82-
href: item.href,
83-
title: item.title || '',
84-
intro: item.intro || '',
85-
}))
86-
}
63+
for (const [carouselKey, articles] of Object.entries(carousels)) {
64+
if (!Array.isArray(articles) || articles.length === 0) continue
8765

88-
const validLinks = links.filter((l) => l.href && l.title)
89-
if (validLinks.length > 0) {
90-
sections.push({
91-
title: 'Recommended',
92-
groups: [{ title: null, links: validLinks }],
93-
})
66+
let links: LinkData[]
67+
if (typeof articles[0] === 'object' && 'title' in articles[0]) {
68+
// Already resolved articles
69+
links = articles.map((item) => ({
70+
href: typeof item === 'string' ? item : item.href,
71+
title: (typeof item === 'object' && item.title) || '',
72+
intro: (typeof item === 'object' && item.intro) || '',
73+
}))
74+
} else {
75+
// Raw paths that need resolution
76+
const linkData = await getLearningTrackLinkData(articles as string[], context, {
77+
title: true,
78+
intro: true,
79+
})
80+
links = (linkData || []).map(
81+
(item: { href: string; title?: string; intro?: string }) => ({
82+
href: item.href,
83+
title: item.title || '',
84+
intro: item.intro || '',
85+
}),
86+
)
87+
}
88+
89+
const validLinks = links.filter((l) => l.href && l.title)
90+
if (validLinks.length > 0) {
91+
// Use carousel key as title (capitalize first letter)
92+
const sectionTitle = carouselKey.charAt(0).toUpperCase() + carouselKey.slice(1)
93+
sections.push({
94+
title: sectionTitle,
95+
groups: [{ title: null, links: validLinks }],
96+
})
97+
}
9498
}
9599
}
96100

0 commit comments

Comments
 (0)