arronhunt / highlightjs-copy

📋❇️ A simple, accessible highlightjs plugin to add a copy button to your code blocks.

Home Page:https://highlightjs-copy.netlify.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Nextjs13 - ReferenceError: self is not defined

greengem opened this issue · comments

I'm trying to integrate this into my nextjs13 site in a markdown file that renders blocks of rich text from my headless CMS, i'm using app router. This is probably do to my lack of knowledge with SSR but thought i'd ask here before giving up.

'use client'
import hljs from 'highlight.js';
import CopyButtonPlugin from 'highlightjs-copy';
hljs.addPlugin(new CopyButtonPlugin());

[BLOCKS.EMBEDDED_ENTRY]: (node) => {
                const entryId = node.data.target.sys.id;
                const entry = description.links.entries.block.find(e => e.sys.id === entryId);
                if (!entry) return null;
                const highlightedCode = hljs.highlight(entry.code, { language: entry.language }).value;
                return (
                    <div className='syntax-custom overflow-x-auto my-10'>
                        <p className="text-tiny uppercase font-bold mb-1 text-danger">{entry.language}</p>
                        <pre className='text-sm hljs-copy-wrapper'>
                            <code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
                        </pre>
                    </div>
                );
            }

Error:

ReferenceError: self is not defined
at eval (./lib/markdown.tsx:34:64)
at (ssr)/./lib/markdown.tsx (/folder/.next/server/app/projects/[slug]/page.js:226:1)
at webpack_require (/folder/.next/server/webpack-runtime.js:33:43)
null

The reason you're getting the error is because NextJS 13 uses server components by default. The 'Use Client' tells the component to use client rendering, but you need to tell it to wait until the code block has loaded before it tries to highlight.

I'm using Sanity and NextJS but this should give you enough of an idea.

  1. useEffect tells it to wait until there is a DOM element to highlight (This will get rid of your error).
  2. We add the plugin which must be done first.
  3. The if statement looks through the codeBlocks and only highlights elements that haven't already been highlighted.
  4. ref assigns a reference to the div to keep track of whether or not it's been highlighted (more or less).
  5. Then we pass everything to the child component.
'use client'
import hljs from 'highlight.js'
import CopyButtonPlugin from 'highlightjs-copy'
import { useEffect, useRef } from 'react'

hljs.addPlugin(new CopyButtonPlugin())

export default function CodeSyntaxHighlightWrapper({ children }) {
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (ref.current) {
      const codeBlocks = ref.current.querySelectorAll(
        'pre code:not([data-highlighted])',
      )
      codeBlocks.forEach((block) => {
        const htmlBlock = block as HTMLElement
        hljs.highlightElement(htmlBlock)
        htmlBlock.dataset.highlighted = 'true'
      })
    }
  }, [])

  return <div ref={ref}>{children}</div>
}

The child component.

export const CodeRendering = ({ children, language }) => {
  return (
    <pre className="hljs hljs-copy-wrapper p-[.0em] ring-1 ring-gray-200 dark:ring-gray-800">
      <code className={`language-${language} whitespace-pre-wrap break-words`}>
        {children}
      </code>
    </pre>
  )
}

The parent

code: ({ value }) => {
        const code = value?.code || 'empty code block'
        const language = value?.language || 'plaintext'
        return (
          <CodeSyntaxHighlightWrapper>
            <CodeRendering language={language}>{code}</CodeRendering>
          </CodeSyntaxHighlightWrapper>
        )
      },