mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: add syntax highlighting for code blocks in blog posts
This commit is contained in:
@@ -6,10 +6,12 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import type React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { Components } from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
import { ZoomableImage } from "./components/ZoomableImage";
|
||||
|
||||
type Props = {
|
||||
@@ -54,6 +56,13 @@ export async function generateStaticParams() {
|
||||
}));
|
||||
}
|
||||
|
||||
interface CodeProps
|
||||
extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> {
|
||||
inline?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }: Props) {
|
||||
const { locale, slug } = params;
|
||||
const t = await getTranslations({ locale, namespace: "blog" });
|
||||
@@ -83,12 +92,16 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
h3: ({ node, ...props }) => (
|
||||
<h3 className="text-xl text-primary/90 font-bold mt-4 mb-2" {...props} />
|
||||
),
|
||||
p: ({ node, ...props }) => (
|
||||
<p
|
||||
className="text-base text-muted-foreground leading-relaxed mb-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
p: ({ node, children, ...props }) => {
|
||||
return (
|
||||
<p
|
||||
className="text-base text-muted-foreground leading-relaxed mb-4"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
a: ({ node, href, ...props }) => (
|
||||
<a
|
||||
href={href}
|
||||
@@ -138,6 +151,63 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
{src && <ZoomableImage src={src} alt={alt || ""} />}
|
||||
</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(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
140
apps/website/app/[locale]/blog/components/CodeBlock.tsx
Normal file
140
apps/website/app/[locale]/blog/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import * as xmlPlugin from "@prettier/plugin-xml";
|
||||
import * as prettier from "prettier";
|
||||
import { Highlight, themes } from "prism-react-renderer";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
|
||||
const [formattedCode, setFormattedCode] = useState(code);
|
||||
|
||||
useEffect(() => {
|
||||
const formatCode = async () => {
|
||||
try {
|
||||
// Determine the parser based on the language
|
||||
let parser = language;
|
||||
let plugins: any[] = [];
|
||||
|
||||
// Map common languages to their appropriate parsers
|
||||
switch (language.toLowerCase()) {
|
||||
case "tsx":
|
||||
case "ts":
|
||||
case "typescript":
|
||||
parser = "babel-ts";
|
||||
plugins = ["@babel/plugin-syntax-typescript"];
|
||||
break;
|
||||
case "jsx":
|
||||
case "js":
|
||||
case "javascript":
|
||||
parser = "babel";
|
||||
break;
|
||||
case "html":
|
||||
case "xml":
|
||||
case "svg":
|
||||
parser = "html";
|
||||
plugins = [xmlPlugin];
|
||||
break;
|
||||
case "json":
|
||||
parser = "json";
|
||||
break;
|
||||
case "css":
|
||||
case "scss":
|
||||
case "less":
|
||||
parser = "css";
|
||||
break;
|
||||
default:
|
||||
// For unknown languages, just clean up the whitespace
|
||||
setFormattedCode(code.trim());
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = await prettier.format(code, {
|
||||
parser,
|
||||
plugins,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
printWidth: 80,
|
||||
});
|
||||
|
||||
setFormattedCode(formatted.trim());
|
||||
} catch (error) {
|
||||
console.warn("Error formatting code:", error);
|
||||
// If formatting fails, just clean up the whitespace
|
||||
setFormattedCode(code.trim());
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
language={language.toLowerCase()}
|
||||
>
|
||||
{({
|
||||
className: preClassName,
|
||||
style,
|
||||
tokens,
|
||||
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);
|
||||
}}
|
||||
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
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="table-cell">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
}
|
||||
@@ -97,6 +97,7 @@ export async function getPost(slug: string): Promise<Post | null> {
|
||||
slug,
|
||||
include: ["authors"],
|
||||
})) as Post;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching post:", error);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
},
|
||||
"browserslist": "defaults, not ie <= 11",
|
||||
"dependencies": {
|
||||
"react-photo-view": "^1.2.7",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
@@ -37,16 +36,21 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9",
|
||||
"@biomejs/biome": "1.7.0",
|
||||
"@prettier/plugin-xml": "^3.4.1",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"prettier": "^3.0.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.2"
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user