Journey tracks for journey landing pages (#57722)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,19 @@ featuredLinks:
|
||||
- /get-started/foo/bar
|
||||
guideCards:
|
||||
- /get-started/foo/autotitling
|
||||
journeyTracks:
|
||||
- id: 'getting_started'
|
||||
title: 'Getting started'
|
||||
description: 'Learn the basics of our platform.'
|
||||
guides:
|
||||
- '/get-started/start-your-journey/hello-world'
|
||||
- '/get-started/foo/bar'
|
||||
- id: 'advanced'
|
||||
title: 'Advanced topics'
|
||||
description: 'Dive deeper into advanced features.'
|
||||
guides:
|
||||
- '/get-started/foo/autotitling'
|
||||
- '/get-started/start-your-journey/hello-world'
|
||||
children:
|
||||
- /start-your-journey
|
||||
- /foo
|
||||
|
||||
@@ -26,6 +26,7 @@ children:
|
||||
# as if the URL had been `/en/free-pro-team@latest/get-started/anything`.
|
||||
- search
|
||||
- get-started
|
||||
- test-journey
|
||||
- early-access
|
||||
- pages
|
||||
- code-security
|
||||
|
||||
24
src/fixtures/fixtures/content/test-journey/index.md
Normal file
24
src/fixtures/fixtures/content/test-journey/index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Test Journey Landing
|
||||
intro: 'Test page for journey tracks functionality'
|
||||
layout: journey-landing
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghes: '*'
|
||||
ghec: '*'
|
||||
journeyTracks:
|
||||
- id: 'getting_started'
|
||||
title: 'Getting started'
|
||||
description: 'Learn the basics of our platform.'
|
||||
guides:
|
||||
- '/get-started/start-your-journey/hello-world'
|
||||
- '/get-started/foo/bar'
|
||||
- id: 'advanced'
|
||||
title: 'Advanced topics'
|
||||
description: 'Dive deeper into advanced features.'
|
||||
guides:
|
||||
- '/get-started/foo/autotitling'
|
||||
- '/get-started/start-your-journey/hello-world'
|
||||
---
|
||||
|
||||
This is a test page for journey tracks.
|
||||
@@ -326,6 +326,11 @@ learning_track_nav:
|
||||
next_guide: Next
|
||||
more_guides: More guides →
|
||||
current_progress: '{i} of {n} in learning path'
|
||||
journey_track_nav:
|
||||
prev_article: Previous
|
||||
next_article: Next
|
||||
more_articles: More articles →
|
||||
current_progress: 'Article {i} of {n}'
|
||||
scroll_button:
|
||||
scroll_to_top: Scroll to top
|
||||
popovers:
|
||||
|
||||
@@ -1056,3 +1056,102 @@ test.describe('LandingCarousel component', () => {
|
||||
await expect(cards).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Journey Tracks', () => {
|
||||
test('displays journey tracks on landing pages', async ({ page }) => {
|
||||
await page.goto('/get-started?feature=journey-landing')
|
||||
|
||||
const journeyTracks = page.locator('[data-testid="journey-tracks"]')
|
||||
await expect(journeyTracks).toBeVisible()
|
||||
|
||||
// Check that at least one track is displayed
|
||||
const tracks = page.locator('[data-testid="journey-track"]')
|
||||
await expect(tracks.first()).toBeVisible()
|
||||
|
||||
// Verify track has proper structure
|
||||
const firstTrack = tracks.first()
|
||||
await expect(firstTrack.locator('h3')).toBeVisible() // Track title
|
||||
await expect(firstTrack.locator('p')).toBeVisible() // Track description
|
||||
})
|
||||
|
||||
test('track expansion and collapse functionality', async ({ page }) => {
|
||||
await page.goto('/get-started?feature=journey-landing')
|
||||
|
||||
const firstTrack = page.locator('[data-testid="journey-track"]').first()
|
||||
const expandButton = firstTrack.locator('summary')
|
||||
|
||||
// Initially collapsed
|
||||
const articlesList = firstTrack.locator('[data-testid="journey-articles"]')
|
||||
await expect(articlesList).not.toBeVisible()
|
||||
|
||||
await expandButton.click()
|
||||
await expect(articlesList).toBeVisible()
|
||||
|
||||
const articles = articlesList.locator('li')
|
||||
await expect(articles.first()).toBeVisible()
|
||||
|
||||
await expandButton.click()
|
||||
await expect(articlesList).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('article navigation within tracks', async ({ page }) => {
|
||||
await page.goto('/get-started?feature=journey-landing')
|
||||
|
||||
const firstTrack = page.locator('[data-testid="journey-track"]').first()
|
||||
const expandButton = firstTrack.locator('summary')
|
||||
|
||||
await expandButton.click()
|
||||
|
||||
// Click on first article
|
||||
const firstArticle = firstTrack.locator('[data-testid="journey-articles"] li a').first()
|
||||
await expect(firstArticle).toBeVisible()
|
||||
|
||||
const articleTitle = await firstArticle.textContent()
|
||||
expect(articleTitle).toBeTruthy()
|
||||
expect(articleTitle!.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('preserves version in journey track links', async ({ page }) => {
|
||||
await page.goto('/enterprise-cloud@latest/get-started?feature=journey-landing')
|
||||
|
||||
const firstTrack = page.locator('[data-testid="journey-track"]').first()
|
||||
const expandButton = firstTrack.locator('summary')
|
||||
await expandButton.click()
|
||||
|
||||
// article links should preserve the language and version
|
||||
const firstArticle = firstTrack.locator('[data-testid="journey-articles"] li a').first()
|
||||
const href = await firstArticle.getAttribute('href')
|
||||
|
||||
expect(href).toContain('/en/')
|
||||
expect(href).toContain('enterprise-cloud@latest')
|
||||
})
|
||||
|
||||
test('handles liquid template rendering in track content', async ({ page }) => {
|
||||
await page.goto('/get-started?feature=journey-landing')
|
||||
|
||||
const tracks = page.locator('[data-testid="journey-track"]')
|
||||
|
||||
// Check that liquid templates are rendered (no raw template syntax visible)
|
||||
const trackContent = await tracks.first().textContent()
|
||||
expect(trackContent).not.toContain('{{')
|
||||
expect(trackContent).not.toContain('}}')
|
||||
expect(trackContent).not.toContain('{%')
|
||||
expect(trackContent).not.toContain('%}')
|
||||
})
|
||||
|
||||
test('journey navigation components show on article pages', async ({ page }) => {
|
||||
// go to an article that's part of a journey track
|
||||
await page.goto('/get-started/start-your-journey/hello-world?feature=journey-navigation')
|
||||
|
||||
// journey next/prev nav components should rende
|
||||
const journeyCard = page.locator('[data-testid="journey-track-card"]')
|
||||
if (await journeyCard.isVisible()) {
|
||||
await expect(journeyCard).toBeVisible()
|
||||
}
|
||||
|
||||
const journeyNav = page.locator('[data-testid="journey-track-nav"]')
|
||||
if (await journeyNav.isVisible()) {
|
||||
await expect(journeyNav).toBeVisible()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DefaultLayout } from '@/frame/components/DefaultLayout'
|
||||
import { ArticleTitle } from '@/frame/components/article/ArticleTitle'
|
||||
import { useArticleContext } from '@/frame/components/context/ArticleContext'
|
||||
import { LearningTrackNav } from '@/learning-track/components/article/LearningTrackNav'
|
||||
import { JourneyTrackNav } from '@/journeys/components/JourneyTrackNav'
|
||||
import { JourneyTrackCard } from '@/journeys/components/JourneyTrackCard'
|
||||
import { MarkdownContent } from '@/frame/components/ui/MarkdownContent'
|
||||
import { Lead } from '@/frame/components/ui/Lead'
|
||||
import { PermissionsStatement } from '@/frame/components/ui/PermissionsStatement'
|
||||
@@ -42,10 +44,14 @@ export const ArticlePage = () => {
|
||||
productVideoUrl,
|
||||
miniTocItems,
|
||||
currentLearningTrack,
|
||||
currentJourneyTrack,
|
||||
supportPortalVaIframeProps,
|
||||
currentLayout,
|
||||
} = useArticleContext()
|
||||
const isLearningPath = !!currentLearningTrack?.trackName
|
||||
const isJourneyPath = !!currentJourneyTrack?.trackId
|
||||
// Only show journey track components when feature flag is enabled
|
||||
const showJourneyTracks = isJourneyPath && router.query?.feature === 'journey-navigation'
|
||||
const { t } = useTranslation(['pages'])
|
||||
|
||||
const introProp = (
|
||||
@@ -72,6 +78,7 @@ export const ArticlePage = () => {
|
||||
const toc = (
|
||||
<>
|
||||
{isLearningPath && <LearningTrackCard track={currentLearningTrack} />}
|
||||
{showJourneyTracks && <JourneyTrackCard journey={currentJourneyTrack} />}
|
||||
{miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
|
||||
</>
|
||||
)
|
||||
@@ -122,6 +129,11 @@ export const ArticlePage = () => {
|
||||
<LearningTrackNav track={currentLearningTrack} />
|
||||
</div>
|
||||
) : null}
|
||||
{showJourneyTracks ? (
|
||||
<div className="container-lg mt-4 px-3">
|
||||
<JourneyTrackNav context={currentJourneyTrack} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="container-xl px-3 px-md-6 my-4">
|
||||
@@ -148,6 +160,12 @@ export const ArticlePage = () => {
|
||||
<LearningTrackNav track={currentLearningTrack} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showJourneyTracks ? (
|
||||
<div className="mt-4">
|
||||
<JourneyTrackNav context={currentJourneyTrack} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SupportPortalVaIframeProps } from '@/frame/components/article/SupportPortalVaIframe'
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { JourneyContext } from '@/journeys/lib/journey-path-resolver'
|
||||
|
||||
export type LearningTrack = {
|
||||
trackTitle: string
|
||||
@@ -34,6 +35,7 @@ export type ArticleContextT = {
|
||||
product?: string
|
||||
productVideoUrl?: string
|
||||
currentLearningTrack?: LearningTrack
|
||||
currentJourneyTrack?: JourneyContext
|
||||
detectedPlatforms: Array<string>
|
||||
detectedTools: Array<string>
|
||||
allTools: Record<string, string>
|
||||
@@ -98,6 +100,7 @@ export const getArticleContextFromRequest = (req: any): ArticleContextT => {
|
||||
product: page.product || '',
|
||||
productVideoUrl: page.product_video || '',
|
||||
currentLearningTrack: req.context.currentLearningTrack,
|
||||
currentJourneyTrack: req.context.currentJourneyTrack,
|
||||
detectedPlatforms: page.detectedPlatforms || [],
|
||||
detectedTools: page.detectedTools || [],
|
||||
allTools: page.allToolsParsed || [], // this is set at the page level, see lib/page.js
|
||||
|
||||
@@ -197,6 +197,39 @@ export const schema = {
|
||||
learningTracks: {
|
||||
type: 'array',
|
||||
},
|
||||
// Journey tracks for journey landing pages
|
||||
journeyTracks: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'title', 'guides'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier for the journey track',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
translatable: true,
|
||||
description: 'Display title for the journey track',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
translatable: true,
|
||||
description: 'Optional description for the journey track',
|
||||
},
|
||||
guides: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'Array of article paths that make up this journey track',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
description: 'Array of journey tracks for journey landing pages',
|
||||
},
|
||||
// Used in `product-landing.html`
|
||||
beta_product: {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -50,6 +50,7 @@ import productExamples from './context/product-examples'
|
||||
import productGroups from './context/product-groups'
|
||||
import featuredLinks from '@/landings/middleware/featured-links'
|
||||
import learningTrack from '@/learning-track/middleware/learning-track'
|
||||
import journeyTrack from '@/journeys/middleware/journey-track'
|
||||
import next from './next'
|
||||
import renderPage from './render-page'
|
||||
import assetPreprocessing from '@/assets/middleware/asset-preprocessing'
|
||||
@@ -270,6 +271,7 @@ export default function (app: Express) {
|
||||
app.use(asyncMiddleware(featuredLinks))
|
||||
app.use(asyncMiddleware(resolveRecommended))
|
||||
app.use(asyncMiddleware(learningTrack))
|
||||
app.use(asyncMiddleware(journeyTrack))
|
||||
|
||||
if (ENABLE_FASTLY_TESTING) {
|
||||
// The fastlyCacheTest middleware is intended to be used with Fastly to test caching behavior.
|
||||
|
||||
52
src/journeys/components/JourneyTrackCard.tsx
Normal file
52
src/journeys/components/JourneyTrackCard.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import type { JourneyContext } from '@/journeys/lib/journey-path-resolver'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
|
||||
type Props = {
|
||||
journey: JourneyContext
|
||||
}
|
||||
|
||||
export function JourneyTrackCard({ journey }: Props) {
|
||||
const { locale } = useRouter()
|
||||
const { t } = useTranslation('journey_track_nav')
|
||||
const { trackTitle, journeyTitle, journeyPath, nextGuide, numberOfGuides, currentGuideIndex } =
|
||||
journey
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="journey-track-card"
|
||||
className="py-3 px-4 rounded color-bg-default border d-flex flex-justify-between mb-4 mx-2"
|
||||
>
|
||||
<div className="d-flex flex-column width-full">
|
||||
<h2 className="h4">
|
||||
<Link href={`/${locale}${journeyPath}`} className="mb-1 text-underline">
|
||||
{journeyTitle}
|
||||
</Link>
|
||||
</h2>
|
||||
<span className="f6 color-fg-muted mb-2">{trackTitle}</span>
|
||||
<span className="f5 color-fg-muted">
|
||||
{t('current_progress')
|
||||
.replace('{n}', `${numberOfGuides}`)
|
||||
.replace('{i}', `${currentGuideIndex + 1}`)}
|
||||
</span>
|
||||
<hr />
|
||||
<span className="h5 color-fg-default">
|
||||
{nextGuide ? (
|
||||
<>
|
||||
{t('next_article')}:
|
||||
<Link href={nextGuide.href} className="text-bold color-fg f5 ml-1">
|
||||
{nextGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link href={`/${locale}${journeyPath}`} className="h5 text-bold color-fg f5 ml-1">
|
||||
{t('more_articles')}
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
src/journeys/components/JourneyTrackNav.tsx
Normal file
50
src/journeys/components/JourneyTrackNav.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import type { JourneyContext } from '@/journeys/lib/journey-path-resolver'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
|
||||
type Props = {
|
||||
context: JourneyContext
|
||||
}
|
||||
|
||||
export function JourneyTrackNav({ context }: Props) {
|
||||
const { t } = useTranslation('journey_track_nav')
|
||||
const { prevGuide, nextGuide, trackTitle, currentGuideIndex, numberOfGuides } = context
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="journey-track-nav"
|
||||
className="py-3 px-4 rounded color-bg-default border d-flex flex-justify-between"
|
||||
>
|
||||
<span className="f5 d-flex flex-column">
|
||||
{prevGuide && (
|
||||
<>
|
||||
<span className="color-fg-default">{t('prev_article')}</span>
|
||||
<Link href={prevGuide.href} className="text-bold color-fg">
|
||||
{prevGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="f5 d-flex flex-column flex-items-center">
|
||||
<span className="color-fg-muted">{trackTitle}</span>
|
||||
<span className="color-fg-muted">
|
||||
{t('current_progress')
|
||||
.replace('{n}', `${numberOfGuides}`)
|
||||
.replace('{i}', `${currentGuideIndex + 1}`)}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="f5 d-flex flex-column flex-items-end">
|
||||
{nextGuide && (
|
||||
<>
|
||||
<span className="color-fg-default">{t('next_article')}</span>
|
||||
<Link href={nextGuide.href} className="text-bold color-fg text-right">
|
||||
{nextGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
2
src/journeys/components/index.ts
Normal file
2
src/journeys/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { JourneyTrackCard } from './JourneyTrackCard'
|
||||
export { JourneyTrackNav } from './JourneyTrackNav'
|
||||
2
src/journeys/lib/get-link-data.ts
Normal file
2
src/journeys/lib/get-link-data.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Re-export the getLinkData function from learning tracks for journey tracks
|
||||
export { default } from '@/learning-track/lib/get-link-data'
|
||||
268
src/journeys/lib/journey-path-resolver.ts
Normal file
268
src/journeys/lib/journey-path-resolver.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { getPathWithoutLanguage, getPathWithoutVersion } from '@/frame/lib/path-utils'
|
||||
import { renderContent } from '@/content-render/index'
|
||||
import { executeWithFallback } from '@/languages/lib/render-with-fallback'
|
||||
import getApplicableVersions from '@/versions/lib/get-applicable-versions'
|
||||
import getLinkData from './get-link-data'
|
||||
|
||||
export interface JourneyContext {
|
||||
trackId: string
|
||||
trackName: string
|
||||
trackTitle: string
|
||||
journeyTitle: string
|
||||
journeyPath: string
|
||||
currentGuideIndex: number
|
||||
numberOfGuides: number
|
||||
nextGuide?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
prevGuide?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface JourneyTrack {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
guides: Array<{
|
||||
href: string
|
||||
title: string
|
||||
}>
|
||||
}
|
||||
|
||||
type JourneyPage = {
|
||||
layout?: string
|
||||
title?: string
|
||||
permalink?: string
|
||||
relativePath?: string
|
||||
versions?: any
|
||||
journeyTracks?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
guides: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
type Pages = Record<string, any>
|
||||
type ContentContext = {
|
||||
currentProduct?: string
|
||||
currentLanguage?: string
|
||||
currentVersion?: string
|
||||
pages?: Pages
|
||||
redirects?: any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// Cache for journey pages so we only filter all pages once
|
||||
let cachedJourneyPages: JourneyPage[] | null = null
|
||||
|
||||
function getJourneyPages(pages: Pages): JourneyPage[] {
|
||||
if (!cachedJourneyPages) {
|
||||
cachedJourneyPages = Object.values(pages).filter(
|
||||
(page: any) => page.journeyTracks && page.journeyTracks.length > 0,
|
||||
) as JourneyPage[]
|
||||
}
|
||||
return cachedJourneyPages
|
||||
}
|
||||
|
||||
function normalizeGuidePath(path: string): string {
|
||||
// First ensure we have a leading slash for consistent processing
|
||||
const pathWithSlash = path.startsWith('/') ? path : `/${path}`
|
||||
|
||||
// Use the same normalization pattern as learning tracks and other middleware
|
||||
const withoutVersion = getPathWithoutVersion(pathWithSlash)
|
||||
const withoutLanguage = getPathWithoutLanguage(withoutVersion)
|
||||
|
||||
// Ensure we always return a path with leading slash for consistent comparison
|
||||
return withoutLanguage && withoutLanguage.startsWith('/')
|
||||
? withoutLanguage
|
||||
: `/${withoutLanguage || path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to append the journey-navigation feature flag to URLs
|
||||
*/
|
||||
function appendJourneyFeatureFlag(href: string): string {
|
||||
if (!href) return href
|
||||
|
||||
try {
|
||||
// we have to pass some URL here, we just throw it away though
|
||||
const url = new URL(href, 'https://docs.github.com')
|
||||
url.searchParams.set('feature', 'journey-navigation')
|
||||
return url.pathname + url.search
|
||||
} catch {
|
||||
// fallback if URL parsing fails
|
||||
const separator = href.includes('?') ? '&' : '?'
|
||||
return `${href}${separator}feature=journey-navigation`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the journey context for a given article path.
|
||||
*
|
||||
* The journey context includes information about the journey track, the current
|
||||
* guide's position within that track, and links to the previous and next
|
||||
* guides if they exist.
|
||||
*/
|
||||
export async function resolveJourneyContext(
|
||||
articlePath: string,
|
||||
pages: Pages,
|
||||
context: ContentContext,
|
||||
currentJourneyPage?: JourneyPage,
|
||||
): Promise<JourneyContext | null> {
|
||||
const normalizedPath = normalizeGuidePath(articlePath)
|
||||
|
||||
// Use the current journey page if provided, otherwise find all journey pages
|
||||
const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages)
|
||||
|
||||
let result: JourneyContext | null = null
|
||||
|
||||
// Search through all journey pages
|
||||
for (const journeyPage of journeyPages) {
|
||||
if (!journeyPage.journeyTracks) continue
|
||||
|
||||
// Check version compatibility - only show journey navigation if the current version
|
||||
// is compatible with the journey landing page's versions (journey track articles
|
||||
// currently inherit the journey landing page's versions)
|
||||
if (journeyPage.versions) {
|
||||
const journeyVersions = getApplicableVersions(journeyPage.versions)
|
||||
if (!journeyVersions.includes(context.currentVersion || '')) {
|
||||
continue // Skip this journey if current version is not supported
|
||||
}
|
||||
}
|
||||
|
||||
for (const track of journeyPage.journeyTracks) {
|
||||
if (!track.guides || !Array.isArray(track.guides)) continue
|
||||
|
||||
// Find if current article is in this track
|
||||
let guideIndex = -1
|
||||
|
||||
for (let i = 0; i < track.guides.length; i++) {
|
||||
const guidePath = track.guides[i]
|
||||
let renderedGuidePath = guidePath
|
||||
|
||||
// Handle Liquid conditionals in guide paths
|
||||
try {
|
||||
renderedGuidePath = await executeWithFallback(
|
||||
context,
|
||||
() => renderContent(guidePath, context, { textOnly: true }),
|
||||
() => guidePath,
|
||||
)
|
||||
} catch {
|
||||
// If rendering fails, use the original path rather than erroring
|
||||
renderedGuidePath = guidePath
|
||||
}
|
||||
|
||||
const normalizedGuidePath = normalizeGuidePath(renderedGuidePath)
|
||||
|
||||
if (normalizedGuidePath === normalizedPath) {
|
||||
guideIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (guideIndex >= 0) {
|
||||
result = {
|
||||
trackId: track.id,
|
||||
trackName: track.id,
|
||||
trackTitle: track.title,
|
||||
journeyTitle: journeyPage.title || '',
|
||||
journeyPath: journeyPage.permalink || `/${journeyPage.relativePath || ''}`,
|
||||
currentGuideIndex: guideIndex,
|
||||
numberOfGuides: track.guides.length,
|
||||
}
|
||||
|
||||
// Set up previous guide
|
||||
if (guideIndex > 0) {
|
||||
const prevGuidePath = track.guides[guideIndex - 1]
|
||||
try {
|
||||
const resultData = await getLinkData(prevGuidePath, context, {
|
||||
title: true,
|
||||
intro: false,
|
||||
fullTitle: false,
|
||||
})
|
||||
if (resultData && resultData.length > 0) {
|
||||
const linkResult = resultData[0]
|
||||
result.prevGuide = {
|
||||
href: appendJourneyFeatureFlag(linkResult.href),
|
||||
title: linkResult.title || '',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not get link data for previous guide:', prevGuidePath, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up next guide
|
||||
if (guideIndex < track.guides.length - 1) {
|
||||
const nextGuidePath = track.guides[guideIndex + 1]
|
||||
try {
|
||||
const resultData = await getLinkData(nextGuidePath, context, {
|
||||
title: true,
|
||||
intro: false,
|
||||
fullTitle: false,
|
||||
})
|
||||
if (resultData && resultData.length > 0) {
|
||||
const linkResult = resultData[0]
|
||||
result.nextGuide = {
|
||||
href: appendJourneyFeatureFlag(linkResult.href),
|
||||
title: linkResult.title || '',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not get link data for next guide:', nextGuidePath, error)
|
||||
}
|
||||
}
|
||||
|
||||
break // Found the track, stop searching
|
||||
}
|
||||
}
|
||||
|
||||
if (result) break // Found the journey, stop searching
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves journey tracks data from frontmatter, including rendering any Liquid.
|
||||
*
|
||||
* Returns an array of JourneyTrack objects with titles, descriptions, and guide links.
|
||||
*/
|
||||
export async function resolveJourneyTracks(
|
||||
journeyTracks: any[],
|
||||
context: ContentContext,
|
||||
): Promise<JourneyTrack[]> {
|
||||
const result = await Promise.all(
|
||||
journeyTracks.map(async (track: any) => {
|
||||
// Render Liquid templates in title and description
|
||||
const renderedTitle = await renderContent(track.title, context, { textOnly: true })
|
||||
const renderedDescription = track.description
|
||||
? await renderContent(track.description, context, { textOnly: true })
|
||||
: undefined
|
||||
|
||||
const guides = await Promise.all(
|
||||
track.guides.map(async (guidePath: string) => {
|
||||
const linkData = await getLinkData(guidePath, context, { title: true })
|
||||
const baseHref = linkData?.[0]?.href || guidePath
|
||||
return {
|
||||
href: appendJourneyFeatureFlag(baseHref),
|
||||
title: linkData?.[0]?.title || 'Untitled Guide',
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
title: renderedTitle,
|
||||
description: renderedDescription,
|
||||
guides,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
27
src/journeys/middleware/journey-track.ts
Normal file
27
src/journeys/middleware/journey-track.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Response, NextFunction } from 'express'
|
||||
import type { ExtendedRequest, Context } from '@/types'
|
||||
import { resolveJourneyContext } from '../lib/journey-path-resolver'
|
||||
|
||||
export default async function journeyTrack(
|
||||
req: ExtendedRequest & { context: Context },
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
if (!req.context) throw new Error('request is not contextualized')
|
||||
if (!req.context.page) return next()
|
||||
|
||||
try {
|
||||
const journeyContext = await resolveJourneyContext(
|
||||
req.pagePath || '',
|
||||
req.context.pages || {},
|
||||
req.context,
|
||||
)
|
||||
|
||||
req.context.currentJourneyTrack = journeyContext
|
||||
} catch (error) {
|
||||
console.warn('Failed to resolve journey context:', error)
|
||||
req.context.currentJourneyTrack = null
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
222
src/journeys/tests/journey-path-resolver.ts
Normal file
222
src/journeys/tests/journey-path-resolver.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveJourneyContext, resolveJourneyTracks } from '../lib/journey-path-resolver'
|
||||
|
||||
// Mock modules since we just want to test journey functions, not their dependencies or
|
||||
// against real content files
|
||||
vi.mock('@/journeys/lib/get-link-data', () => ({
|
||||
default: async (path: string) => [
|
||||
{
|
||||
href: `/en/enterprise-cloud@latest${path}`,
|
||||
title: `Mock Title for ${path}`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/content-render/index', () => ({
|
||||
renderContent: async (content: string) => content,
|
||||
}))
|
||||
|
||||
vi.mock('@/languages/lib/render-with-fallback', () => ({
|
||||
executeWithFallback: async (fn: () => Promise<string>, fallback: () => string) => {
|
||||
try {
|
||||
return await fn()
|
||||
} catch {
|
||||
return fallback()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe('journey-path-resolver', () => {
|
||||
describe('resolveJourneyContext', () => {
|
||||
const mockContext = {
|
||||
currentProduct: 'github',
|
||||
currentLanguage: 'en',
|
||||
currentVersion: 'enterprise-cloud@latest',
|
||||
}
|
||||
|
||||
const mockPages = {
|
||||
'enterprise-onboarding/index': {
|
||||
layout: 'journey-landing',
|
||||
title: 'Enterprise onboarding',
|
||||
permalink: '/enterprise-onboarding',
|
||||
journeyTracks: [
|
||||
{
|
||||
id: 'getting_started',
|
||||
title: 'Getting started',
|
||||
description: 'Learn the basics',
|
||||
guides: [
|
||||
'/enterprise-onboarding/setup',
|
||||
'/enterprise-onboarding/config',
|
||||
'/enterprise-onboarding/deploy',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
test('returns null for article not in any journey track', async () => {
|
||||
const result = await resolveJourneyContext('/some-other-article', mockPages, mockContext)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test('finds article in journey track', async () => {
|
||||
const result = await resolveJourneyContext(
|
||||
'/enterprise-onboarding/config',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.trackId).toBe('getting_started')
|
||||
expect(result?.trackTitle).toBe('Getting started')
|
||||
expect(result?.currentGuideIndex).toBe(1)
|
||||
expect(result?.numberOfGuides).toBe(3)
|
||||
})
|
||||
|
||||
test('sets up previous guide navigation', async () => {
|
||||
const result = await resolveJourneyContext(
|
||||
'/enterprise-onboarding/config',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
expect(result?.prevGuide).toEqual({
|
||||
href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation',
|
||||
title: 'Mock Title for /enterprise-onboarding/setup',
|
||||
})
|
||||
})
|
||||
|
||||
test('sets up next guide navigation', async () => {
|
||||
const result = await resolveJourneyContext(
|
||||
'/enterprise-onboarding/config',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
expect(result?.nextGuide).toEqual({
|
||||
href: '/en/enterprise-cloud@latest/enterprise-onboarding/deploy?feature=journey-navigation',
|
||||
title: 'Mock Title for /enterprise-onboarding/deploy',
|
||||
})
|
||||
})
|
||||
|
||||
test('handles first article in track (no previous)', async () => {
|
||||
const result = await resolveJourneyContext(
|
||||
'/enterprise-onboarding/setup',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
expect(result?.prevGuide).toBeUndefined()
|
||||
expect(result?.currentGuideIndex).toBe(0)
|
||||
})
|
||||
|
||||
test('handles last article in track (no next)', async () => {
|
||||
const result = await resolveJourneyContext(
|
||||
'/enterprise-onboarding/deploy',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
expect(result?.nextGuide).toBeUndefined()
|
||||
expect(result?.currentGuideIndex).toBe(2)
|
||||
})
|
||||
|
||||
test('normalizes article paths without leading slash', async () => {
|
||||
// The resolver should handle paths without leading slashes
|
||||
// by normalizing them to match the guide paths in the data
|
||||
const result = await resolveJourneyContext(
|
||||
'enterprise-onboarding/config',
|
||||
mockPages,
|
||||
mockContext,
|
||||
)
|
||||
|
||||
// This should find the same track as the version with leading slash
|
||||
expect(result?.trackId).toBe('getting_started')
|
||||
expect(result?.currentGuideIndex).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveJourneyTracks', () => {
|
||||
const mockContext = {
|
||||
currentProduct: 'github',
|
||||
currentLanguage: 'en',
|
||||
currentVersion: 'enterprise-cloud@latest',
|
||||
}
|
||||
|
||||
const mockJourneyTracks = [
|
||||
{
|
||||
id: 'getting_started',
|
||||
title: 'Getting started with {% data variables.product.company_short %}',
|
||||
description: 'Learn the {% data variables.product.company_short %} basics',
|
||||
guides: ['/enterprise-onboarding/setup', '/enterprise-onboarding/config'],
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Advanced configuration',
|
||||
description: 'Advanced topics for experts',
|
||||
guides: ['/enterprise-onboarding/advanced-setup'],
|
||||
},
|
||||
]
|
||||
|
||||
test('resolves all journey tracks', async () => {
|
||||
const result = await resolveJourneyTracks(mockJourneyTracks, mockContext)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe('getting_started')
|
||||
expect(result[1].id).toBe('advanced')
|
||||
})
|
||||
|
||||
test('renders liquid templates in titles and descriptions', async () => {
|
||||
const result = await resolveJourneyTracks(mockJourneyTracks, mockContext)
|
||||
|
||||
// Should return the content as-is since our mock renderContent is a passthrough
|
||||
expect(result[0].title).toBe(
|
||||
'Getting started with {% data variables.product.company_short %}',
|
||||
)
|
||||
expect(result[0].description).toBe(
|
||||
'Learn the {% data variables.product.company_short %} basics',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolves guide links with proper versioning', async () => {
|
||||
const result = await resolveJourneyTracks(mockJourneyTracks, mockContext)
|
||||
|
||||
expect(result[0].guides).toHaveLength(2)
|
||||
expect(result[0].guides[0]).toEqual({
|
||||
href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation',
|
||||
title: 'Mock Title for /enterprise-onboarding/setup',
|
||||
})
|
||||
})
|
||||
|
||||
test('handles tracks with no guides', async () => {
|
||||
const emptyTrack = [
|
||||
{
|
||||
id: 'empty',
|
||||
title: 'Empty track',
|
||||
description: 'No guides here',
|
||||
guides: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = await resolveJourneyTracks(emptyTrack, mockContext)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].guides).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('handles missing optional descriptions', async () => {
|
||||
const trackWithoutDescription = [
|
||||
{
|
||||
id: 'no_desc',
|
||||
title: 'Track without description',
|
||||
guides: ['/some-guide'],
|
||||
},
|
||||
]
|
||||
|
||||
const result = await resolveJourneyTracks(trackWithoutDescription, mockContext)
|
||||
|
||||
expect(result[0].description).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,94 +3,8 @@ import { useLandingContext } from '@/landings/context/LandingContext'
|
||||
import { LandingHero } from '@/landings/components/shared/LandingHero'
|
||||
import { JourneyLearningTracks } from './JourneyLearningTracks'
|
||||
|
||||
export type JourneyLearningTrack = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
trackName: string
|
||||
trackProduct: string
|
||||
guides?: Array<{
|
||||
href: string
|
||||
title: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const JourneyLanding = () => {
|
||||
const { title, intro, heroImage, introLinks } = useLandingContext()
|
||||
|
||||
// Temp until we hookup real data
|
||||
const stubLearningTracks: JourneyLearningTrack[] = [
|
||||
{
|
||||
id: 'admin:get_started_with_your_enterprise_account',
|
||||
title: 'Get started with your enterprise account',
|
||||
description:
|
||||
'Set up your enterprise account and configure initial settings for your organization.',
|
||||
trackName: 'get_started_with_your_enterprise_account',
|
||||
trackProduct: 'admin',
|
||||
guides: [
|
||||
{
|
||||
href: '/admin/overview/about-enterprise-accounts?learn=get_started_with_your_enterprise_account&learnProduct=admin',
|
||||
title: 'About enterprise accounts',
|
||||
},
|
||||
{
|
||||
href: '/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/inviting-people-to-manage-your-enterprise?learn=get_started_with_your_enterprise_account&learnProduct=admin',
|
||||
title: 'Inviting people to manage your enterprise',
|
||||
},
|
||||
{
|
||||
href: '/admin/policies/enforcing-policies-for-your-enterprise/about-enterprise-policies?learn=get_started_with_your_enterprise_account&learnProduct=admin',
|
||||
title: 'About enterprise policies',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin:adopting_github_actions_for_your_enterprise_ghec',
|
||||
title: 'Adopt GitHub Actions for your enterprise',
|
||||
description:
|
||||
'Learn how to plan and implement a rollout of GitHub Actions in your enterprise.',
|
||||
trackName: 'adopting_github_actions_for_your_enterprise_ghec',
|
||||
trackProduct: 'admin',
|
||||
guides: [
|
||||
{
|
||||
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/about-github-actions-for-enterprises?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
|
||||
title: 'About GitHub Actions for enterprises',
|
||||
},
|
||||
{
|
||||
href: '/actions/get-started/understand-github-actions?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
|
||||
title: 'Understanding GitHub Actions',
|
||||
},
|
||||
{
|
||||
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/introducing-github-actions-to-your-enterprise?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
|
||||
title: 'Introducing GitHub Actions to your enterprise',
|
||||
},
|
||||
{
|
||||
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
|
||||
title: 'Migrating your enterprise to GitHub Actions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'actions:continuous-integration',
|
||||
title: 'Continuous integration with GitHub Actions',
|
||||
description:
|
||||
'Set up automated testing and building for your projects using GitHub Actions workflows.',
|
||||
trackName: 'continuous-integration',
|
||||
trackProduct: 'actions',
|
||||
guides: [
|
||||
{
|
||||
href: '/actions/automating-builds-and-tests/about-continuous-integration?learn=continuous-integration&learnProduct=actions',
|
||||
title: 'About continuous integration',
|
||||
},
|
||||
{
|
||||
href: '/actions/automating-builds-and-tests/building-and-testing-nodejs?learn=continuous-integration&learnProduct=actions',
|
||||
title: 'Building and testing Node.js',
|
||||
},
|
||||
{
|
||||
href: '/actions/automating-builds-and-tests/building-and-testing-python?learn=continuous-integration&learnProduct=actions',
|
||||
title: 'Building and testing Python',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const { title, intro, heroImage, introLinks, journeyTracks } = useLandingContext()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
@@ -98,7 +12,7 @@ export const JourneyLanding = () => {
|
||||
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
|
||||
|
||||
<div className="container-xl px-3 px-md-6 mt-6 mb-4">
|
||||
<JourneyLearningTracks tracks={stubLearningTracks} />
|
||||
<JourneyLearningTracks tracks={journeyTracks ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* filepath: /workspaces/docs-internal/src/landings/components/journey/JourneyLearningTracks.tsx */
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@primer/octicons-react'
|
||||
import { Button, Details, Timeline, Token, useDetails } from '@primer/react'
|
||||
import type { JourneyLearningTrack } from './JourneyLanding'
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import { JourneyTrack } from '@/journeys/lib/journey-path-resolver'
|
||||
import styles from './JourneyLearningTracks.module.css'
|
||||
|
||||
type JourneyLearningTracksProps = {
|
||||
tracks: JourneyLearningTrack[]
|
||||
tracks: JourneyTrack[]
|
||||
}
|
||||
|
||||
export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) => {
|
||||
@@ -13,7 +14,7 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) =>
|
||||
return null
|
||||
}
|
||||
|
||||
const renderTrackContent = (track: JourneyLearningTrack, trackIndex: number) => {
|
||||
const renderTrackContent = (track: JourneyTrack, trackIndex: number) => {
|
||||
const { getDetailsProps, open } = useDetails({})
|
||||
|
||||
return (
|
||||
@@ -36,12 +37,12 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) =>
|
||||
>
|
||||
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</Button>
|
||||
<ol className={styles.trackGuides}>
|
||||
{(track.guides || []).map((guide) => (
|
||||
<li key={guide.title}>
|
||||
<a href={guide.href} className={`text-semibold ${styles.guideLink}`}>
|
||||
{guide.title}
|
||||
</a>
|
||||
<ol className={styles.trackGuides} data-testid="journey-articles">
|
||||
{(track.guides || []).map((article: { href: string; title: string }) => (
|
||||
<li key={article.title}>
|
||||
<Link href={article.href} className={`text-semibold ${styles.guideLink}`}>
|
||||
{article.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@@ -51,7 +52,7 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="journey-tracks">
|
||||
{/* Desktop: Timeline component */}
|
||||
<div className={styles.timelineContainer}>
|
||||
<Timeline clipSidebar className={styles.timelineThinLine}>
|
||||
@@ -60,7 +61,9 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) =>
|
||||
<Timeline.Item key={track.id}>
|
||||
<Timeline.Badge className={styles.timelineBadge}>{trackIndex + 1}</Timeline.Badge>
|
||||
<Timeline.Body className={styles.learningTracks}>
|
||||
<div className="position-relative">{renderTrackContent(track, trackIndex)}</div>
|
||||
<div className="position-relative" data-testid="journey-track">
|
||||
{renderTrackContent(track, trackIndex)}
|
||||
</div>
|
||||
</Timeline.Body>
|
||||
</Timeline.Item>
|
||||
)
|
||||
@@ -74,12 +77,14 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) =>
|
||||
<div key={track.id} className={styles.mobileItem}>
|
||||
<div className={styles.mobileBadge}>{trackIndex + 1}</div>
|
||||
<div className={styles.mobileTile}>
|
||||
<div className="position-relative">{renderTrackContent(track, trackIndex)}</div>
|
||||
<div className="position-relative" data-testid="journey-track">
|
||||
{renderTrackContent(track, trackIndex)}
|
||||
</div>
|
||||
</div>
|
||||
{trackIndex < tracks.length - 1 && <div className={styles.mobileConnector} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getFeaturedLinksFromReq } from '@/landings/components/ProductLandingCon
|
||||
import { mapRawTocItemToTocItem } from '@/landings/types'
|
||||
import type { TocItem } from '@/landings/types'
|
||||
import type { LearningTrack } from '@/types'
|
||||
import type { JourneyTrack } from '@/journeys/lib/journey-path-resolver'
|
||||
import type { FeaturedLink } from '@/landings/components/ProductLandingContext'
|
||||
|
||||
export type LandingType = 'bespoke' | 'discovery' | 'journey'
|
||||
@@ -23,6 +24,8 @@ export type LandingContextT = {
|
||||
// For discovery landing pages
|
||||
recommended?: Array<{ title: string; intro: string; href: string; category: string[] }> // Resolved article data
|
||||
introLinks?: Record<string, string>
|
||||
// For journey landing pages
|
||||
journeyTracks?: JourneyTrack[]
|
||||
}
|
||||
|
||||
export const LandingContext = createContext<LandingContextT | null>(null)
|
||||
@@ -55,6 +58,13 @@ export const getLandingContextFromRequest = async (
|
||||
}
|
||||
}
|
||||
|
||||
let journeyTracks: JourneyTrack[] = []
|
||||
if (landingType === 'journey' && page.journeyTracks) {
|
||||
// Need a dynamic import because journey-path-resolver uses Node fs apis
|
||||
const { resolveJourneyTracks } = await import('@/journeys/lib/journey-path-resolver')
|
||||
journeyTracks = await resolveJourneyTracks(page.journeyTracks, req.context)
|
||||
}
|
||||
|
||||
return {
|
||||
landingType,
|
||||
title: page.title,
|
||||
@@ -72,5 +82,6 @@ export const getLandingContextFromRequest = async (
|
||||
heroImage: page.heroImage || '/assets/images/banner-images/hero-1.png',
|
||||
introLinks: page.introLinks || null,
|
||||
recommended,
|
||||
journeyTracks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,9 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
||||
if (props.articleContext.currentLearningTrack?.trackName) {
|
||||
additionalUINamespaces.push('learning_track_nav')
|
||||
}
|
||||
if (props.articleContext.currentJourneyTrack?.trackId) {
|
||||
additionalUINamespaces.push('journey_track_nav')
|
||||
}
|
||||
}
|
||||
|
||||
addUINamespaces(req, props.mainContext.data.ui, additionalUINamespaces)
|
||||
|
||||
Reference in New Issue
Block a user