From 9d274a0e91692a4b4e56e41da95080fe9074c8c3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:18:51 +0000 Subject: [PATCH] Changes Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com> --- src/App.tsx | 4 ++ src/content/blog/hello-world.md | 19 ++++++ src/lib/blog.ts | 62 +++++++++++++++++++ src/pages/Blog.tsx | 102 ++++++++++++++++++++++++++++++++ src/pages/BlogPost.tsx | 85 ++++++++++++++++++++++++++ src/pages/Index.tsx | 2 +- 6 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 src/content/blog/hello-world.md create mode 100644 src/lib/blog.ts create mode 100644 src/pages/Blog.tsx create mode 100644 src/pages/BlogPost.tsx diff --git a/src/App.tsx b/src/App.tsx index ab6e1ed..e707365 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( } /> } /> + } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/content/blog/hello-world.md b/src/content/blog/hello-world.md new file mode 100644 index 0000000..388f80f --- /dev/null +++ b/src/content/blog/hello-world.md @@ -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. diff --git a/src/lib/blog.ts b/src/lib/blog.ts new file mode 100644 index 0000000..a8c0e83 --- /dev/null +++ b/src/lib/blog.ts @@ -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; + +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', + }); +} diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx new file mode 100644 index 0000000..45936a7 --- /dev/null +++ b/src/pages/Blog.tsx @@ -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 ( +
setMousePos({ x: e.clientX, y: e.clientY })} + className="min-h-screen bg-background relative overflow-hidden" + > +
+ +
+ + + back + + +
+
+ +

+ $ blog +

+
+

+ Notes on self-hosting, linux, and whatever else. +

+
+ + {posts.length === 0 ? ( + +

No posts yet. Check back soon.

+
+ ) : ( +
    + {posts.map((post) => ( +
  • + + +
    +
    +

    + {formatDate(post.date)} +

    +

    + {post.title} +

    + {post.description && ( +

    + {post.description} +

    + )} + {post.tags && post.tags.length > 0 && ( +
    + {post.tags.map((tag) => ( + + {tag} + + ))} +
    + )} +
    + +
    +
    + +
  • + ))} +
+ )} +
+
+ ); +}; + +export default Blog; diff --git a/src/pages/BlogPost.tsx b/src/pages/BlogPost.tsx new file mode 100644 index 0000000..c92f2a3 --- /dev/null +++ b/src/pages/BlogPost.tsx @@ -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 ; + + return ( +
setMousePos({ x: e.clientX, y: e.clientY })} + className="min-h-screen bg-background relative overflow-hidden" + > +
+ +
+ + + all posts + + +
+

+ {formatDate(post.date)} +

+

+ {post.title} +

+ {post.description && ( +

{post.description}

+ )} + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ +
+ + {post.content} + +
+
+
+ ); +}; + +export default BlogPost; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 07cb5fc..c30f92b 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -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' }, ];