mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: add syntax highlighting and code formatting for blog posts
This commit is contained in:
@@ -11,6 +11,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import TurndownService from "turndown";
|
||||||
import { CodeBlock } from "../components/CodeBlock";
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
import { ZoomableImage } from "./components/ZoomableImage";
|
import { ZoomableImage } from "./components/ZoomableImage";
|
||||||
|
|
||||||
@@ -76,6 +77,13 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
notFound();
|
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, {
|
const formattedDate = new Date(post.published_at).toLocaleDateString(locale, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
@@ -152,60 +160,14 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
code: ({ inline, className, children, ...props }: CodeProps) => {
|
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 || "");
|
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 (
|
return (
|
||||||
<div className="not-prose my-6">
|
<CodeBlock
|
||||||
<CodeBlock
|
code={children?.toString() || ""}
|
||||||
code={codeContent}
|
language={match ? match[1] : "text"}
|
||||||
language={match ? match[1] : "text"}
|
className="my-6"
|
||||||
className="my-6"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -298,7 +260,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
rehypePlugins={[rehypeRaw]}
|
rehypePlugins={[rehypeRaw]}
|
||||||
components={components}
|
components={components}
|
||||||
>
|
>
|
||||||
{post.html}
|
{markdown}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -75,18 +75,10 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
|
|||||||
formatCode();
|
formatCode();
|
||||||
}, [code, language]);
|
}, [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 (
|
return (
|
||||||
<Highlight
|
<Highlight
|
||||||
theme={themes.dracula}
|
theme={themes.dracula}
|
||||||
code={processedCode}
|
code={formattedCode}
|
||||||
language={language.toLowerCase()}
|
language={language.toLowerCase()}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
@@ -96,43 +88,40 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
|
|||||||
getLineProps,
|
getLineProps,
|
||||||
getTokenProps,
|
getTokenProps,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="relative group">
|
<div
|
||||||
<pre
|
className={`${preClassName} ${className} overflow-x-auto p-4 rounded-lg text-[14px] leading-[1.5] font-mono relative group`}
|
||||||
className={`${preClassName} ${className} overflow-x-auto p-4 rounded-lg text-[13px] leading-[1.5] font-mono`}
|
style={{
|
||||||
style={{
|
...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
|
||||||
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
type="button"
|
||||||
<button
|
onClick={() => {
|
||||||
type="button"
|
navigator.clipboard.writeText(formattedCode);
|
||||||
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
|
||||||
}}
|
}}
|
||||||
className="px-2 py-1 text-xs rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
|
|
||||||
>
|
>
|
||||||
Copy
|
{i + 1}
|
||||||
</button>
|
</span>
|
||||||
|
<span className="table-cell">
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span key={key} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@tabler/icons-react": "3.21.0",
|
"@tabler/icons-react": "3.21.0",
|
||||||
"@tryghost/content-api": "^1.11.21",
|
"@tryghost/content-api": "^1.11.21",
|
||||||
"@types/node": "20.4.6",
|
"@types/node": "20.4.6",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"turndown": "^7.2.0",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -143,6 +143,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.4.6
|
specifier: 20.4.6
|
||||||
version: 20.4.6
|
version: 20.4.6
|
||||||
|
'@types/turndown':
|
||||||
|
specifier: ^5.0.5
|
||||||
|
version: 5.0.5
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.12
|
specifier: ^10.4.12
|
||||||
version: 10.4.19(postcss@8.4.47)
|
version: 10.4.19(postcss@8.4.47)
|
||||||
@@ -188,6 +191,9 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.7)
|
version: 1.0.7(tailwindcss@3.4.7)
|
||||||
|
turndown:
|
||||||
|
specifier: ^7.2.0
|
||||||
|
version: 7.2.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.1.6
|
specifier: 5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
@@ -968,6 +974,9 @@ packages:
|
|||||||
'@mdx-js/mdx@3.1.0':
|
'@mdx-js/mdx@3.1.0':
|
||||||
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
|
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
|
||||||
|
|
||||||
|
'@mixmark-io/domino@2.2.0':
|
||||||
|
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
||||||
|
|
||||||
'@next/env@14.2.2':
|
'@next/env@14.2.2':
|
||||||
resolution: {integrity: sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw==}
|
resolution: {integrity: sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw==}
|
||||||
|
|
||||||
@@ -1660,6 +1669,9 @@ packages:
|
|||||||
'@types/react@18.3.5':
|
'@types/react@18.3.5':
|
||||||
resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==}
|
resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==}
|
||||||
|
|
||||||
|
'@types/turndown@5.0.5':
|
||||||
|
resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
|
||||||
|
|
||||||
'@types/unist@2.0.10':
|
'@types/unist@2.0.10':
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
|
|
||||||
@@ -3517,6 +3529,9 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
turndown@7.2.0:
|
||||||
|
resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==}
|
||||||
|
|
||||||
typescript@5.1.6:
|
typescript@5.1.6:
|
||||||
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
|
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -4322,6 +4337,8 @@ snapshots:
|
|||||||
- acorn
|
- acorn
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@mixmark-io/domino@2.2.0': {}
|
||||||
|
|
||||||
'@next/env@14.2.2': {}
|
'@next/env@14.2.2': {}
|
||||||
|
|
||||||
'@next/env@15.0.3': {}
|
'@next/env@15.0.3': {}
|
||||||
@@ -5279,6 +5296,8 @@ snapshots:
|
|||||||
'@types/prop-types': 15.7.12
|
'@types/prop-types': 15.7.12
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
'@types/turndown@5.0.5': {}
|
||||||
|
|
||||||
'@types/unist@2.0.10': {}
|
'@types/unist@2.0.10': {}
|
||||||
|
|
||||||
'@types/unist@3.0.2': {}
|
'@types/unist@3.0.2': {}
|
||||||
@@ -7576,6 +7595,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
turndown@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@mixmark-io/domino': 2.2.0
|
||||||
|
|
||||||
typescript@5.1.6: {}
|
typescript@5.1.6: {}
|
||||||
|
|
||||||
typescript@5.6.3: {}
|
typescript@5.6.3: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user