1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Add sidebarLink frontmatter property for custom sidebar links (#56238)

This commit is contained in:
Kevin Heis
2025-07-23 14:00:15 -07:00
committed by GitHub
parent d6ae404dfc
commit 492685b76a
17 changed files with 231 additions and 8 deletions

View File

@@ -10,6 +10,9 @@ versions:
topics:
- Copilot
layout: category-landing
sidebarLink:
text: All prompts
href: /copilot/copilot-chat-cookbook
spotlight:
- article: /testing-code/generate-unit-tests
image: /assets/images/copilot-landing/generating_unit_tests.png

View File

@@ -1,7 +1,7 @@
import type { Response } from 'express'
import { Context } from '@/types'
import { ExtendedRequestWithPageInfo } from '../types'
import { ExtendedRequestWithPageInfo } from '@/article-api/types'
import contextualize from '@/frame/middleware/context/context'
export async function getArticleBody(req: ExtendedRequestWithPageInfo) {

View File

@@ -48,4 +48,52 @@ describe(frontmatterSchema.names.join(' - '), () => {
expect(errors[0].lineNumber).toBe(1)
expect(errors[0].errorRange).toEqual(null)
})
test('sidebarLink with valid object properties passes', async () => {
const markdown = [
'---',
'title: Title',
'versions:',
" fpt: '*'",
'sidebarLink:',
' text: "All prompts"',
' href: "/copilot/copilot-chat-cookbook"',
'---',
].join('\n')
const result = await runRule(frontmatterSchema, { strings: { markdown }, ...fmOptions })
const errors = result.markdown
expect(errors.length).toBe(0)
})
test('sidebarLink with missing text property fails', async () => {
const markdown = [
'---',
'title: Title',
'versions:',
" fpt: '*'",
'sidebarLink:',
' href: "/copilot/copilot-chat-cookbook"',
'---',
].join('\n')
const result = await runRule(frontmatterSchema, { strings: { markdown }, ...fmOptions })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(5)
})
test('sidebarLink with missing href property fails', async () => {
const markdown = [
'---',
'title: Title',
'versions:',
" fpt: '*'",
'sidebarLink:',
' text: "All prompts"',
'---',
].join('\n')
const result = await runRule(frontmatterSchema, { strings: { markdown }, ...fmOptions })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(5)
})
})

View File

@@ -19,6 +19,7 @@ featuredLinks:
children:
- /start-your-journey
- /foo
- /sidebar-test
- /video-transcripts
- /minitocs
- /liquid
@@ -31,5 +32,5 @@ communityRedirect:
name: Provide HubGit Feedback
href: 'https://hubgit.com/orgs/community/discussions/categories/get-started'
product_video: 'https://www.yourube.com/abc123'
product_video_transcript: '/get-started/video-transcripts/transcript--my-awesome-video'
product_video_transcript: '/video-transcripts/transcript--my-awesome-video'
---

View File

@@ -0,0 +1,15 @@
---
title: Sidebar Test Page
intro: 'Test page for sidebar custom link functionality'
versions:
fpt: '*'
ghes: '*'
ghec: '*'
sidebarLink:
text: All sidebar test items
href: /get-started/sidebar-test
children:
- /test-child
---
This is a test page for the sidebar custom link functionality.

View File

@@ -0,0 +1,10 @@
---
title: Test Child Page
intro: 'Child page for testing sidebar functionality'
versions:
fpt: '*'
ghes: '*'
ghec: '*'
---
This is a test child page under the sidebar test section.

View File

@@ -32,6 +32,7 @@ children:
- actions
- rest
- webhooks
- video-transcripts
# - account-and-profile
# - authentication
# - repositories

View File

@@ -0,0 +1,12 @@
---
title: Video transcripts
intro: 'Collection of video transcripts for accessibility and reference.'
versions:
fpt: '*'
ghes: '*'
ghec: '*'
children:
- /transcript--my-awesome-video
---
This section contains transcripts for videos used throughout the documentation.

View File

@@ -0,0 +1,10 @@
---
title: Transcript - My awesome video
product_video: 'https://www.yourube.com/abc123'
versions:
fpt: '*'
ghes: '*'
ghec: '*'
---
This is a transcript

View File

@@ -324,6 +324,19 @@ test('navigate with side bar into article inside a subcategory inside a category
await expect(page).toHaveURL(/actions\/category\/subcategory\/article/)
})
test('sidebar custom link functionality works', async ({ page }) => {
// Test that sidebar functionality is not broken by custom links feature
await page.goto('/get-started')
await expect(page).toHaveTitle(/Getting started with HubGit/)
// Verify that regular sidebar navigation still works by clicking on known sections
await page.getByTestId('product-sidebar').getByText('Start your journey').click()
await page.getByTestId('product-sidebar').getByText('Hello World').click()
await expect(page).toHaveURL(/\/en\/get-started\/start-your-journey\/hello-world/)
await expect(page).toHaveTitle(/Hello World - GitHub Docs/)
})
test.describe('hover cards', () => {
test('hover over link', async ({ page }) => {
await page.goto('/pages/quickstart')

View File

@@ -8,7 +8,7 @@ describe('transcripts', () => {
test('video link from product landing page leads to video', async () => {
const $: cheerio.Root = await getDOM('/en/get-started')
expect($('a#product-video').attr('href')).toBe(
'/en/get-started/video-transcripts/transcript--my-awesome-video',
'/en/video-transcripts/transcript--my-awesome-video',
)
})
})

View File

@@ -3,6 +3,7 @@ import pick from 'lodash/pick'
import type { BreadcrumbT } from '@/frame/components/page-header/Breadcrumbs'
import type { FeatureFlags } from '@/frame/components/hooks/useFeatureFlags'
import type { SidebarLink } from '@/types'
export type ProductT = {
external: boolean
@@ -54,6 +55,7 @@ export type ProductTreeNode = {
title: string
href: string
childPages: Array<ProductTreeNode>
sidebarLink?: SidebarLink
layout?: string
}

View File

@@ -1,7 +1,7 @@
// when updating to typescript,
// update links in content/contributing as well
import parse from './read-frontmatter'
import parse from '@/frame/lib/read-frontmatter'
import { allVersions } from '@/versions/lib/all-versions'
import { allTools } from '@/tools/lib/all-tools'
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data'
@@ -291,6 +291,20 @@ export const schema = {
type: 'string',
},
// END category landing tags
// Custom sidebar link for category pages
sidebarLink: {
type: 'object',
required: ['text', 'href'],
properties: {
text: {
type: 'string',
translatable: true,
},
href: {
type: 'string',
},
},
},
// Spotlight configuration for category landing pages
spotlight: {
type: 'array',

View File

@@ -125,6 +125,7 @@ async function getCurrentProductTreeTitles(input: Tree, context: Context): Promi
childPages: childPages.filter(Boolean),
}
if (page.hidden) node.hidden = true
if (page.sidebarLink) node.sidebarLink = page.sidebarLink
if (page.layout && typeof page.layout === 'string') node.layout = page.layout
return node
}
@@ -138,18 +139,20 @@ function excludeHidden(tree: TitlesTree) {
documentType: tree.documentType,
childPages: tree.childPages.map(excludeHidden).filter(Boolean) as TitlesTree[],
}
if (tree.sidebarLink) newTree.sidebarLink = tree.sidebarLink
if (tree.layout && typeof tree.layout === 'string') newTree.layout = tree.layout
return newTree
}
function sidebarTree(tree: TitlesTree) {
const { href, title, shortTitle, childPages } = tree
const { href, title, shortTitle, childPages, sidebarLink } = tree
const childChildPages = childPages.map(sidebarTree)
const newTree: TitlesTree = {
href,
title: shortTitle || title,
childPages: childChildPages,
}
if (sidebarLink) newTree.sidebarLink = sidebarLink
if (tree.layout && typeof tree.layout === 'string') newTree.layout = tree.layout
return newTree
}

View File

@@ -5,7 +5,7 @@ import { NavList } from '@primer/react'
import { ProductTreeNode, useMainContext } from '@/frame/components/context/MainContext'
import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext'
import { nonAutomatedRestPaths } from '../../rest/lib/config'
import { nonAutomatedRestPaths } from '@/rest/lib/config'
export const SidebarProduct = () => {
const router = useRouter()
@@ -91,13 +91,24 @@ function NavListItem({ childPage }: { childPage: ProductTreeNode }) {
{childPage.title}
{childPage.childPages.length > 0 && (
<NavList.SubNav aria-label={`${childPage.title} submenu`} sx={{ '*': { fontSize: 1 } }}>
{childPage.sidebarLink && (
<NavList.Item
href={childPage.sidebarLink.href}
as={Link}
aria-current={
routePath === `/${locale}${childPage.sidebarLink.href}` ? 'page' : false
}
>
{childPage.sidebarLink.text}
</NavList.Item>
)}
{specialCategory && (
<NavList.Item href={childPage.href} as={Link} aria-current={isActive ? 'page' : false}>
{childPage.title}
</NavList.Item>
)}
{childPage.childPages.map((childPage) => (
<NavListItem key={childPage.href} childPage={childPage} />
{childPage.childPages.map((subPage) => (
<NavListItem key={subPage.href} childPage={subPage} />
))}
</NavList.SubNav>
)}

View File

@@ -0,0 +1,72 @@
import { describe, expect, test } from 'vitest'
import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
describe('sidebar custom links', () => {
test.skip('page with sidebarLink frontmatter shows custom link in sidebar', async () => {
// Test that a page with sidebarLink frontmatter property shows the custom link
const $ = await getDOM('/get-started/sidebar-test')
// Check that the custom sidebar link appears
const customLink = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLink.length).toBe(1)
expect(customLink.attr('href')).toBe('/get-started/sidebar-test')
})
test('page without sidebarLink frontmatter does not show custom link', async () => {
// Test that pages without sidebarLink don't show custom links
// Using a page that's not in the get-started section to avoid seeing the foo sidebarLink
const $ = await getDOM('/actions')
// Check that no custom sidebar links appear
const customLinks = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLinks.length).toBe(0)
})
test.skip('sidebarLink with custom text appears correctly', async () => {
// Test that custom text in sidebarLink appears correctly
const $ = await getDOM('/get-started/sidebar-test')
// The fixture sidebar-test page should have "All sidebar test items" as custom text
const customLink = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLink.text().trim()).toBe('All sidebar test items')
})
test.skip('sidebarLink appears in correct location within sidebar', async () => {
// Test that the custom link appears as the first item in the subnav
const $ = await getDOM('/get-started/sidebar-test')
// Find the custom link directly in the sidebar
const customLink = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLink.length).toBe(1)
expect(customLink.attr('href')).toBe('/get-started/sidebar-test')
// Verify it appears before other child pages
const testSection = customLink.closest('[role="group"], ul')
const allLinks = testSection.find('a')
const customLinkIndex = allLinks.index(customLink)
expect(customLinkIndex).toBe(0) // Should be the first link in the subnav
})
test.skip('sidebar custom link has correct aria attributes', async () => {
// Test accessibility attributes on custom sidebar links
const $ = await getDOM('/get-started/sidebar-test')
const customLink = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLink.length).toBe(1)
// Verify the custom link has proper attributes (aria-current depends on current page logic)
expect(customLink.attr('href')).toBeDefined()
expect(customLink.text().trim()).toBe('All sidebar test items')
})
test('sidebar custom link does not appear on unrelated pages', async () => {
// Test that custom links only appear in relevant contexts
// Using actions page which is completely unrelated to get-started/foo
const $ = await getDOM('/actions')
// The sidebar test custom link should not appear on unrelated pages
const customLink = $('[data-testid="sidebar"] a:contains("All sidebar test items")')
expect(customLink.length).toBe(0)
})
})

View File

@@ -55,6 +55,7 @@ export type PageFrontmatter = {
defaultPlatform?: 'mac' | 'windows' | 'linux'
defaultTool?: string
childGroups?: ChildGroup[]
sidebarLink?: SidebarLink
spotlight?: SpotlightItem[]
}
@@ -373,6 +374,12 @@ export type Page = {
category?: string[]
complexity?: string[]
industry?: string[]
sidebarLink?: SidebarLink
}
export type SidebarLink = {
text: string
href: string
}
type ChangeLog = {
@@ -388,6 +395,7 @@ export type TitlesTree = {
documentType?: string
childPages: TitlesTree[]
hidden?: boolean
sidebarLink?: SidebarLink
layout?: string
}