Skip to content

Commit 2ee9eb1

Browse files
dydanzclaude
andcommitted
Add markdown blog system with first post
- Add public/blogs/ folder for markdown files - Add src/blogs/manifest.js as post index (add entry here per new post) - Rewrite Blogs container: post list → click → rendered markdown view - Install react-markdown@6 for rendering - Add permanent Blog link to header (not gated on display flag) - Enable blogSection.display and simplify its config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 921dc8d commit 2ee9eb1

8 files changed

Lines changed: 730 additions & 117 deletions

File tree

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"react-headroom": "^3.2.1",
1616
"react-icons": "^4.10.1",
1717
"react-lottie": "^1.2.3",
18+
"react-markdown": "^6.0.3",
1819
"react-reveal": "^1.2.2",
1920
"react-scripts": "^5.0.1",
2021
"react-test-renderer": "^16.14.0",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
title: Hello, World.
3+
date: 2026-04-18
4+
slug: hello-world
5+
excerpt: No grand plan, no content calendar — just a commitment to show up and write.
6+
---
7+
8+
Hello, world.
9+
10+
This is the first post on my corner of the internet. No grand plan, no content calendar — just a commitment to show up and write.
11+
12+
I've been meaning to start writing for a while. Notes from the trenches of engineering leadership, lessons learned the hard way, things I wish I had read earlier. Maybe some technical deep-dives. Maybe some honest reflections on what it means to build teams and systems that actually work.
13+
14+
Starting is the hardest part. So here it is: started.
15+
16+
If you're reading this — welcome. More to come.

src/blogs/manifest.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const blogManifest = [
2+
{
3+
slug: "hello-world",
4+
title: "Hello, World.",
5+
date: "2026-04-18",
6+
excerpt: "No grand plan, no content calendar — just a commitment to show up and write.",
7+
file: "/blogs/2026-04-18-hello-world.md"
8+
}
9+
];
10+
11+
export default blogManifest;

src/components/header/Header.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,9 @@ function Header() {
5959
<a href="#achievements">Achievements</a>
6060
</li>
6161
)}
62-
{viewBlog && (
63-
<li>
64-
<a href="#blogs">Blogs</a>
65-
</li>
66-
)}
62+
<li>
63+
<a href="#blogs">Blog</a>
64+
</li>
6765
{viewTalks && (
6866
<li>
6967
<a href="#talks">Talks</a>

src/containers/blogs/Blog.scss

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,112 @@
6868
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
6969
}
7070
}
71+
72+
/* Blog Cards */
73+
.blog-card {
74+
background: $textColorDark;
75+
border-radius: 8px;
76+
padding: 28px 32px;
77+
cursor: pointer;
78+
box-shadow: 0px 10px 30px $darkBoxShadow;
79+
transition: transform 0.2s ease, box-shadow 0.2s ease;
80+
display: flex;
81+
flex-direction: column;
82+
gap: 10px;
83+
84+
&:hover {
85+
transform: translateY(-4px);
86+
box-shadow: 0 20px 40px $lightBoxShadow;
87+
}
88+
89+
&.dark-mode {
90+
background: $darkBackground;
91+
}
92+
}
93+
94+
.blog-card-date {
95+
font-size: 12px;
96+
color: $subTitle;
97+
margin: 0;
98+
text-transform: uppercase;
99+
letter-spacing: 0.08em;
100+
}
101+
102+
.blog-card-title {
103+
font-size: 22px;
104+
font-weight: 600;
105+
margin: 0;
106+
color: $titleColor;
107+
}
108+
109+
.blog-card-excerpt {
110+
font-size: 15px;
111+
color: $subTitle;
112+
margin: 0;
113+
line-height: 1.6;
114+
}
115+
116+
.blog-card-read {
117+
font-size: 14px;
118+
font-weight: 500;
119+
color: $buttonColor;
120+
margin-top: 4px;
121+
}
122+
123+
/* Blog Post View */
124+
.blog-back-btn {
125+
background: none;
126+
border: none;
127+
cursor: pointer;
128+
font-size: 15px;
129+
color: $buttonColor;
130+
padding: 0;
131+
margin-bottom: 32px;
132+
font-weight: 500;
133+
134+
&:hover {
135+
text-decoration: underline;
136+
}
137+
}
138+
139+
.blog-post-content {
140+
max-width: 680px;
141+
142+
h1, h2, h3 {
143+
color: $titleColor;
144+
line-height: 1.3;
145+
}
146+
147+
p {
148+
font-size: 17px;
149+
line-height: 1.8;
150+
color: $subTitle;
151+
margin-bottom: 1.2em;
152+
}
153+
154+
code {
155+
background: $darkBackground;
156+
padding: 2px 6px;
157+
border-radius: 4px;
158+
font-size: 14px;
159+
}
160+
161+
&.dark-mode p {
162+
color: $textColor;
163+
}
164+
}
165+
166+
.blog-post-title {
167+
font-size: 40px;
168+
font-weight: 700;
169+
margin-bottom: 8px;
170+
color: $titleColor;
171+
}
172+
173+
.blog-post-meta {
174+
font-size: 13px;
175+
color: $subTitle;
176+
text-transform: uppercase;
177+
letter-spacing: 0.08em;
178+
margin-bottom: 4px !important;
179+
}

src/containers/blogs/Blogs.js

Lines changed: 62 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,79 @@
11
import React, {useState, useEffect, useContext} from "react";
2+
import ReactMarkdown from "react-markdown";
23
import "./Blog.scss";
3-
import BlogCard from "../../components/blogCard/BlogCard";
4-
import {blogSection} from "../../portfolio";
54
import {Fade} from "react-reveal";
65
import StyleContext from "../../contexts/StyleContext";
6+
import blogManifest from "../../blogs/manifest";
7+
78
export default function Blogs() {
89
const {isDark} = useContext(StyleContext);
9-
const [mediumBlogs, setMediumBlogs] = useState([]);
10-
function setMediumBlogsFunction(array) {
11-
setMediumBlogs(array);
12-
}
13-
//Medium API returns blogs' content in HTML format. Below function extracts blogs' text content within paragraph tags
14-
function extractTextContent(html) {
15-
return typeof html === "string"
16-
? html
17-
.split("p>")
18-
.filter(el => !el.includes(">"))
19-
.map(el => el.replace("</", ".").replace("<", ""))
20-
.join(" ")
21-
: NaN;
22-
}
10+
const [selectedPost, setSelectedPost] = useState(null);
11+
const [markdownContent, setMarkdownContent] = useState("");
12+
const [loading, setLoading] = useState(false);
13+
2314
useEffect(() => {
24-
if (blogSection.displayMediumBlogs === "true") {
25-
const getProfileData = () => {
26-
fetch("/blogs.json")
27-
.then(result => {
28-
if (result.ok) {
29-
return result.json();
30-
}
31-
})
32-
.then(response => {
33-
setMediumBlogsFunction(response.items);
34-
})
35-
.catch(function (error) {
36-
console.error(
37-
`${error} (because of this error Blogs section could not be displayed. Blogs section has reverted to default)`
38-
);
39-
setMediumBlogsFunction("Error");
40-
blogSection.displayMediumBlogs = "false";
41-
});
42-
};
43-
getProfileData();
44-
}
45-
}, []);
46-
if (!blogSection.display) {
47-
return null;
15+
if (!selectedPost) return;
16+
setLoading(true);
17+
fetch(selectedPost.file)
18+
.then(res => res.text())
19+
.then(text => {
20+
// Strip YAML frontmatter before rendering
21+
const stripped = text.replace(/^---[\s\S]*?---\n/, "");
22+
setMarkdownContent(stripped);
23+
setLoading(false);
24+
})
25+
.catch(() => {
26+
setMarkdownContent("Failed to load post.");
27+
setLoading(false);
28+
});
29+
}, [selectedPost]);
30+
31+
if (selectedPost) {
32+
return (
33+
<Fade bottom duration={600} distance="20px">
34+
<div className="main" id="blogs">
35+
<button
36+
className={isDark ? "blog-back-btn dark-mode" : "blog-back-btn"}
37+
onClick={() => setSelectedPost(null)}
38+
>
39+
← Back
40+
</button>
41+
<div className={isDark ? "blog-post-content dark-mode" : "blog-post-content"}>
42+
<p className="blog-post-meta">{selectedPost.date}</p>
43+
<h1 className="blog-post-title">{selectedPost.title}</h1>
44+
{loading ? (
45+
<p>Loading...</p>
46+
) : (
47+
<ReactMarkdown>{markdownContent}</ReactMarkdown>
48+
)}
49+
</div>
50+
</div>
51+
</Fade>
52+
);
4853
}
54+
4955
return (
5056
<Fade bottom duration={1000} distance="20px">
5157
<div className="main" id="blogs">
5258
<div className="blog-header">
53-
<h1 className="blog-header-text">{blogSection.title}</h1>
54-
<p
55-
className={
56-
isDark ? "dark-mode blog-subtitle" : "subTitle blog-subtitle"
57-
}
58-
>
59-
{blogSection.subtitle}
59+
<h1 className="blog-header-text">Writing</h1>
60+
<p className={isDark ? "dark-mode blog-subtitle" : "subTitle blog-subtitle"}>
61+
Notes from the trenches
6062
</p>
6163
</div>
62-
<div className="blog-main-div">
63-
<div className="blog-text-div">
64-
{blogSection.displayMediumBlogs !== "true" ||
65-
mediumBlogs === "Error"
66-
? blogSection.blogs.map((blog, i) => {
67-
return (
68-
<BlogCard
69-
key={i}
70-
isDark={isDark}
71-
blog={{
72-
url: blog.url,
73-
image: blog.image,
74-
title: blog.title,
75-
description: blog.description
76-
}}
77-
/>
78-
);
79-
})
80-
: mediumBlogs.map((blog, i) => {
81-
return (
82-
<BlogCard
83-
key={i}
84-
isDark={isDark}
85-
blog={{
86-
url: blog.link,
87-
title: blog.title,
88-
description: extractTextContent(blog.content)
89-
}}
90-
/>
91-
);
92-
})}
93-
</div>
64+
<div className="blog-text-div">
65+
{blogManifest.map((post, i) => (
66+
<div
67+
key={i}
68+
className={isDark ? "blog-card dark-mode" : "blog-card"}
69+
onClick={() => setSelectedPost(post)}
70+
>
71+
<p className="blog-card-date">{post.date}</p>
72+
<h2 className="blog-card-title">{post.title}</h2>
73+
<p className="blog-card-excerpt">{post.excerpt}</p>
74+
<span className="blog-card-read">Read →</span>
75+
</div>
76+
))}
9477
</div>
9578
</div>
9679
</Fade>

src/portfolio.js

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const illustration = {
2121
};
2222

2323
const greeting = {
24-
username: "Dandi",
24+
username: "Dandi Diputra",
2525
title: "Hi all, I'm Dandi",
2626
subTitle: emoji("A passionate Software Engineer 🚀 having an experience of working with Embedded Systems, Real-Time Operating Systems, Telecommunications Core Systems, Financial Technology and Software Engineering Management. Good working knowledge in Go, Java, C/C++, Python and Software Architecture/System Design."),
2727
resumeLink:
@@ -250,25 +250,7 @@ const achievementSection = {
250250
// Blogs Section
251251

252252
const blogSection = {
253-
title: "Blogs",
254-
subtitle:
255-
"With Love for Developing cool stuff, I love to write and teach others what I have learnt.",
256-
displayMediumBlogs: "true", // Set true to display fetched medium blogs instead of hardcoded ones
257-
blogs: [
258-
{
259-
url: "https://blog.usejournal.com/create-a-google-assistant-action-and-win-a-google-t-shirt-and-cloud-credits-4a8d86d76eae",
260-
title: "Win a Google Assistant Tshirt and $200 in Google Cloud Credits",
261-
description:
262-
"Do you want to win $200 and Google Assistant Tshirt by creating a Google Assistant Action in less then 30 min?"
263-
},
264-
{
265-
url: "https://medium.com/@saadpasta/why-react-is-the-best-5a97563f423e",
266-
title: "Why REACT is The Best?",
267-
description:
268-
"React is a JavaScript library for building User Interface. It is maintained by Facebook and a community of individual developers and companies."
269-
}
270-
],
271-
display: false // Set false to hide this section, defaults to true
253+
display: true
272254
};
273255

274256
// Talks Sections

0 commit comments

Comments
 (0)