diff --git a/scripts/generate-rss.js b/scripts/generate-rss.js
index 50d9bcc..286f43d 100644
--- a/scripts/generate-rss.js
+++ b/scripts/generate-rss.js
@@ -6,6 +6,7 @@ const { format } = require("date-fns");
const xmlFormat = require("xml-formatter");
const BLOGS_DIR = path.join(process.cwd(), "_blogs");
+const CASE_STUDIES_DIR = path.join(process.cwd(), "_case-studies");
const OUTPUT_FILE = path.join(process.cwd(), "public", "rss.xml");
function escapeXML(str) {
@@ -18,9 +19,30 @@ function escapeXML(str) {
}
function generateRSS() {
- const files = fs.readdirSync(BLOGS_DIR);
+ let blogFiles = [];
+ let caseStudyFiles = [];
- const posts = files.map((file) => {
+ try {
+ if (fs.existsSync(BLOGS_DIR)) {
+ blogFiles = fs.readdirSync(BLOGS_DIR);
+ } else {
+ console.warn("Blogs directory does not exist:", BLOGS_DIR);
+ }
+ } catch (error) {
+ console.error("Error reading blogs directory:", error);
+ }
+
+ try {
+ if (fs.existsSync(CASE_STUDIES_DIR)) {
+ caseStudyFiles = fs.readdirSync(CASE_STUDIES_DIR);
+ } else {
+ console.warn("Case studies directory does not exist:", CASE_STUDIES_DIR);
+ }
+ } catch (error) {
+ console.error("Error reading case studies directory:", error);
+ }
+
+ const posts = blogFiles.map((file) => {
const filePath = path.join(BLOGS_DIR, file);
const content = fs.readFileSync(filePath, "utf-8");
const { data } = matter(content);
@@ -32,7 +54,19 @@ function generateRSS() {
};
});
- const rssItems = posts
+ const caseStudies = caseStudyFiles.map((file) => {
+ const filePath = path.join(CASE_STUDIES_DIR, file);
+ const content = fs.readFileSync(filePath, "utf-8");
+ const { data } = matter(content);
+ return {
+ title: data.title,
+ description: data.excerpt,
+ link: `https://umairjibran.com/case-studies/${file.replace(/\.md$/, "")}`,
+ pubDate: data.date,
+ };
+ });
+
+ const rssItems = [...posts, ...caseStudies]
.map(
(post) =>
`${escapeXML(post.title)} ${escapeXML(post.link)}${escapeXML(post.description)} ${new Date(post.pubDate).toUTCString()} ${post.link} `,
@@ -40,7 +74,7 @@ function generateRSS() {
.join("");
const rssFeed =
- `Umair Jibran's Blog https://umairjibran.com/blogsLatest articles from Umair Jibran's blog. en-us ${new Date().toUTCString()} ${rssItems} `.trim();
+ `Umair Jibran's Writings https://umairjibran.com/writingsLatest articles from Umair Jibran's writings. en-us ${new Date().toUTCString()} ${rssItems} `.trim();
fs.writeFileSync(OUTPUT_FILE, xmlFormat(rssFeed), "utf-8");
console.log(`RSS feed generated at ${OUTPUT_FILE}`);
diff --git a/src/app/(blogs)/blogs/[slug]/page.tsx b/src/app/(blogs)/blogs/[slug]/page.tsx
deleted file mode 100644
index 19e898c..0000000
--- a/src/app/(blogs)/blogs/[slug]/page.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Metadata } from "next";
-import { notFound } from "next/navigation";
-import { getAllBlog, getBlogBySlug } from "@/lib/api";
-import markdownToHtml from "@/lib/markdownToHtml";
-import { StoryBody } from "@/components/StoryBody";
-import { StoryHeader } from "@/components/StoryHeader";
-
-import profile from "@/data/profile.json";
-
-export default async function Blog({ params }: Params) {
- const blog = getBlogBySlug(params.slug);
-
- if (!blog) {
- return notFound();
- }
-
- const content = await markdownToHtml(blog.content || "");
-
- return (
-
-
-
-
-
-
- );
-}
-
-type Params = {
- params: {
- slug: string;
- };
-};
-
-export function generateMetadata({ params }: Params): Metadata {
- const blog = getBlogBySlug(params.slug);
-
- if (!blog) {
- return notFound();
- }
-
- const title = `${blog.title} | Blog | ${profile.name.firstName} ${profile.name.lastName}`;
-
- return {
- title,
- openGraph: {
- title,
- images: [blog.ogImage.url],
- },
- twitter: {
- card: "summary_large_image",
- },
- description: blog.excerpt,
- authors: [blog.author],
- keywords: blog.tags,
- };
-}
-
-export async function generateStaticParams() {
- const blogs = getAllBlog();
-
- return blogs.map((blog) => ({
- slug: blog.slug,
- }));
-}
diff --git a/src/app/(blogs)/blogs/page.tsx b/src/app/(blogs)/blogs/page.tsx
deleted file mode 100644
index b8ec770..0000000
--- a/src/app/(blogs)/blogs/page.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { HeroStory } from "@/components/HeroStory";
-import { MoreStories } from "@/components/MoreStories";
-import { getAllBlog } from "@/lib/api";
-
-export default function Index() {
- const allBlogs = getAllBlog();
- const heroBlog = allBlogs[0];
- const moreBlogs = allBlogs.slice(1);
-
- if (allBlogs.length === 0) {
- return (
-
- oops... I should write some up
-
- );
- }
-
- return (
-
-
- {moreBlogs.length > 0 && }
-
- );
-}
diff --git a/src/app/(blogs)/layout.tsx b/src/app/(blogs)/layout.tsx
deleted file mode 100644
index 20f0eba..0000000
--- a/src/app/(blogs)/layout.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Metadata } from "next";
-import profile from "@/data/profile.json";
-import meta from "@/data/meta.json";
-
-export const metadata: Metadata = {
- ...meta,
- metadataBase: new URL(meta.metadataBase),
- title: `Thoughts and blogs by ${profile.name.firstName} ${profile.name.lastName}`,
- description: `Blogs authored by ${profile.name.firstName} ${profile.name.lastName}`,
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return
{children}
;
-}
diff --git a/src/app/(caseStudies)/case-studies/[slug]/page.tsx b/src/app/(caseStudies)/case-studies/[slug]/page.tsx
deleted file mode 100644
index 91490c5..0000000
--- a/src/app/(caseStudies)/case-studies/[slug]/page.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Metadata } from "next";
-import { notFound } from "next/navigation";
-import { getAllCaseStudies, getCaseStudyBySlug } from "@/lib/api";
-import markdownToHtml from "@/lib/markdownToHtml";
-import { StoryBody } from "@/components/StoryBody";
-import { StoryHeader } from "@/components/StoryHeader";
-
-import profile from "@/data/profile.json";
-
-export default async function CaseStudy({ params }: Params) {
- const caseStudy = getCaseStudyBySlug(params.slug);
-
- if (!caseStudy) {
- return notFound();
- }
-
- const content = await markdownToHtml(caseStudy.content || "");
-
- return (
-
-
-
-
-
-
- );
-}
-
-type Params = {
- params: {
- slug: string;
- };
-};
-
-export function generateMetadata({ params }: Params): Metadata {
- const CaseStudy = getCaseStudyBySlug(params.slug);
-
- if (!CaseStudy) {
- return notFound();
- }
-
- const title = `${CaseStudy.title} | Case Study | ${profile.name.firstName} ${profile.name.lastName}`;
-
- return {
- title,
- openGraph: {
- title,
- images: [CaseStudy.ogImage.url],
- },
- twitter: {
- card: "summary_large_image",
- },
- description: CaseStudy.excerpt,
- authors: [CaseStudy.author],
- keywords: CaseStudy.tags,
- };
-}
-
-export async function generateStaticParams() {
- const caseStudies = getAllCaseStudies();
-
- return caseStudies.map((caseStudy) => ({
- slug: caseStudy.slug,
- }));
-}
diff --git a/src/app/(caseStudies)/case-studies/page.tsx b/src/app/(caseStudies)/case-studies/page.tsx
deleted file mode 100644
index 62a4448..0000000
--- a/src/app/(caseStudies)/case-studies/page.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { HeroStory } from "@/components/HeroStory";
-import { MoreStories } from "@/components/MoreStories";
-import { getAllCaseStudies } from "@/lib/api";
-
-export default function Index() {
- const allCaseStudies = getAllCaseStudies();
-
- if (allCaseStudies.length === 0) {
- return (
-
- oops... I should write some up
-
- );
- }
-
- const heroCaseStudy = allCaseStudies[0];
- const moreCaseStudies = allCaseStudies.slice(1);
-
- return (
-
-
- {moreCaseStudies.length > 0 && }
-
- );
-}
diff --git a/src/app/(caseStudies)/layout.tsx b/src/app/(caseStudies)/layout.tsx
deleted file mode 100644
index 96c31a5..0000000
--- a/src/app/(caseStudies)/layout.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Metadata } from "next";
-import profile from "@/data/profile.json";
-import meta from "@/data/meta.json";
-
-export const metadata: Metadata = {
- ...meta,
- metadataBase: new URL(meta.metadataBase),
- title: `Case Studies by ${profile.name.firstName} ${profile.name.lastName}`,
- description: `Case Studies authored by ${profile.name.firstName} ${profile.name.lastName}`,
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return {children}
;
-}
diff --git a/src/app/writing/[slug]/page.tsx b/src/app/writing/[slug]/page.tsx
new file mode 100644
index 0000000..0586072
--- /dev/null
+++ b/src/app/writing/[slug]/page.tsx
@@ -0,0 +1,73 @@
+import { getAllBlog, getAllCaseStudies } from "@/lib/api";
+import { notFound } from "next/navigation";
+import markdownToHtml from "@/lib/markdownToHtml";
+import { StoryBody } from "@/components/StoryBody";
+import { RelatedStories } from "@/components/RelatedStories";
+
+export default async function StoryPage({ params }: { params: { slug: string } }) {
+ const allStories = [
+ ...getAllBlog(),
+ ...getAllCaseStudies()
+ ];
+
+ // Find the story in either blogs or case studies
+ const story = allStories.find(story => story.slug === params.slug);
+
+ if (!story) {
+ notFound();
+ }
+
+ const content = await markdownToHtml(story.content || "");
+
+ return (
+
+ {/* Header with gradient */}
+
+
+
+
+
+ {story.type.replace("-", " ")}
+
+ {new Date(story.date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })}
+
+ {story.title}
+ {story.excerpt}
+
+
+
+
+ {/* Content with sidebar */}
+
+
+ {/* Main content */}
+
+
+
+
+ {/* Sidebar */}
+
+
+
+
+ );
+}
+
+export async function generateStaticParams() {
+ const allContent = [
+ ...getAllBlog(),
+ ...getAllCaseStudies()
+ ];
+
+ return allContent.map((story) => ({
+ slug: story.slug,
+ }));
+}
\ No newline at end of file
diff --git a/src/app/writing/page.tsx b/src/app/writing/page.tsx
new file mode 100644
index 0000000..3d4d7e4
--- /dev/null
+++ b/src/app/writing/page.tsx
@@ -0,0 +1,52 @@
+import { MoreStories } from "@/components/MoreStories";
+import { getAllBlog, getAllCaseStudies } from "@/lib/api";
+import Link from "next/link";
+
+export default function Index() {
+ // Combine and sort all content
+ const allContent = [
+ ...getAllBlog(),
+ ...getAllCaseStudies()
+ ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ if (allContent.length === 0) {
+ return (
+
+ oops... I should write some up
+
+ );
+ }
+
+ return (
+
+ {/* Header section with gradient background */}
+
+
+
Writing
+
+
+ I write about software engineering, web development, and the technical challenges I encounter.
+ Here you will find in-depth case studies, technical articles, and development stories.
+
+
+ Subscribe via{" "}
+
+ RSS
+
+ {" "}or follow me on{" "}
+
+ LinkedIn
+
+ {" "}to stay updated.
+
+
+
+
+
+ {/* All content */}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/CoverImage.tsx b/src/components/CoverImage.tsx
index 20604ca..8caa5b2 100644
--- a/src/components/CoverImage.tsx
+++ b/src/components/CoverImage.tsx
@@ -1,23 +1,27 @@
import cn from "classnames";
import Link from "next/link";
import Image from "next/image";
+import { CSSProperties } from "react";
type Props = {
title: string;
src: string;
slug?: string;
+ className?: string;
+ style?: CSSProperties;
};
-const CoverImage = ({ title, src, slug }: Props) => {
+const CoverImage = ({ title, src, slug, className, style }: Props) => {
const image = (
);
return (
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index da729c0..f2ba89f 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -18,14 +18,8 @@ export default function Header() {
);
navComponents.push(
-
- Blogs
- ,
- );
-
- navComponents.push(
-
- Case Studies
+
+ Writing
,
);
diff --git a/src/components/MoreStories.tsx b/src/components/MoreStories.tsx
index 7f1e2fd..37299ce 100644
--- a/src/components/MoreStories.tsx
+++ b/src/components/MoreStories.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import { Story } from "@/types/story";
import { StoryPreview } from "@/components/StoryPreview";
@@ -5,25 +7,93 @@ type Props = {
stories: Story[];
};
+type GroupedStories = {
+ [key: string]: {
+ month: string;
+ year: string;
+ stories: Story[];
+ };
+};
+
export function MoreStories({ stories }: Props) {
+ // Group stories by month and year
+ const groupedStories = stories.reduce((acc: GroupedStories, story) => {
+ const date = new Date(story.date);
+ const key = `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`;
+ const month = date.toLocaleString('default', { month: 'long' });
+ const year = date.getFullYear().toString();
+
+ if (!acc[key]) {
+ acc[key] = {
+ month,
+ year,
+ stories: []
+ };
+ }
+ acc[key].stories.push(story);
+ return acc;
+ }, {});
+
+ // Sort stories within each month by date (newest first)
+ Object.values(groupedStories).forEach(group => {
+ group.stories.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+ });
+
+ // Sort keys in reverse chronological order
+ const sortedKeys = Object.keys(groupedStories).sort().reverse();
+
+ // Calculate the height for the last section (last month's stories)
+ const lastMonthKey = sortedKeys[sortedKeys.length - 1];
+ const lastMonthStoriesCount = groupedStories[lastMonthKey]?.stories.length || 0;
+ const lastSectionHeight = lastMonthStoriesCount * 96 + 24; // height per story + padding
+
return (
-
-
- More Stories
-
-
- {stories.map((story) => (
-
- ))}
+
+
+ {/* Main continuous timeline */}
+
+
+
+ {sortedKeys.map((key, groupIndex) => (
+
+ {/* Month marker */}
+
+
+ {/* Month bullet */}
+
+ {/* Month label */}
+
+
+ {groupedStories[key].month} {groupedStories[key].year}
+
+
+
+
+
+ {groupedStories[key].stories.map((story) => (
+
+
+
+ ))}
+
+
+ ))}
+
);
diff --git a/src/components/RelatedStories.tsx b/src/components/RelatedStories.tsx
new file mode 100644
index 0000000..1748af1
--- /dev/null
+++ b/src/components/RelatedStories.tsx
@@ -0,0 +1,102 @@
+import { Story } from "@/types/story";
+import Link from "next/link";
+
+type Props = {
+ currentStory: Story;
+ allStories: Story[];
+};
+
+type RelatedStoryResult = {
+ story: Story;
+ matchingTag?: string;
+};
+
+function findRelatedStory(stories: Story[], currentStory: Story, type: "blog" | "case-study"): RelatedStoryResult | null {
+ // Filter stories by type and exclude current story
+ const typeStories = stories.filter(s => s.type === type && s.slug !== currentStory.slug);
+ if (typeStories.length === 0) return null;
+
+ // Try to find a story with matching tags
+ if (currentStory.tags && currentStory.tags.length > 0) {
+ const relatedByTags = typeStories.map(story => {
+ const matchingTags = story.tags?.filter(tag => currentStory.tags.includes(tag)) || [];
+ return {
+ story,
+ matchingTags,
+ matchingCount: matchingTags.length
+ };
+ })
+ .sort((a, b) => b.matchingCount - a.matchingCount);
+
+ if (relatedByTags[0].matchingCount > 0) {
+ return {
+ story: relatedByTags[0].story,
+ matchingTag: relatedByTags[0].matchingTags[0]
+ };
+ }
+ }
+
+ // If no matching tags, return the latest story
+ return { story: typeStories[0] };
+}
+
+export function RelatedStories({ currentStory, allStories }: Props) {
+ const relatedBlog = findRelatedStory(allStories, currentStory, "blog");
+ const relatedCaseStudy = findRelatedStory(allStories, currentStory, "case-study");
+
+ if (!relatedBlog && !relatedCaseStudy) return null;
+
+ return (
+
+ Related Stories
+
+ {relatedBlog && (
+
+
From the Blog
+
+
+
+
+ blog
+
+ {relatedBlog.matchingTag && (
+
+ {relatedBlog.matchingTag}
+
+ )}
+
+
+ {relatedBlog.story.title}
+
+
{relatedBlog.story.excerpt}
+
+
+
+ )}
+
+ {relatedCaseStudy && (
+
+
From Case Studies
+
+
+
+
+ case study
+
+ {relatedCaseStudy.matchingTag && (
+
+ {relatedCaseStudy.matchingTag}
+
+ )}
+
+
+ {relatedCaseStudy.story.title}
+
+
{relatedCaseStudy.story.excerpt}
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/StoryBody.tsx b/src/components/StoryBody.tsx
index d8cf992..01f2623 100644
--- a/src/components/StoryBody.tsx
+++ b/src/components/StoryBody.tsx
@@ -7,7 +7,7 @@ type Props = {
export function StoryBody({ content }: Props) {
return (
-
+
);
diff --git a/src/components/StoryHeader.tsx b/src/components/StoryHeader.tsx
deleted file mode 100644
index 57913d6..0000000
--- a/src/components/StoryHeader.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import Avatar from "@/components/Avatar";
-import CoverImage from "@/components/CoverImage";
-import DateFormatter from "@/components/DateFormatter";
-import { BlogTitle } from "@/components/BlogTitle";
-import { Author } from "@/types/author";
-
-type Props = {
- title: string;
- coverImage: string;
- date: string;
- author: Author;
-};
-
-export function StoryHeader({ title, coverImage, date, author }: Props) {
- return (
- <>
-
{title}
-
-
-
-
-
- >
- );
-}
diff --git a/src/components/StoryPreview.tsx b/src/components/StoryPreview.tsx
index 80522c6..b6b2147 100644
--- a/src/components/StoryPreview.tsx
+++ b/src/components/StoryPreview.tsx
@@ -1,8 +1,7 @@
+'use client';
+
import { Author } from "@/types/author";
import Link from "next/link";
-import CoverImage from "@/components/CoverImage";
-import DateFormatter from "@/components/DateFormatter";
-import Avatar from "@/components/Avatar";
type Props = {
title: string;
@@ -12,6 +11,7 @@ type Props = {
author: Author;
slug: string;
type: "blog" | "case-study";
+ tags?: string[];
};
export function StoryPreview({
@@ -22,25 +22,50 @@ export function StoryPreview({
author,
slug,
type,
+ tags = [],
}: Props) {
const root = type === "blog" ? "/blogs" : "/case-studies";
+ const day = new Date(date).getDate();
+
return (
-
-
-
-
-
-
- {title}
-
-
-
-
+
+ {/* Left side - metadata */}
+
+ {day}
-
- {excerpt}
-
-
+
+ {/* Timeline dot */}
+
+
+ {/* Right side - content */}
+
+
+
+
+ {type.replace("-", " ")}
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {title}
+
+
+ {excerpt}
+
+
+
);
}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..eb97163
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server'
+import type { NextRequest } from 'next/server'
+
+export function middleware(request: NextRequest) {
+ const { pathname } = request.nextUrl
+
+ // Redirect /blogs and /case-studies to /writing
+ if (pathname === '/blogs' || pathname === '/case-studies') {
+ return NextResponse.redirect(new URL('/writing', request.url))
+ }
+
+ // Redirect individual blog posts and case studies
+ if (pathname.startsWith('/blogs/') || pathname.startsWith('/case-studies/')) {
+ const slug = pathname.split('/').pop()
+ return NextResponse.redirect(new URL(`/writing/${slug}`, request.url))
+ }
+
+ return NextResponse.next()
+}
+
+export const config = {
+ matcher: [
+ '/blogs',
+ '/blogs/:path*',
+ '/case-studies',
+ '/case-studies/:path*',
+ ],
+}
\ No newline at end of file