Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ const nextConfig = {
destination: '/pricing',
permanent: true,
},
{
source: '/blogs',
destination: '/blog',
permanent: true,
},
];
},
};
Expand Down
86 changes: 86 additions & 0 deletions apps/web/src/app/(main)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { notFound } from "next/navigation";
import Link from "next/link";
import { getAllSlugs, getPostBySlug } from "@/lib/blog";
import type { Metadata } from "next";
import BlogThemeSelector from "../blog-theme";

export async function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};

return {
title: `${post.frontmatter.title} - Opensox Blog`,
description: post.frontmatter.description,
};
}

export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();

const date = new Date(post.frontmatter.date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
timeZone: "UTC",
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<main className="blog-page min-h-screen">
<article className="max-w-2xl mx-auto px-6 py-20">
<div className="flex items-center justify-between">
<Link
href="/blog"
className="text-sm blog-link transition-colors"
>
&larr; Blog
</Link>
<BlogThemeSelector />
</div>

<header className="mt-8 mb-10">
<h1 className="font-heading text-3xl sm:text-4xl font-bold leading-tight">
{post.frontmatter.title}
</h1>
<div className="flex items-center gap-3 mt-4 text-sm blog-text-muted">
<span>{post.frontmatter.author}</span>
<span>&middot;</span>
<time>{date}</time>
</div>
</header>

<div
className="prose-blog"
dangerouslySetInnerHTML={{ __html: post.html }}
/>

{post.frontmatter.tweetUrl && (
<div className="mt-12 pt-8 border-t blog-border">
<Link
href={post.frontmatter.tweetUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 text-sm border rounded-full blog-link transition-colors"
>
View original thread on X &rarr;
</Link>
</div>
)}
</article>
</main>
);
}
106 changes: 106 additions & 0 deletions apps/web/src/app/(main)/blog/blog-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client";

import { useState } from "react";
import Link from "next/link";
import type { BlogMeta, BlogTag } from "@/lib/blog";
import BlogThemeSelector from "./blog-theme";

const tags: BlogTag[] = ["engineering", "startup", "distribution", "misc"];

function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
});
}

export default function BlogList({ posts }: { posts: BlogMeta[] }) {
const [activeTag, setActiveTag] = useState<BlogTag | null>(null);

const filtered = activeTag
? posts.filter((p) => p.tag === activeTag)
: posts;

return (
<main className="blog-page min-h-screen">
<div className="max-w-2xl mx-auto px-6 py-20">
<header className="mb-12">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-sm blog-link transition-colors"
>
&larr; Home
</Link>
<BlogThemeSelector />
</div>
<h1 className="font-heading text-4xl font-bold mt-6">
Opensox Blog
</h1>
<p className="blog-text-secondary mt-2">
Thoughts on open source, startups, and building in public.
</p>
</header>

{/* Tag filters */}
<div className="flex gap-2 mb-10 flex-wrap">
<button
onClick={() => setActiveTag(null)}
aria-pressed={activeTag === null}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
activeTag === null
? "blog-tag-active"
: "blog-link"
}`}
>
All
</button>
{tags.map((tag) => (
<button
key={tag}
onClick={() => setActiveTag(tag)}
aria-pressed={activeTag === tag}
className={`px-3 py-1 text-sm rounded-full border transition-colors capitalize ${
activeTag === tag
? "blog-tag-active"
: "blog-link"
}`}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
{tag}
</button>
))}
</div>

{/* Blog list */}
<div className="flex flex-col">
{filtered.length === 0 ? (
<p className="blog-text-muted py-8">No posts found.</p>
) : (
filtered.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group py-5 border-b blog-border first:border-t"
>
<div className="flex items-baseline justify-between gap-4">
<h2 className="font-heading text-lg font-medium blog-title transition-colors">
{post.title}
</h2>
<time className="text-sm blog-text-muted whitespace-nowrap font-mono">
{formatDate(post.date)}
</time>
</div>
<p className="blog-text-secondary text-sm mt-1.5 line-clamp-2">
{post.description}
</p>
</Link>
))
)}
</div>
</div>
</main>
);
}
47 changes: 47 additions & 0 deletions apps/web/src/app/(main)/blog/blog-theme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { useState, useEffect } from "react";

const themes = [
{ id: "dark", label: "Dark" },
{ id: "light", label: "Light" },
{ id: "sepia", label: "Sepia" },
{ id: "green", label: "Green" },
] as const;

type ThemeId = (typeof themes)[number]["id"];

const validThemes = new Set(themes.map((t) => t.id));

function getSavedTheme(): ThemeId {
if (typeof window === "undefined") return "dark";
const saved = localStorage.getItem("blog-theme");
return saved && validThemes.has(saved as ThemeId) ? (saved as ThemeId) : "dark";
}

export default function BlogThemeSelector() {
const [theme, setTheme] = useState<ThemeId>(getSavedTheme);
Comment thread
apsinghdev marked this conversation as resolved.

useEffect(() => {
document.documentElement.setAttribute("data-blog-theme", theme);
localStorage.setItem("blog-theme", theme);
return () => {
document.documentElement.removeAttribute("data-blog-theme");
};
}, [theme]);

return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as ThemeId)}
aria-label="Blog theme"
className="blog-theme-select"
>
{themes.map((t) => (
<option key={t.id} value={t.id}>
{t.label}
</option>
))}
</select>
);
}
14 changes: 14 additions & 0 deletions apps/web/src/app/(main)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getAllPosts } from "@/lib/blog";
import type { Metadata } from "next";
import BlogList from "./blog-list";

export const metadata: Metadata = {
title: "Opensox Blog",
description: "Thoughts on open source, startups, and building in public.",
};

export default function BlogPage() {
const posts = getAllPosts();

return <BlogList posts={posts} />;
}
88 changes: 0 additions & 88 deletions apps/web/src/app/(main)/blogs/page.tsx

This file was deleted.

Loading