show journey nav and support journey article branching text frontmatter (#58848)
This commit is contained in:
@@ -259,7 +259,9 @@ includeGuides:
|
|||||||
- `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page.
|
- `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page.
|
||||||
- `title` (required): Display title for the journey (supports Liquid variables)
|
- `title` (required): Display title for the journey (supports Liquid variables)
|
||||||
- `description` (optional): Description of the journey (supports Liquid variables)
|
- `description` (optional): Description of the journey (supports Liquid variables)
|
||||||
- `guides` (required): Array of article paths that make up this journey
|
- `guides` (required): Array of guide objects that make up this journey. Each guide object has:
|
||||||
|
- `href` (required): Path to the article
|
||||||
|
- `alternativeNextStep` (optional): Custom text to guide users to alternative paths in the journey. Supports Liquid variables and `[AUTOTITLE]`.
|
||||||
- Only applicable when used with `layout: journey-landing`.
|
- Only applicable when used with `layout: journey-landing`.
|
||||||
- Optional.
|
- Optional.
|
||||||
|
|
||||||
@@ -271,15 +273,16 @@ journeyTracks:
|
|||||||
title: 'Getting started with {% data variables.product.prodname_actions %}'
|
title: 'Getting started with {% data variables.product.prodname_actions %}'
|
||||||
description: 'Learn the basics of GitHub Actions.'
|
description: 'Learn the basics of GitHub Actions.'
|
||||||
guides:
|
guides:
|
||||||
- '/actions/quickstart'
|
- href: '/actions/quickstart'
|
||||||
- '/actions/learn-github-actions'
|
- href: '/actions/learn-github-actions'
|
||||||
- '/actions/using-workflows'
|
alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/actions/using-workflows).'
|
||||||
|
- href: '/actions/using-workflows'
|
||||||
- id: 'advanced'
|
- id: 'advanced'
|
||||||
title: 'Advanced {% data variables.product.prodname_actions %}'
|
title: 'Advanced {% data variables.product.prodname_actions %}'
|
||||||
description: 'Dive deeper into advanced features.'
|
description: 'Dive deeper into advanced features.'
|
||||||
guides:
|
guides:
|
||||||
- '/actions/using-workflows/workflow-syntax-for-github-actions'
|
- href: '/actions/using-workflows/workflow-syntax-for-github-actions'
|
||||||
- '/actions/deployment/deploying-with-github-actions'
|
- href: '/actions/deployment/deploying-with-github-actions'
|
||||||
```
|
```
|
||||||
|
|
||||||
### `type`
|
### `type`
|
||||||
|
|||||||
@@ -257,7 +257,9 @@ includeGuides:
|
|||||||
* `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page.
|
* `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page.
|
||||||
* `title` (required): Display title for the journey (supports Liquid variables)
|
* `title` (required): Display title for the journey (supports Liquid variables)
|
||||||
* `description` (optional): Description of the journey (supports Liquid variables)
|
* `description` (optional): Description of the journey (supports Liquid variables)
|
||||||
* `guides` (required): Array of article paths that make up this journey
|
* `guides` (required): Array of guide objects that make up this journey. Each guide object has:
|
||||||
|
* `href` (required): Path to the article
|
||||||
|
* `alternativeNextStep` (optional): Custom text to guide users to alternative paths in the journey. Supports Liquid variables and `[AUTOTITLE]`.
|
||||||
* Only applicable when used with `layout: journey-landing`.
|
* Only applicable when used with `layout: journey-landing`.
|
||||||
* Optional.
|
* Optional.
|
||||||
|
|
||||||
@@ -269,15 +271,16 @@ journeyTracks:
|
|||||||
title: 'Getting started with {% data variables.product.prodname_actions %}'
|
title: 'Getting started with {% data variables.product.prodname_actions %}'
|
||||||
description: 'Learn the basics of GitHub Actions.'
|
description: 'Learn the basics of GitHub Actions.'
|
||||||
guides:
|
guides:
|
||||||
- '/actions/quickstart'
|
- href: '/actions/quickstart'
|
||||||
- '/actions/learn-github-actions'
|
- href: '/actions/learn-github-actions'
|
||||||
- '/actions/using-workflows'
|
alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/actions/using-workflows).'
|
||||||
|
- href: '/actions/using-workflows'
|
||||||
- id: 'advanced'
|
- id: 'advanced'
|
||||||
title: 'Advanced {% data variables.product.prodname_actions %}'
|
title: 'Advanced {% data variables.product.prodname_actions %}'
|
||||||
description: 'Dive deeper into advanced features.'
|
description: 'Dive deeper into advanced features.'
|
||||||
guides:
|
guides:
|
||||||
- '/actions/using-workflows/workflow-syntax-for-github-actions'
|
- href: '/actions/using-workflows/workflow-syntax-for-github-actions'
|
||||||
- '/actions/deployment/deploying-with-github-actions'
|
- href: '/actions/deployment/deploying-with-github-actions'
|
||||||
```
|
```
|
||||||
|
|
||||||
### `type`
|
### `type`
|
||||||
|
|||||||
@@ -16,56 +16,56 @@ journeyTracks:
|
|||||||
title: 'Getting started with your enterprise'
|
title: 'Getting started with your enterprise'
|
||||||
description: 'Master the fundamentals of {% data variables.product.prodname_ghe_cloud %} and get started with a trial.'
|
description: 'Master the fundamentals of {% data variables.product.prodname_ghe_cloud %} and get started with a trial.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/getting-started-with-your-enterprise/choose-an-enterprise-type'
|
- href: '/enterprise-onboarding/getting-started-with-your-enterprise/choose-an-enterprise-type'
|
||||||
- '/enterprise-onboarding/getting-started-with-your-enterprise/setting-up-a-trial-of-github-enterprise'
|
- href: '/enterprise-onboarding/getting-started-with-your-enterprise/setting-up-a-trial-of-github-enterprise'
|
||||||
- '/enterprise-onboarding/getting-started-with-your-enterprise/adding-users-to-your-enterprise'
|
- href: '/enterprise-onboarding/getting-started-with-your-enterprise/adding-users-to-your-enterprise'
|
||||||
- '/enterprise-onboarding/getting-started-with-your-enterprise/about-enterprise-billing'
|
- href: '/enterprise-onboarding/getting-started-with-your-enterprise/about-enterprise-billing'
|
||||||
- '/enterprise-onboarding/getting-started-with-your-enterprise/about-migrating-to-github-enterprise-cloud'
|
- href: '/enterprise-onboarding/getting-started-with-your-enterprise/about-migrating-to-github-enterprise-cloud'
|
||||||
- id: 'setting_up_organizations_and_teams'
|
- id: 'setting_up_organizations_and_teams'
|
||||||
title: 'Setting up organizations and teams in your enterprise'
|
title: 'Setting up organizations and teams in your enterprise'
|
||||||
description: 'Organize work effectively and ensure people have the access they need to resources and administrative settings.'
|
description: 'Organize work effectively and ensure people have the access they need to resources and administrative settings.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/best-practices'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/best-practices'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/setting-up-an-organization'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/setting-up-an-organization'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/about-roles-in-an-enterprise'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/about-roles-in-an-enterprise'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/identify-role-requirements'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/identify-role-requirements'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/creating-custom-roles'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/creating-custom-roles'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/about-teams-in-an-enterprise'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/about-teams-in-an-enterprise'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/creating-teams'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/creating-teams'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/assigning-roles-to-teams-and-users'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/assigning-roles-to-teams-and-users'
|
||||||
- '/enterprise-onboarding/setting-up-organizations-and-teams/use-innersource'
|
- href: '/enterprise-onboarding/setting-up-organizations-and-teams/use-innersource'
|
||||||
- id: 'support_for_your_enterprise'
|
- id: 'support_for_your_enterprise'
|
||||||
title: 'Creating a support model for your enterprise'
|
title: 'Creating a support model for your enterprise'
|
||||||
description: 'Find out how to get help and choose who will be able to contact Support.'
|
description: 'Find out how to get help and choose who will be able to contact Support.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/support-for-your-enterprise/understanding-support'
|
- href: '/enterprise-onboarding/support-for-your-enterprise/understanding-support'
|
||||||
- '/enterprise-onboarding/support-for-your-enterprise/using-the-support-portal'
|
- href: '/enterprise-onboarding/support-for-your-enterprise/using-the-support-portal'
|
||||||
- '/enterprise-onboarding/support-for-your-enterprise/managing-support-entitlements'
|
- href: '/enterprise-onboarding/support-for-your-enterprise/managing-support-entitlements'
|
||||||
- id: 'govern_people_and_repositories'
|
- id: 'govern_people_and_repositories'
|
||||||
title: 'Governing people and repositories'
|
title: 'Governing people and repositories'
|
||||||
description: 'Implement policies, custom properties, and rulesets to govern users and repositories across your enterprise.'
|
description: 'Implement policies, custom properties, and rulesets to govern users and repositories across your enterprise.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-policies'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-policies'
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/create-custom-properties'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/create-custom-properties'
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/create-repository-policies'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/create-repository-policies'
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/protect-branches'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/protect-branches'
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/using-the-audit-log-for-your-enterprise'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/using-the-audit-log-for-your-enterprise'
|
||||||
- '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-security'
|
- href: '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-security'
|
||||||
- id: 'github_apps'
|
- id: 'github_apps'
|
||||||
title: 'Automating processes with GitHub Apps'
|
title: 'Automating processes with GitHub Apps'
|
||||||
description: 'Create and install apps to automate processes securely in your enterprise and organizations.'
|
description: 'Create and install apps to automate processes securely in your enterprise and organizations.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/github-apps/create-enterprise-apps'
|
- href: '/enterprise-onboarding/github-apps/create-enterprise-apps'
|
||||||
- '/enterprise-onboarding/github-apps/install-enterprise-apps'
|
- href: '/enterprise-onboarding/github-apps/install-enterprise-apps'
|
||||||
- id: 'github_actions_for_your_enterprise'
|
- id: 'github_actions_for_your_enterprise'
|
||||||
title: 'Setting up CI/CD with GitHub Actions'
|
title: 'Setting up CI/CD with GitHub Actions'
|
||||||
description: 'Explore {% data variables.product.prodname_actions %}, plan your rollout, and get started.'
|
description: 'Explore {% data variables.product.prodname_actions %}, plan your rollout, and get started.'
|
||||||
guides:
|
guides:
|
||||||
- '/enterprise-onboarding/github-actions-for-your-enterprise/about-github-actions-for-enterprises'
|
- href: '/enterprise-onboarding/github-actions-for-your-enterprise/about-github-actions-for-enterprises'
|
||||||
- '/enterprise-onboarding/github-actions-for-your-enterprise/actions-components'
|
- href: '/enterprise-onboarding/github-actions-for-your-enterprise/actions-components'
|
||||||
- '/enterprise-onboarding/github-actions-for-your-enterprise/planning-a-rollout-of-github-actions'
|
- href: '/enterprise-onboarding/github-actions-for-your-enterprise/planning-a-rollout-of-github-actions'
|
||||||
- '/enterprise-onboarding/github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions'
|
- href: '/enterprise-onboarding/github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions'
|
||||||
- '/enterprise-onboarding/github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud'
|
- href: '/enterprise-onboarding/github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud'
|
||||||
versions:
|
versions:
|
||||||
ghec: '*'
|
ghec: '*'
|
||||||
topics:
|
topics:
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ journeyTracks:
|
|||||||
title: 'Get started'
|
title: 'Get started'
|
||||||
description: 'Master the fundamentals of {% data variables.product.github %} and Git.'
|
description: 'Master the fundamentals of {% data variables.product.github %} and Git.'
|
||||||
guides:
|
guides:
|
||||||
- '/get-started/start-your-journey/about-github-and-git'
|
- href: '/get-started/start-your-journey/about-github-and-git'
|
||||||
- '/get-started/start-your-journey/creating-an-account-on-github'
|
- href: '/get-started/start-your-journey/creating-an-account-on-github'
|
||||||
- '/get-started/start-your-journey/hello-world'
|
- href: '/get-started/start-your-journey/hello-world'
|
||||||
- '/get-started/start-your-journey/setting-up-your-profile'
|
- href: '/get-started/start-your-journey/setting-up-your-profile'
|
||||||
- '/get-started/start-your-journey/finding-inspiration-on-github'
|
- href: '/get-started/start-your-journey/finding-inspiration-on-github'
|
||||||
- '/get-started/start-your-journey/downloading-files-from-github'
|
- href: '/get-started/start-your-journey/downloading-files-from-github'
|
||||||
- '/get-started/start-your-journey/uploading-a-project-to-github'
|
- href: '/get-started/start-your-journey/uploading-a-project-to-github'
|
||||||
- '/get-started/start-your-journey/git-and-github-learning-resources'
|
- href: '/get-started/start-your-journey/git-and-github-learning-resources'
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -71,14 +71,19 @@ export const journeyTracksGuidePathExists = {
|
|||||||
const trackObj = track as Record<string, unknown>
|
const trackObj = track as Record<string, unknown>
|
||||||
if (trackObj.guides && Array.isArray(trackObj.guides)) {
|
if (trackObj.guides && Array.isArray(trackObj.guides)) {
|
||||||
for (let guideIndex = 0; guideIndex < trackObj.guides.length; guideIndex++) {
|
for (let guideIndex = 0; guideIndex < trackObj.guides.length; guideIndex++) {
|
||||||
const guide: string = trackObj.guides[guideIndex]
|
const guideObj = trackObj.guides[guideIndex]
|
||||||
if (typeof guide === 'string') {
|
|
||||||
if (!isValidGuidePath(guide, params.name)) {
|
// Validate guide is an object with expected properties
|
||||||
|
if (!guideObj || typeof guideObj !== 'object') continue
|
||||||
|
|
||||||
|
// Validate href property
|
||||||
|
if ('href' in guideObj && typeof guideObj.href === 'string') {
|
||||||
|
if (!isValidGuidePath(guideObj.href, params.name)) {
|
||||||
addError(
|
addError(
|
||||||
onError,
|
onError,
|
||||||
journeyTracksLineNumber,
|
journeyTracksLineNumber,
|
||||||
`Journey track guide path does not exist: ${guide} (track ${trackIndex + 1}, guide ${guideIndex + 1})`,
|
`Journey track guide path does not exist: ${guideObj.href} (track ${trackIndex + 1}, guide ${guideIndex + 1})`,
|
||||||
guide,
|
guideObj.href,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,16 +75,38 @@ export const journeyTracksLiquid = {
|
|||||||
|
|
||||||
if (track.guides && Array.isArray(track.guides)) {
|
if (track.guides && Array.isArray(track.guides)) {
|
||||||
for (let guideIndex = 0; guideIndex < track.guides.length; guideIndex++) {
|
for (let guideIndex = 0; guideIndex < track.guides.length; guideIndex++) {
|
||||||
const guide: string = track.guides[guideIndex]
|
const guideObj = track.guides[guideIndex]
|
||||||
if (typeof guide === 'string') {
|
|
||||||
|
// Validate guide is an object with expected properties
|
||||||
|
if (!guideObj || typeof guideObj !== 'object') continue
|
||||||
|
|
||||||
|
// Validate href property
|
||||||
|
if ('href' in guideObj && typeof guideObj.href === 'string') {
|
||||||
try {
|
try {
|
||||||
liquid.parse(guide)
|
liquid.parse(guideObj.href)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
addError(
|
addError(
|
||||||
onError,
|
onError,
|
||||||
trackLineNumber,
|
trackLineNumber,
|
||||||
`Invalid Liquid syntax in journey track guide (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`,
|
`Invalid Liquid syntax in journey track guide href (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`,
|
||||||
guide,
|
guideObj.href,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate alternativeNextStep property if present
|
||||||
|
if (
|
||||||
|
'alternativeNextStep' in guideObj &&
|
||||||
|
typeof guideObj.alternativeNextStep === 'string'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
liquid.parse(guideObj.alternativeNextStep)
|
||||||
|
} catch (error: any) {
|
||||||
|
addError(
|
||||||
|
onError,
|
||||||
|
trackLineNumber,
|
||||||
|
`Invalid Liquid syntax in journey track guide alternativeNextStep (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`,
|
||||||
|
guideObj.alternativeNextStep,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ journeyTracks:
|
|||||||
- id: duplicate-id
|
- id: duplicate-id
|
||||||
title: "First Track"
|
title: "First Track"
|
||||||
guides:
|
guides:
|
||||||
- /article-one
|
- href: /article-one
|
||||||
- id: unique-id
|
- id: unique-id
|
||||||
title: "Unique Track"
|
title: "Unique Track"
|
||||||
guides:
|
guides:
|
||||||
- /article-two
|
- href: /article-two
|
||||||
- id: duplicate-id
|
- id: duplicate-id
|
||||||
title: "Second Track with Same ID"
|
title: "Second Track with Same ID"
|
||||||
guides:
|
guides:
|
||||||
- /subdir/article-three
|
- href: /subdir/article-three
|
||||||
---
|
---
|
||||||
|
|
||||||
# Journey with Duplicate IDs
|
# Journey with Duplicate IDs
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ journeyTracks:
|
|||||||
- id: track-1
|
- id: track-1
|
||||||
title: "Track with Invalid Guides"
|
title: "Track with Invalid Guides"
|
||||||
guides:
|
guides:
|
||||||
- /article-one
|
- href: /article-one
|
||||||
- /nonexistent/guide
|
- href: /nonexistent/guide
|
||||||
- /another/invalid/path
|
- href: /another/invalid/path
|
||||||
---
|
---
|
||||||
|
|
||||||
# Journey with Invalid Paths
|
# Journey with Invalid Paths
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ journeyTracks:
|
|||||||
- id: track-1
|
- id: track-1
|
||||||
title: "Should be ignored"
|
title: "Should be ignored"
|
||||||
guides:
|
guides:
|
||||||
- /nonexistent/path
|
- href: /nonexistent/path
|
||||||
---
|
---
|
||||||
|
|
||||||
# Non-Journey Page
|
# Non-Journey Page
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ journeyTracks:
|
|||||||
- id: track-1
|
- id: track-1
|
||||||
title: "Getting Started Track"
|
title: "Getting Started Track"
|
||||||
guides:
|
guides:
|
||||||
- /article-one
|
- href: /article-one
|
||||||
- /article-two
|
- href: /article-two
|
||||||
- id: track-2
|
- id: track-2
|
||||||
title: "Advanced Track"
|
title: "Advanced Track"
|
||||||
guides:
|
guides:
|
||||||
- /subdir/article-three
|
- href: /subdir/article-three
|
||||||
---
|
---
|
||||||
|
|
||||||
# Valid Journey Landing
|
# Valid Journey Landing
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ journeyTracks:
|
|||||||
title: "Track with {% invalid liquid"
|
title: "Track with {% invalid liquid"
|
||||||
description: "Description with {{ unclosed liquid"
|
description: "Description with {{ unclosed liquid"
|
||||||
guides:
|
guides:
|
||||||
- /article-one
|
- href: /article-one
|
||||||
---
|
---
|
||||||
|
|
||||||
# Journey with Liquid Issues
|
# Journey with Liquid Issues
|
||||||
@@ -55,6 +55,36 @@ This journey landing page has invalid liquid syntax in journeyTracks.
|
|||||||
expect(result['test-invalid-liquid.md'][0].ruleDescription).toMatch(/liquid syntax/i)
|
expect(result['test-invalid-liquid.md'][0].ruleDescription).toMatch(/liquid syntax/i)
|
||||||
expect(result['test-invalid-liquid.md'][1].ruleDescription).toMatch(/liquid syntax/i)
|
expect(result['test-invalid-liquid.md'][1].ruleDescription).toMatch(/liquid syntax/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('invalid liquid syntax in alternativeNextStep fails', async () => {
|
||||||
|
const invalidAlternativeNextStepContent = `---
|
||||||
|
title: Journey with Invalid Alternative Next Step
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
journeyTracks:
|
||||||
|
- id: track-1
|
||||||
|
title: "Test Track"
|
||||||
|
guides:
|
||||||
|
- href: /article-one
|
||||||
|
alternativeNextStep: "Want to skip? See {% invalid liquid syntax"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Journey with Invalid Alternative Next Step
|
||||||
|
`
|
||||||
|
const result = await runRule(journeyTracksLiquid, {
|
||||||
|
strings: { 'test-invalid-alternative-next-step.md': invalidAlternativeNextStepContent },
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result['test-invalid-alternative-next-step.md']).toHaveLength(1)
|
||||||
|
expect(result['test-invalid-alternative-next-step.md'][0].errorDetail).toMatch(
|
||||||
|
/alternativeNextStep/,
|
||||||
|
)
|
||||||
|
expect(result['test-invalid-alternative-next-step.md'][0].errorDetail).toMatch(/liquid syntax/i)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('journey-tracks-guide-path-exists', () => {
|
describe('journey-tracks-guide-path-exists', () => {
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ children:
|
|||||||
- /page-with-permissions-and-product-callout
|
- /page-with-permissions-and-product-callout
|
||||||
- /table-with-ifversions
|
- /table-with-ifversions
|
||||||
- /code-snippet-with-hashbang
|
- /code-snippet-with-hashbang
|
||||||
|
- /journey-test-article
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Journey Test Article
|
||||||
|
intro: This article is used for testing journey branching text.
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghes: '*'
|
||||||
|
ghec: '*'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Article
|
||||||
|
|
||||||
|
This article exists solely for testing journey navigation features.
|
||||||
@@ -21,14 +21,14 @@ journeyTracks:
|
|||||||
title: 'Getting started'
|
title: 'Getting started'
|
||||||
description: 'Learn the basics of our platform.'
|
description: 'Learn the basics of our platform.'
|
||||||
guides:
|
guides:
|
||||||
- '/get-started/start-your-journey/hello-world'
|
- href: '/get-started/start-your-journey/hello-world'
|
||||||
- '/get-started/foo/bar'
|
- href: '/get-started/foo/bar'
|
||||||
- id: 'advanced'
|
- id: 'advanced'
|
||||||
title: 'Advanced topics'
|
title: 'Advanced topics'
|
||||||
description: 'Dive deeper into advanced features.'
|
description: 'Dive deeper into advanced features.'
|
||||||
guides:
|
guides:
|
||||||
- '/get-started/foo/autotitling'
|
- href: '/get-started/foo/autotitling'
|
||||||
- '/get-started/start-your-journey/hello-world'
|
- href: '/get-started/start-your-journey/hello-world'
|
||||||
children:
|
children:
|
||||||
- /start-your-journey
|
- /start-your-journey
|
||||||
- /foo
|
- /foo
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ versions:
|
|||||||
ghes: '*'
|
ghes: '*'
|
||||||
ghec: '*'
|
ghec: '*'
|
||||||
journeyTracks:
|
journeyTracks:
|
||||||
- id: 'getting_started'
|
- id: 'first_track'
|
||||||
title: 'Getting started'
|
title: 'First Track'
|
||||||
description: 'Learn the basics of our platform.'
|
description: 'The first track in the journey.'
|
||||||
guides:
|
guides:
|
||||||
- '/get-started/start-your-journey/hello-world'
|
- href: '/get-started/start-your-journey/hello-world'
|
||||||
- '/get-started/foo/bar'
|
- href: '/get-started/foo/journey-test-article'
|
||||||
- id: 'advanced'
|
alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/get-started/start-your-journey/hello-world)'
|
||||||
title: 'Advanced topics'
|
- id: 'second_track'
|
||||||
description: 'Dive deeper into advanced features.'
|
title: 'Next Track'
|
||||||
|
description: 'The second track in the journey.'
|
||||||
guides:
|
guides:
|
||||||
- '/get-started/foo/autotitling'
|
- href: '/get-started/foo/autotitling'
|
||||||
- '/get-started/start-your-journey/hello-world'
|
- href: '/get-started/start-your-journey/hello-world'
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a test page for journey tracks.
|
This is a test page for journey tracks.
|
||||||
|
|||||||
@@ -1139,6 +1139,53 @@ test.describe('Journey Tracks', () => {
|
|||||||
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')
|
||||||
|
|
||||||
|
// journey card should be visible in sidebar
|
||||||
|
const journeyCard = page.locator('[data-testid="journey-track-card"]')
|
||||||
|
await expect(journeyCard).toBeVisible()
|
||||||
|
|
||||||
|
// journey footer nav should be visible
|
||||||
|
const journeyNav = page.locator('[data-testid="journey-track-nav"]')
|
||||||
|
await expect(journeyNav).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('journey footer nav component links to first article in next track from last article in previous track', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/get-started/foo/bar')
|
||||||
|
|
||||||
|
const journeyNav = page.locator('[data-testid="journey-track-nav"]')
|
||||||
|
await expect(journeyNav).toBeVisible()
|
||||||
|
|
||||||
|
// Link should display the next track's title and go to its first article
|
||||||
|
const nextTrackLink = journeyNav.locator('a').filter({ hasText: 'Advanced topics' })
|
||||||
|
await expect(nextTrackLink).toBeVisible()
|
||||||
|
|
||||||
|
const href = await nextTrackLink.getAttribute('href')
|
||||||
|
expect(href).toContain('/get-started/foo/autotitling')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('journey card displays branching text when present', async ({ page }) => {
|
||||||
|
await page.goto('/get-started/foo/journey-test-article')
|
||||||
|
|
||||||
|
const journeyCard = page.locator('[data-testid="journey-track-card"]')
|
||||||
|
await expect(journeyCard).toBeVisible()
|
||||||
|
|
||||||
|
// Branching text should be rendered with markdown links
|
||||||
|
await expect(journeyCard).toContainText('Want to skip ahead?')
|
||||||
|
|
||||||
|
// AUTOTITLE should be resolved to actual article title
|
||||||
|
const branchingLink = journeyCard.locator('a').filter({ hasText: 'Hello World' })
|
||||||
|
await expect(branchingLink).toBeVisible()
|
||||||
|
await expect(journeyCard).not.toContainText('AUTOTITLE')
|
||||||
|
|
||||||
|
const href = await branchingLink.getAttribute('href')
|
||||||
|
expect(href).toContain('/get-started/start-your-journey/hello-world')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('LandingArticleGridWithFilter component', () => {
|
test.describe('LandingArticleGridWithFilter component', () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Link } from '@/frame/components/Link'
|
|||||||
import { useTranslation } from '@/languages/components/useTranslation'
|
import { useTranslation } from '@/languages/components/useTranslation'
|
||||||
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
|
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
|
||||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||||
|
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'
|
||||||
|
|
||||||
const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), {
|
const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -42,10 +43,12 @@ export const ArticlePage = () => {
|
|||||||
productVideoUrl,
|
productVideoUrl,
|
||||||
miniTocItems,
|
miniTocItems,
|
||||||
currentLearningTrack,
|
currentLearningTrack,
|
||||||
|
currentJourneyTrack,
|
||||||
supportPortalVaIframeProps,
|
supportPortalVaIframeProps,
|
||||||
currentLayout,
|
currentLayout,
|
||||||
} = useArticleContext()
|
} = useArticleContext()
|
||||||
const isLearningPath = !!currentLearningTrack?.trackName
|
const isLearningPath = !!currentLearningTrack?.trackName
|
||||||
|
const isJourneyTrack = !!currentJourneyTrack?.trackId
|
||||||
const { t } = useTranslation(['pages'])
|
const { t } = useTranslation(['pages'])
|
||||||
|
|
||||||
const introProp = (
|
const introProp = (
|
||||||
@@ -72,6 +75,7 @@ export const ArticlePage = () => {
|
|||||||
const toc = (
|
const toc = (
|
||||||
<>
|
<>
|
||||||
{isLearningPath && <LearningTrackCard track={currentLearningTrack} />}
|
{isLearningPath && <LearningTrackCard track={currentLearningTrack} />}
|
||||||
|
{isJourneyTrack && <JourneyTrackCard journey={currentJourneyTrack} />}
|
||||||
{miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
|
{miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -122,6 +126,11 @@ export const ArticlePage = () => {
|
|||||||
<LearningTrackNav track={currentLearningTrack} />
|
<LearningTrackNav track={currentLearningTrack} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isJourneyTrack ? (
|
||||||
|
<div className="container-lg mt-4 px-3">
|
||||||
|
<JourneyTrackNav context={currentJourneyTrack} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="container-xl px-3 px-md-6 my-4">
|
<div className="container-xl px-3 px-md-6 my-4">
|
||||||
@@ -148,6 +157,11 @@ export const ArticlePage = () => {
|
|||||||
<LearningTrackNav track={currentLearningTrack} />
|
<LearningTrackNav track={currentLearningTrack} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isJourneyTrack ? (
|
||||||
|
<div className="container-lg mt-4 px-3">
|
||||||
|
<JourneyTrackNav context={currentJourneyTrack} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
|||||||
@@ -247,7 +247,20 @@ export const schema: Schema = {
|
|||||||
guides: {
|
guides: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'object',
|
||||||
|
required: ['href'],
|
||||||
|
properties: {
|
||||||
|
href: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to the article in the journey track',
|
||||||
|
},
|
||||||
|
alternativeNextStep: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Optional branching text for the article when guiding users through the journey',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
description: 'Array of article paths that make up this journey track',
|
description: 'Array of article paths that make up this journey track',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ export function JourneyTrackCard({ journey }: Props) {
|
|||||||
const { locale } = useRouter()
|
const { locale } = useRouter()
|
||||||
const { currentVersion } = useVersion()
|
const { currentVersion } = useVersion()
|
||||||
const { t } = useTranslation('journey_track_nav')
|
const { t } = useTranslation('journey_track_nav')
|
||||||
const { trackTitle, journeyTitle, journeyPath, nextGuide, numberOfGuides, currentGuideIndex } =
|
const { trackTitle, journeyPath, nextGuide, numberOfGuides, currentGuideIndex } = journey
|
||||||
journey
|
const fullPath = `/${locale}/${currentVersion}${journeyPath}`
|
||||||
const fullPath = `/${locale}/${currentVersion}${journeyPath}?feature=journey-landing`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -25,10 +24,9 @@ export function JourneyTrackCard({ journey }: Props) {
|
|||||||
<div className="d-flex flex-column width-full">
|
<div className="d-flex flex-column width-full">
|
||||||
<h2 className="h4">
|
<h2 className="h4">
|
||||||
<Link href={fullPath} className="mb-1 text-underline">
|
<Link href={fullPath} className="mb-1 text-underline">
|
||||||
{journeyTitle}
|
{trackTitle}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<span className="f6 color-fg-muted mb-2">{trackTitle}</span>
|
|
||||||
<span className="f5 color-fg-muted">
|
<span className="f5 color-fg-muted">
|
||||||
{t('current_progress')
|
{t('current_progress')
|
||||||
.replace('{n}', `${numberOfGuides}`)
|
.replace('{n}', `${numberOfGuides}`)
|
||||||
@@ -49,6 +47,12 @@ export function JourneyTrackCard({ journey }: Props) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
{journey.alternativeNextStep && (
|
||||||
|
<div
|
||||||
|
className="mt-4"
|
||||||
|
dangerouslySetInnerHTML={{ __html: journey.alternativeNextStep }}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Props = {
|
|||||||
|
|
||||||
export function JourneyTrackNav({ context }: Props) {
|
export function JourneyTrackNav({ context }: Props) {
|
||||||
const { t } = useTranslation('journey_track_nav')
|
const { t } = useTranslation('journey_track_nav')
|
||||||
const { prevGuide, nextGuide, trackTitle, currentGuideIndex, numberOfGuides } = context
|
const { prevGuide, nextGuide, nextTrackFirstGuide } = context
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -26,24 +26,22 @@ export function JourneyTrackNav({ context }: Props) {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</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">
|
<span className="f5 d-flex flex-column flex-items-end">
|
||||||
{nextGuide && (
|
{nextGuide ? (
|
||||||
<>
|
<>
|
||||||
<span className="color-fg-default">{t('next_article')}</span>
|
<span className="color-fg-default">{t('next_article')}</span>
|
||||||
<Link href={nextGuide.href} className="text-bold color-fg text-right">
|
<Link href={nextGuide.href} className="text-bold color-fg text-right">
|
||||||
{nextGuide.title}
|
{nextGuide.title}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
) : nextTrackFirstGuide ? (
|
||||||
|
<>
|
||||||
|
<span className="color-fg-default">{t('next_article')}</span>
|
||||||
|
<Link href={nextTrackFirstGuide.href} className="text-bold color-fg text-right">
|
||||||
|
{nextTrackFirstGuide.trackTitle}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export interface JourneyContext {
|
|||||||
journeyPath: string
|
journeyPath: string
|
||||||
currentGuideIndex: number
|
currentGuideIndex: number
|
||||||
numberOfGuides: number
|
numberOfGuides: number
|
||||||
|
nextTrackFirstGuide?: {
|
||||||
|
href: string
|
||||||
|
title: string
|
||||||
|
trackTitle: string
|
||||||
|
}
|
||||||
nextGuide?: {
|
nextGuide?: {
|
||||||
href: string
|
href: string
|
||||||
title: string
|
title: string
|
||||||
@@ -21,6 +26,7 @@ export interface JourneyContext {
|
|||||||
href: string
|
href: string
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
alternativeNextStep?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JourneyTrack {
|
export interface JourneyTrack {
|
||||||
@@ -43,7 +49,10 @@ type JourneyPage = {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
guides: string[]
|
guides: Array<{
|
||||||
|
href: string
|
||||||
|
alternativeNextStep?: string
|
||||||
|
}>
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +92,32 @@ function normalizeGuidePath(path: string): string {
|
|||||||
: `/${withoutLanguage || path}`
|
: `/${withoutLanguage || path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to fetch guide data (href and title) for a given path
|
||||||
|
*/
|
||||||
|
async function fetchGuideData(
|
||||||
|
guidePath: string,
|
||||||
|
context: ContentContext,
|
||||||
|
): Promise<{ href: string; title: string } | null> {
|
||||||
|
try {
|
||||||
|
const resultData = await getLinkData(guidePath, context, {
|
||||||
|
title: true,
|
||||||
|
intro: false,
|
||||||
|
fullTitle: false,
|
||||||
|
})
|
||||||
|
if (resultData && resultData.length > 0) {
|
||||||
|
const linkResult = resultData[0]
|
||||||
|
return {
|
||||||
|
href: linkResult.href,
|
||||||
|
title: linkResult.title || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not get link data for guide:', guidePath, error)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the journey context for a given article path.
|
* Resolves the journey context for a given article path.
|
||||||
*
|
*
|
||||||
@@ -117,6 +152,8 @@ export async function resolveJourneyContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let trackIndex = 0
|
||||||
|
let foundTrackIndex = 0
|
||||||
for (const track of journeyPage.journeyTracks) {
|
for (const track of journeyPage.journeyTracks) {
|
||||||
if (!track.guides || !Array.isArray(track.guides)) continue
|
if (!track.guides || !Array.isArray(track.guides)) continue
|
||||||
|
|
||||||
@@ -124,7 +161,7 @@ export async function resolveJourneyContext(
|
|||||||
let guideIndex = -1
|
let guideIndex = -1
|
||||||
|
|
||||||
for (let i = 0; i < track.guides.length; i++) {
|
for (let i = 0; i < track.guides.length; i++) {
|
||||||
const guidePath = track.guides[i]
|
const guidePath = track.guides[i].href
|
||||||
let renderedGuidePath = guidePath
|
let renderedGuidePath = guidePath
|
||||||
|
|
||||||
// Handle Liquid conditionals in guide paths
|
// Handle Liquid conditionals in guide paths
|
||||||
@@ -148,6 +185,21 @@ export async function resolveJourneyContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (guideIndex >= 0) {
|
if (guideIndex >= 0) {
|
||||||
|
const alternativeNextStep = track.guides[guideIndex].alternativeNextStep || ''
|
||||||
|
let renderedAlternativeNextStep = alternativeNextStep
|
||||||
|
|
||||||
|
// Handle Liquid conditionals in branching text which likely has links
|
||||||
|
try {
|
||||||
|
renderedAlternativeNextStep = await executeWithFallback(
|
||||||
|
context,
|
||||||
|
() => renderContent(alternativeNextStep, context),
|
||||||
|
() => alternativeNextStep,
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// If rendering fails, use the original branching text rather than erroring
|
||||||
|
renderedAlternativeNextStep = alternativeNextStep
|
||||||
|
}
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
trackId: track.id,
|
trackId: track.id,
|
||||||
trackName: track.id,
|
trackName: track.id,
|
||||||
@@ -157,52 +209,51 @@ export async function resolveJourneyContext(
|
|||||||
journeyPage.permalink || Permalink.relativePathToSuffix(journeyPage.relativePath || ''),
|
journeyPage.permalink || Permalink.relativePathToSuffix(journeyPage.relativePath || ''),
|
||||||
currentGuideIndex: guideIndex,
|
currentGuideIndex: guideIndex,
|
||||||
numberOfGuides: track.guides.length,
|
numberOfGuides: track.guides.length,
|
||||||
|
alternativeNextStep: renderedAlternativeNextStep,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up previous guide
|
// Set up previous guide
|
||||||
if (guideIndex > 0) {
|
if (guideIndex > 0) {
|
||||||
const prevGuidePath = track.guides[guideIndex - 1]
|
const prevGuidePath = track.guides[guideIndex - 1].href
|
||||||
try {
|
const guideData = await fetchGuideData(prevGuidePath, context)
|
||||||
const resultData = await getLinkData(prevGuidePath, context, {
|
if (guideData) {
|
||||||
title: true,
|
result.prevGuide = guideData
|
||||||
intro: false,
|
|
||||||
fullTitle: false,
|
|
||||||
})
|
|
||||||
if (resultData && resultData.length > 0) {
|
|
||||||
const linkResult = resultData[0]
|
|
||||||
result.prevGuide = {
|
|
||||||
href: linkResult.href,
|
|
||||||
title: linkResult.title || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not get link data for previous guide:', prevGuidePath, error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up next guide
|
// Set up next guide
|
||||||
if (guideIndex < track.guides.length - 1) {
|
if (guideIndex < track.guides.length - 1) {
|
||||||
const nextGuidePath = track.guides[guideIndex + 1]
|
const nextGuidePath = track.guides[guideIndex + 1].href
|
||||||
try {
|
const guideData = await fetchGuideData(nextGuidePath, context)
|
||||||
const resultData = await getLinkData(nextGuidePath, context, {
|
if (guideData) {
|
||||||
title: true,
|
result.nextGuide = guideData
|
||||||
intro: false,
|
}
|
||||||
fullTitle: false,
|
}
|
||||||
})
|
|
||||||
if (resultData && resultData.length > 0) {
|
// Only populate nextTrackFirstGuide when on the last guide of the track
|
||||||
const linkResult = resultData[0]
|
if (guideIndex === track.guides.length - 1) {
|
||||||
result.nextGuide = {
|
foundTrackIndex = trackIndex
|
||||||
href: linkResult.href,
|
|
||||||
title: linkResult.title || '',
|
if (
|
||||||
|
journeyPage.journeyTracks[foundTrackIndex + 1] &&
|
||||||
|
journeyPage.journeyTracks[foundTrackIndex + 1].guides.length > 0
|
||||||
|
) {
|
||||||
|
const nextTrack = journeyPage.journeyTracks[foundTrackIndex + 1]
|
||||||
|
const nextTrackFirstGuidePath = nextTrack.guides[0].href
|
||||||
|
const guideData = await fetchGuideData(nextTrackFirstGuidePath, context)
|
||||||
|
if (guideData) {
|
||||||
|
result.nextTrackFirstGuide = {
|
||||||
|
...guideData,
|
||||||
|
trackTitle: nextTrack.title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not get link data for next guide:', nextGuidePath, error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break // Found the track, stop searching
|
break // Found the track, stop searching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) break // Found the journey, stop searching
|
if (result) break // Found the journey, stop searching
|
||||||
@@ -217,11 +268,15 @@ export async function resolveJourneyContext(
|
|||||||
* Returns an array of JourneyTrack objects with titles, descriptions, and guide links.
|
* Returns an array of JourneyTrack objects with titles, descriptions, and guide links.
|
||||||
*/
|
*/
|
||||||
export async function resolveJourneyTracks(
|
export async function resolveJourneyTracks(
|
||||||
journeyTracks: any[],
|
journeyTracks: JourneyPage['journeyTracks'],
|
||||||
context: ContentContext,
|
context: ContentContext,
|
||||||
): Promise<JourneyTrack[]> {
|
): Promise<JourneyTrack[]> {
|
||||||
|
if (!journeyTracks || journeyTracks.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
journeyTracks.map(async (track: any) => {
|
journeyTracks.map(async (track) => {
|
||||||
// Render Liquid templates in title and description
|
// Render Liquid templates in title and description
|
||||||
const renderedTitle = await renderContent(track.title, context, { textOnly: true })
|
const renderedTitle = await renderContent(track.title, context, { textOnly: true })
|
||||||
const renderedDescription = track.description
|
const renderedDescription = track.description
|
||||||
@@ -229,9 +284,9 @@ export async function resolveJourneyTracks(
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const guides = await Promise.all(
|
const guides = await Promise.all(
|
||||||
track.guides.map(async (guidePath: string) => {
|
track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => {
|
||||||
const linkData = await getLinkData(guidePath, context, { title: true })
|
const linkData = await getLinkData(guide.href, context, { title: true })
|
||||||
const baseHref = linkData?.[0]?.href || guidePath
|
const baseHref = linkData?.[0]?.href || guide.href
|
||||||
return {
|
return {
|
||||||
href: baseHref,
|
href: baseHref,
|
||||||
title: linkData?.[0]?.title || 'Untitled Guide',
|
title: linkData?.[0]?.title || 'Untitled Guide',
|
||||||
|
|||||||
@@ -9,29 +9,21 @@ export default async function journeyTrack(
|
|||||||
if (!req.context) throw new Error('request is not contextualized')
|
if (!req.context) throw new Error('request is not contextualized')
|
||||||
if (!req.context.page) return next()
|
if (!req.context.page) return next()
|
||||||
|
|
||||||
// Only run journey resolution if the page has journey tracks defined
|
|
||||||
if (!(req.context.page as any).journeyTracks) {
|
|
||||||
req.context.currentJourneyTrack = null
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Import and use the journey resolver which uses renderContent, need the
|
|
||||||
// async import since it uses fs Node apis
|
|
||||||
const journeyResolver = await import('../lib/journey-path-resolver')
|
const journeyResolver = await import('../lib/journey-path-resolver')
|
||||||
|
|
||||||
// resolve the journey tracks which renders the journey content like the
|
// If this page has journey tracks defined, resolve them for the landing page
|
||||||
// description to handle liquid rendering
|
if ((req.context.page as any).journeyTracks) {
|
||||||
const resolvedTracks = await journeyResolver.resolveJourneyTracks(
|
const resolvedTracks = await journeyResolver.resolveJourneyTracks(
|
||||||
(req.context.page as any).journeyTracks,
|
(req.context.page as any).journeyTracks,
|
||||||
req.context,
|
req.context,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store resolved tracks on the page context for later use in getServerSideProps
|
// Store resolved tracks on the page context for later use in getServerSideProps
|
||||||
;(req.context.page as any).resolvedJourneyTracks = resolvedTracks
|
;(req.context.page as any).resolvedJourneyTracks = resolvedTracks
|
||||||
|
}
|
||||||
|
|
||||||
// resolve the current journey context since we're on a journey track page
|
// Always try to resolve journey context (for navigation on guide articles)
|
||||||
// i.e. next/prev articles in the track, this article's position in the track
|
|
||||||
const journeyContext = await journeyResolver.resolveJourneyContext(
|
const journeyContext = await journeyResolver.resolveJourneyContext(
|
||||||
req.pagePath || '',
|
req.pagePath || '',
|
||||||
req.context.pages || {},
|
req.context.pages || {},
|
||||||
|
|||||||
@@ -46,11 +46,21 @@ describe('journey-path-resolver', () => {
|
|||||||
title: 'Getting started',
|
title: 'Getting started',
|
||||||
description: 'Learn the basics',
|
description: 'Learn the basics',
|
||||||
guides: [
|
guides: [
|
||||||
'/enterprise-onboarding/setup',
|
{ href: '/enterprise-onboarding/setup' },
|
||||||
'/enterprise-onboarding/config',
|
{
|
||||||
'/enterprise-onboarding/deploy',
|
href: '/enterprise-onboarding/config',
|
||||||
|
alternativeNextStep:
|
||||||
|
'Ready for more? Visit [AUTOTITLE](/enterprise-onboarding/advanced-setup)',
|
||||||
|
},
|
||||||
|
{ href: '/enterprise-onboarding/deploy' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'advanced',
|
||||||
|
title: 'Advanced configuration',
|
||||||
|
description: 'Configure advanced options',
|
||||||
|
guides: [{ href: '/enterprise-onboarding/advanced-setup' }],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -100,6 +110,28 @@ describe('journey-path-resolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('includes alternative next step when provided', async () => {
|
||||||
|
const result = await resolveJourneyContext(
|
||||||
|
'/enterprise-onboarding/config',
|
||||||
|
mockPages,
|
||||||
|
mockContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result?.alternativeNextStep).toBe(
|
||||||
|
'Ready for more? Visit [AUTOTITLE](/enterprise-onboarding/advanced-setup)',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not populate next track guide when not on last guide', async () => {
|
||||||
|
const result = await resolveJourneyContext(
|
||||||
|
'/enterprise-onboarding/config',
|
||||||
|
mockPages,
|
||||||
|
mockContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result?.nextTrackFirstGuide).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test('handles first article in track (no previous)', async () => {
|
test('handles first article in track (no previous)', async () => {
|
||||||
const result = await resolveJourneyContext(
|
const result = await resolveJourneyContext(
|
||||||
'/enterprise-onboarding/setup',
|
'/enterprise-onboarding/setup',
|
||||||
@@ -122,6 +154,20 @@ describe('journey-path-resolver', () => {
|
|||||||
expect(result?.currentGuideIndex).toBe(2)
|
expect(result?.currentGuideIndex).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('populates next track guide when on last guide', async () => {
|
||||||
|
const result = await resolveJourneyContext(
|
||||||
|
'/enterprise-onboarding/deploy',
|
||||||
|
mockPages,
|
||||||
|
mockContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result?.nextTrackFirstGuide).toEqual({
|
||||||
|
href: '/en/enterprise-cloud@latest/enterprise-onboarding/advanced-setup',
|
||||||
|
title: 'Mock Title for /enterprise-onboarding/advanced-setup',
|
||||||
|
trackTitle: 'Advanced configuration',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('normalizes article paths without leading slash', async () => {
|
test('normalizes article paths without leading slash', async () => {
|
||||||
// The resolver should handle paths without leading slashes
|
// The resolver should handle paths without leading slashes
|
||||||
// by normalizing them to match the guide paths in the data
|
// by normalizing them to match the guide paths in the data
|
||||||
@@ -149,13 +195,16 @@ describe('journey-path-resolver', () => {
|
|||||||
id: 'getting_started',
|
id: 'getting_started',
|
||||||
title: 'Getting started with {% data variables.product.company_short %}',
|
title: 'Getting started with {% data variables.product.company_short %}',
|
||||||
description: 'Learn the {% data variables.product.company_short %} basics',
|
description: 'Learn the {% data variables.product.company_short %} basics',
|
||||||
guides: ['/enterprise-onboarding/setup', '/enterprise-onboarding/config'],
|
guides: [
|
||||||
|
{ href: '/enterprise-onboarding/setup' },
|
||||||
|
{ href: '/enterprise-onboarding/config' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'advanced',
|
id: 'advanced',
|
||||||
title: 'Advanced configuration',
|
title: 'Advanced configuration',
|
||||||
description: 'Advanced topics for experts',
|
description: 'Advanced topics for experts',
|
||||||
guides: ['/enterprise-onboarding/advanced-setup'],
|
guides: [{ href: '/enterprise-onboarding/advanced-setup' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -210,7 +259,7 @@ describe('journey-path-resolver', () => {
|
|||||||
{
|
{
|
||||||
id: 'no_desc',
|
id: 'no_desc',
|
||||||
title: 'Track without description',
|
title: 'Track without description',
|
||||||
guides: ['/some-guide'],
|
guides: [{ href: '/some-guide' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user