@primer/doctocat-nextjs 0.0.0-20250716161640 → 0.0.0-20250904115752

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 CHANGED
@@ -1,16 +1,28 @@
1
1
  # @primer/doctocat-nextjs
2
2
 
3
- ## 0.0.0-20250716161640
3
+ ## 0.0.0-20250904115752
4
4
 
5
5
  ### Patch Changes
6
6
 
7
7
  - Fake entry to force publishing
8
8
 
9
- ## 0.0.0-20250716161638
9
+ ## 0.0.0-20250904115750
10
10
 
11
11
  ### Minor Changes
12
12
 
13
- - [#63](https://github.com/primer/doctocat-nextjs/pull/63) [`45ebb5b`](https://github.com/primer/doctocat-nextjs/commit/45ebb5b1ceb165e2eaed7563558b52c52c1c1c59) Thanks [@rezrah](https://github.com/rezrah)! - Moves `@primer/react-brand` to a peer-dependency.
13
+ - [`84ff648`](https://github.com/primer/doctocat-nextjs/commit/84ff648a8248712d61e13a7e6432ab2ab8e07435) Thanks [@rezrah](https://github.com/rezrah)! - Updated Next.js compatibility to v15.5.x, Nextra to v4, and fix React code block rendering
14
+
15
+ - **Next.js v15.5.2**: Upgraded to latest stable version across all workspaces
16
+ - **Nextra v4 compatibility**: Updated type definitions for `ReactNode` titles
17
+ - **Fixed code block rendering**: Added client-side rendering for interactive code examples to handle React lazy components properly
18
+
19
+ 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.
20
+
21
+ ## 0.6.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#63](https://github.com/primer/doctocat-nextjs/pull/63) [`9a090ee`](https://github.com/primer/doctocat-nextjs/commit/9a090eec47c64dc46f8eac14ec00548d421e8019) Thanks [@rezrah](https://github.com/rezrah)! - Moves `@primer/react-brand` to a peer-dependency.
14
26
 
15
27
  This change prevents a mismatch of versions between Doctocat and the project using it. Going forward, the latter will be preferred.
16
28
 
@@ -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,7 +34,29 @@ type CodeBlockProps = {
11
34
 
12
35
  export function CodeBlock(props: CodeBlockProps) {
13
36
  if (['tsx', 'jsx'].includes(props['data-language'])) {
14
- return <ReactCodeBlock {...props} />
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
+ try {
41
+ const childrenAsString = renderToStaticMarkup(<>{props.children}</>)
42
+
43
+ const textContent = childrenAsString.replace(/<[^>]*>/g, '')
44
+
45
+ // Restore escaped chars
46
+ const decodedText = textContent
47
+ .replace(/&lt;/g, '<')
48
+ .replace(/&gt;/g, '>')
49
+ .replace(/&quot;/g, '"')
50
+ .replace(/&#x27;/g, "'")
51
+ .replace(/&amp;/g, '&')
52
+
53
+ return <ReactCodeBlock {...props} code={decodedText} />
54
+ } catch (error) {
55
+ // eslint-disable-next-line no-console
56
+ console.log('Error extracting code snippet. Forwarding children directly:', error)
57
+ // Fallback to original children-based approach
58
+ return <ReactCodeBlock {...props} />
59
+ }
15
60
  }
16
61
 
17
62
  return <Pre>{props.children}</Pre>
@@ -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)
@@ -28,7 +28,10 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
28
28
  const intersection = pageKeywords.filter(keyword => currentPageKeywords.includes(keyword))
29
29
 
30
30
  if (intersection.length) {
31
- matches.push(page)
31
+ matches.push({
32
+ ...page,
33
+ title: titleToString(page.title), // Convert ReactNode to string
34
+ })
32
35
  }
33
36
  }
34
37
 
@@ -36,14 +39,12 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
36
39
  for (const link of relatedLinks) {
37
40
  if (!link.title || !link.href || link.href === route) continue
38
41
  if (link.href.startsWith('/')) {
39
- const page = flatDocsDirectories.find(localPage => localPage.route === link.href) as
40
- | RelatedContentLink
41
- | undefined
42
+ const page = flatDocsDirectories.find(localPage => localPage.route === link.href)
42
43
 
43
44
  if (page) {
44
45
  const entry = {
45
46
  ...page,
46
- title: link.title || page.title,
47
+ title: link.title || titleToString(page.title),
47
48
  route: link.href || page.route,
48
49
  }
49
50
  matches.push(entry)
@@ -55,3 +56,9 @@ export const getRelatedPages: GetRelatedPages = (route, activeMetadata, flatDocs
55
56
 
56
57
  return matches
57
58
  }
59
+
60
+ function titleToString(title: React.ReactNode): string {
61
+ if (typeof title === 'string') return title
62
+ if (typeof title === 'number') return title.toString()
63
+ return ''
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/doctocat-nextjs",
3
- "version": "0.0.0-20250716161640",
3
+ "version": "0.0.0-20250904115752",
4
4
  "description": "A Next.js theme for building Primer documentation sites",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -21,17 +21,16 @@
21
21
  "react-dom": ">=18.0.0 <20.0.0"
22
22
  },
23
23
  "dependencies": {
24
- "@next/mdx": "15.3.5",
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.2.17",
31
- "react": "18.3.1",
32
- "react-dom": "18.3.1",
33
- "react-focus-on": "3.9.4",
34
- "react-live": "^4.1.8"
30
+ "nextra": "4.4.0",
31
+ "react": ">=18.0.0 <20.0.0",
32
+ "react-dom": ">=18.0.0 <20.0.0",
33
+ "react-focus-on": "3.9.4"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@github/prettier-config": "^0.0.6",
@@ -47,7 +46,9 @@
47
46
  "@vitest/ui": "^3.2.4",
48
47
  "clsx": "2.1.1",
49
48
  "jsdom": "^26.0.1",
50
- "next": "15.3.5",
49
+ "next": "15.5.2",
50
+ "react-live": "^4.1.8",
51
+ "react-live-runner": "^1.0.7",
51
52
  "styled-components": "5.3.11",
52
53
  "vitest": "^3.2.4"
53
54
  },
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: string
23
+ title: ReactNode
23
24
  type: string
24
25
  children?: DocsItem[]
25
26
  firstChildRoute?: string