Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add related content support and navigation using keywords #8

Merged
merged 11 commits into from
Jan 17, 2024
Merged
23 changes: 23 additions & 0 deletions .changeset/mean-turkeys-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@primer/doctocat-nextjs': patch
---

Enabled related content navigation using `keywords` field in Markdown frontmatter.

Example:

```
---
title: Page A
keywords: ['keyword', 'another keyword']
---
```

```
---
title: Page B
keywords: ['keyword', 'another keyword']
---
```

The matching keyword values above across both pages, will enable automatic related content navigation between the two pages.
3 changes: 1 addition & 2 deletions .github/workflows/release_canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ on:

jobs:
release-canary:
if: false
#if: ${{ github.repository == 'primer/doctocat-nextjs' }}
if: ${{ github.repository == 'primer/doctocat-nextjs' }}
name: Canary
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions packages/site/pages/content-examples/kitchen-sink.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ action-1-text: Primary action
action-1-link: /
action-2-text: Secondary action
action-2-link: /content-examples/kitchen-sink
keywords: ['accessibility', 'introduction', 'simple']
---

import {DoDontContainer, Do, Dont, Caption} from '@primer/doctocat-nextjs'
Expand Down
1 change: 1 addition & 0 deletions packages/site/pages/content-examples/simple.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: Simple
description: A simple page with plain markdown
keywords: ['kitchen sink', 'introduction', 'simple']
---

## Arva qua ferarum victa
Expand Down
2 changes: 2 additions & 0 deletions packages/site/pages/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Introduction
description: Learn how to create a new site with Doctocat
keywords: ['accessibility', 'introduction', 'simple']
related: [{title: External link example, href: https://primer.style}]
---

This guide will walk you through creating, customizing, and deploying a new documentation site powered by Doctocat on [Next.js](https://nextjs.org/).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.wrapper {
width: 220px;
}

.heading {
font-size: var(--base-size-12);
padding-inline-start: var(--base-size-16);
margin-block-end: var(--base-size-8);
text-transform: uppercase;
}

.item {
margin-block-end: var(--base-size-4);
transition: transform var(--brand-animation-duration-fast) var(--brand-animation-easing-default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import {NavList} from '@primer/react'
import {Text} from '@primer/react-brand'
import {MdxFile} from 'nextra'

import styles from './RelatedContentLinks.module.css'
import Link from 'next/link'
import {LinkExternalIcon} from '@primer/octicons-react'

export type RelatedContentLink = MdxFile & {
title: string
}

export type RelatedContentLinksProps = {
links: RelatedContentLink[]
}

export function RelatedContentLinks({links}: RelatedContentLinksProps) {
if (!links.length) return null

return (
<aside className={styles.wrapper}>
<Text as="p" size="100" variant="muted" weight="normal" className={styles.heading}>
Related content
</Text>
<NavList>
{links.map(page => (
<NavItem
className={styles.item}
key={page.title}
id={`toc-page-${page.route.replace(/\//g, '-')}`}
href={page.route}
>
{page.title}
</NavItem>
))}
</NavList>
</aside>
)
}

function NavItem({href, children, ...rest}) {
return (
<Link href={href} legacyBehavior passHref {...rest}>
<NavList.Item>
{children}{' '}
{href.startsWith('http') ? (
<NavList.TrailingVisual>
<LinkExternalIcon />
</NavList.TrailingVisual>
) : null}
</NavList.Item>
</Link>
)
}
69 changes: 65 additions & 4 deletions packages/theme/components/layout/root-layout/Theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {IndexCards} from '../index-cards/IndexCards'
import {useColorMode} from '../../context/color-modes/useColorMode'
import {getComponents} from '../../mdx-components/mdx-components'
import {SkipToMainContent} from '../skip-to-main-content/SkipToMainContent'
import {RelatedContentLink, RelatedContentLinks} from '../related-content-links/RelatedContentLinks'

const {publicRuntimeConfig} = getConfig()

Expand Down Expand Up @@ -64,6 +65,55 @@ export function Theme({children, pageOpts}: NextraThemeLayoutProps) {
? ((data as Folder).children.filter(child => child.kind === 'MdxPage') as MdxFile[])
: []

/**
* Uses a frontmatter 'keywords' value (as an array)
* to find adjacent pages that share the same values.
* @returns {RelatedContentLink[]}
*/
const getRelatedPages = () => {
const currentPageKeywords = frontMatter.keywords || []
const relatedLinks = frontMatter['related'] || []
const matches: RelatedContentLink[] = []

// 1. Check keywords property and find local matches
for (const page of flatDocsDirectories) {
if (page.route === route) continue

if ('frontMatter' in page) {
const pageKeywords = page.frontMatter?.keywords || []
const intersection = pageKeywords.filter(keyword => currentPageKeywords.includes(keyword))

if (intersection.length) {
matches.push(page)
}
}
}

// 2. Check related property for internal and external links
for (const link of relatedLinks) {
if (!link.title || !link.href || link.href === route) continue

if (link.href.startsWith('/')) {
const page = flatDocsDirectories.find(localPage => localPage.route === link.href) as
| RelatedContentLink
| undefined

if (page) {
const entry = {
...page,
title: link.title || page.title,
route: link.href || page.route,
}
matches.push(entry)
}
} else {
matches.push({...link, route: link.href})
}
}

return matches
}

return (
<>
<BrandThemeProvider dir="ltr" colorMode={colorMode}>
Expand Down Expand Up @@ -203,11 +253,22 @@ export function Theme({children, pageOpts}: NextraThemeLayoutProps) {
</footer>
</Stack>
</PRCBox>
{!isHomePage && headings.length > 0 && (
<PRCBox sx={{py: 2, pr: 3, display: ['none', null, null, null, 'block']}}>
<TableOfContents headings={headings} />
<PRCBox sx={{py: 2, pr: 3, display: ['none', null, null, null, 'block']}}>
<PRCBox
sx={{
position: 'sticky',
top: 112,
width: 220,
}}
>
{!isHomePage && headings.length > 0 && <TableOfContents headings={headings} />}
{getRelatedPages().length > 0 && (
<PRCBox sx={{pt: 5}}>
<RelatedContentLinks links={getRelatedPages()} />
</PRCBox>
)}
</PRCBox>
)}
</PRCBox>
</PRCBox>
</main>
</PageLayout.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
.wrapper {
position: sticky;
top: var(--base-size-112);
width: 220px;
}

.heading {
font-size: var(--base-size-12);
padding-inline-start: var(--base-size-16);
Expand All @@ -15,6 +9,7 @@
margin-block-end: var(--base-size-4);
transition: transform var(--brand-animation-duration-fast) var(--brand-animation-easing-default);
}

.item[aria-current='location'] {
transform: translateX(var(--base-size-4));
}
Loading