Skip to content

Commit bf0f54a

Browse files
authored
Merge pull request #42364 from github/repo-sync
Repo sync
2 parents d2dceda + fd67821 commit bf0f54a

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import { get } from '@/tests/helpers/e2etest'
4+
5+
const makeURL = (pathname: string): string =>
6+
`/api/article/body?${new URLSearchParams({ pathname })}`
7+
8+
describe('product guides transformer', () => {
9+
test('renders a product guides page with learning tracks', async () => {
10+
// Product guides pages use layout: product-guides
11+
// /en/codespaces/guides is a product guides page
12+
const res = await get(makeURL('/en/codespaces/guides'))
13+
expect(res.statusCode).toBe(200)
14+
expect(res.headers['content-type']).toContain('text/markdown')
15+
16+
// Should have Links section
17+
expect(res.body).toContain('## Links')
18+
})
19+
20+
test('includes guide cards if present', async () => {
21+
const res = await get(makeURL('/en/codespaces/guides'))
22+
expect(res.statusCode).toBe(200)
23+
24+
// If includeGuides are present, they should appear under Guides
25+
// The rendering depends on what's in the frontmatter
26+
expect(res.body).toMatch(/##|###/)
27+
})
28+
29+
test('includes learning tracks if present', async () => {
30+
const res = await get(makeURL('/en/codespaces/guides'))
31+
expect(res.statusCode).toBe(200)
32+
33+
// Learning tracks should be rendered as sections with their titles
34+
// The actual content depends on frontmatter configuration
35+
expect(res.body).toContain('## Links')
36+
})
37+
})

src/article-api/transformers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { GraphQLTransformer } from './graphql-transformer'
77
import { GithubAppsTransformer } from './github-apps-transformer'
88
import { WebhooksTransformer } from './webhooks-transformer'
99
import { TocTransformer } from './toc-transformer'
10+
import { ProductGuidesTransformer } from './product-guides-transformer'
1011
import { ProductLandingTransformer } from './product-landing-transformer'
1112

1213
/**
@@ -23,6 +24,7 @@ transformerRegistry.register(new GraphQLTransformer())
2324
transformerRegistry.register(new GithubAppsTransformer())
2425
transformerRegistry.register(new WebhooksTransformer())
2526
transformerRegistry.register(new TocTransformer())
27+
transformerRegistry.register(new ProductGuidesTransformer())
2628
transformerRegistry.register(new ProductLandingTransformer())
2729

2830
export { TransformerRegistry } from './types'
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { Context, Page } from '@/types'
2+
import type { PageTransformer, TemplateData, Section, LinkGroup, LinkData } from './types'
3+
import { renderContent } from '@/content-render/index'
4+
import { loadTemplate } from '@/article-api/lib/load-template'
5+
6+
interface ProcessedLink {
7+
href: string
8+
title?: string
9+
intro?: string
10+
}
11+
12+
interface LearningTrack {
13+
title: string
14+
guides: ProcessedLink[]
15+
}
16+
17+
/**
18+
* ProductGuidesPage extends Page with optional guide and learning track fields.
19+
* - includeGuides/rawIncludeGuides: Curated list of guide articles (processed objects vs raw paths)
20+
* - learningTracks/rawLearningTracks: Grouped tutorials (processed objects vs raw track IDs)
21+
*/
22+
interface ProductGuidesPage extends Page {
23+
includeGuides?: ProcessedLink[]
24+
rawIncludeGuides?: string[]
25+
learningTracks?: LearningTrack[]
26+
rawLearningTracks?: string[]
27+
}
28+
29+
/**
30+
* Transforms product-guides pages into markdown format.
31+
* Handles includeGuides (curated articles) and learningTracks (grouped tutorials).
32+
*/
33+
export class ProductGuidesTransformer implements PageTransformer {
34+
templateName = 'landing-page.template.md'
35+
36+
canTransform(page: Page): boolean {
37+
return page.layout === 'product-guides'
38+
}
39+
40+
async transform(page: Page, pathname: string, context: Context): Promise<string> {
41+
const templateData = await this.prepareTemplateData(page, pathname, context)
42+
const templateContent = loadTemplate(this.templateName)
43+
44+
return await renderContent(templateContent, {
45+
...context,
46+
...templateData,
47+
markdownRequested: true,
48+
})
49+
}
50+
51+
private async prepareTemplateData(
52+
page: Page,
53+
pathname: string,
54+
context: Context,
55+
): Promise<TemplateData> {
56+
const guidesPage = page as ProductGuidesPage
57+
const sections: Section[] = []
58+
const groups: LinkGroup[] = []
59+
60+
// Include guides
61+
const includeGuidesData = guidesPage.includeGuides ?? guidesPage.rawIncludeGuides
62+
if (includeGuidesData && includeGuidesData.length > 0) {
63+
const { default: getLinkData } = await import('@/learning-track/lib/get-link-data')
64+
65+
const isProcessed = typeof includeGuidesData[0] === 'object'
66+
67+
let processedLinks: ProcessedLink[]
68+
if (isProcessed) {
69+
processedLinks = includeGuidesData as ProcessedLink[]
70+
} else {
71+
processedLinks =
72+
(await getLinkData(includeGuidesData as string[], context, {
73+
title: true,
74+
intro: true,
75+
})) || []
76+
}
77+
78+
const links: LinkData[] = (processedLinks || []).map((item) => ({
79+
href: item.href,
80+
title: item.title || '',
81+
intro: item.intro || '',
82+
}))
83+
84+
const validLinks = links.filter((l) => l.href)
85+
if (validLinks.length > 0) {
86+
groups.push({ title: 'Guides', links: validLinks })
87+
}
88+
}
89+
90+
// Learning tracks
91+
const learningTracksData = guidesPage.learningTracks ?? guidesPage.rawLearningTracks
92+
if (learningTracksData && learningTracksData.length > 0) {
93+
let processedTracks: LearningTrack[]
94+
if (Array.isArray(guidesPage.learningTracks) && guidesPage.learningTracks.length > 0) {
95+
processedTracks = guidesPage.learningTracks
96+
} else {
97+
const { default: processLearningTracks } = await import(
98+
'@/learning-track/lib/process-learning-tracks'
99+
)
100+
const { learningTracks } = await processLearningTracks(
101+
learningTracksData as string[],
102+
context,
103+
)
104+
processedTracks = learningTracks
105+
}
106+
107+
for (const track of processedTracks) {
108+
if (!track.guides || !Array.isArray(track.guides)) continue
109+
110+
const links: LinkData[] = track.guides.map((guide) => ({
111+
href: guide.href,
112+
title: guide.title || '',
113+
intro: guide.intro || '',
114+
}))
115+
116+
if (links.length > 0) {
117+
groups.push({ title: track.title, links })
118+
}
119+
}
120+
}
121+
122+
if (groups.length > 0) {
123+
sections.push({
124+
title: 'Links',
125+
groups,
126+
})
127+
}
128+
129+
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
130+
const title = await page.renderTitle(context, { unwrap: true })
131+
132+
return {
133+
title,
134+
intro,
135+
sections,
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)