Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-05-19 15:18:51 +00:00
parent f4a4c03de2
commit 9d274a0e91
6 changed files with 273 additions and 1 deletions

View file

@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index"; import Index from "./pages/Index";
import WhoAmI from "./pages/WhoAmI"; import WhoAmI from "./pages/WhoAmI";
import Blog from "./pages/Blog";
import BlogPost from "./pages/BlogPost";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -18,6 +20,8 @@ const App = () => (
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route path="/whoami" element={<WhoAmI />} /> <Route path="/whoami" element={<WhoAmI />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogPost />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>

View file

@ -0,0 +1,19 @@
---
title: Hello, world
date: 2026-05-19
description: First post on the new site. A quick note on what's coming.
tags: [meta]
---
# Hello, world
This is the first post on the rebuilt site. Posts here live as plain markdown
files inside `src/content/blog/` — no database, no CMS, just files.
## What to expect
- Notes on self-hosting (Forgejo, SearXNG, and friends)
- Linux tinkering
- Random thoughts
Stay tuned.

62
src/lib/blog.ts Normal file
View file

@ -0,0 +1,62 @@
import matter from 'gray-matter';
// Make Buffer available for gray-matter in the browser
import { Buffer } from 'buffer';
if (typeof window !== 'undefined' && !(window as any).Buffer) {
(window as any).Buffer = Buffer;
}
export interface PostMeta {
slug: string;
title: string;
date: string;
description?: string;
tags?: string[];
draft?: boolean;
}
export interface Post extends PostMeta {
content: string;
}
const files = import.meta.glob('../content/blog/*.md', {
query: '?raw',
import: 'default',
eager: true,
}) as Record<string, string>;
function parseAll(): Post[] {
return Object.entries(files)
.map(([path, raw]) => {
const slug = path.split('/').pop()!.replace(/\.md$/, '');
const { data, content } = matter(raw);
return {
slug,
title: data.title ?? slug,
date: data.date ? new Date(data.date).toISOString() : new Date().toISOString(),
description: data.description,
tags: data.tags ?? [],
draft: data.draft ?? false,
content,
} as Post;
})
.sort((a, b) => b.date.localeCompare(a.date));
}
const all = parseAll();
export function getAllPosts(includeDrafts = false): Post[] {
return includeDrafts ? all : all.filter((p) => !p.draft);
}
export function getPostBySlug(slug: string): Post | undefined {
return all.find((p) => p.slug === slug);
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}

102
src/pages/Blog.tsx Normal file
View file

@ -0,0 +1,102 @@
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { GlassCard } from '@/components/GlassCard';
import { getAllPosts, formatDate } from '@/lib/blog';
import { ArrowLeft, ArrowUpRight, Rss } from 'lucide-react';
const Blog = () => {
const posts = getAllPosts();
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
document.title = 'Blog — damaj.tech';
}, []);
return (
<div
onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}
className="min-h-screen bg-background relative overflow-hidden"
>
<div
className="fixed pointer-events-none z-0"
style={{
left: mousePos.x,
top: mousePos.y,
width: 500,
height: 500,
transform: 'translate(-50%, -50%)',
background: 'radial-gradient(circle, hsl(25 95% 55% / 0.06) 0%, transparent 70%)',
}}
/>
<div className="relative z-10 max-w-3xl mx-auto px-4 sm:px-6 py-12 md:py-20">
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors font-mono mb-8"
>
<ArrowLeft className="w-4 h-4" />
back
</Link>
<header className="mb-12">
<div className="flex items-center gap-3 mb-3">
<Rss className="w-6 h-6 text-primary" />
<h1 className="text-3xl md:text-4xl font-heading font-bold text-foreground">
<span className="text-primary font-mono">$</span> blog
</h1>
</div>
<p className="text-muted-foreground">
Notes on self-hosting, linux, and whatever else.
</p>
</header>
{posts.length === 0 ? (
<GlassCard className="p-8 text-center">
<p className="text-muted-foreground">No posts yet. Check back soon.</p>
</GlassCard>
) : (
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.slug}>
<Link to={`/blog/${post.slug}`} className="block link-arrow-parent">
<GlassCard className="p-5 md:p-6 cursor-pointer">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground font-mono mb-1">
{formatDate(post.date)}
</p>
<h2 className="font-heading font-semibold text-lg md:text-xl text-foreground mb-1">
{post.title}
</h2>
{post.description && (
<p className="text-sm text-muted-foreground leading-relaxed">
{post.description}
</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-3">
{post.tags.map((tag) => (
<span
key={tag}
className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
>
{tag}
</span>
))}
</div>
)}
</div>
<ArrowUpRight className="w-4 h-4 text-muted-foreground link-arrow shrink-0 mt-1" />
</div>
</GlassCard>
</Link>
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default Blog;

85
src/pages/BlogPost.tsx Normal file
View file

@ -0,0 +1,85 @@
import { Link, useParams, Navigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import { getPostBySlug, formatDate } from '@/lib/blog';
import { ArrowLeft } from 'lucide-react';
import 'highlight.js/styles/github-dark.css';
const BlogPost = () => {
const { slug } = useParams<{ slug: string }>();
const post = slug ? getPostBySlug(slug) : undefined;
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
if (post) {
document.title = `${post.title} — damaj.tech`;
const meta = document.querySelector('meta[name="description"]');
if (meta && post.description) meta.setAttribute('content', post.description);
}
}, [post]);
if (!post) return <Navigate to="/blog" replace />;
return (
<div
onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}
className="min-h-screen bg-background relative overflow-hidden"
>
<div
className="fixed pointer-events-none z-0"
style={{
left: mousePos.x,
top: mousePos.y,
width: 500,
height: 500,
transform: 'translate(-50%, -50%)',
background: 'radial-gradient(circle, hsl(25 95% 55% / 0.06) 0%, transparent 70%)',
}}
/>
<article className="relative z-10 max-w-3xl mx-auto px-4 sm:px-6 py-12 md:py-20">
<Link
to="/blog"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors font-mono mb-8"
>
<ArrowLeft className="w-4 h-4" />
all posts
</Link>
<header className="mb-10 pb-6 border-b border-border">
<p className="text-xs text-muted-foreground font-mono mb-2">
{formatDate(post.date)}
</p>
<h1 className="text-3xl md:text-4xl font-heading font-bold text-foreground mb-3">
{post.title}
</h1>
{post.description && (
<p className="text-muted-foreground text-lg leading-relaxed">{post.description}</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-4">
{post.tags.map((tag) => (
<span
key={tag}
className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
>
{tag}
</span>
))}
</div>
)}
</header>
<div className="prose-blog">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
{post.content}
</ReactMarkdown>
</div>
</article>
</div>
);
};
export default BlogPost;

View file

@ -6,7 +6,7 @@ import { MessageCircle, Rss, GitBranch, Search, ExternalLink, ArrowUpRight } fro
const items = [ const items = [
{ icon: MessageCircle, label: 'Who Am I?', href: '/whoami', desc: 'About me', internal: true }, { icon: MessageCircle, label: 'Who Am I?', href: '/whoami', desc: 'About me', internal: true },
{ icon: Rss, label: 'Blog', href: 'https://damaj.tech/site/blog', desc: 'My writings' }, { icon: Rss, label: 'Blog', href: '/blog', desc: 'My writings', internal: true },
{ icon: GitBranch, label: 'Git Server', href: 'https://git.damaj.tech/', desc: 'Self-hosted Forgejo' }, { icon: GitBranch, label: 'Git Server', href: 'https://git.damaj.tech/', desc: 'Self-hosted Forgejo' },
{ icon: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search' }, { icon: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search' },
]; ];