From 1d04e5f13ecd9c9069373eee4d78a04b212e904e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 02:03:53 +0000 Subject: [PATCH] feat: redesign landing page, post page, and writing page with tags - Landing page: GitHub README-style profile header with avatar, bio, social links, readme section with stats, featured latest post, and recent posts grid - Blog post page: Hashnode-style layout with reading time estimate, tags above title, author meta bar, hero image, author card at bottom, and tag links - Writing page: added client-side tag filter bar with URL param support (?tag=kubernetes), post count display - Content schema: added optional tags field to posts collection - All 4 posts: tagged with relevant topics (kubernetes, gitops, k3s, fluxcd, docker, devops, gaming, web, email, etc.) - CSS: new components for profile header, readme section, featured card, tag pills, tag filter bar, author card, reading time meta https://claude.ai/code/session_01HSKQgRDxyFMuqBsH6PmT9c --- src/content/config.ts | 1 + .../2025-12-13-gitops-k3s-first-deployment.md | 1 + ...6-03-14-deploying-vikunja-in-kubernetes.md | 1 + ...duction - The Enshrouded Docker Project.md | 1 + .../2026-1-22-html-outlook-signature-guide.md | 2 +- src/pages/index.astro | 121 +++++- src/pages/writing/[slug].astro | 55 ++- src/pages/writing/index.astro | 76 +++- src/styles/global.css | 394 +++++++++++++++++- 9 files changed, 616 insertions(+), 36 deletions(-) diff --git a/src/content/config.ts b/src/content/config.ts index 36f19b4..69935b8 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -7,6 +7,7 @@ export const collections = { date: z.date(), description: z.string(), hero: z.string().optional(), // store WITHOUT leading slash + tags: z.array(z.string()).optional(), }), }), }; diff --git a/src/content/posts/2025-12-13-gitops-k3s-first-deployment.md b/src/content/posts/2025-12-13-gitops-k3s-first-deployment.md index 9ae51e1..e18104a 100644 --- a/src/content/posts/2025-12-13-gitops-k3s-first-deployment.md +++ b/src/content/posts/2025-12-13-gitops-k3s-first-deployment.md @@ -3,6 +3,7 @@ title: "Kubernetes Journey: GitOps on Raspberry Pi with FluxCD" date: 2025-12-13 description: "GitOps on a Raspberry Pi k3s cluster with FluxCD, and my first real workload: Linkding." hero: "posts/2025-12-13/hero.png" +tags: ["kubernetes", "gitops", "k3s", "fluxcd", "raspberry-pi"] --- # Kubernetes Journey: GitOps on Raspberry Pi with FluxCD diff --git a/src/content/posts/2026-03-14-deploying-vikunja-in-kubernetes.md b/src/content/posts/2026-03-14-deploying-vikunja-in-kubernetes.md index 139e466..907ae37 100644 --- a/src/content/posts/2026-03-14-deploying-vikunja-in-kubernetes.md +++ b/src/content/posts/2026-03-14-deploying-vikunja-in-kubernetes.md @@ -3,6 +3,7 @@ title: "Deploying Vikunja on a Raspberry Pi k3s Cluster with FluxCD and Cloudfla date: 2026-03-14 description: "GitOps on a Raspberry Pi k3s cluster with FluxCD, and my second app: Vikunja." hero: "posts/2026-03-14/03.14.26.hero.svg" +tags: ["kubernetes", "gitops", "k3s", "fluxcd", "cloudflare", "self-hosted"] --- I have been running a Raspberry Pi 4 as a single-node k3s cluster managed entirely with FluxCD and GitOps principles. The idea is simple: if it is not committed to Git, it does not exist on the cluster. No manual kubectl apply for workloads, no configuration drift, just Git as the source of truth. diff --git a/src/content/posts/2026-03-15-Treating Game Servers Like Production - The Enshrouded Docker Project.md b/src/content/posts/2026-03-15-Treating Game Servers Like Production - The Enshrouded Docker Project.md index 5070bc6..4105fd1 100644 --- a/src/content/posts/2026-03-15-Treating Game Servers Like Production - The Enshrouded Docker Project.md +++ b/src/content/posts/2026-03-15-Treating Game Servers Like Production - The Enshrouded Docker Project.md @@ -3,6 +3,7 @@ title: "Treating Game Servers Like Production: The Enshrouded Docker Project" date: 2026-03-15 description: "Enshrouded Docker Container Project" hero: "posts/2026-03-15/enshrouded_docker_blog_hero.svg" +tags: ["docker", "devops", "gaming", "self-hosted"] --- # Treating Game Servers Like Production: The Enshrouded Docker Project diff --git a/src/content/posts/2026-1-22-html-outlook-signature-guide.md b/src/content/posts/2026-1-22-html-outlook-signature-guide.md index f3581d1..a386163 100644 --- a/src/content/posts/2026-1-22-html-outlook-signature-guide.md +++ b/src/content/posts/2026-1-22-html-outlook-signature-guide.md @@ -2,7 +2,7 @@ title: "Outlook HTML Email Signature Guide (Enterprise‑Safe)" date: 2026-01-22 description: "How to build a professional, Outlook-compatible HTML signature for enterprise environments." -hero: "assets/logo.png" # (optional—remove if not needed) +tags: ["web", "email", "enterprise", "html"] --- # Outlook HTML Email Signature Guide (Enterprise‑Safe) diff --git a/src/pages/index.astro b/src/pages/index.astro index 0b32886..cd42ae5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,34 +2,109 @@ import BaseLayout from "../layouts/BaseLayout.astro"; import { getCollection } from "astro:content"; -const base = import.meta.env.BASE_URL.replace(/\/$/, ""); // <-- removes trailing slash if present +const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const posts = (await getCollection("posts")).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +const featured = posts[0]; +const recent = posts.slice(1, 4); --- -
-

Security Engineer’s Build Log

-

Building. Breaking. Fixing. Securing. Learning — one post at a time.

-
- Weekly - GitHub Pages - Markdown + +
+
JD
+
+

Jonathan DeLeon @MrGuato

+

+ Security Engineer · GitOps Practitioner · Builder. Building. Breaking. Fixing. Securing. Learning — one commit at a time. +

+
+ 📍 Boston, MA + 🔐 CISM® + ☁️ Kubernetes / k3s / FluxCD +
+
- -
- {posts.map((p) => ( - -
- \ No newline at end of file + + {recent.length > 0 && ( +
+
+ + View all → +
+ +
+ )} + diff --git a/src/pages/writing/[slug].astro b/src/pages/writing/[slug].astro index b430803..ebfa0ad 100644 --- a/src/pages/writing/[slug].astro +++ b/src/pages/writing/[slug].astro @@ -8,23 +8,70 @@ export async function getStaticPaths() { } const post = Astro.props; -const { Content } = await post.render(); +const { Content, remarkPluginFrontmatter } = await post.render(); const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const heroSrc = post.data.hero ? `${base}/${post.data.hero}` : null; + +// Estimate reading time (~200 wpm) +const wordCount = post.body.split(/\s+/).length; +const readingTime = Math.max(1, Math.ceil(wordCount / 200)); --- ← Back to Writing - + + {post.data.tags && post.data.tags.length > 0 && ( + + )}

{post.data.title}

+ + + {heroSrc && {post.data.title}}
+ + +
+
JD
+
+
Jonathan DeLeon @MrGuato
+

Security Engineer based in Boston, MA. Building production-minded infrastructure with a security-first mindset. If it's not committed, it doesn't exist.

+ +
+
+ + + {post.data.tags && post.data.tags.length > 0 && ( + + )}
diff --git a/src/pages/writing/index.astro b/src/pages/writing/index.astro index 4f0be86..68c3432 100644 --- a/src/pages/writing/index.astro +++ b/src/pages/writing/index.astro @@ -6,6 +6,9 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const posts = (await getCollection("posts")).sort( (a, b) => b.data.date.valueOf() - a.data.date.valueOf() ); + +// Collect all unique tags +const allTags = [...new Set(posts.flatMap((p) => p.data.tags ?? []))].sort(); --- @@ -14,9 +17,24 @@ const posts = (await getCollection("posts")).sort(

All build logs and notes, published through Git.

-
+ +
+ + {allTags.map((tag) => ( + + ))} +
+ + +
{posts.length} posts
+ +
{posts.map((p) => ( - +
- \ No newline at end of file + + + diff --git a/src/styles/global.css b/src/styles/global.css index 077f472..ba75c3f 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -7,6 +7,10 @@ --shadow: 0 12px 32px rgba(0,0,0,.06); --radius: 16px; --max: 880px; + --accent: #2563eb; + --accent-bg: rgba(37, 99, 235, .08); + --tag-bg: rgba(37, 99, 235, .10); + --tag-text: #1d4ed8; } @media (prefers-color-scheme: dark) { @@ -17,12 +21,19 @@ --border: rgba(240, 246, 252, .10); --card: rgba(22, 27, 34, .80); --shadow: 0 12px 32px rgba(0, 0, 0, .30); + --accent: #58a6ff; + --accent-bg: rgba(88, 166, 255, .10); + --tag-bg: rgba(88, 166, 255, .12); + --tag-text: #79b8ff; } :root:not([data-theme="light"]) .nav { background: rgba(13, 17, 23, .82); } :root:not([data-theme="light"]) .callout { background: rgba(240, 246, 252, .04); } :root:not([data-theme="light"]) .card:hover { box-shadow: 0 18px 40px rgba(0,0,0,.40); } :root:not([data-theme="light"]) .prose pre { background: #161b22; color: #e6edf3; } :root:not([data-theme="light"]) .prose :not(pre) > code { background: #161b22; } + :root:not([data-theme="light"]) .readme-section { background: rgba(22, 27, 34, .60); } + :root:not([data-theme="light"]) .author-card { background: rgba(22, 27, 34, .80); } + :root:not([data-theme="light"]) .tag-filter.active { background: rgba(88, 166, 255, .20); color: #79b8ff; border-color: rgba(88, 166, 255, .40); } } [data-theme="dark"] { @@ -32,12 +43,19 @@ --border: rgba(240, 246, 252, .10); --card: rgba(22, 27, 34, .80); --shadow: 0 12px 32px rgba(0, 0, 0, .30); + --accent: #58a6ff; + --accent-bg: rgba(88, 166, 255, .10); + --tag-bg: rgba(88, 166, 255, .12); + --tag-text: #79b8ff; } [data-theme="dark"] .nav { background: rgba(13, 17, 23, .82); } [data-theme="dark"] .callout { background: rgba(240, 246, 252, .04); } [data-theme="dark"] .card:hover { box-shadow: 0 18px 40px rgba(0,0,0,.40); } [data-theme="dark"] .prose pre { background: #161b22; color: #e6edf3; } [data-theme="dark"] .prose :not(pre) > code { background: #161b22; } +[data-theme="dark"] .readme-section { background: rgba(22, 27, 34, .60); } +[data-theme="dark"] .author-card { background: rgba(22, 27, 34, .80); } +[data-theme="dark"] .tag-filter.active { background: rgba(88, 166, 255, .20); color: #79b8ff; border-color: rgba(88, 166, 255, .40); } *{ box-sizing:border-box; } html,body{ height:100%; } @@ -102,6 +120,252 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } } .theme-toggle:hover { color: var(--text); } +/* ─── Profile header (landing) ─── */ +.profile-header { + display: flex; + gap: 20px; + align-items: flex-start; + padding: 36px 0 28px; +} +.profile-avatar { + width: 72px; + height: 72px; + border-radius: 50%; + background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); + color: #fff; + font-size: 22px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex: none; + letter-spacing: -0.02em; +} +.profile-name { + margin: 0 0 6px; + font-size: clamp(22px, 3vw, 30px); + letter-spacing: -0.03em; + line-height: 1.1; +} +.profile-handle { + font-weight: 400; + color: var(--muted); + font-size: 0.72em; + letter-spacing: 0; +} +.profile-bio { + margin: 0 0 10px; + color: var(--muted); + font-size: 15px; + max-width: 60ch; +} +.profile-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 18px; + margin-bottom: 14px; +} +.profile-stat { + font-size: 13px; + color: var(--muted); +} +.profile-links { + display: flex; + gap: 10px; + flex-wrap: wrap; +} +.profile-link { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--muted); + border: 1px solid var(--border); + border-radius: 8px; + padding: 5px 12px; + transition: color 120ms, border-color 120ms, background 120ms; +} +.profile-link:hover { + color: var(--text); + border-color: var(--accent); + background: var(--accent-bg); + text-decoration: none; +} + +/* ─── README section ─── */ +.readme-section { + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + margin-bottom: 32px; + background: rgba(249, 250, 251, .6); +} +.readme-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-bottom: 1px solid var(--border); + font-size: 13px; + color: var(--muted); + font-weight: 500; +} +.readme-icon { font-size: 15px; } +.readme-body { + padding: 16px 20px; +} +.readme-body p { margin: 0 0 14px; font-size: 15px; color: var(--muted); } +.readme-stats { + display: flex; + gap: 20px; + flex-wrap: wrap; +} +.readme-stat { + font-size: 13px; + color: var(--muted); + border: 1px solid var(--border); + border-radius: 6px; + padding: 4px 10px; +} +.readme-stat strong { color: var(--text); } + +/* ─── Section headers ─── */ +.section-label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + margin: 0; +} +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; + margin-top: 36px; +} +.section-more { + font-size: 13px; + color: var(--accent); +} +.section-more:hover { text-decoration: underline; text-underline-offset: 3px; } + +/* ─── Featured card ─── */ +.featured-section { margin: 20px 0 0; } +.featured-section > .section-label { margin-bottom: 14px; } +.featured-card { + display: flex; + flex-direction: column; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + background: var(--card); + box-shadow: var(--shadow); + transition: transform 160ms ease, box-shadow 160ms ease; + will-change: transform; +} +.featured-card:hover { + transform: translateY(-3px); + box-shadow: 0 20px 48px rgba(0,0,0,.12); + text-decoration: none; +} +.featured-img-wrap { + width: 100%; + aspect-ratio: 16/7; + overflow: hidden; +} +.featured-img-wrap img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.featured-content { + padding: 22px 24px 26px; +} +.featured-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + font-size: 13px; + color: var(--muted); + margin-bottom: 10px; +} +.featured-title { + margin: 0 0 10px; + font-size: clamp(22px, 2.5vw, 30px); + letter-spacing: -0.03em; + line-height: 1.15; + color: var(--text); +} +.featured-desc { + margin: 0 0 16px; + color: var(--muted); + font-size: 15px; + max-width: 68ch; +} +.featured-cta { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 14px; + font-weight: 600; + color: var(--accent); + letter-spacing: -0.01em; +} + +/* ─── Tags ─── */ +.tag { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + text-transform: lowercase; + letter-spacing: 0.02em; + background: var(--tag-bg); + color: var(--tag-text); + border-radius: 6px; + padding: 3px 8px; + border: 1px solid transparent; + transition: background 120ms, border-color 120ms; +} +.tag:hover { border-color: var(--accent); text-decoration: none; } +.tag-sm { font-size: 10px; padding: 2px 6px; } +.tag-lg { font-size: 12px; padding: 4px 10px; } + +/* ─── Tag filter bar (writing page) ─── */ +.tag-filter-bar { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 20px 0 10px; +} +.tag-filter { + background: none; + border: 1px solid var(--border); + border-radius: 999px; + padding: 5px 14px; + font-size: 13px; + color: var(--muted); + cursor: pointer; + transition: color 120ms, border-color 120ms, background 120ms; +} +.tag-filter:hover { color: var(--text); border-color: var(--accent); } +.tag-filter.active { + background: var(--accent-bg); + color: var(--accent); + border-color: var(--accent); + font-weight: 600; +} +.posts-count { + font-size: 13px; + color: var(--muted); + margin-bottom: 6px; +} + +/* ─── Grid / Cards ─── */ .hero{ padding:40px 0 24px; } .hero h1{ margin:0; @@ -155,6 +419,10 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } box-shadow: 0 18px 40px rgba(0, 0, 0, .10); } .card .meta{ + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; color:var(--muted); font-size:13px; } @@ -178,12 +446,120 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } flex:none; } +/* ─── Post page (Hashnode-style) ─── */ .post-title{ - font-size: clamp(30px, 3.2vw, 44px); + font-size: clamp(28px, 3.2vw, 44px); letter-spacing:-0.04em; line-height:1.1; - margin:12px 0 10px; + margin: 8px 0 0; +} +.post-tags-top { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 28px; + margin-bottom: 6px; } +.post-meta-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0 14px; + border-bottom: 1px solid var(--border); + margin-bottom: 4px; +} +.post-meta-author { + display: flex; + align-items: center; + gap: 12px; +} +.author-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); + color: #fff; + font-size: 13px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex: none; + letter-spacing: -0.02em; +} +.author-name { + font-size: 14px; + font-weight: 600; +} +.author-sub { + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} + +/* Author card at bottom of post */ +.author-card { + display: flex; + gap: 18px; + align-items: flex-start; + margin-top: 48px; + padding: 24px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(249, 250, 251, .6); +} +.author-card-avatar { + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); + color: #fff; + font-size: 17px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex: none; + letter-spacing: -0.02em; +} +.author-card-name { + font-size: 16px; + font-weight: 700; + margin-bottom: 6px; +} +.author-card-handle { + font-weight: 400; + color: var(--muted); + font-size: 0.8em; +} +.author-card-bio { + margin: 0 0 12px; + font-size: 14px; + color: var(--muted); +} +.author-card-links { + display: flex; + gap: 12px; +} +.author-card-links a { + font-size: 13px; + color: var(--accent); + font-weight: 500; +} +.author-card-links a:hover { text-decoration: underline; text-underline-offset: 3px; } + +.post-tags-bottom { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 24px; +} +.post-tags-label { + font-size: 13px; + color: var(--muted); +} + .prose{ font-size:17px; } .prose h2{ letter-spacing:-0.02em; margin-top:40px; } .prose img{ @@ -228,7 +604,7 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } .hero-img { width: 100%; height: auto; - max-height: 420px; + max-height: 440px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--border); @@ -242,7 +618,7 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } gap: 6px; color: var(--muted); font-size: 14px; - margin-bottom: 28px; + margin-bottom: 20px; transition: color 120ms ease, gap 120ms ease; } .back-link:hover { @@ -265,4 +641,12 @@ a:hover{ text-decoration:underline; text-underline-offset:4px; } border-top: 1px solid var(--border); color:var(--muted); font-size:13px; -} \ No newline at end of file +} + +/* Responsive tweaks */ +@media (max-width: 600px) { + .profile-header { flex-direction: column; gap: 14px; } + .profile-avatar { width: 56px; height: 56px; font-size: 17px; } + .featured-content { padding: 16px; } + .author-card { flex-direction: column; gap: 12px; } +}