@primer/doctocat-nextjs 0.6.0-rc.2deb8be → 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 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(/&lt;/g, '<')
58
+ .replace(/&gt;/g, '>')
59
+ .replace(/&quot;/g, '"')
60
+ .replace(/&#x27;/g, "'")
61
+ .replace(/&amp;/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(page)
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) as
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.6.0-rc.2deb8be",
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.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",
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.3.5",
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: string
23
+ title: ReactNode
23
24
  type: string
24
25
  children?: DocsItem[]
25
26
  firstChildRoute?: string