feat: enhance blog post page with markdown rendering and related posts

This commit is contained in:
Mauricio Siu
2025-02-27 23:42:24 -06:00
parent 2e09e76e50
commit 8868d7f586
8 changed files with 330 additions and 52 deletions

View File

@@ -5,6 +5,11 @@ import { getTranslations } from "next-intl/server";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
type Props = {
params: { locale: string; slug: string };
@@ -52,6 +57,10 @@ export default async function BlogPostPage({ params }: Props) {
const { locale, slug } = params;
const t = await getTranslations({ locale, namespace: "blog" });
const post = await getPost(slug);
const allPosts = await getPosts();
// Get related posts (excluding current post)
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3); // Show only 3 related posts
if (!post) {
notFound();
@@ -63,6 +72,50 @@ export default async function BlogPostPage({ params }: Props) {
day: "numeric",
});
const components: Partial<Components> = {
h1: ({ node, ...props }) => (
<h1 className="text-3xl font-bold mt-8 mb-4" {...props} />
),
h2: ({ node, ...props }) => (
<h2 className="text-2xl font-bold mt-6 mb-3" {...props} />
),
h3: ({ node, ...props }) => (
<h3 className="text-xl font-bold mt-4 mb-2" {...props} />
),
p: ({ node, ...props }) => (
<p className="text-base leading-relaxed mb-4" {...props} />
),
a: ({ node, href, ...props }) => (
<a
href={href}
className="text-primary hover:text-primary/80 transition-colors"
target="_blank"
rel="noopener noreferrer"
{...props}
/>
),
ul: ({ node, ...props }) => (
<ul className="list-disc list-inside space-y-2 mb-4" {...props} />
),
ol: ({ node, ...props }) => (
<ol className="list-decimal list-inside space-y-2 mb-4" {...props} />
),
li: ({ node, ...props }) => (
<li className="text-base leading-relaxed" {...props} />
),
blockquote: ({ node, ...props }) => (
<blockquote
className="border-l-4 border-primary pl-4 py-2 my-4 bg-muted/50"
{...props}
/>
),
img: ({ node, src, alt }) => (
<div className="relative w-full h-64 my-6 rounded-lg overflow-hidden">
<Image src={src || ""} alt={alt || ""} fill className="object-cover" />
</div>
),
};
return (
<article className="container mx-auto px-4 py-12 max-w-4xl">
<Link
@@ -84,59 +137,115 @@ export default async function BlogPostPage({ params }: Props) {
{t("backToBlog")}
</Link>
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center mb-6">
{post.primary_author?.profile_image && (
<div className="relative h-12 w-12 rounded-full overflow-hidden mr-4">
<div className="bg-card rounded-lg p-8 shadow-lg border border-border">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center mb-6">
{post.primary_author?.profile_image && (
<div className="relative h-12 w-12 rounded-full overflow-hidden mr-4">
<Image
src={post.primary_author.profile_image}
alt={post.primary_author.name}
fill
className="object-cover"
/>
</div>
)}
<div>
<p className="font-medium">
{post.primary_author?.name || "Unknown Author"}
</p>
<p className="text-sm text-muted-foreground">
{formattedDate} {post.reading_time} min read
</p>
</div>
</div>
{post.feature_image && (
<div className="relative h-96 w-full rounded-lg overflow-hidden mb-8">
<Image
src={post.primary_author.profile_image}
alt={post.primary_author.name}
src={post.feature_image}
alt={post.title}
fill
className="object-cover"
priority
/>
</div>
)}
<div>
<p className="font-medium">
{post.primary_author?.name || "Unknown Author"}
</p>
<p className="text-sm text-muted-foreground">
{formattedDate} {post.reading_time} min read
</p>
</div>
</header>
<div className="prose prose-lg max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={components}
>
{post.html}
</ReactMarkdown>
</div>
{post.feature_image && (
<div className="relative h-96 w-full rounded-lg overflow-hidden">
<Image
src={post.feature_image}
alt={post.title}
fill
className="object-cover"
priority
/>
{post.tags && post.tags.length > 0 && (
<div className="mt-12 pt-6 border-t border-border">
<h2 className="text-xl font-semibold mb-4">{t("tags")}</h2>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link
key={tag.id}
href={`/blog/tag/${tag.slug}`}
className="px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
>
{tag.name}
</Link>
))}
</div>
</div>
)}
</header>
</div>
<div
className="prose prose-lg max-w-none prose-headings:text-foreground prose-a:text-primary hover:prose-a:text-primary/80"
dangerouslySetInnerHTML={{ __html: post.html }}
/>
{relatedPosts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">{t("relatedPosts")}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedPosts.map((relatedPost) => {
const relatedPostDate = new Date(
relatedPost.published_at,
).toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric",
});
{post.tags && post.tags.length > 0 && (
<div className="mt-12 pt-6 border-t border-border">
<h2 className="text-xl font-semibold mb-4">{t("tags")}</h2>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link
key={tag.id}
href={`/blog/tag/${tag.slug}`}
className="px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
>
{tag.name}
</Link>
))}
return (
<Link
key={relatedPost.id}
href={`/blog/${relatedPost.slug}`}
className="group"
>
<div className="bg-card rounded-lg overflow-hidden h-full shadow-lg transition-all duration-300 hover:shadow-xl border border-border">
{relatedPost.feature_image && (
<div className="relative h-48 w-full">
<Image
src={relatedPost.feature_image}
alt={relatedPost.title}
fill
className="object-cover"
/>
</div>
)}
<div className="p-6">
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary transition-colors line-clamp-2">
{relatedPost.title}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{relatedPostDate} {relatedPost.reading_time} min read
</p>
<p className="text-muted-foreground line-clamp-2">
{relatedPost.excerpt}
</p>
</div>
</div>
</Link>
);
})}
</div>
</div>
)}

View File

@@ -80,11 +80,10 @@ export interface Post {
export async function getPosts(options = {}): Promise<Post[]> {
try {
const result = (await api.posts.browse({
include: "authors",
limit: "all",
include: ["tags", "authors"],
})) as Post[];
console.log(result);
console.log("Posts data from Ghost API:", JSON.stringify(result, null, 2));
return result;
} catch (error) {
console.error("Error fetching posts:", error);
@@ -94,7 +93,10 @@ export async function getPosts(options = {}): Promise<Post[]> {
export async function getPost(slug: string): Promise<Post | null> {
try {
const result = (await api.posts.read({ slug })) as Post;
const result = (await api.posts.read({
slug,
include: ["authors"],
})) as Post;
return result;
} catch (error) {
console.error("Error fetching post:", error);

View File

@@ -206,6 +206,7 @@
"postsTaggedWith": "Posts tagged with",
"foundPosts": "{count, plural, =0 {No posts found} one {# post found} other {# posts found}}",
"tagTitle": "Posts tagged with {tag}",
"tagDescription": "Browse all blog posts tagged with {tag}"
"tagDescription": "Browse all blog posts tagged with {tag}",
"relatedPosts": "Related Posts"
}
}

View File

@@ -205,6 +205,7 @@
"postsTaggedWith": "Articles tagués avec",
"foundPosts": "{count, plural, =0 {Aucun article trouvé} one {# article trouvé} other {# articles trouvés}}",
"tagTitle": "Articles tagués avec {tag}",
"tagDescription": "Parcourir tous les articles de blog tagués avec {tag}"
"tagDescription": "Parcourir tous les articles de blog tagués avec {tag}",
"relatedPosts": "Articles similaires"
}
}

View File

@@ -210,6 +210,7 @@
"postsTaggedWith": "标签为",
"foundPosts": "{count, plural, =0 {未找到文章} other {找到 # 篇文章}}",
"tagTitle": "标签为 {tag} 的文章",
"tagDescription": "浏览所有标签为 {tag} 的博客文章"
"tagDescription": "浏览所有标签为 {tag} 的博客文章",
"relatedPosts": "相关文章"
}
}

View File

@@ -14,6 +14,8 @@ const nextConfig = {
domains: [
"static.ghost.org",
"testing-ghost-8423be-31-220-108-27.traefik.me",
"images.unsplash.com",
"www.gravatar.com",
],
},
};

View File

@@ -18,12 +18,12 @@
"prepare": "node .husky/install.mjs"
},
"devDependencies": {
"lint-staged": "^15.2.7",
"@biomejs/biome": "1.8.3",
"husky": "^9.1.6",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/node": "^20.9.0"
"@types/node": "^20.9.0",
"husky": "^9.1.6",
"lint-staged": "^15.2.7"
},
"packageManager": "pnpm@9.5.0",
"engines": {
@@ -41,5 +41,10 @@
"resolutions": {
"@types/react": "18.3.5",
"@types/react-dom": "18.3.0"
},
"dependencies": {
"react-markdown": "^10.0.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1"
}
}

157
pnpm-lock.yaml generated
View File

@@ -11,6 +11,16 @@ overrides:
importers:
.:
dependencies:
react-markdown:
specifier: ^10.0.0
version: 10.0.0(@types/react@18.3.5)(react@18.3.1)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
devDependencies:
'@biomejs/biome':
specifier: 1.8.3
@@ -1914,6 +1924,10 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -2205,6 +2219,15 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
hast-util-raw@9.1.0:
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
hast-util-to-estree@3.1.0:
resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==}
@@ -2214,12 +2237,21 @@ packages:
hast-util-to-jsx-runtime@2.3.2:
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
hast-util-to-string@3.0.1:
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -2796,6 +2828,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse5@7.2.1:
resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
path-exists@5.0.0:
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -2947,6 +2982,9 @@ packages:
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -2975,6 +3013,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-markdown@10.0.0:
resolution: {integrity: sha512-4mTz7Sya/YQ1jYOrkwO73VcFdkFJ8L8I9ehCxdcV0XrClHyOJGKbBk5FR4OOOG+HnyKw5u+C/Aby9TwinCteYA==}
peerDependencies:
'@types/react': 18.3.5
react: '>=18'
react-medium-image-zoom@5.2.10:
resolution: {integrity: sha512-JBYf4u0zsocezIDtrjwStD+8sX+c8XuLsdz+HxPbojRj0sCicua0XOQKysuPetoFyX+YgStfj+vEtZ+699O/pg==}
peerDependencies:
@@ -3055,12 +3099,18 @@ packages:
regex@4.4.0:
resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==}
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
rehype-recma@1.0.0:
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
remark-gfm@4.0.0:
resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-mdx@3.0.1:
resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==}
@@ -3402,12 +3452,18 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
vfile@6.0.2:
resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==}
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -5295,6 +5351,8 @@ snapshots:
emoji-regex@9.2.2: {}
entities@4.5.0: {}
env-paths@2.2.1: {}
environment@1.1.0: {}
@@ -5691,6 +5749,37 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.2
devlop: 1.1.0
hastscript: 9.0.1
property-information: 7.0.0
vfile: 6.0.2
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw@9.1.0:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.2
'@ungap/structured-clone': 1.2.0
hast-util-from-parse5: 8.0.3
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
parse5: 7.2.1
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-estree@3.1.0:
dependencies:
'@types/estree': 1.0.5
@@ -5746,6 +5835,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 6.5.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-string@3.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -5754,6 +5853,16 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
hastscript@9.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.0.0
space-separated-tokens: 2.0.2
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
human-signals@5.0.0: {}
@@ -6566,6 +6675,10 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse5@7.2.1:
dependencies:
entities: 4.5.0
path-exists@5.0.0: {}
path-key@3.1.1: {}
@@ -6647,6 +6760,8 @@ snapshots:
property-information@6.5.0: {}
property-information@7.0.0: {}
proxy-from-env@1.1.0: {}
queue-microtask@1.2.3: {}
@@ -6673,6 +6788,24 @@ snapshots:
dependencies:
react: 18.3.1
react-markdown@10.0.0(@types/react@18.3.5)(react@18.3.1):
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/react': 18.3.5
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.2
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.0
react: 18.3.1
remark-parse: 11.0.0
remark-rehype: 11.1.1
unified: 11.0.5
unist-util-visit: 5.0.0
vfile: 6.0.2
transitivePeerDependencies:
- supports-color
react-medium-image-zoom@5.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
@@ -6784,6 +6917,12 @@ snapshots:
regex@4.4.0: {}
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw: 9.1.0
vfile: 6.0.2
rehype-recma@1.0.0:
dependencies:
'@types/estree': 1.0.5
@@ -6803,6 +6942,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
mdast-util-gfm: 3.0.0
micromark-extension-gfm: 3.0.0
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-mdx@3.0.1:
dependencies:
mdast-util-mdx: 3.0.0
@@ -7224,6 +7374,11 @@ snapshots:
util-deprecate@1.0.2: {}
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.2
vfile: 6.0.2
vfile-message@4.0.2:
dependencies:
'@types/unist': 3.0.2
@@ -7235,6 +7390,8 @@ snapshots:
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2
web-namespaces@2.0.1: {}
which@2.0.2:
dependencies:
isexe: 2.0.0