Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/article-api/tests/product-guides-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test } from 'vitest'

import { get } from '@/tests/helpers/e2etest'

const makeURL = (pathname: string): string =>
`/api/article/body?${new URLSearchParams({ pathname })}`

describe('product guides transformer', () => {
test('renders a product guides page with learning tracks', async () => {
// Product guides pages use layout: product-guides
// /en/codespaces/guides is a product guides page
const res = await get(makeURL('/en/codespaces/guides'))
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')

// Should have Links section
expect(res.body).toContain('## Links')
})

test('includes guide cards if present', async () => {
const res = await get(makeURL('/en/codespaces/guides'))
expect(res.statusCode).toBe(200)

// If includeGuides are present, they should appear under Guides
// The rendering depends on what's in the frontmatter
expect(res.body).toMatch(/##|###/)
})

test('includes learning tracks if present', async () => {
const res = await get(makeURL('/en/codespaces/guides'))
expect(res.statusCode).toBe(200)

// Learning tracks should be rendered as sections with their titles
// The actual content depends on frontmatter configuration
expect(res.body).toContain('## Links')
})
})
2 changes: 2 additions & 0 deletions src/article-api/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GraphQLTransformer } from './graphql-transformer'
import { GithubAppsTransformer } from './github-apps-transformer'
import { WebhooksTransformer } from './webhooks-transformer'
import { TocTransformer } from './toc-transformer'
import { ProductGuidesTransformer } from './product-guides-transformer'
import { ProductLandingTransformer } from './product-landing-transformer'

/**
Expand All @@ -23,6 +24,7 @@ transformerRegistry.register(new GraphQLTransformer())
transformerRegistry.register(new GithubAppsTransformer())
transformerRegistry.register(new WebhooksTransformer())
transformerRegistry.register(new TocTransformer())
transformerRegistry.register(new ProductGuidesTransformer())
transformerRegistry.register(new ProductLandingTransformer())

export { TransformerRegistry } from './types'
Expand Down
138 changes: 138 additions & 0 deletions src/article-api/transformers/product-guides-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { Context, Page } from '@/types'
import type { PageTransformer, TemplateData, Section, LinkGroup, LinkData } from './types'
import { renderContent } from '@/content-render/index'
import { loadTemplate } from '@/article-api/lib/load-template'

interface ProcessedLink {
href: string
title?: string
intro?: string
}

interface LearningTrack {
title: string
guides: ProcessedLink[]
}

/**
* ProductGuidesPage extends Page with optional guide and learning track fields.
* - includeGuides/rawIncludeGuides: Curated list of guide articles (processed objects vs raw paths)
* - learningTracks/rawLearningTracks: Grouped tutorials (processed objects vs raw track IDs)
*/
interface ProductGuidesPage extends Page {
includeGuides?: ProcessedLink[]
rawIncludeGuides?: string[]
learningTracks?: LearningTrack[]
rawLearningTracks?: string[]
}

/**
* Transforms product-guides pages into markdown format.
* Handles includeGuides (curated articles) and learningTracks (grouped tutorials).
*/
export class ProductGuidesTransformer implements PageTransformer {
templateName = 'landing-page.template.md'

canTransform(page: Page): boolean {
return page.layout === 'product-guides'
}

async transform(page: Page, pathname: string, context: Context): Promise<string> {
const templateData = await this.prepareTemplateData(page, pathname, context)
const templateContent = loadTemplate(this.templateName)

return await renderContent(templateContent, {
...context,
...templateData,
markdownRequested: true,
})
}

private async prepareTemplateData(
page: Page,
pathname: string,
context: Context,
): Promise<TemplateData> {
const guidesPage = page as ProductGuidesPage
const sections: Section[] = []
const groups: LinkGroup[] = []

// Include guides
const includeGuidesData = guidesPage.includeGuides ?? guidesPage.rawIncludeGuides
if (includeGuidesData && includeGuidesData.length > 0) {
const { default: getLinkData } = await import('@/learning-track/lib/get-link-data')

const isProcessed = typeof includeGuidesData[0] === 'object'

let processedLinks: ProcessedLink[]
if (isProcessed) {
processedLinks = includeGuidesData as ProcessedLink[]
} else {
processedLinks =
(await getLinkData(includeGuidesData as string[], context, {
title: true,
intro: true,
})) || []
}

const links: LinkData[] = (processedLinks || []).map((item) => ({
href: item.href,
title: item.title || '',
intro: item.intro || '',
}))

const validLinks = links.filter((l) => l.href)
if (validLinks.length > 0) {
groups.push({ title: 'Guides', links: validLinks })
}
}

// Learning tracks
const learningTracksData = guidesPage.learningTracks ?? guidesPage.rawLearningTracks
if (learningTracksData && learningTracksData.length > 0) {
let processedTracks: LearningTrack[]
if (Array.isArray(guidesPage.learningTracks) && guidesPage.learningTracks.length > 0) {
processedTracks = guidesPage.learningTracks
} else {
const { default: processLearningTracks } = await import(
'@/learning-track/lib/process-learning-tracks'
)
const { learningTracks } = await processLearningTracks(
learningTracksData as string[],
context,
)
processedTracks = learningTracks
}

for (const track of processedTracks) {
if (!track.guides || !Array.isArray(track.guides)) continue

const links: LinkData[] = track.guides.map((guide) => ({
href: guide.href,
title: guide.title || '',
intro: guide.intro || '',
}))

if (links.length > 0) {
groups.push({ title: track.title, links })
}
}
}

if (groups.length > 0) {
sections.push({
title: 'Links',
groups,
})
}

const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
const title = await page.renderTitle(context, { unwrap: true })

return {
title,
intro,
sections,
}
}
}
Loading