Changes
Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com>
This commit is contained in:
parent
f4a4c03de2
commit
9d274a0e91
6 changed files with 273 additions and 1 deletions
|
|
@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import WhoAmI from "./pages/WhoAmI";
|
||||
import Blog from "./pages/Blog";
|
||||
import BlogPost from "./pages/BlogPost";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
@ -18,6 +20,8 @@ const App = () => (
|
|||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<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 */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
|
|
|||
19
src/content/blog/hello-world.md
Normal file
19
src/content/blog/hello-world.md
Normal 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
62
src/lib/blog.ts
Normal 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
102
src/pages/Blog.tsx
Normal 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
85
src/pages/BlogPost.tsx
Normal 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;
|
||||
|
|
@ -6,7 +6,7 @@ import { MessageCircle, Rss, GitBranch, Search, ExternalLink, ArrowUpRight } fro
|
|||
|
||||
const items = [
|
||||
{ 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: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search' },
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue