Skip to content

Commit 9716088

Browse files
authored
feat: add journey landing transformer for Article API (#59076)
1 parent 8a6fee3 commit 9716088

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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('journey landing transformer', () => {
9+
test('renders a journey landing page in markdown', async () => {
10+
// /en/get-started/test-journey is a journey landing page in the fixtures
11+
const res = await get(makeURL('/en/get-started/test-journey'))
12+
expect(res.statusCode).toBe(200)
13+
expect(res.headers['content-type']).toContain('text/markdown')
14+
15+
// Check for journey tracks (now under Links section with track title as h3)
16+
expect(res.body).toContain('## Links')
17+
expect(res.body).toContain('### First Track')
18+
expect(res.body).toContain('* [Hello World](/en/get-started/start-your-journey/hello-world)')
19+
})
20+
})

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 { JourneyLandingTransformer } from './journey-landing-transformer'
1011
import { CategoryLandingTransformer } from './category-landing-transformer'
1112
import { DiscoveryLandingTransformer } from './discovery-landing-transformer'
1213
import { ProductGuidesTransformer } from './product-guides-transformer'
@@ -26,6 +27,7 @@ transformerRegistry.register(new GraphQLTransformer())
2627
transformerRegistry.register(new GithubAppsTransformer())
2728
transformerRegistry.register(new WebhooksTransformer())
2829
transformerRegistry.register(new TocTransformer())
30+
transformerRegistry.register(new JourneyLandingTransformer())
2931
transformerRegistry.register(new CategoryLandingTransformer())
3032
transformerRegistry.register(new DiscoveryLandingTransformer())
3133
transformerRegistry.register(new ProductGuidesTransformer())
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { Context, Page } from '@/types'
2+
import type { PageTransformer, TemplateData, Section, LinkData, LinkGroup } from './types'
3+
import { renderContent } from '@/content-render/index'
4+
import { loadTemplate } from '@/article-api/lib/load-template'
5+
import { resolvePath } from '@/article-api/lib/resolve-path'
6+
import { getLinkData } from '@/article-api/lib/get-link-data'
7+
8+
interface JourneyGuide {
9+
href: string
10+
alternativeNextStep?: string
11+
}
12+
13+
interface JourneyTrack {
14+
id: string
15+
title: string
16+
description: string
17+
guides: JourneyGuide[]
18+
}
19+
20+
interface JourneyPage extends Page {
21+
journeyTracks?: JourneyTrack[]
22+
children?: string[]
23+
}
24+
25+
/**
26+
* Transforms journey-landing pages into markdown format.
27+
* Handles journey tracks (grouped learning paths) with guides,
28+
* falling back to children listings when tracks aren't available.
29+
*/
30+
export class JourneyLandingTransformer implements PageTransformer {
31+
templateName = 'landing-page.template.md'
32+
33+
canTransform(page: Page): boolean {
34+
return page.layout === 'journey-landing'
35+
}
36+
37+
async transform(page: Page, pathname: string, context: Context): Promise<string> {
38+
const templateData = await this.prepareTemplateData(page, pathname, context)
39+
const templateContent = loadTemplate(this.templateName)
40+
41+
return await renderContent(templateContent, {
42+
...context,
43+
...templateData,
44+
markdownRequested: true,
45+
})
46+
}
47+
48+
private async prepareTemplateData(
49+
page: Page,
50+
pathname: string,
51+
context: Context,
52+
): Promise<TemplateData> {
53+
const journeyPage = page as JourneyPage
54+
const languageCode = page.languageCode || 'en'
55+
const sections: Section[] = []
56+
57+
// Journey tracks
58+
const journeyTracks = journeyPage.journeyTracks
59+
if (journeyTracks) {
60+
const groups: LinkGroup[] = []
61+
for (const track of journeyTracks) {
62+
const links = await Promise.all(
63+
(track.guides || []).map(async (guide) => {
64+
const guideHref = guide.href
65+
if (!guideHref) return null
66+
const linkData = await getLinkData(
67+
guideHref,
68+
languageCode,
69+
pathname,
70+
context,
71+
resolvePath,
72+
)
73+
return linkData
74+
}),
75+
)
76+
const validLinks = links.filter((l): l is LinkData => l !== null && !!l.href)
77+
if (validLinks.length > 0) {
78+
groups.push({ title: track.title, links: validLinks })
79+
}
80+
}
81+
82+
if (groups.length > 0) {
83+
sections.push({
84+
title: 'Links',
85+
groups,
86+
})
87+
}
88+
}
89+
90+
// Children fallback
91+
if (sections.length === 0 && journeyPage.children) {
92+
const links = await Promise.all(
93+
journeyPage.children.map(async (childHref) => {
94+
return await getLinkData(childHref, languageCode, pathname, context, resolvePath)
95+
}),
96+
)
97+
const validLinks = links.filter((l): l is LinkData => !!l.href)
98+
if (validLinks.length > 0) {
99+
sections.push({
100+
title: 'Links',
101+
groups: [{ title: null, links: validLinks }],
102+
})
103+
}
104+
}
105+
106+
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
107+
const title = await page.renderTitle(context, { unwrap: true })
108+
109+
return {
110+
title,
111+
intro,
112+
sections,
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)