Skip to content

Commit d0c5bd4

Browse files
authored
Enable support for Next.js trailingSlash config (#35)
1 parent 1d67884 commit d0c5bd4

File tree

6 files changed

+75
-42
lines changed

6 files changed

+75
-42
lines changed

.changeset/many-spiders-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/doctocat-nextjs': patch
3+
---
4+
5+
Enable support for `trailingSlash: true` in `next.config.js`

packages/theme/components/layout/index-cards/IndexCards.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ type IndexCardsProps = {
1212

1313
export function IndexCards({route, folderData}: IndexCardsProps) {
1414
// We don't want to show children of these pages. E.g. tabbed pages
15-
const onlyDirectChildren = folderData.filter(
16-
item => item.route.includes(`${route}/`) && item.route.split('/').length === 3,
17-
)
15+
const onlyDirectChildren = folderData.filter(item => {
16+
// Normalize paths regardless of trailing slash enablement
17+
const normalizedRoute = route.endsWith('/') ? route : `${route}/`
18+
const normalizedItemRoute = item.route.endsWith('/') ? item.route : `${item.route}/`
19+
20+
const isChild = normalizedItemRoute.startsWith(normalizedRoute)
21+
if (!isChild) return false
22+
23+
const routeSegments = normalizedRoute.split('/').filter(Boolean).length
24+
const itemSegments = normalizedItemRoute.split('/').filter(Boolean).length
1825

19-
const filteredData = onlyDirectChildren.filter(item => item.type === 'doc' && item.route.includes(`${route}/`))
26+
return itemSegments === routeSegments + 1
27+
})
2028

29+
const filteredData = onlyDirectChildren.filter(item => item.type === 'doc')
2130
return (
2231
<Stack direction="vertical" padding="none" gap="spacious">
2332
{filteredData.map((item: DocsItem) => {

packages/theme/components/layout/root-layout/Theme.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,13 @@ export type ThemeProps = PropsWithChildren<{
5353

5454
export function Theme({pageMap, children}: ThemeProps) {
5555
const pathname = usePathname()
56+
const pathHasTrailingSlash = pathname.endsWith('/')
5657

5758
const normalizedPages = normalizePages({
5859
list: pageMap,
5960
route: pathname,
6061
})
6162

62-
const {activeMetadata} = normalizedPages
63-
64-
const {filePath = '', title = ''} = activeMetadata || {}
65-
6663
const route = usePathname()
6764

6865
const fsPath = useFSRoute()
@@ -79,33 +76,42 @@ export function Theme({pageMap, children}: ThemeProps) {
7976
// eslint-disable-next-line i18n-text/no-en
8077
const siteTitle = process.env.NEXT_PUBLIC_SITE_TITLE || 'Example Site'
8178
const isHomePage = route === '/'
82-
const isIndexPage =
83-
/index\.mdx?$/.test(filePath) && !isHomePage && activeMetadata && activeMetadata['show-tabs'] === undefined
79+
80+
const activeFile = isHomePage
81+
? undefined
82+
: (normalizedPages.flatDocsDirectories.find(
83+
item => `${item.route}${pathHasTrailingSlash ? '/' : ''}` === pathname,
84+
) as MdxFile)
85+
86+
const activeMetadata = activeFile?.frontMatter || {}
87+
const filePath = activeMetadata.filePath || ''
88+
const title = activeMetadata.title || ''
89+
90+
const isIndexPage = /index\.mdx?$/.test(filePath) && !isHomePage && activeMetadata['show-tabs'] === undefined
8491
const data = !isHomePage && activePath[activePath.length - 2]
8592
const filteredTabData: MdxFile[] = data && hasChildren(data) ? ((data as Folder).children as MdxFile[]) : []
8693

8794
const relatedLinks = getRelatedPages(route, activeMetadata, flatDocsDirectories)
88-
const disablePageAnimation = activeMetadata?.options?.disablePageAnimation || false
95+
const disablePageAnimation = activeMetadata.options?.disablePageAnimation || false
96+
8997
return (
9098
<>
9199
<BrandThemeProvider dir="ltr" colorMode={colorMode}>
92100
<ThemeProvider colorMode={colorMode}>
93101
<BaseStyles>
94-
{activeMetadata && (
95-
<Head>
96-
<title>{title}</title>
97-
{activeMetadata.description && <meta name="description" content={activeMetadata.description} />}
98-
<meta property="og:type" content="website" />
99-
<meta property="og:title" content={title} />
100-
{activeMetadata.description && <meta property="og:description" content={activeMetadata.description} />}
101-
<meta property="og:image" content={activeMetadata.image || '/og-image.png'} />
102-
{/* X (Twitter) OG */}
103-
<meta name="twitter:card" content="summary_large_image" />
104-
<meta name="twitter:title" content={title} />
105-
{activeMetadata.description && <meta name="twitter:description" content={activeMetadata.description} />}
106-
<meta name="twitter:image" content={activeMetadata.image || '/og-image.png'} />
107-
</Head>
108-
)}
102+
<Head>
103+
<title>{title}</title>
104+
{activeMetadata.description && <meta name="description" content={activeMetadata.description} />}
105+
<meta property="og:type" content="website" />
106+
<meta property="og:title" content={title} />
107+
{activeMetadata.description && <meta property="og:description" content={activeMetadata.description} />}
108+
<meta property="og:image" content={activeMetadata.image || '/og-image.png'} />
109+
{/* X (Twitter) OG */}
110+
<meta name="twitter:card" content="summary_large_image" />
111+
<meta name="twitter:title" content={title} />
112+
{activeMetadata.description && <meta name="twitter:description" content={activeMetadata.description} />}
113+
<meta name="twitter:image" content={activeMetadata.image || '/og-image.png'} />
114+
</Head>
109115

110116
<ContentWrapper disableAnimations={disablePageAnimation}>
111117
<PRCBox
@@ -159,22 +165,22 @@ export function Theme({pageMap, children}: ThemeProps) {
159165

160166
<Box>
161167
<Stack direction="vertical" padding="none" gap={12} alignItems="flex-start">
162-
{activeMetadata?.title && (
168+
{activeMetadata.title && (
163169
<Heading as="h1" size="3">
164170
{activeMetadata.title}
165171
</Heading>
166172
)}
167-
{activeMetadata?.description && (
173+
{activeMetadata.description && (
168174
<Text as="p" variant="muted" size="300">
169175
{activeMetadata.description}
170176
</Text>
171177
)}
172-
{activeMetadata?.image && (
178+
{activeMetadata.image && (
173179
<Box paddingBlockStart={16} style={{width: '100%'}}>
174180
<Hero.Image src={activeMetadata.image} alt={activeMetadata['image-alt']} />
175181
</Box>
176182
)}
177-
{activeMetadata && activeMetadata['action-1-text'] && (
183+
{activeMetadata['action-1-text'] && (
178184
<Box paddingBlockStart={16}>
179185
<ButtonGroup>
180186
<Button as="a" href={activeMetadata['action-1-link']}>
@@ -190,9 +196,7 @@ export function Theme({pageMap, children}: ThemeProps) {
190196
)}
191197
</Stack>
192198
</Box>
193-
{activeMetadata && activeMetadata['show-tabs'] && (
194-
<UnderlineNav tabData={filteredTabData} />
195-
)}
199+
{activeMetadata['show-tabs'] && <UnderlineNav tabData={filteredTabData} />}
196200
</>
197201
)}
198202
<article>

packages/theme/components/layout/sidebar/Sidebar.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ export function Sidebar({pageMap}: SidebarProps) {
6363
)
6464
}
6565

66+
const normalizePath = (path: string) => {
67+
// Remove trailing slash unless it's the root path
68+
return path === '/' ? path : path.replace(/\/+$/, '')
69+
}
70+
6671
return (
6772
<NavList.Group title={subNavName} key={item.name} sx={{mb: 24}}>
6873
{item.children
@@ -73,12 +78,13 @@ export function Sidebar({pageMap}: SidebarProps) {
7378
if (!hasChildren(child)) {
7479
const {name, route} = child as MdxFile
7580

81+
const cleanPathname = normalizePath(pathname)
7682
return (
7783
<NavList.Item
7884
as={NextLink}
7985
key={name}
8086
href={route}
81-
aria-current={route === pathname ? 'page' : undefined}
87+
aria-current={route === cleanPathname ? 'page' : undefined}
8288
>
8389
{(child as MdxFile).frontMatter?.title || name}
8490
</NavList.Item>
@@ -88,17 +94,24 @@ export function Sidebar({pageMap}: SidebarProps) {
8894
if (hasChildren(child)) {
8995
const landingPageItem = (child as Folder).children.find(
9096
innerChild => (innerChild as DocsItem).name === 'index',
91-
)
97+
) as MdxFile
98+
99+
// Then inside your component where you need to compare paths:
100+
const cleanPathname = normalizePath(pathname)
101+
const cleanRoute = normalizePath(landingPageItem.route)
102+
103+
// For checking if current path is this route or a direct child:
104+
const isCurrentOrChild = cleanPathname === cleanRoute || cleanPathname.startsWith(`${cleanRoute}/`)
92105

93106
return (
94107
<NavList.Item
95108
as={NextLink}
96-
key={(landingPageItem as MdxFile).route}
97-
href={(landingPageItem as MdxFile).route}
109+
key={landingPageItem.route}
110+
href={landingPageItem.route}
98111
sx={{textTransform: 'capitalize'}}
99-
aria-current={(landingPageItem as MdxFile).route === pathname ? 'page' : undefined}
112+
aria-current={isCurrentOrChild ? 'page' : undefined}
100113
>
101-
{(landingPageItem as MdxFile).frontMatter?.title || item.name}
114+
{landingPageItem.frontMatter?.title || item.name}
102115
</NavList.Item>
103116
)
104117
}

packages/theme/components/layout/underline-nav/UnderlineNav.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ type UnderlineNavProps = {
1212
export function UnderlineNav({tabData}: UnderlineNavProps) {
1313
const [isClient, setIsClient] = useState(false)
1414
const pathname = usePathname()
15-
16-
const currentRoute = pathname
15+
const pathHasTrailingSlash = pathname.endsWith('/')
1716

1817
// Reorders tabData so the tab with name === index is always first
1918
if (tabData.length > 1) {
@@ -34,12 +33,14 @@ export function UnderlineNav({tabData}: UnderlineNavProps) {
3433
<PrimerUnderlineNav aria-label="Sibling pages">
3534
{tabData.length > 1 &&
3635
tabData.reverse().map(item => {
36+
const cleanPathname = pathname === '/' ? '/' : pathHasTrailingSlash ? pathname.slice(0, -1) : pathname
37+
3738
return (
3839
<PrimerUnderlineNav.Item
3940
as={Link}
4041
key={item.name}
4142
href={`${item.route}`}
42-
aria-current={currentRoute === item.route ? 'page' : undefined}
43+
aria-current={cleanPathname === item.route ? 'page' : undefined}
4344
>
4445
{(item.frontMatter && (item.frontMatter['tab-label'] || item.frontMatter.title)) || item.name}
4546
</PrimerUnderlineNav.Item>

packages/theme/css/prose.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
.Prose ul:not(:global(.custom-component) ul) {
151151
list-style-type: image;
152152
list-style-image: var(--brand-Prose-unorderedList-imageUrl);
153+
margin-block-end: var(--base-size-24) !important;
153154
}
154155

155156
.Prose li:not(:global(.custom-component) li) {

0 commit comments

Comments
 (0)