Skip to content

Commit 1d04e5f

Browse files
committed
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
1 parent 229afd0 commit 1d04e5f

9 files changed

Lines changed: 616 additions & 36 deletions

src/content/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const collections = {
77
date: z.date(),
88
description: z.string(),
99
hero: z.string().optional(), // store WITHOUT leading slash
10+
tags: z.array(z.string()).optional(),
1011
}),
1112
}),
1213
};

src/content/posts/2025-12-13-gitops-k3s-first-deployment.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: "Kubernetes Journey: GitOps on Raspberry Pi with FluxCD"
33
date: 2025-12-13
44
description: "GitOps on a Raspberry Pi k3s cluster with FluxCD, and my first real workload: Linkding."
55
hero: "posts/2025-12-13/hero.png"
6+
tags: ["kubernetes", "gitops", "k3s", "fluxcd", "raspberry-pi"]
67
---
78

89
# Kubernetes Journey: GitOps on Raspberry Pi with FluxCD

src/content/posts/2026-03-14-deploying-vikunja-in-kubernetes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: "Deploying Vikunja on a Raspberry Pi k3s Cluster with FluxCD and Cloudfla
33
date: 2026-03-14
44
description: "GitOps on a Raspberry Pi k3s cluster with FluxCD, and my second app: Vikunja."
55
hero: "posts/2026-03-14/03.14.26.hero.svg"
6+
tags: ["kubernetes", "gitops", "k3s", "fluxcd", "cloudflare", "self-hosted"]
67
---
78

89
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.

src/content/posts/2026-03-15-Treating Game Servers Like Production - The Enshrouded Docker Project.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: "Treating Game Servers Like Production: The Enshrouded Docker Project"
33
date: 2026-03-15
44
description: "Enshrouded Docker Container Project"
55
hero: "posts/2026-03-15/enshrouded_docker_blog_hero.svg"
6+
tags: ["docker", "devops", "gaming", "self-hosted"]
67
---
78

89
# Treating Game Servers Like Production: The Enshrouded Docker Project

src/content/posts/2026-1-22-html-outlook-signature-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Outlook HTML Email Signature Guide (Enterprise‑Safe)"
33
date: 2026-01-22
44
description: "How to build a professional, Outlook-compatible HTML signature for enterprise environments."
5-
hero: "assets/logo.png" # (optional—remove if not needed)
5+
tags: ["web", "email", "enterprise", "html"]
66
---
77

88
# Outlook HTML Email Signature Guide (Enterprise‑Safe)

src/pages/index.astro

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,109 @@
22
import BaseLayout from "../layouts/BaseLayout.astro";
33
import { getCollection } from "astro:content";
44
5-
const base = import.meta.env.BASE_URL.replace(/\/$/, ""); // <-- removes trailing slash if present
5+
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
66
const posts = (await getCollection("posts")).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
7+
const featured = posts[0];
8+
const recent = posts.slice(1, 4);
79
---
810

911
<BaseLayout title="GitOps Notes">
10-
<section class="hero">
11-
<h1>Security Engineer’s Build Log</h1>
12-
<p>Building. Breaking. Fixing. Securing. Learning — one post at a time.</p>
13-
<div class="badges">
14-
<span class="badge">Weekly</span>
15-
<span class="badge">GitHub Pages</span>
16-
<span class="badge">Markdown</span>
12+
<!-- Profile / README-style header -->
13+
<section class="profile-header">
14+
<div class="profile-avatar">JD</div>
15+
<div class="profile-info">
16+
<h1 class="profile-name">Jonathan DeLeon <span class="profile-handle">@MrGuato</span></h1>
17+
<p class="profile-bio">
18+
Security Engineer · GitOps Practitioner · Builder. Building. Breaking. Fixing. Securing. Learning — one commit at a time.
19+
</p>
20+
<div class="profile-meta">
21+
<span class="profile-stat">📍 Boston, MA</span>
22+
<span class="profile-stat">🔐 CISM®</span>
23+
<span class="profile-stat">☁️ Kubernetes / k3s / FluxCD</span>
24+
</div>
25+
<div class="profile-links">
26+
<a class="profile-link" href="https://github.com/MrGuato" target="_blank" rel="noopener">
27+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
28+
GitHub
29+
</a>
30+
<a class="profile-link" href="https://www.linkedin.com/in/jonathan-deleon-cism/" target="_blank" rel="noopener">
31+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 1.146C0 .514.53 0 1.183 0h13.634C15.47 0 16 .514 16 1.146v13.708c0 .632-.53 1.146-1.183 1.146H1.183C.53 16 0 15.486 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4z"/></svg>
32+
LinkedIn
33+
</a>
34+
<a class="profile-link" href="https://hashnode.com/@mrcyberleon" target="_blank" rel="noopener">
35+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="8" cy="8" r="8"/></svg>
36+
Hashnode
37+
</a>
38+
</div>
1739
</div>
1840
</section>
19-
20-
<section class="grid">
21-
{posts.map((p) => (
22-
<a class="card" href={`${base}/writing/${p.slug}`}>
23-
<div class="card-body">
24-
<div class="meta">
25-
{p.data.date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
26-
</div>
27-
<h2>{p.data.title}</h2>
28-
<p>{p.data.description}</p>
41+
42+
<!-- About this repo -->
43+
<section class="readme-section">
44+
<div class="readme-header">
45+
<span class="readme-icon">📖</span>
46+
<span>README.md</span>
47+
</div>
48+
<div class="readme-body">
49+
<p>This is my personal build log — raw notes, post-mortems, and deep-dives from running a home Kubernetes cluster, building security tooling, and learning in public. Everything is version-controlled and deployed via GitOps.</p>
50+
<div class="readme-stats">
51+
<span class="readme-stat"><strong>{posts.length}</strong> posts</span>
52+
<span class="readme-stat"><strong>Astro</strong> framework</span>
53+
<span class="readme-stat"><strong>GitHub Pages</strong> hosting</span>
54+
<span class="readme-stat"><strong>GitOps</strong> workflow</span>
2955
</div>
56+
</div>
57+
</section>
58+
59+
<!-- Featured post -->
60+
{featured && (
61+
<section class="featured-section">
62+
<h2 class="section-label">Featured Post</h2>
63+
<a class="featured-card" href={`${base}/writing/${featured.slug}`}>
64+
{featured.data.hero && (
65+
<div class="featured-img-wrap">
66+
<img src={`${base}/${featured.data.hero}`} alt={featured.data.title} loading="lazy" />
67+
</div>
68+
)}
69+
<div class="featured-content">
70+
<div class="featured-meta">
71+
{featured.data.date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
72+
{featured.data.tags && featured.data.tags.slice(0, 3).map((tag: string) => (
73+
<span class="tag">{tag}</span>
74+
))}
75+
</div>
76+
<h3 class="featured-title">{featured.data.title}</h3>
77+
<p class="featured-desc">{featured.data.description}</p>
78+
<span class="featured-cta">Read post →</span>
79+
</div>
80+
</a>
81+
</section>
82+
)}
3083

31-
{p.data.hero && <img class="thumb" src={`${base}/${p.data.hero}`} alt="" loading="lazy" />}
32-
</a>
33-
))}
34-
</section>
35-
</BaseLayout>
84+
<!-- Recent posts -->
85+
{recent.length > 0 && (
86+
<section>
87+
<div class="section-header">
88+
<h2 class="section-label">Recent Posts</h2>
89+
<a href={`${base}/writing`} class="section-more">View all →</a>
90+
</div>
91+
<div class="grid">
92+
{recent.map((p) => (
93+
<a class="card" href={`${base}/writing/${p.slug}`}>
94+
<div class="card-body">
95+
<div class="meta">
96+
{p.data.date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
97+
{p.data.tags && p.data.tags.slice(0, 2).map((tag: string) => (
98+
<span class="tag tag-sm">{tag}</span>
99+
))}
100+
</div>
101+
<h2>{p.data.title}</h2>
102+
<p>{p.data.description}</p>
103+
</div>
104+
{p.data.hero && <img class="thumb" src={`${base}/${p.data.hero}`} alt="" loading="lazy" />}
105+
</a>
106+
))}
107+
</div>
108+
</section>
109+
)}
110+
</BaseLayout>

src/pages/writing/[slug].astro

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,70 @@ export async function getStaticPaths() {
88
}
99
1010
const post = Astro.props;
11-
const { Content } = await post.render();
11+
const { Content, remarkPluginFrontmatter } = await post.render();
1212
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
1313
const heroSrc = post.data.hero ? `${base}/${post.data.hero}` : null;
14+
15+
// Estimate reading time (~200 wpm)
16+
const wordCount = post.body.split(/\s+/).length;
17+
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
1418
---
1519

1620
<BaseLayout title={post.data.title} description={post.data.description}>
1721
<a href={`${base}/writing`} class="back-link">← Back to Writing</a>
1822

19-
<div class="post-meta">
20-
{post.data.date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
21-
</div>
23+
<!-- Tags above title -->
24+
{post.data.tags && post.data.tags.length > 0 && (
25+
<div class="post-tags-top">
26+
{post.data.tags.map((tag: string) => (
27+
<a href={`${base}/writing?tag=${tag}`} class="tag tag-lg">{tag}</a>
28+
))}
29+
</div>
30+
)}
2231

2332
<h1 class="post-title">{post.data.title}</h1>
2433

34+
<!-- Meta bar -->
35+
<div class="post-meta-bar">
36+
<div class="post-meta-author">
37+
<div class="author-avatar">JD</div>
38+
<div>
39+
<div class="author-name">Jonathan DeLeon</div>
40+
<div class="author-sub">
41+
{post.data.date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
42+
&nbsp;·&nbsp;{readingTime} min read
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
2548
{heroSrc && <img class="hero-img" src={heroSrc} alt={post.data.title} />}
2649

2750
<div class="prose">
2851
<Content />
2952
</div>
53+
54+
<!-- Author card at bottom -->
55+
<div class="author-card">
56+
<div class="author-card-avatar">JD</div>
57+
<div class="author-card-info">
58+
<div class="author-card-name">Jonathan DeLeon <span class="author-card-handle">@MrGuato</span></div>
59+
<p class="author-card-bio">Security Engineer based in Boston, MA. Building production-minded infrastructure with a security-first mindset. If it's not committed, it doesn't exist.</p>
60+
<div class="author-card-links">
61+
<a href="https://github.com/MrGuato" target="_blank" rel="noopener">GitHub</a>
62+
<a href="https://www.linkedin.com/in/jonathan-deleon-cism/" target="_blank" rel="noopener">LinkedIn</a>
63+
<a href="https://hashnode.com/@mrcyberleon" target="_blank" rel="noopener">Hashnode</a>
64+
</div>
65+
</div>
66+
</div>
67+
68+
<!-- Tags at bottom -->
69+
{post.data.tags && post.data.tags.length > 0 && (
70+
<div class="post-tags-bottom">
71+
<span class="post-tags-label">Tagged:</span>
72+
{post.data.tags.map((tag: string) => (
73+
<a href={`${base}/writing?tag=${tag}`} class="tag tag-lg">{tag}</a>
74+
))}
75+
</div>
76+
)}
3077
</BaseLayout>

src/pages/writing/index.astro

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
66
const posts = (await getCollection("posts")).sort(
77
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
88
);
9+
10+
// Collect all unique tags
11+
const allTags = [...new Set(posts.flatMap((p) => p.data.tags ?? []))].sort();
912
---
1013

1114
<BaseLayout title="Writing">
@@ -14,16 +17,34 @@ const posts = (await getCollection("posts")).sort(
1417
<p>All build logs and notes, published through Git.</p>
1518
</section>
1619

17-
<section class="grid">
20+
<!-- Tag filter bar -->
21+
<div class="tag-filter-bar">
22+
<button class="tag-filter active" data-tag="all">All</button>
23+
{allTags.map((tag) => (
24+
<button class="tag-filter" data-tag={tag}>{tag}</button>
25+
))}
26+
</div>
27+
28+
<!-- Post count -->
29+
<div class="posts-count" id="posts-count">{posts.length} posts</div>
30+
31+
<section class="grid" id="posts-grid">
1832
{posts.map((p) => (
19-
<a class="card" href={`${base}/writing/${p.slug}`}>
33+
<a
34+
class="card"
35+
href={`${base}/writing/${p.slug}`}
36+
data-tags={JSON.stringify(p.data.tags ?? [])}
37+
>
2038
<div class="card-body">
2139
<div class="meta">
2240
{p.data.date.toLocaleDateString("en-US", {
2341
year: "numeric",
2442
month: "short",
2543
day: "numeric",
2644
})}
45+
{p.data.tags && p.data.tags.slice(0, 2).map((tag: string) => (
46+
<span class="tag tag-sm">{tag}</span>
47+
))}
2748
</div>
2849
<h2>{p.data.title}</h2>
2950
<p>{p.data.description}</p>
@@ -40,4 +61,53 @@ const posts = (await getCollection("posts")).sort(
4061
</a>
4162
))}
4263
</section>
43-
</BaseLayout>
64+
</BaseLayout>
65+
66+
<script>
67+
// Read tag from URL on page load
68+
const params = new URLSearchParams(window.location.search);
69+
const initialTag = params.get("tag") ?? "all";
70+
71+
const buttons = document.querySelectorAll<HTMLButtonElement>(".tag-filter");
72+
const cards = document.querySelectorAll<HTMLElement>(".card");
73+
const countEl = document.getElementById("posts-count");
74+
75+
function filterByTag(tag: string) {
76+
let visible = 0;
77+
cards.forEach((card) => {
78+
const tags: string[] = JSON.parse(card.dataset.tags ?? "[]");
79+
const show = tag === "all" || tags.includes(tag);
80+
card.style.display = show ? "" : "none";
81+
if (show) visible++;
82+
});
83+
84+
buttons.forEach((btn) => {
85+
btn.classList.toggle("active", btn.dataset.tag === tag);
86+
});
87+
88+
if (countEl) {
89+
countEl.textContent = tag === "all"
90+
? `${visible} posts`
91+
: `${visible} post${visible !== 1 ? "s" : ""} tagged "${tag}"`;
92+
}
93+
}
94+
95+
// Apply initial tag from URL
96+
filterByTag(initialTag);
97+
98+
// Activate buttons on click
99+
buttons.forEach((btn) => {
100+
btn.addEventListener("click", () => {
101+
const tag = btn.dataset.tag ?? "all";
102+
filterByTag(tag);
103+
// Update URL without reload
104+
const url = new URL(window.location.href);
105+
if (tag === "all") {
106+
url.searchParams.delete("tag");
107+
} else {
108+
url.searchParams.set("tag", tag);
109+
}
110+
history.replaceState(null, "", url.toString());
111+
});
112+
});
113+
</script>

0 commit comments

Comments
 (0)