feat: add syntax highlighting and code formatting for blog posts

This commit is contained in:
Mauricio Siu
2025-03-02 16:12:26 -06:00
parent 2f6455027a
commit 5f66cfd234
4 changed files with 71 additions and 95 deletions

View File

@@ -11,6 +11,7 @@ import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import TurndownService from "turndown";
import { CodeBlock } from "../components/CodeBlock";
import { ZoomableImage } from "./components/ZoomableImage";
@@ -76,6 +77,13 @@ export default async function BlogPostPage({ params }: Props) {
notFound();
}
// Convertir HTML a Markdown
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
});
const markdown = turndownService.turndown(post.html);
const formattedDate = new Date(post.published_at).toLocaleDateString(locale, {
year: "numeric",
month: "long",
@@ -152,60 +160,14 @@ export default async function BlogPostPage({ params }: Props) {
</div>
),
code: ({ inline, className, children, ...props }: CodeProps) => {
if (inline) {
return (
<code
className="px-1.5 py-0.5 rounded-md bg-muted font-mono text-sm"
{...props}
>
{children}
</code>
);
}
const match = /language-(\w+)/.exec(className || "");
// Extraer el contenido del código de la estructura anidada
const extractCodeContent = (children: React.ReactNode): string => {
if (typeof children === "string") {
return children;
}
if (Array.isArray(children)) {
return children
.map((child) => {
if (typeof child === "string") {
return child;
}
if (child && typeof child === "object" && "props" in child) {
return extractCodeContent(child.props.children);
}
return "";
})
.join("");
}
if (children && typeof children === "object" && "props" in children) {
return extractCodeContent(children.props.children);
}
return "";
};
const codeContent = extractCodeContent(children)
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&amp;/g, "&")
.trim();
// Wrap CodeBlock in a div to prevent it from being inside a p tag
return (
<div className="not-prose my-6">
<CodeBlock
code={codeContent}
language={match ? match[1] : "text"}
className="my-6"
/>
</div>
<CodeBlock
code={children?.toString() || ""}
language={match ? match[1] : "text"}
className="my-6"
/>
);
},
};
@@ -298,7 +260,7 @@ export default async function BlogPostPage({ params }: Props) {
rehypePlugins={[rehypeRaw]}
components={components}
>
{post.html}
{markdown}
</ReactMarkdown>
</div>

View File

@@ -75,18 +75,10 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
formatCode();
}, [code, language]);
// Ensure we're working with a string and remove any potential duplicate line breaks
const processedCode =
typeof formattedCode === "string"
? formattedCode.replace(/\n+/g, "\n")
: typeof formattedCode === "object"
? JSON.stringify(formattedCode, null, 2)
: String(formattedCode);
return (
<Highlight
theme={themes.dracula}
code={processedCode}
code={formattedCode}
language={language.toLowerCase()}
>
{({
@@ -96,43 +88,40 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
getLineProps,
getTokenProps,
}) => (
<div className="relative group">
<pre
className={`${preClassName} ${className} overflow-x-auto p-4 rounded-lg text-[13px] leading-[1.5] font-mono`}
style={{
...style,
backgroundColor: "rgb(40, 42, 54)", // Dracula background
}}
>
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(processedCode);
<div
className={`${preClassName} ${className} overflow-x-auto p-4 rounded-lg text-[14px] leading-[1.5] font-mono relative group`}
style={{
...style,
}}
>
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(formattedCode);
}}
className="px-2 py-1 text-xs rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
>
Copy
</button>
</div>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })} className="table-row">
<span
className="table-cell text-right pr-4 select-none w-[2.5em] text-zinc-500 text-xs"
style={{
color: "rgb(98, 114, 164)", // Dracula comment color
}}
className="px-2 py-1 text-xs rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
>
Copy
</button>
{i + 1}
</span>
<span className="table-cell">
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })} className="table-row">
<span
className="table-cell text-right pr-4 select-none w-[2.5em] text-zinc-500 text-xs"
style={{
color: "rgb(98, 114, 164)", // Dracula comment color
}}
>
{i + 1}
</span>
<span className="table-cell">
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
))}
</pre>
))}
</div>
)}
</Highlight>

View File

@@ -25,6 +25,7 @@
"@tabler/icons-react": "3.21.0",
"@tryghost/content-api": "^1.11.21",
"@types/node": "20.4.6",
"@types/turndown": "^5.0.5",
"autoprefixer": "^10.4.12",
"axios": "^1.8.1",
"class-variance-authority": "^0.7.0",
@@ -40,6 +41,7 @@
"tailwind-merge": "^2.2.2",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.0",
"typescript": "5.1.6"
},
"devDependencies": {