Introducción
Esta guía explica cómo configurar Shiki como resaltador de sintaxis en un blog desarrollado con Next.js y MDX, incluyendo la creación de un componente reutilizable y altamente personalizable.
📦 Instalación de dependencias
Instalación de la dependencia principal:
npm install -D shiki
Creando un server component de resaltado de sintaxis
Vamos a crear un componente que se encargue de resaltar la sintaxis de bloques de código. Este componente usará codeToHtml
de Shiki para convertir el código en HTML resaltado.
import { cn } from "@/lib/utils";
import React from "react";
import type { BundledLanguage, BundledTheme } from "shiki";
import { addClassToHast, codeToHtml } from "shiki";
import CopyClipboard from "./copyClipboard";
interface Props {
children: string;
lang: BundledLanguage;
theme?: BundledTheme;
enabledNumbers?: boolean;
classNames?: {
root?: string;
content?: string;
pre?: string;
code?: string;
lineNumber?: string;
lineHighlight?: string;
};
}
export default async function MarkdownSyntaxHighlighterSSR(props: Props) {
const getHighlight = async () => {
const out = await codeToHtml(props.children, {
lang: props.lang,
theme: props.theme || "vitesse-dark",
cssVariablePrefix: "shikiji",
transformers: [
{
code(node) {
if (props.enabledNumbers) {
addClassToHast(node, cn("enabledLineNumbers", props.classNames?.code));
}
addClassToHast(node, cn("block min-w-full w-fit overflow-auto", props.classNames?.code));
},
line(hast, line) {
addClassToHast(hast, cn("shikiji-line-number text-sm", props.classNames?.lineNumber));
if ([1, 3, 4].includes(line)) {
addClassToHast(hast, cn("shikiji-line-highlight", props.classNames?.lineHighlight));
}
},
span(hast, line, col, lineElement) {
addClassToHast(hast, cn("shikiji-line-highlight-span"));
if (lineElement) {
addClassToHast(lineElement, cn("shikiji-line-highlight-span"));
}
},
pre(hast) {
addClassToHast(hast, cn("shikiji-pre leading-6 min-w-full rounded-2xl p-6 overflow-auto", props.classNames?.pre));
},
},
],
});
return out;
};
const highlight = await getHighlight();
if (!highlight) {
return null;
}
return (
<div className={cn("relative", props.classNames?.root)}>
<div dangerouslySetInnerHTML={{ __html: highlight }} className={cn("w-full relative", props.classNames?.content)} />
<CopyClipboard code={props.children} />
</div>
);
}
Este componente:
- Usa
codeToHtml
para transformar el código en HTML resaltado. - Permite personalizar clases para elementos individuales como
pre
,line
,span
, etc. - Soporta temas personalizados como
vitesse-dark
.
Componente para copiar al portapapeles
Mejoramos la experiencia del usuario añadiendo un botón para copiar el código al portapapeles. Este componente se puede integrar fácilmente en el resaltador de sintaxis.
"use client";
import { useCopyToClipboard } from "@/app/hooks/useCopyToClipboard";
import React from "react";
export default function CopyClipboard({ code }: { code: string }) {
const [, copy] = useCopyToClipboard();
const [isCopied, setIsCopied] = React.useState(false);
const handleCopy = async () => {
try {
await copy(code);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 500);
} catch (error) {
console.error("Copy failed", error);
}
};
return (
<button
className="absolute right-4 top-4 z-10 w-10 h-10 cursor-pointer rounded-full inline-flex items-center justify-center motion-safe:transition-colors motion-safe:duration-200 bg-black/80 hover:bg-black/90 text-white dark:bg-white/5 dark:hover:bg-white/10"
onClick={handleCopy}
>
{isCopied ? (
<span className="material-symbols-rounded material-symbols-md material-symbols-weight-300">check</span>
) : (
<span className="material-symbols-rounded material-symbols-md material-symbols-weight-300">content_copy</span>
)}
</button>
);
}
Hook para copiar al portapapeles
Este hook se encarga de copiar el texto al portapapeles. Puedes usarlo en cualquier parte de tu aplicación.
"use client";
import { useCallback, useState } from "react";
type CopiedValue = string | null;
type CopyFn = (_text: string) => Promise<boolean>;
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
const copy: CopyFn = useCallback(async (text) => {
if (!navigator?.clipboard) {
console.warn("Clipboard not supported");
return false;
}
// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
return true;
} catch (error) {
console.warn("Copy failed", error);
setCopiedText(null);
return false;
}
}, []);
return [copiedText, copy];
}
Integración con MDX
Integramos el componente anterior en el contexto de MDX para reemplazar el elemento <pre>
por nuestro resaltador personalizado:
import type { MDXComponents } from "mdx/types";
import MarkdownSyntaxHighlighter from "./markdown-syntax-highlighter";
export const useMDXComponents: MDXComponents = {
// Otros componentes personalizados...
pre: (props) => {
const code = props.children.props.children as string;
const lang = props.children.props.className.replace("language-", "") as "js" | "ts" | "tsx" | "jsx" | "html" | "css" | "scss" | "json";
const theme = "vitesse-dark";
return (
<MarkdownSyntaxHighlighterSSR lang={lang} theme="vitesse-dark" enabledNumbers={true}>
{[code.trim()].join("\n")}
</MarkdownSyntaxHighlighterSSR>
);
},
};
Este patrón permite interceptar bloques de código renderizados por MDX y aplicarles el resaltado con Shiki automáticamente.
Personalización de estilos
Puedes pasar clases personalizadas a través de la prop classNames
, lo cual te permite adaptar el diseño del resaltador a tu sistema de estilos o temas oscuros/claros.
Ejemplo:
type classNames = {
root?: string
content?: string
pre?: string
code?: string
lineNumber?: string
lineHighlight?: string
}
classNames={{
root: 'relative',
pre: 'bg-zinc-900 p-6 rounded-xl',
lineNumber: 'text-sm text-zinc-500',
lineHighlight: 'bg-zinc-800',
code: 'font-mono',
}}
Conclusión
Con esta configuración, puedes resaltar la sintaxis de bloques de código en tu blog de Next.js utilizando Shiki. La personalización de estilos y la integración con MDX hacen que sea fácil adaptarlo a tus necesidades específicas. ¡Feliz codificación!