);
diff --git a/src/components/sections/breadcrumb.tsx b/src/components/sections/breadcrumb.tsx
new file mode 100644
index 0000000..2fb5c35
--- /dev/null
+++ b/src/components/sections/breadcrumb.tsx
@@ -0,0 +1,56 @@
+import { BreadcrumbItem } from '@/types/breadcrumb-item';
+import Link from 'next/link';
+
+type BreadCrumbProps = {
+ items: BreadcrumbItem[];
+ currentLabel: string;
+};
+
+const Breadcrumb = ({ items, currentLabel }: BreadCrumbProps) => {
+ return (
+
+ );
+};
+
+export default Breadcrumb;
diff --git a/src/components/ui/view-all-link.tsx b/src/components/ui/view-all-link.tsx
new file mode 100644
index 0000000..7bf16ac
--- /dev/null
+++ b/src/components/ui/view-all-link.tsx
@@ -0,0 +1,24 @@
+import Link from 'next/link';
+
+type ViewAllLinkProps = {
+ href: string;
+};
+
+const ViewAllLink = ({ href }: ViewAllLinkProps) => {
+ return (
+
+
+ View all
+
+ ↗
+
+ →
+
+
+ );
+};
+
+export default ViewAllLink;
diff --git a/src/content/blog/blog-3.mdx b/src/content/blog/blog-3.mdx
new file mode 100644
index 0000000..4b0c0a9
--- /dev/null
+++ b/src/content/blog/blog-3.mdx
@@ -0,0 +1,79 @@
+Habit stacking is one of my goals this year. I've been so focused on improving my software engineering skills, staying fit, and networking. But I realized I have completely ignored one habit: _learning how money works_.
+
+Software engineering is a lucrative career but that doesn't mean we are automatically good with money.
+
+_Keeping money is a completely different skill from making money._
+
+Because of this, I've decided to read one chapter a day from the book **"The Psychology of Money"** and these are the lessons I think every software engineer should hear.
+
+---
+
+## hugeSalary !== beingWealthy
+
+Staying wealthy is a skill in itself. Saving consistently and letting compounding do the work is the way to go. I know, _revolutionary_. But that's the point. It's boring, and it works.
+
+> "Wealth is what we don't see."
+
+Many of us, myself included, dream of landing a FAANG-level salary. But we also have to check our lifestyle inflation. Huge salaries don't matter if you keep spending more _"just because"_ you needed a bigger apartment, a new car, or more expensive habits.
+
+---
+
+## Never Underestimate the Power of Compounding
+
+Maybe some of you shrugged off my initial remark on compounding. Honestly, I wouldn't blame you for it. I also underestimated compounding but the more I read about it, the more I am fascinated by its power to change people's lives.
+
+We software engineers stack skills almost every single day to keep up with the latest tech. Each day, we improve and that compounds. It's the same thing with money.
+
+What's stopping us from applying that same mindset to our finances?
+
+_Nothing._
+
+---
+
+## Freedom Is the End Goal
+
+As I read through the book, one theme keeps lingering inside my head and that's _freedom_. When you have something saved, you have options.
+
+> "Controlling your time is the highest dividend money pays."
+
+The goal is not just to be rich but to be able to use your money to save your ultimate currency: _your time_. Then to be able to spend it on things that matter the most is one of the most beautiful gifts of life.
+
+I love the tech industry but I also have plans _outside of it_ and I'm sure many of us are not planning to code through our golden years.
+
+---
+
+## We're All on Different Branches
+
+Someone you know got hired by Google at 18 years old. Someone else got promoted while you've been waiting for months. Plus many other stories that make you feel small.
+
+But these are stories you can't control. Timing exists, luck exists, risk exists, factors you don't even have to power to change exists. These are real life scenarios and each of them happens differently for each of us.
+
+_Everyone has different circumstances._
+
+We are all committing to our own Git branches on the main timeline. If you stop and inspect other people's branches, you'll just end up wasting time that could have been used for growing yours.
+
+Looking at other people's finances is inviting _comparison, envy, and elitism_. But if we focus on our own, we'll spend less time feeling _bitter_ and more time getting _better_.
+
+---
+
+## Work, Work, Work
+
+All of these will be wasted if you are inconsistent. Investing in your engineering skills while investing your money should grow together.
+
+_For this to work, you have to do the work._
+
+Sounds easy on paper but it's definitely hard to do. There will be days when your body feels sluggish that you want to just lie down and rest. Learn to maneuver around that feeling to stay consistent.
+
+It may not be what you'd hoped for that day, but at least you did the bare minimum. There's still progress.
+
+Compounding skills and investments only works if you _stay in the game long enough_.
+
+---
+
+## Some Remarks
+
+What I love most about this book is that it didn't tell what to invest in. It taught me _how to think about money_. It taught me how little my knowledge is when compared to the world's history of wealth and economics.
+
+I also believe the lessons can be applied to every level of the tech career ladder. That's why I was so excited to make it the first topic for my first solo blog post on this platform.
+
+I am still learning and applying the lessons every day. Maybe five or ten years from now, I'll come back and read this post I wrote at 2:00 AM. I hope that by then, all the small, consistent efforts I made in my career and finances have compounded into a life I'm proud of.
diff --git a/src/content/blog/blog-4.mdx b/src/content/blog/blog-4.mdx
new file mode 100644
index 0000000..4e70f64
--- /dev/null
+++ b/src/content/blog/blog-4.mdx
@@ -0,0 +1,85 @@
+Two months ago, I was given access to a codebase with an unfamiliar language and framework. _I felt a lot of pressure_. Coming from a React and JavaScript background, contributing to a PHP and Laravel project felt intimidating.
+
+I feared that learning a new environment would take me _a few months_ before I could make a meaningful contribution.
+
+But this wasn't the case.
+
+While cleaning my keyboard one afternoon, I looked at the keycaps I removed _(pictured above)_ and was reminded of solving puzzles as a kid. You don't solve a puzzle by staring at all the pieces at once. You connect one familiar piece to another until the picture slowly appears.
+
+That's exactly how I ended up learning Laravel.
+
+As I explored the codebase, I realized I was mentally creating key steps to learning an unfamiliar technology.
+
+---
+
+## Don't Start From Scratch
+
+When I started, I literally felt like I was _back on square one_. But after a few weeks, I realized I wasn't starting from scratch.
+
+I already knew concepts like package managers, Git, debugging, and project structure. I just didn't know what those concepts were called in Laravel.
+
+My question then gradually changed from _"How do I learn Laravel?"_ to _"What is Laravel's version of something I already know?"_ It was like connecting new knowledge to concepts I was already comfortable with. I'm just actively expanding it.
+
+React has components, while Laravel has Blade templates and Livewire components. Different tools, but same concepts.
+
+
+
+Every framework has its own syntax that solves the same problem. It's up to us to _find the parallels._
+
+This kind of approach can be applied to anything really. If you want to learn something new, try to relate it to something that is kind of similar to what you already know.
+
+Ideas start connecting faster and learning will become _much easier_.
+
+---
+
+## Learn the Critical 20%
+
+In the past, I was the type of student who wants to know _everything_ or else I'll end up not understanding at all. But now, as I grew older I realized knowing enough to get started _is more than enough._
+
+> Learn enough to become productive.
+
+Learning too deeply about something on the first day is a _death sentence_ for the brain.
+
+
+
+I didn't try to learn everything. Instead, I focused on the _20% of Laravel_ that would let me accomplish _80% of my daily work_.
+
+Now this is the critical part. I made mistakes and most of the time they are quite _laughable_. If I were still the junior high school me, I would be too disappointed in myself.
+
+But now, I'm just happy I'm finding these mistakes. It shows me exactly _what I need to learn_. It's not like school where we are given the syllabus. It's more of discovering the lessons yourself once a mistake happens.
+
+We just need to have the discipline to not skip these lessons because that's what makes this setup work. The struggle of understanding new lessons is what makes a good engineer.
+
+---
+
+## Start Before You're Ready
+
+This step is harder than it looks, especially if you're like me who likes everything ready from the start. But the truth is, the more you go inside the colosseum, the more you _understand your weaknesses._
+
+> No one can tell you where you are lacking better than you.
+
+So I decided facing it head-on and lo and behold, I failed miserably. It was my first time setting up a Docker environment, Linux, WSL, Ubuntu, and everything that came with it. It took me _around three weeks_ to get the local dev environment working before I could contribute.
+
+Oh boy, let me tell you, I learned a lot. I mean a lot.
+
+I even learned CI during the process and I quickly applied it in my web portfolio. I also made a fun video to document my experience:
+
+
+
+You can watch the video here in _[this pull request](https://github.com/CodingWithJiro/elmarchavez/pull/10)_. I feel like we all had this feeling once in a while 🤣
+
+Overall, I experienced the real thing. A messy real world web application that wasn't mine, but one I was responsible for fixing, maintaining, and scaling. No amount of personal projects could top that.
+
+---
+
+## Final Thoughts
+
+Looking back, I'm glad that I adapted, contributed, and presented my work during our tech meetings.
+
+> I am becoming more comfortable living in the uncomfortable.
+
+I asked questions when I needed help, and helped others whenever I could. _It was fulfilling._ This is something I can confidently say after my early experiences in tech, and I'm excited for what comes next.
+
+_If you made it this far, thank you for reading my blog. See you again when inspiration strikes!_
+
+
diff --git a/src/data/blog-list.test.ts b/src/data/blog-list.test.ts
index ecd15c2..08a09fe 100644
--- a/src/data/blog-list.test.ts
+++ b/src/data/blog-list.test.ts
@@ -1,30 +1,34 @@
-import { blogList } from './blog-list';
+import { BLOGS } from './blog-list';
import { isNotEmpty, isValidUrl } from '@/tests/utils';
describe('Blog list data', () => {
test('every blog has a non-empty title', () => {
- const titles = blogList.map(({ title }) => title);
+ const titles = BLOGS.map(({ title }) => title);
expect(isNotEmpty(titles)).toBe(true);
});
test('every blog has a non-empty description', () => {
- const descriptions = blogList.map(({ description }) => description);
+ const descriptions = BLOGS.map(({ description }) => description);
expect(isNotEmpty(descriptions)).toBe(true);
});
test('every blog uses a valid HTTPS URL', () => {
- const urls = blogList.map(({ blogUrl }) => blogUrl);
+ const urls = BLOGS.map(({ blogUrl }) => blogUrl);
expect(isValidUrl(urls)).toBe(true);
});
+ test('every blog has a non-empty img alt description', () => {
+ const imgAlts = BLOGS.map(({ imgAlt }) => imgAlt);
+ expect(isNotEmpty(imgAlts)).toBe(true);
+ });
test('every blog has a non-empty image path', () => {
- const imgUrls = blogList.map(({ imgUrl }) => imgUrl);
+ const imgUrls = BLOGS.map(({ imgUrl }) => imgUrl);
expect(isNotEmpty(imgUrls)).toBe(true);
});
test('every blog has at least three tags', () => {
- const tagList = blogList.map(({ tags }) => tags);
+ const tagList = BLOGS.map(({ tags }) => tags);
const hasThreeTags = tagList.every((tags) => tags.length >= 3);
expect(hasThreeTags).toBe(true);
});
test('every blog has non-empty tag names', () => {
- const tagList = blogList.map(({ tags }) => tags);
+ const tagList = BLOGS.map(({ tags }) => tags);
const hasNonEmptyTags = tagList.every((tags) => isNotEmpty(tags));
expect(hasNonEmptyTags).toBe(true);
});
diff --git a/src/data/blog-list.ts b/src/data/blog-list.ts
index 7d6ad45..0ed2541 100644
--- a/src/data/blog-list.ts
+++ b/src/data/blog-list.ts
@@ -1,6 +1,7 @@
+import { getSlug } from '@/lib/utils';
import { BlogType } from '@/types/blog';
-const BLOGS: BlogType[] = [
+export const BLOGS: BlogType[] = [
{
id: 1,
title:
@@ -13,6 +14,8 @@ const BLOGS: BlogType[] = [
blogUrl:
'https://dev.to/devengers/should-you-use-gemma-4-for-your-development-a-multiversal-analysis-to-determine-if-gemma-4-is-2iol',
imgUrl: '/img/blog-1.webp',
+ imgAlt:
+ 'A collection of anime characters for a multiversal collaboration for an article.',
},
{
id: 2,
@@ -26,6 +29,8 @@ const BLOGS: BlogType[] = [
blogUrl:
'https://dev.to/devengers/two-devs-and-a-copilot-created-classifierai-a-prototype-chrome-extension-that-automatically-4fge',
imgUrl: '/img/blog-2.webp',
+ imgAlt:
+ 'A funny backgroups GIF image where Elmar and Francis are dancing with strange creatures.',
},
{
id: 3,
@@ -35,9 +40,9 @@ const BLOGS: BlogType[] = [
publishDate: 'June 2026',
readTime: '3 min read',
tags: ['#career', '#productivity', '#finances'],
- blogUrl:
- 'https://dev.to/devengers/why-every-software-engineer-should-read-the-psychology-of-money-2b6e',
+ blogUrl: `/blog/${getSlug('Why Every Software Engineer Should Read "The Psychology of Money"')}`,
imgUrl: '/img/blog-3.jpg',
+ imgAlt: 'A meme image for stonks.',
},
{
id: 4,
@@ -47,9 +52,9 @@ const BLOGS: BlogType[] = [
publishDate: 'August 2026',
readTime: '4 min read',
tags: ['#laravel', '#learning', '#webdev'],
- blogUrl:
- 'https://dev.to/devengers/how-i-contributed-to-a-laravel-application-without-knowing-laravel-1dkl',
+ blogUrl: `/blog/${getSlug('How I Contributed to a Laravel Application Without Knowing Laravel')}`,
imgUrl: '/img/blog-4.jpg',
+ imgAlt: 'Keyboard caps laid down on a white table.',
},
];
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391..d5e3949 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,40 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { BLOGS } from '@/data/blog-list';
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+export function getSlug(title: string) {
+ return title
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, '')
+ .replace(/\s+/g, '-');
+}
+
+export function getBlogMeta(slug: string) {
+ const blog = BLOGS.find(({ blogUrl }) => blogUrl.endsWith(getSlug(slug)));
+
+ if (!blog) {
+ return {
+ id: -99,
+ title: 'Blog not found',
+ description: "Seems like this file doesn't exist ¯\\\_(ツ)_/¯",
+ publishDate: 'No publish date.',
+ readTime: 'No read time.',
+ tags: [],
+ blogUrl: 'No blog URL.',
+ imgUrl: 'No image URL.',
+ imgAlt: 'No image alt text.',
+ };
+ }
+
+ return blog;
+}
+
+export async function getBlogArticle(id: number) {
+ const article = await import(`@/content/blog/blog-${id}.mdx`);
+ return article.default;
}
diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx
new file mode 100644
index 0000000..63e51ae
--- /dev/null
+++ b/src/mdx-components.tsx
@@ -0,0 +1,7 @@
+import type { MDXComponents } from 'mdx/types';
+
+const components: MDXComponents = {};
+
+export function useMDXComponents(): MDXComponents {
+ return components;
+}
diff --git a/src/tests/utils.ts b/src/tests/utils.ts
index 87fe3d4..1abf9e7 100644
--- a/src/tests/utils.ts
+++ b/src/tests/utils.ts
@@ -7,7 +7,7 @@ export const isValidUrl = (urlLinks: string[]) => {
try {
return new URL(urlLink).protocol === 'https:';
} catch {
- return false;
+ return urlLink.startsWith('/');
}
});
};
diff --git a/src/types/blog.ts b/src/types/blog.ts
index 1187542..cdd0d07 100644
--- a/src/types/blog.ts
+++ b/src/types/blog.ts
@@ -7,4 +7,5 @@ export type BlogType = {
tags: string[];
blogUrl: string;
imgUrl: string;
+ imgAlt: string;
};
diff --git a/src/types/breadcrumb-item.ts b/src/types/breadcrumb-item.ts
new file mode 100644
index 0000000..6744cd7
--- /dev/null
+++ b/src/types/breadcrumb-item.ts
@@ -0,0 +1,4 @@
+export type BreadcrumbItem = {
+ label: string;
+ href: string;
+};
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 973bb2c..48ef01a 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -1,15 +1,12 @@
import { test, expect } from '@playwright/test';
import { blogList } from '@/data/blog-list';
-test('visitor can open a blog link in a new tab', async ({ page }) => {
+test('blog article link has correct href', async ({ page }) => {
await page.goto('/');
const blog = blogList[0];
expect(blog).toBeDefined();
const link = page.getByRole('link', {
name: `Read more about ${blog.title}`,
});
- const newPagePromise = page.waitForEvent('popup');
- await link.click();
- const newPage = await newPagePromise;
- await expect(newPage).toHaveURL(blog.blogUrl);
+ await expect(link).toHaveAttribute('href', blog.blogUrl);
});
diff --git a/tests/keyboard.spec.ts b/tests/keyboard.spec.ts
index f8bbcad..dce755f 100644
--- a/tests/keyboard.spec.ts
+++ b/tests/keyboard.spec.ts
@@ -81,7 +81,7 @@ test.describe('Keyboard Accessibility', () => {
const newPage = await newPagePromise;
await expect(newPage).toHaveURL('https://github.com/CodingWithJiro');
});
- test('visitor can tab to blog link and open in a new tab', async ({
+ test('visitor can tab to blog link and opens article page', async ({
page,
browserName,
}) => {
@@ -93,10 +93,8 @@ test.describe('Keyboard Accessibility', () => {
const link = page.getByRole('link', { name: blog.title });
await tabUntilFocused(page, link, blog.title);
expect(link).toBeFocused();
- const newPagePromise = page.waitForEvent('popup');
await page.keyboard.press('Enter');
- const newPage = await newPagePromise;
- await expect(newPage).toHaveURL(blog.blogUrl);
+ await expect(page).toHaveURL(blog.blogUrl);
});
test('visitor can tab to a certificate link and open in a new tab', async ({
page,