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.
Installation
You need to have fumadocs setup ready and running
Step 1: Copy following code anywhere into your components
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}
/>
);
}
}
Step 2: Add to MDX Components
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,
};
}
Basic Usage
<GithubCodeBlock url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json" />
Props
The component accepts the following props:
Prop | Type | Default | Description |
---|---|---|---|
url |
string |
(required) | GitHub URL to the file you want to display |
extractLines |
boolean |
false |
Whether to extract specific lines from the file |
highlightLines |
string |
undefined |
Lines to highlight in the format "{1,3-4}"
|
wrapper |
object |
undefined |
CodeBlockProps to pass to the underlying CodeBlock component |
Examples
Display a Complete File
<GithubCodeBlock url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json" />
Extract Specific Lines
You can extract specific lines from a file by adding a line reference to the URL and setting extractLines
to true
:
<GithubCodeBlock
url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json#L2-L4"
extractLines={true}
/>
Highlight Specific Lines
You can highlight specific lines using the highlightLines
prop:
<GithubCodeBlock
url="https://github.com/rjvim/react-component-library-starter/blob/main/lerna.json"
highlightLines="{2,4}"
/>
Launch Post
Note: In dev.to to editor I am not able to show the output of using the component, you can check that on: https://rjv.im/blog/solution/github-code-block
Top comments (2)
Sick how you can pull code straight from GitHub and show just the lines you want, kinda like clipping out only the best part of a picture, but it's pretty good. How might grabbing live code like this help teams keep docs and code more in sync?
I was writing documentation on how to setup functionality, but the source code was in another repo and docs were in another repo. And I found myself improving the source code itself - copy/pasting every-time was getting complex.
I got this idea from docusaurus !!raw-loader, where you can load any file from local filesystem. This is possible using fumadocs too using tags. I just improvised over those to pull raw content from github.
I need to prepare examples like above, to be shown to user along with preview. So, these are my parts to build the final component. ShadCN setup seems complicated to me, as in, in that they read file using "fs", and format it and store in some other files to cache etc., I tried that but was constantly getting errors cuz of implementation issues. So, I went with above.