diff --git a/.github/workflows/package-lock-lint.yml b/.github/workflows/package-lock-lint.yml
index b912a11cbb2e..1b9581f94536 100644
--- a/.github/workflows/package-lock-lint.yml
+++ b/.github/workflows/package-lock-lint.yml
@@ -37,6 +37,9 @@ jobs:
run: |
npm --version
+ # Save the current top-level dependencies from package-lock.json
+ node -e "console.log(JSON.stringify(require('./package-lock.json').packages['']))" > /tmp/before.json
+
# From https://docs.npmjs.com/cli/v7/commands/npm-install
#
# The --package-lock-only argument will only update the
@@ -45,9 +48,16 @@ jobs:
#
npm install --package-lock-only --ignore-scripts --include=optional
- # If the package.json (dependencies and devDependencies) is
- # in correct sync with package-lock.json running the above command
- # should *not* make an edit to the package-lock.json. I.e.
- # running `git status` should
- # say "nothing to commit, working tree clean".
- git diff --exit-code
+ # Extract the top-level dependencies after regeneration
+ node -e "console.log(JSON.stringify(require('./package-lock.json').packages['']))" > /tmp/after.json
+
+ # Compare only the top-level package dependencies
+ # This ignores platform-specific differences in nested dependency resolution
+ # (like "peer" flags) that don't affect actual installed versions
+ if ! diff /tmp/before.json /tmp/after.json; then
+ echo "ERROR: Top-level dependencies in package-lock.json are out of sync with package.json"
+ echo "Please run 'npm install' locally and commit the updated package-lock.json"
+ exit 1
+ fi
+
+ echo "✓ Top-level dependencies are in sync"
diff --git a/data/reusables/copilot/ccr-model-settings.md b/data/reusables/copilot/ccr-model-settings.md
index 2840cf0449be..50ad303d5cc1 100644
--- a/data/reusables/copilot/ccr-model-settings.md
+++ b/data/reusables/copilot/ccr-model-settings.md
@@ -1,2 +1,4 @@
> [!NOTE]
-> {% data variables.copilot.copilot_code-review_short %} may use models that are not enabled on your organization's "Models" settings page. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies).
+> {% data variables.copilot.copilot_code-review_short %} may use models that are not enabled on your organization's "Models" settings page. The "Models" settings page only controls {% data variables.copilot.copilot_chat_short %}.
+>
+> Since {% data variables.copilot.copilot_code-review_short %} is generally available, all model usage will be subject to the generally available terms. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies).
diff --git a/eslint-plugins.d.ts b/eslint-plugins.d.ts
new file mode 100644
index 000000000000..acbb6a189037
--- /dev/null
+++ b/eslint-plugins.d.ts
@@ -0,0 +1,57 @@
+// Type declarations for ESLint plugins without official TypeScript definitions
+
+declare module 'eslint-plugin-github' {
+ import type { ESLint, Linter } from 'eslint'
+
+ const plugin: ESLint.Plugin & {
+ configs: {
+ recommended: Linter.FlatConfig
+ }
+ }
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-primer-react' {
+ import type { ESLint, Linter } from 'eslint'
+
+ const plugin: ESLint.Plugin & {
+ configs: {
+ recommended: Linter.FlatConfig
+ }
+ }
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-eslint-comments' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-i18n-text' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-filenames' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-no-only-tests' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
diff --git a/eslint.config.js b/eslint.config.ts
similarity index 65%
rename from eslint.config.js
rename to eslint.config.ts
index 8fa152a4b420..aeb30e33a9ad 100644
--- a/eslint.config.js
+++ b/eslint.config.ts
@@ -14,89 +14,6 @@ import prettier from 'eslint-config-prettier'
import globals from 'globals'
export default [
- // JavaScript and MJS files configuration
- {
- files: ['**/*.{js,mjs}'],
- languageOptions: {
- ecmaVersion: 2022,
- sourceType: 'module',
- globals: {
- ...globals.browser,
- ...globals.node,
- ...globals.commonjs,
- ...globals.es2020,
- },
- parserOptions: {
- requireConfigFile: false,
- },
- },
- settings: {
- 'import/resolver': {
- typescript: true,
- node: true,
- },
- },
- plugins: {
- github,
- import: importPlugin,
- 'eslint-comments': eslintComments,
- 'i18n-text': i18nText,
- filenames,
- 'no-only-tests': noOnlyTests,
- prettier: prettierPlugin,
- },
- rules: {
- // ESLint recommended rules
- ...js.configs.recommended.rules,
-
- // GitHub plugin recommended rules
- ...github.configs.recommended.rules,
-
- // Import plugin error rules
- ...importPlugin.configs.errors.rules,
-
- // JavaScript-specific overrides
- 'import/no-extraneous-dependencies': [
- 'error',
- {
- packageDir: '.',
- },
- ],
- 'import/extensions': 'off',
- 'no-console': 'off',
- camelcase: 'off',
- 'no-shadow': 'off',
- 'prefer-template': 'off',
- 'no-constant-condition': 'off',
- 'no-unused-vars': 'off',
- 'import/no-named-as-default-member': 'off',
- 'one-var': 'off',
- 'import/no-namespace': 'off',
- 'import/no-anonymous-default-export': 'off',
- 'object-shorthand': 'off',
- 'no-empty': 'off',
- 'prefer-const': 'off',
- 'import/no-named-as-default': 'off',
- 'no-useless-concat': 'off',
- 'func-style': 'off',
-
- // Disable GitHub plugin rules that were disabled in original config
- 'github/array-foreach': 'off',
- 'github/no-then': 'off',
-
- // Disable rules that might not exist or cause issues initially
- 'i18n-text/no-en': 'off',
- 'filenames/match-regex': 'off',
- 'eslint-comments/no-use': 'off',
- 'eslint-comments/no-unused-disable': 'off',
- 'eslint-comments/no-unlimited-disable': 'off',
-
- // Disable new ESLint 9 rules that are causing issues
- 'no-constant-binary-expression': 'off',
- },
- },
-
- // TypeScript and TSX files configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
diff --git a/package-lock.json b/package-lock.json
index d3f93f38b24f..2a7d689f08f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -116,6 +116,7 @@
"@types/connect-timeout": "1.9.0",
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.8",
+ "@types/eslint-plugin-jsx-a11y": "^6.10.1",
"@types/event-to-promise": "^0.7.5",
"@types/express": "^5.0.3",
"@types/imurmurhash": "^0.1.4",
@@ -156,6 +157,7 @@
"graphql": "^16.9.0",
"http-status-code": "^2.1.0",
"husky": "^9.1.7",
+ "jiti": "^2.6.1",
"json-schema-merge-allof": "^0.8.1",
"lint-staged": "^16.0.0",
"markdownlint": "^0.34.0",
@@ -4063,6 +4065,16 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/eslint-plugin-jsx-a11y": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz",
+ "integrity": "sha512-5RtuPVe0xz8BAhrkn2oww6Uw885atf962Q4fqZo48QdO3EQA7oCEDSXa6optgJ1ZMds3HD9ITK5bfm4AWuoXFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint": "^9"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -10183,6 +10195,15 @@
"version": "2.1.0",
"license": "MIT"
},
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
diff --git a/package.json b/package.json
index a2e3b945928b..eae47fc34dcc 100644
--- a/package.json
+++ b/package.json
@@ -258,6 +258,7 @@
"@types/connect-timeout": "1.9.0",
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.8",
+ "@types/eslint-plugin-jsx-a11y": "^6.10.1",
"@types/event-to-promise": "^0.7.5",
"@types/express": "^5.0.3",
"@types/imurmurhash": "^0.1.4",
@@ -298,6 +299,7 @@
"graphql": "^16.9.0",
"http-status-code": "^2.1.0",
"husky": "^9.1.7",
+ "jiti": "^2.6.1",
"json-schema-merge-allof": "^0.8.1",
"lint-staged": "^16.0.0",
"markdownlint": "^0.34.0",
diff --git a/src/ai-tools/scripts/ai-tools.ts b/src/ai-tools/scripts/ai-tools.ts
index 0d2cc4b25f37..0237c017b2c7 100644
--- a/src/ai-tools/scripts/ai-tools.ts
+++ b/src/ai-tools/scripts/ai-tools.ts
@@ -1,5 +1,3 @@
-#!/usr/bin/env node
-
import { fileURLToPath } from 'url'
import { Command } from 'commander'
import fs from 'fs'
diff --git a/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts
new file mode 100644
index 000000000000..e7cd45bd8b17
--- /dev/null
+++ b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts
@@ -0,0 +1,89 @@
+import fs from 'fs'
+import path from 'path'
+// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
+import { addError } from 'markdownlint-rule-helpers'
+
+import { getFrontmatter } from '../helpers/utils'
+import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'
+
+interface Frontmatter {
+ heroImage?: string
+ [key: string]: any
+}
+
+// Get the list of valid hero images
+function getValidHeroImages(): string[] {
+ const ROOT = process.env.ROOT || '.'
+ const heroImageDir = path.join(ROOT, 'assets/images/banner-images')
+
+ try {
+ if (!fs.existsSync(heroImageDir)) {
+ return []
+ }
+
+ const files = fs.readdirSync(heroImageDir)
+ // Return absolute paths as they would appear in frontmatter
+ return files.map((file) => `/assets/images/banner-images/${file}`)
+ } catch {
+ return []
+ }
+}
+
+export const frontmatterHeroImage: Rule = {
+ names: ['GHD061', 'frontmatter-hero-image'],
+ description:
+ 'Hero image paths must be absolute and point to valid images in /assets/images/banner-images/',
+ tags: ['frontmatter', 'images'],
+ function: (params: RuleParams, onError: RuleErrorCallback) => {
+ // Only check index.md files
+ if (!params.name.endsWith('index.md')) return
+
+ const fm = getFrontmatter(params.lines) as Frontmatter | null
+ if (!fm || !fm.heroImage) return
+
+ const heroImage = fm.heroImage
+
+ // Check if heroImage is an absolute path
+ if (!heroImage.startsWith('/')) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ addError(
+ onError,
+ lineNumber,
+ `Hero image path must be absolute (start with /). Found: ${heroImage}`,
+ line || '',
+ null, // No fix possible
+ )
+ return
+ }
+
+ // Check if heroImage points to banner-images directory
+ if (!heroImage.startsWith('/assets/images/banner-images/')) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ addError(
+ onError,
+ lineNumber,
+ `Hero image must point to /assets/images/banner-images/. Found: ${heroImage}`,
+ line || '',
+ null, // No fix possible
+ )
+ return
+ }
+
+ // Check if the file actually exists
+ const validHeroImages = getValidHeroImages()
+ if (validHeroImages.length > 0 && !validHeroImages.includes(heroImage)) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ const availableImages = validHeroImages.join(', ')
+ addError(
+ onError,
+ lineNumber,
+ `Hero image file does not exist: ${heroImage}. Available images: ${availableImages}`,
+ line || '',
+ null, // No fix possible
+ )
+ }
+ },
+}
diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts
index eaa9706da08f..d3cabfa4489d 100644
--- a/src/content-linter/lib/linting-rules/index.ts
+++ b/src/content-linter/lib/linting-rules/index.ts
@@ -61,6 +61,7 @@ import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema'
import { journeyTracksLiquid } from './journey-tracks-liquid'
import { journeyTracksGuidePathExists } from './journey-tracks-guide-path-exists'
import { journeyTracksUniqueIds } from './journey-tracks-unique-ids'
+import { frontmatterHeroImage } from './frontmatter-hero-image'
// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
// The elements in the array have a 'names' property that contains rule identifiers
@@ -130,6 +131,7 @@ export const gitHubDocsMarkdownlint = {
journeyTracksLiquid, // GHD058
journeyTracksGuidePathExists, // GHD059
journeyTracksUniqueIds, // GHD060
+ frontmatterHeroImage, // GHD061
// Search-replace rules
searchReplace, // Open-source plugin
diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts
index 8d53d4631fcb..eddf742b46e6 100644
--- a/src/content-linter/style/github-docs.ts
+++ b/src/content-linter/style/github-docs.ts
@@ -348,6 +348,12 @@ export const githubDocsFrontmatterConfig = {
'partial-markdown-files': false,
'yml-files': false,
},
+ 'frontmatter-hero-image': {
+ // GHD061
+ severity: 'error',
+ 'partial-markdown-files': false,
+ 'yml-files': false,
+ },
}
// Configures rules from the `github/markdownlint-github` repo
diff --git a/src/content-linter/tests/unit/frontmatter-hero-image.ts b/src/content-linter/tests/unit/frontmatter-hero-image.ts
new file mode 100644
index 000000000000..ac3096c70385
--- /dev/null
+++ b/src/content-linter/tests/unit/frontmatter-hero-image.ts
@@ -0,0 +1,128 @@
+import { describe, expect, test } from 'vitest'
+
+import { runRule } from '../../lib/init-test'
+import { frontmatterHeroImage } from '../../lib/linting-rules/frontmatter-hero-image'
+
+const fmOptions = { markdownlintOptions: { frontMatter: null } }
+
+describe(frontmatterHeroImage.names.join(' - '), () => {
+ test('valid absolute heroImage path passes', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/banner-images/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('non-index.md file is ignored', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: 'invalid-path.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/article.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/article.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('missing heroImage is ignored', async () => {
+ const markdown = ['---', 'title: Test', '---', '', '# Test'].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('relative heroImage path fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: 'images/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('must be absolute')
+ })
+
+ test('non-banner-images path fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/other/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('/assets/images/banner-images/')
+ })
+
+ test('non-existent heroImage file fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/banner-images/non-existent.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('does not exist')
+ })
+
+ test('all valid hero images pass', async () => {
+ // Test each valid hero image
+ const validImages = [
+ "heroImage: '/assets/images/banner-images/hero-1.png'",
+ "heroImage: '/assets/images/banner-images/hero-2.png'",
+ "heroImage: '/assets/images/banner-images/hero-3.png'",
+ "heroImage: '/assets/images/banner-images/hero-4.png'",
+ "heroImage: '/assets/images/banner-images/hero-5.png'",
+ "heroImage: '/assets/images/banner-images/hero-6.png'",
+ ]
+
+ for (const heroImageLine of validImages) {
+ const markdown = ['---', 'title: Test', heroImageLine, '---', '', '# Test'].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ }
+ })
+})
diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts
index 17776def96ac..99bd97eb2684 100644
--- a/src/fixtures/tests/playwright-rendering.spec.ts
+++ b/src/fixtures/tests/playwright-rendering.spec.ts
@@ -1138,22 +1138,6 @@ test.describe('Journey Tracks', () => {
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()
- }
- })
})
test.describe('LandingArticleGridWithFilter component', () => {
diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx
index b6d42702003a..89ef49ed8842 100644
--- a/src/frame/components/article/ArticlePage.tsx
+++ b/src/frame/components/article/ArticlePage.tsx
@@ -7,8 +7,6 @@ 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'
@@ -44,14 +42,10 @@ 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 = (
@@ -78,7 +72,6 @@ export const ArticlePage = () => {
const toc = (
<>
{isLearningPath &&
Test intro
', - href: '/en/copilot/tutorials/article', + href: '/copilot/tutorials/article', category: ['copilot', 'tutorials'], }, ]) @@ -195,7 +195,7 @@ describe('resolveRecommended middleware', () => { { title: 'Valid Article', intro: 'Valid intro
', - href: '/en/test/valid', + href: '/test/valid', category: ['test'], }, ]) @@ -256,10 +256,64 @@ describe('resolveRecommended middleware', () => { { title: 'Relative Article', intro: 'Relative intro
', - href: '/en/copilot/relative-article', + href: '/copilot/relative-article', // Updated to clean path category: ['copilot'], }, ]) expect(mockNext).toHaveBeenCalled() }) + + test('returns paths without language or version prefixes', async () => { + const testPage: PartialTutorial intro
', + href: '/copilot/tutorials/tutorial-page', + category: ['copilot', 'tutorials', 'tutorial-page'], + }, + ]) + expect(mockNext).toHaveBeenCalled() + }) }) diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts index f2a69d4265cc..1bc9b22088bc 100644 --- a/src/journeys/lib/journey-path-resolver.ts +++ b/src/journeys/lib/journey-path-resolver.ts @@ -83,24 +83,6 @@ function normalizeGuidePath(path: string): string { : `/${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. * @@ -189,7 +171,7 @@ export async function resolveJourneyContext( if (resultData && resultData.length > 0) { const linkResult = resultData[0] result.prevGuide = { - href: appendJourneyFeatureFlag(linkResult.href), + href: linkResult.href, title: linkResult.title || '', } } @@ -210,7 +192,7 @@ export async function resolveJourneyContext( if (resultData && resultData.length > 0) { const linkResult = resultData[0] result.nextGuide = { - href: appendJourneyFeatureFlag(linkResult.href), + href: linkResult.href, title: linkResult.title || '', } } @@ -251,7 +233,7 @@ export async function resolveJourneyTracks( const linkData = await getLinkData(guidePath, context, { title: true }) const baseHref = linkData?.[0]?.href || guidePath return { - href: appendJourneyFeatureFlag(baseHref), + href: baseHref, title: linkData?.[0]?.title || 'Untitled Guide', } }), diff --git a/src/journeys/tests/journey-path-resolver.ts b/src/journeys/tests/journey-path-resolver.ts index 624d92f06087..510b14a99a8b 100644 --- a/src/journeys/tests/journey-path-resolver.ts +++ b/src/journeys/tests/journey-path-resolver.ts @@ -82,7 +82,7 @@ describe('journey-path-resolver', () => { ) expect(result?.prevGuide).toEqual({ - href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation', + href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup', title: 'Mock Title for /enterprise-onboarding/setup', }) }) @@ -95,7 +95,7 @@ describe('journey-path-resolver', () => { ) expect(result?.nextGuide).toEqual({ - href: '/en/enterprise-cloud@latest/enterprise-onboarding/deploy?feature=journey-navigation', + href: '/en/enterprise-cloud@latest/enterprise-onboarding/deploy', title: 'Mock Title for /enterprise-onboarding/deploy', }) }) @@ -184,7 +184,7 @@ describe('journey-path-resolver', () => { expect(result[0].guides).toHaveLength(2) expect(result[0].guides[0]).toEqual({ - href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation', + href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup', title: 'Mock Title for /enterprise-onboarding/setup', }) }) diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx index 1009ce4bdcbd..abc3d20205ff 100644 --- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx +++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx @@ -61,7 +61,9 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => { // Extract unique categories from the articles const categories: string[] = [ ALL_CATEGORIES, - ...new Set(flatArticles.flatMap((item) => item.category || [])), + ...Array.from(new Set(flatArticles.flatMap((item) => item.category || []))).sort((a, b) => + a.localeCompare(b), + ), ] const applyFilters = () => { diff --git a/src/landings/components/shared/LandingCarousel.tsx b/src/landings/components/shared/LandingCarousel.tsx index f779d1488aba..0ec75e24cf69 100644 --- a/src/landings/components/shared/LandingCarousel.tsx +++ b/src/landings/components/shared/LandingCarousel.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useRef } from 'react' +import { useRouter } from 'next/router' import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react' import cx from 'classnames' import type { ResolvedArticle } from '@/types' import { useTranslation } from '@/languages/components/useTranslation' +import { useVersion } from '@/versions/components/useVersion' import styles from './LandingCarousel.module.scss' type LandingCarouselProps = { @@ -42,6 +44,8 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr const [isAnimating, setIsAnimating] = useState(false) const itemsPerView = useResponsiveItemsPerView() const { t } = useTranslation('product_landing') + const router = useRouter() + const { currentVersion } = useVersion() const headingText = heading || t('carousel.recommended') // Ref to store timeout IDs for cleanup const animationTimeoutRef = useRef