solutionApril 18, 2025

GithubCodeBlock Component

A codeblock component to display code from public GitHub repositories in Fuma Docs

reactcomponent

The GithubCodeBlock component allows you to embed and display code from GitHub repositories directly in your Fuma Docs application. It supports line extraction, syntax highlighting, and line highlighting features.

Launch Post

View post on X

Installation

You need to have fumadocs setup ready and running

1. Copy following code anywhere into your components

I am still working on a better way if there is one like using shadcn cli

github-code-block.tsx
import * as Base from "fumadocs-ui/components/codeblock";
import { highlight } from "fumadocs-core/highlight";
import { transformerMetaHighlight } from "@shikijs/transformers";
 
// Types
export interface CodeBlockProps {
  code: string;
  wrapper?: Base.CodeBlockProps;
  lang: string;
  highlightLines?: string;
}
 
interface GithubCodeBlockProps {
  url: string;
  extractLines?: boolean;
  highlightLines?: string;
  wrapper?: Base.CodeBlockProps;
}
 
interface GitHubReference {
  rawUrl: string;
  fromLine?: number;
  toLine?: number;
  highlightLines?: string;
}
 
// Helper functions
function formatHighlightLines(highlightLines?: string): string | undefined {
  if (!highlightLines) return undefined;
  return highlightLines.startsWith("{") && highlightLines.endsWith("}")
    ? highlightLines
    : `{${highlightLines}}`;
}
 
function getLanguageFromUrl(url: string): string {
  try {
    return url.split(".").pop()?.toLowerCase() || "";
  } catch {
    return "";
  }
}
 
function parseGitHubUrl(url: string): GitHubReference {
  try {
    // Split the URL to separate the line reference part
    const [githubUrl, loc] = url.split("#");
 
    if (!githubUrl) {
      throw new Error("Invalid GitHub URL");
    }
 
    // Initialize line reference variables
    let fromLine: number | undefined;
    let toLine: number | undefined;
    let highlightLines: string | undefined;
 
    // Parse line references if present
    if (loc) {
      const lineParts = loc.split("-");
 
      if (lineParts[0]?.startsWith("L")) {
        fromLine = parseInt(lineParts[0].slice(1), 10) - 1;
 
        if (lineParts[1]?.startsWith("L")) {
          toLine = parseInt(lineParts[1].slice(1), 10) - 1;
        } else {
          toLine = fromLine;
        }
 
        // Always generate highlight lines from location
        // These will be used if no explicit highlightLines prop is provided
        if (fromLine !== undefined && toLine !== undefined) {
          const startLine = fromLine + 1;
          const endLine = toLine + 1;
          highlightLines =
            startLine === endLine
              ? `{${startLine}}`
              : `{${startLine}-${endLine}}`;
        }
      }
    }
 
    // Parse GitHub URL to create raw URL
    const urlObj = new URL(githubUrl);
    const pathParts = urlObj.pathname.split("/").slice(1);
 
    if (pathParts.length < 5) {
      throw new Error("Invalid GitHub repository path");
    }
 
    const [org, repo, _, branch, ...pathSeg] = pathParts;
 
    if (!org || !repo || !branch || pathSeg.length === 0) {
      throw new Error("Missing required GitHub path components");
    }
 
    // Create reference object with raw URL and line info
    return {
      rawUrl: `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${pathSeg.join("/")}`,
      fromLine,
      toLine,
      highlightLines,
    };
  } catch (error) {
    console.error("Error parsing GitHub URL:", error);
    throw new Error(
      `Invalid GitHub URL: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}
 
async function fetchCode(url: string, fromLine?: number, toLine?: number) {
  try {
    const response = await fetch(url, { cache: "force-cache" });
 
    if (!response.ok) {
      throw new Error(
        `Failed to fetch code: ${response.status} ${response.statusText}`
      );
    }
 
    const content = await response.text();
 
    // Return full content if no line numbers are specified
    if (fromLine === undefined || toLine === undefined) {
      return content;
    }
 
    // Extract specific lines
    const lines = content.split("\n");
    const selectedLines = lines.slice(fromLine, toLine + 1);
 
    if (selectedLines.length === 0) {
      return content;
    }
 
    // Calculate common indentation to remove
    const commonIndent = selectedLines.reduce(
      (indent: number, line: string) => {
        if (line.length === 0) return indent;
        const spaces = line.match(/^\s+/);
        return spaces ? Math.min(indent, spaces[0].length) : 0;
      },
      Infinity
    );
 
    // Remove common indentation and join lines
    return selectedLines
      .map((line) => {
        if (line.length === 0) return line;
        return line.slice(commonIndent < Infinity ? commonIndent : 0);
      })
      .join("\n");
  } catch (error) {
    console.error("Error fetching code:", error);
    return `// Error fetching code: ${error instanceof Error ? error.message : String(error)}`;
  }
}
 
// Components
export async function CodeBlock({
  code,
  lang,
  wrapper,
  highlightLines,
}: CodeBlockProps) {
  const rendered = await highlight(code, {
    lang,
    meta: highlightLines ? { __raw: highlightLines } : undefined,
    themes: {
      light: "github-light",
      dark: "github-dark",
    },
    components: {
      pre: Base.Pre,
    },
    transformers: [transformerMetaHighlight()],
  });
 
  return <Base.CodeBlock {...wrapper}>{rendered}</Base.CodeBlock>;
}
 
export default async function GithubCodeBlock({
  url,
  extractLines = false,
  highlightLines,
  wrapper,
}: GithubCodeBlockProps) {
  try {
    // Validate GitHub URL
    if (!url.includes("github.com")) {
      throw new Error("This component only supports GitHub URLs");
    }
 
    // Parse GitHub URL to get raw URL and line info
    const reference = parseGitHubUrl(url);
 
    // Format highlight lines for Shiki
    // Priority: explicitly provided highlightLines prop > lines from URL loc
    const formattedHighlightLines = formatHighlightLines(
      highlightLines || reference.highlightLines
    );
 
    // Fetch the code content, extracting specific lines if needed
    const code = await fetchCode(
      reference.rawUrl,
      extractLines ? reference.fromLine : undefined,
      extractLines ? reference.toLine : undefined
    );
 
    const lang = getLanguageFromUrl(reference.rawUrl);
 
    return (
      <CodeBlock
        lang={lang}
        code={code}
        highlightLines={formattedHighlightLines}
        wrapper={wrapper}
      />
    );
  } catch (error) {
    console.error("Error in GithubCodeBlock:", error);
    return (
      <CodeBlock
        lang="text"
        code={`// Error: ${error instanceof Error ? error.message : String(error)}`}
        wrapper={wrapper}
      />
    );
  }
}

2. Add to MDX Components

mdx-components.tsx
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
import GithubCodeBlock from "./components/github-code-block";
 
export function getMDXComponents(components?: MDXComponents): MDXComponents {
  return {
    ...defaultMdxComponents,
    GithubCodeBlock: GithubCodeBlock,
    ...components,
  };
}

3. Basic Usage

usage
<GithubCodeBlock url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json" />
result
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.6.0",
  "packages": [
    "packages/*",
    "apps/*"
  ]
}

Props

The component accepts the following props:

PropTypeDefaultDescription
urlstring(required)GitHub URL to the file you want to display
extractLinesbooleanfalseWhether to extract specific lines from the file
highlightLinesstringundefinedLines to highlight in the format "{1,3-4}"
wrapperobjectundefinedCodeBlockProps to pass to the underlying CodeBlock component

Examples

Display a Complete File

usage
<GithubCodeBlock url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json" />
result
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.6.0",
  "packages": [
    "packages/*",
    "apps/*"
  ]
}

Extract Specific Lines

You can extract specific lines from a file by adding a line reference to the URL and setting extractLines to true:

usage
<GithubCodeBlock
  url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json#L2-L4"
  extractLines={true}
/>
result
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.6.0",
"packages": [

Highlight Specific Lines

You can highlight specific lines using the highlightLines prop:

usage
<GithubCodeBlock
  url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json"
  highlightLines="{2,4}"
/>
result
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.6.0",
  "packages": [
    "packages/*",
    "apps/*"
  ]
}

Extract and Highlight Lines

You can extract specific lines and highlight them at the same time:

usage
<GithubCodeBlock
  url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json#L2-L4"
  extractLines={true}
  highlightLines="{2}"
/>
result
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.6.0",
"packages": [

Automatic Highlighting from URL

The component will automatically use line references in the URL hash for highlighting if no highlightLines prop is provided:

usage
<GithubCodeBlock url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json#L2-L4" />

This will highlight lines 2-4 if extractLines is false:

result
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.6.0",
  "packages": [
    "packages/*",
    "apps/*"
  ]
}

Override URL Highlighting

You can override the automatic URL-based highlighting by providing your own highlightLines prop:

usage
<GithubCodeBlock
  url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json#L2-L4"
  highlightLines="{3}"
/>
result
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.6.0",
  "packages": [
    "packages/*",
    "apps/*"
  ]
}

Highlight Format

The highlightLines prop accepts a string in the format "{lineNumber,lineRange}" where:

  • Single lines are specified as numbers: "{1,3,5}"
  • Ranges are specified with a hyphen: "{1-3,5-7}"
  • You can combine both: "{1,3-5,7,9-11}"

Last updated on