@primer/doctocat-nextjs 0.6.0 → 0.7.0-rc.b4901f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/components/layout/code-block/CodeBlock.tsx +60 -1
- package/components/layout/code-block/ReactCodeBlock.tsx +2 -1
- package/components/layout/related-content-links/getRelatedPages.tsx +23 -5
- package/components/layout/root-layout/Theme.tsx +1 -1
- package/package.json +6 -6
- package/types.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @primer/doctocat-nextjs
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#65](https://github.com/primer/doctocat-nextjs/pull/65) [`dc680ec`](https://github.com/primer/doctocat-nextjs/commit/dc680ec51605bd7a40dedc71cd3bb107632035dd) Thanks [@rezrah](https://github.com/rezrah)! - Updated Next.js compatibility to v15.5.x, Nextra to v4, and fix React code block rendering
|
|
8
|
+
|
|
9
|
+
- **Next.js v15.5.2**: Upgraded to latest stable version across all workspaces
|
|
10
|
+
- **Nextra v4 compatibility**: Updated type definitions for `ReactNode` titles
|
|
11
|
+
- **Fixed code block rendering**: Added client-side rendering for interactive code examples to handle React lazy components properly
|
|
12
|
+
|
|
13
|
+
Next.js v15.4+ changed how lazy components render on the server, breaking interactive code blocks. This update uses client-side code snippet extraction to convert lazy components to clean code strings for the live editor, preventing hydration mismatches and preserving existing behavior.
|
|
14
|
+
|
|
15
|
+
- **Improved 404 page experience**: New 404 page to replace default Next.js version. Also removed stray 0 in top-left.
|
|
16
|
+
|
|
3
17
|
## 0.6.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import React, {PropsWithChildren} from 'react'
|
|
3
|
+
import dynamic from 'next/dynamic'
|
|
4
|
+
import {renderToStaticMarkup} from 'react-dom/server'
|
|
3
5
|
|
|
4
|
-
import {ReactCodeBlock} from './ReactCodeBlock'
|
|
5
6
|
import {Pre} from 'nextra/components'
|
|
7
|
+
import {Box, Stack, Text} from '@primer/react-brand'
|
|
8
|
+
import {Spinner} from '@primer/react'
|
|
9
|
+
|
|
10
|
+
// We need to load this on the client to avoid a hydration mismatch.
|
|
11
|
+
const ReactCodeBlock = dynamic(
|
|
12
|
+
async () => {
|
|
13
|
+
const module = await import('./ReactCodeBlock')
|
|
14
|
+
return {default: module.ReactCodeBlock}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
ssr: false,
|
|
18
|
+
loading: () => (
|
|
19
|
+
<Box borderStyle="solid" borderWidth="thin" borderColor="default" borderRadius="medium" marginBlockEnd="normal">
|
|
20
|
+
<Stack style={{minHeight: 340}} alignItems="center" justifyContent="center">
|
|
21
|
+
<Text>
|
|
22
|
+
<Spinner />
|
|
23
|
+
</Text>
|
|
24
|
+
</Stack>
|
|
25
|
+
</Box>
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
)
|
|
6
29
|
|
|
7
30
|
type CodeBlockProps = {
|
|
8
31
|
'data-language': string
|
|
@@ -11,6 +34,42 @@ type CodeBlockProps = {
|
|
|
11
34
|
|
|
12
35
|
export function CodeBlock(props: CodeBlockProps) {
|
|
13
36
|
if (['tsx', 'jsx'].includes(props['data-language'])) {
|
|
37
|
+
// Next.js v15.4+ will lazy render components on the server, which prevents us from
|
|
38
|
+
// sending usable React nodes to the ReactCodeBlock component.
|
|
39
|
+
// Workaround is to convert the code snippets to string on the client and pass to react-live.
|
|
40
|
+
|
|
41
|
+
// suppresses compilation warnings
|
|
42
|
+
if (typeof window !== 'undefined') {
|
|
43
|
+
try {
|
|
44
|
+
const childrenAsString = renderToStaticMarkup(<>{props.children}</>)
|
|
45
|
+
|
|
46
|
+
// Extract text content using browser's HTML parser (immune to regex bypass attacks)
|
|
47
|
+
const cleanHtmlTag = (str: string): string => {
|
|
48
|
+
const parser = new DOMParser()
|
|
49
|
+
const doc = parser.parseFromString(str, 'text/html')
|
|
50
|
+
return doc.body.textContent || doc.body.innerText || ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const textContent = cleanHtmlTag(childrenAsString)
|
|
54
|
+
|
|
55
|
+
// Restore escaped chars
|
|
56
|
+
const decodedText = textContent
|
|
57
|
+
.replace(/</g, '<')
|
|
58
|
+
.replace(/>/g, '>')
|
|
59
|
+
.replace(/"/g, '"')
|
|
60
|
+
.replace(/'/g, "'")
|
|
61
|
+
.replace(/&/g, '&')
|
|
62
|
+
|
|
63
|
+
return <ReactCodeBlock {...props} code={decodedText} />
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log('Error extracting code snippet. Forwarding children directly:', error)
|
|
67
|
+
// Fallback to original children-based approach
|
|
68
|
+
return <ReactCodeBlock {...props} />
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// During SSR/build, use children-based approach
|
|
14
73
|
return <ReactCodeBlock {...props} />
|
|
15
74
|
}
|
|
16
75
|
|
|
@@ -18,6 +18,7 @@ const COLLAPSE_HEIGHT = 400 // TODO: Hoist this to config to make user customiza
|
|
|
18
18
|
type ReactCodeBlockProps = {
|
|
19
19
|
'data-language': string
|
|
20
20
|
'data-filename'?: string
|
|
21
|
+
code?: string
|
|
21
22
|
jsxScope: Record<string, unknown>
|
|
22
23
|
} & PropsWithChildren<HTMLElement>
|
|
23
24
|
|
|
@@ -34,7 +35,7 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
34
35
|
const uniqueId = useId()
|
|
35
36
|
const {colorMode, setColorMode} = useColorMode()
|
|
36
37
|
const {basePath} = useConfig()
|
|
37
|
-
const initialCode = getCodeFromChildren(props.children)
|
|
38
|
+
const initialCode = props.code || getCodeFromChildren(props.children)
|
|
38
39
|
const [code, setCode] = useState(initialCode)
|
|
39
40
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
40
41
|
const [isCodePaneCollapsed, setIsCodePaneCollapsed] = useState<boolean | null>(null)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {DocsItem, FrontMatter} from '../../../types'
|
|
2
2
|
import {RelatedContentLink} from './RelatedContentLinks'
|
|
3
|
+
import React from 'react'
|
|
3
4
|
|
|
4
5
|
type GetRelatedPages = (
|
|
5
6
|
route: string,
|
|
@@ -28,7 +29,10 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
|
|
|
28
29
|
const intersection = pageKeywords.filter(keyword => currentPageKeywords.includes(keyword))
|
|
29
30
|
|
|
30
31
|
if (intersection.length) {
|
|
31
|
-
matches.push(
|
|
32
|
+
matches.push({
|
|
33
|
+
...page,
|
|
34
|
+
title: titleToString(page.title), // Convert ReactNode to string
|
|
35
|
+
})
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -36,14 +40,12 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
|
|
|
36
40
|
for (const link of relatedLinks) {
|
|
37
41
|
if (!link.title || !link.href || link.href === route) continue
|
|
38
42
|
if (link.href.startsWith('/')) {
|
|
39
|
-
const page = flatDocsDirectories.find(localPage => localPage.route === link.href)
|
|
40
|
-
| RelatedContentLink
|
|
41
|
-
| undefined
|
|
43
|
+
const page = flatDocsDirectories.find(localPage => localPage.route === link.href)
|
|
42
44
|
|
|
43
45
|
if (page) {
|
|
44
46
|
const entry = {
|
|
45
47
|
...page,
|
|
46
|
-
title: link.title || page.title,
|
|
48
|
+
title: link.title || titleToString(page.title),
|
|
47
49
|
route: link.href || page.route,
|
|
48
50
|
}
|
|
49
51
|
matches.push(entry)
|
|
@@ -55,3 +57,19 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
|
|
|
55
57
|
|
|
56
58
|
return matches
|
|
57
59
|
}
|
|
60
|
+
|
|
61
|
+
function titleToString(title: React.ReactNode): string {
|
|
62
|
+
const children = React.Children.toArray(title)
|
|
63
|
+
|
|
64
|
+
return children
|
|
65
|
+
.map(child => {
|
|
66
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
67
|
+
return child.toString()
|
|
68
|
+
}
|
|
69
|
+
if (React.isValidElement(child) && child.props.children) {
|
|
70
|
+
return titleToString(child.props.children)
|
|
71
|
+
}
|
|
72
|
+
return ''
|
|
73
|
+
})
|
|
74
|
+
.join('')
|
|
75
|
+
}
|
|
@@ -149,7 +149,7 @@ export function Theme({pageMap, children}: ThemeProps) {
|
|
|
149
149
|
<Stack direction="vertical" padding="none" gap="spacious">
|
|
150
150
|
{!isHomePage && (
|
|
151
151
|
<>
|
|
152
|
-
{activePath.length && (
|
|
152
|
+
{activePath.length > 0 && (
|
|
153
153
|
<Breadcrumbs>
|
|
154
154
|
{(activeHeaderLink || siteTitle) && (
|
|
155
155
|
<Breadcrumbs.Item
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primer/doctocat-nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-rc.b4901f1",
|
|
4
4
|
"description": "A Next.js theme for building Primer documentation sites",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"react-dom": ">=18.0.0 <20.0.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@next/mdx": "15.
|
|
24
|
+
"@next/mdx": "15.5.2",
|
|
25
25
|
"@primer/octicons-react": "19.15.1",
|
|
26
26
|
"@primer/react": "^37.11.0",
|
|
27
27
|
"@types/lodash.debounce": "^4.0.9",
|
|
28
28
|
"framer-motion": "12.4.0",
|
|
29
29
|
"lodash.debounce": "^4.0.8",
|
|
30
|
-
"nextra": "4.
|
|
31
|
-
"react": "18.
|
|
32
|
-
"react-dom": "18.
|
|
30
|
+
"nextra": "4.4.0",
|
|
31
|
+
"react": ">=18.0.0 <20.0.0",
|
|
32
|
+
"react-dom": ">=18.0.0 <20.0.0",
|
|
33
33
|
"react-focus-on": "3.9.4",
|
|
34
34
|
"react-live": "^4.1.8"
|
|
35
35
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@vitest/ui": "^3.2.4",
|
|
48
48
|
"clsx": "2.1.1",
|
|
49
49
|
"jsdom": "^26.0.1",
|
|
50
|
-
"next": "15.
|
|
50
|
+
"next": "15.5.2",
|
|
51
51
|
"styled-components": "5.3.11",
|
|
52
52
|
"vitest": "^3.2.4"
|
|
53
53
|
},
|
package/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {Folder, PageMapItem, MdxFile} from 'nextra'
|
|
2
|
+
import {ReactNode} from 'react'
|
|
2
3
|
|
|
3
4
|
export type ThemeConfig = {
|
|
4
5
|
docsRepositoryBase: string
|
|
@@ -19,7 +20,7 @@ export type ExtendedPageItem = PageMapItem & {
|
|
|
19
20
|
export type FolderWithoutChildren = Omit<Folder, 'children'>
|
|
20
21
|
|
|
21
22
|
export type DocsItem = MdxFile & {
|
|
22
|
-
title:
|
|
23
|
+
title: ReactNode
|
|
23
24
|
type: string
|
|
24
25
|
children?: DocsItem[]
|
|
25
26
|
firstChildRoute?: string
|