-
Notifications
You must be signed in to change notification settings - Fork 246
Feature/blog redesign #384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
apsinghdev
merged 15 commits into
apsinghdev:main
from
MistakenPirate:feature/blog-redesign
Jun 1, 2026
Merged
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bf07d7f
feat: rebuild /blog with mdx support and tag filtering
MistakenPirate ee471c4
feat: add tweet links and blog content for gsoc + open source posts
MistakenPirate b707778
feat: add gsoc part 2 blog content and fix link color
MistakenPirate 9d9f7f3
add layoff-proof and company registration blog content
MistakenPirate 1cd1953
add 5 more blog posts from twitter threads
MistakenPirate 373f395
fix blog date timezone, sanitize html, hide draft posts, and add a11y
MistakenPirate 74465e8
add content for gsoc part 3, open source, and online presence blogs
MistakenPirate 5542750
add images to gsoc part 3 and open source blog posts
MistakenPirate 8f25fc3
fix snapshot blog formatting and move image to bottom
MistakenPirate 329ae4b
add dark, light, sepia, and green theme picker to blog
MistakenPirate 077edb5
fix typo in open source blog post
MistakenPirate 863df55
Merge remote-tracking branch 'upstream/main' into feature/blog-redesign
MistakenPirate efb5be0
fix double border on blog list and improve social icon accessibility
MistakenPirate 2ed717d
Merge remote-tracking branch 'upstream/main' into feature/blog-redesign
MistakenPirate 69bdfb2
chore: add new fonts
apsinghdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| }); | ||
|
|
||
| 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" | ||
| > | ||
| ← 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>·</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 → | ||
| </Link> | ||
| </div> | ||
| )} | ||
| </article> | ||
| </main> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| > | ||
| ← 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" | ||
| }`} | ||
|
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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} />; | ||
| } |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.