Skip to content

Commit d4a8262

Browse files
committed
feature: add image caption and image rounding
1 parent 6a72796 commit d4a8262

File tree

6 files changed

+155
-4
lines changed

6 files changed

+155
-4
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,22 @@
2626
"reading-time": "^1.5.0",
2727
"rehype-highlight": "^7.0.2",
2828
"rehype-katex": "^7.0.1",
29+
"rehype-parse": "^9.0.1",
30+
"rehype-stringify": "^10.0.1",
2931
"remark": "^15.0.1",
3032
"remark-gfm": "^4.0.1",
3133
"remark-html": "^16.0.1",
34+
"remark-parse": "^11.0.0",
35+
"remark-rehype": "^11.1.2",
3236
"sharp": "^0.34.5",
3337
"tailwind-merge": "^3.4.0",
34-
"turndown": "^7.2.2"
38+
"turndown": "^7.2.2",
39+
"unified": "^11.0.5",
40+
"unist-util-visit": "^5.0.0"
3541
},
3642
"devDependencies": {
3743
"@tailwindcss/postcss": "^4",
44+
"@types/hast": "^3.0.4",
3845
"@types/jsdom": "^27.0.0",
3946
"@types/node": "^20",
4047
"@types/react": "^19",

pnpm-lock.yaml

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/globals.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,22 @@ a:hover {
8989
text-transform: uppercase;
9090
letter-spacing: 0.5px;
9191
}
92+
93+
/* Figure and figcaption styling */
94+
.prose figure.image-figure {
95+
margin: 2rem 0;
96+
}
97+
98+
.prose figure.image-figure img {
99+
margin: 0;
100+
border-radius: 0.5rem;
101+
}
102+
103+
.prose figure.image-figure figcaption {
104+
margin-top: 0.75rem;
105+
font-size: 0.875rem;
106+
line-height: 1.5;
107+
color: var(--text-light);
108+
text-align: center;
109+
font-style: italic;
110+
}

src/components/PostCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function PostCard({ post }: PostCardProps) {
2424
<article className="group flex flex-col bg-white">
2525
<Link href={`/blog/${post.slug}`} className="mb-4 block">
2626
{post.featuredImage && (
27-
<div className="relative aspect-video overflow-hidden">
27+
<div className="relative aspect-video overflow-hidden rounded">
2828
<Image
2929
src={post.featuredImage}
3030
alt={post.title}

src/lib/markdown-posts.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import matter from 'gray-matter';
44
import { remark } from 'remark';
55
import html from 'remark-html';
66
import gfm from 'remark-gfm';
7+
import { unified } from 'unified';
8+
import remarkParse from 'remark-parse';
9+
import remarkRehype from 'remark-rehype';
10+
import rehypeStringify from 'rehype-stringify';
11+
import rehypeFigure from './rehype-figure';
712

813
const postsDirectory = path.join(process.cwd(), 'content', 'posts');
914

@@ -233,9 +238,12 @@ export function getAllPostSlugs(): string[] {
233238
}
234239

235240
export async function markdownToHtml(markdown: string): Promise<string> {
236-
const result = await remark()
241+
const result = await unified()
242+
.use(remarkParse)
237243
.use(gfm)
238-
.use(html, { sanitize: false })
244+
.use(remarkRehype, { allowDangerousHtml: true })
245+
.use(rehypeFigure)
246+
.use(rehypeStringify, { allowDangerousHtml: true })
239247
.process(markdown);
240248
return result.toString();
241249
}

src/lib/rehype-figure.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { visit } from 'unist-util-visit';
2+
import type { Root, Element, Parent, ElementContent } from 'hast';
3+
4+
/**
5+
* Rehype plugin to wrap images in figure elements with figcaption
6+
* Uses the image's alt text as the caption
7+
*/
8+
export default function rehypeFigure() {
9+
return (tree: Root) => {
10+
visit(tree, 'element', (node: Element, index, parent: Parent | undefined) => {
11+
// Only process img elements
12+
if (
13+
node.tagName === 'img' &&
14+
parent &&
15+
index !== undefined
16+
) {
17+
const parentElement = parent as Element;
18+
const isInParagraph = parent.type === 'element' && parentElement.tagName === 'p';
19+
const isInRoot = parent.type === 'root';
20+
21+
if (!isInParagraph && !isInRoot) {
22+
return;
23+
}
24+
25+
const alt = node.properties?.alt as string;
26+
27+
// Create figure element
28+
const figure: Element = {
29+
type: 'element',
30+
tagName: 'figure',
31+
properties: {
32+
className: ['image-figure'],
33+
},
34+
children: [
35+
{
36+
...node,
37+
properties: {
38+
...node.properties,
39+
},
40+
},
41+
],
42+
};
43+
44+
// Add figcaption if alt text exists
45+
if (alt && alt.trim()) {
46+
figure.children.push({
47+
type: 'element',
48+
tagName: 'figcaption',
49+
properties: {},
50+
children: [
51+
{
52+
type: 'text',
53+
value: alt,
54+
},
55+
],
56+
});
57+
}
58+
59+
// Replace the image with the figure
60+
parent.children[index] = figure as ElementContent;
61+
}
62+
});
63+
};
64+
}

0 commit comments

Comments
 (0)