new site style (#1)

* initial phase

* remove pagefind

* phase 2

* restyle blog post pages, markdown rendering, pagination, and social link components.

* use dark-plus theme for markdown code

* update base layout and header components, and update the remote deployment directory

* use expressive code for code styling

* adjust inline code style

* format code

* re-add pagefind

* add sidebar with dev qotd

* add sidebar component with dynamic quote fetching and caching

* add Docker setup with Dockerfile, docker-compose, and dockerignore for the Astro site

* integrate Docker Compose with Traefik proxy and remove the legacy PowerShell deployment script
This commit is contained in:
fiatcode 2026-02-17 18:25:45 +07:00
parent bfb12ded11
commit 8d615bd421
37 changed files with 522 additions and 240 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="oklch(92.2% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-at-sign"><circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path></svg>

After

Width:  |  Height:  |  Size: 326 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="oklch(92.2% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-facebook"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>

After

Width:  |  Height:  |  Size: 307 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="oklch(92.2% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>

After

Width:  |  Height:  |  Size: 531 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="oklch(92.2% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="oklch(92.2% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>

After

Width:  |  Height:  |  Size: 412 B

View file

@ -1,51 +0,0 @@
---
import Tag from "@/components/Tag.astro";
interface Props {
title: string;
description: string;
date: Date;
tags?: string[];
}
const { title, description, date, tags } = Astro.props;
---
<article data-pagefind-body>
<header class="mb-12">
<h1 class="text-4xl font-bold mb-2">{title}</h1>
<h2 class="text-lg mb-2">{description}</h2>
<div class="flex gap-4 text-gray-600 text-sm">
<time datetime={new Date(date).toISOString()}>
{
date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
</time>
</div>
</header>
<div
class="max-w-3xl mb-8
prose prose-invert
prose-h1:border-b prose-h1:pb-4
prose-a:underline prose-a:underline-offset-4
prose-p:text-zinc-300 prose-p:text-justify prose-img:rounded-xl
prose-headings:text-zinc-300"
>
<slot />
</div>
<ul class="flex flex-wrap gap-2">
{
tags?.map((tag) => (
<li>
<Tag name={tag} variant="small" />
</li>
))
}
</ul>
</article>

View file

@ -1,26 +0,0 @@
---
interface Props {
id: string;
date: Date;
title: string;
description: string;
}
const { id, date, title, description } = Astro.props;
---
<a href={`/posts/${id}`}>
<div class="rounded-lg p-4 bg-zinc-800">
<div class="flex items-center text-zinc-400 text-sm mb-2">
{
date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
</div>
<h2 class="text-2xl font-semibold mb-1">{title}</h2>
<p class="text-base text-zinc-400">{description}</p>
</div>
</a>

View file

@ -0,0 +1,28 @@
---
import Link from "@/components/Link.astro";
interface Props {
id: string;
date: Date;
title: string;
description: string;
}
const { id, date, title, description } = Astro.props;
---
<div>
<Link href={`/posts/${id}`}>
<h2 class="text-lg font-semibold">{title}</h2>
</Link>
<div class="flex items-center text-neutral-500 text-sm">
{
date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
</div>
<p class="text-base text-neutral-400">{description}</p>
</div>

View file

@ -6,4 +6,4 @@ interface Props {
const { message } = Astro.props;
---
<p class="text-lg text-zinc-400">{message}</p>
<p class="text-lg text-neutral-400">{message}</p>

View file

@ -1,5 +1,6 @@
<footer class="w-full text-center">
<p class="text-xs text-zinc-400">
&copy; {new Date().getFullYear()} Dhemas Nurjaya. All rights reserved.
<footer class="w-full p-6 text-sm text-neutral-400">
<p>
&copy; {new Date().getFullYear()} Dhemas Nurjaya.
</p>
<p>All rights reserved.</p>
</footer>

View file

@ -1,19 +1,27 @@
---
import NavLink from "@/components/NavLink.astro";
import Link from "@/components/Link.astro";
---
<header class="w-full">
<nav class="flex flex-row justify-between" aria-label="Main navigation">
<div class="flex gap-2 items-center">
<a href="/" aria-label="Home">
<p class="text-2xl font-semibold">#!</p>
</a>
{/* <ThemeToggle /> */}
</div>
<div class="flex gap-6 items-center justify-end">
<NavLink href="/posts" ariaLabel="Posts">Posts</NavLink>
<NavLink href="/tags" ariaLabel="Tags">Tags</NavLink>
<NavLink href="/search" ariaLabel="Search">Search</NavLink>
</div>
<header class="w-full p-6">
<nav aria-label="Main navigation">
<ul class="flex flex-wrap gap-4 items-center">
<li>
<a
href="/"
aria-label="Home"
class="flex before:content-['['] after:content-[']_#'] p-1 bg-green-600 text-neutral-800 hover:bg-green-500 transition-colors"
>dhemasnurjaya</a
>
</li>
<li class="flex before:content-['./'] before:text-neutral-500">
<Link href="/posts" ariaLabel="Posts">posts</Link>
</li>
<li class="flex before:content-['./'] before:text-neutral-500">
<Link href="/tags" ariaLabel="Tags">tags</Link>
</li>
<li class="flex before:content-['./'] before:text-neutral-500">
<Link href="/search" ariaLabel="Search">search</Link>
</li>
</ul>
</nav>
</header>

View file

@ -1,25 +1,39 @@
---
import { Image } from "astro:assets";
import profileImage from "@/assets/images/profile.webp";
import { Linkedin, Github, Twitter, Facebook, Mail } from "lucide-astro";
import PageTitle from "@/components/PageTitle.astro";
import SocialLink from "@/components/SocialLink.astro";
import Linkedin from "@/assets/images/linkedin.svg";
import Github from "@/assets/images/github.svg";
import Twitter from "@/assets/images/twitter.svg";
import Facebook from "@/assets/images/facebook.svg";
import Mail from "@/assets/images/at-sign.svg";
---
<div class="flex flex-col min-h-full items-center justify-center">
<Image
src={profileImage}
loading="eager"
fetchpriority="high"
alt="Dhemas Nurjaya"
class="size-36 sm:size-48 rounded-2xl"
/>
<h1 class="text-3xl sm:text-5xl font-bold mt-4 text-center">
Dhemas Nurjaya
</h1>
<h2 class="text-lg sm:text-xl mt-2 text-center">
Passionate Software Engineer
</h2>
<div class="flex flex-row gap-8 mt-8" aria-label="social links">
<div class="flex flex-col">
<PageTitle title="Hi, I'm Dhemas 👋" />
<p class="mb-4">
Cross-platform developer. Linux fan stuck on Windows. Craftsman at heart.
</p>
<p class="mb-4">
I write about building apps, learning new tech, and the occasional
existential crisis that comes with debugging production code. This is where
I share what I'm working on, what I'm learning, and thoughts that don't fit
in a commit message.
</p>
<p class="mb-4">
Currently working at a local startup where "full-stack" means Flutter 📱,
Spring ☕, and React Router 🌐—sometimes all in the same day. I follow The
Craftsman's Way: quality is not negotiable. Clean Architecture, TDD, and DDD
aren't just buzzwords here; they're how I try to keep my sanity intact.
</p>
<p class="mb-4">
Also, I built this blog with Astro ⚡ because life's too short for slow
websites.
</p>
<p class="mb-8">
Stick around if you're into cross-platform dev, software craftsmanship, or
just want to see someone figure things out in public. 🚀
</p>
<div class="flex gap-6 items-center" aria-label="social links">
<SocialLink
href="https://www.linkedin.com/in/dhemas-nurjaya-030890bb"
label="Linkedin"
@ -42,5 +56,4 @@ import SocialLink from "@/components/SocialLink.astro";
icon={Mail}
/>
</div>
<div class="mt-24"></div>
</div>

17
src/components/Link.astro Normal file
View file

@ -0,0 +1,17 @@
---
interface Props {
href: string;
ariaLabel?: string;
class?: string;
}
const { href, ariaLabel, class: className } = Astro.props;
---
<a
href={href}
class={`text-emerald-300 hover:text-emerald-500 transition-colors underline underline-offset-4 ${className ?? ""}`}
aria-label={ariaLabel}
>
<slot />
</a>

View file

@ -0,0 +1,14 @@
<div
class="max-w-5xl mb-8
prose prose-invert
prose-p:text-neutral-300
prose-headings:pb-4 prose-headings:text-neutral-300 prose-headings:before:mr-2 prose-headings:before:text-neutral-500
prose-h1:border-b prose-h1:border-neutral-500 prose-h1:text-xl prose-h1:before:content-['#']
prose-h2:font-bold prose-h2:text-lg prose-h2:before:content-['##']
prose-h3:font-bold prose-h3:text-base prose-h3:before:content-['###']
prose-h4:font-semibold prose-h4:text-base prose-h4:before:content-['####']
prose-a:underline prose-a:underline-offset-4 prose-a:text-emerald-300 prose-a:hover:text-emerald-500 prose-a:transition-colors
prose-code:before:content-none prose-code:after:content-none prose-code:bg-neutral-900 prose-code:px-1 prose-code:py-0.5 prose-code:font-normal"
>
<slot />
</div>

View file

@ -1,17 +0,0 @@
---
interface Props {
href: string;
ariaLabel?: string;
class?: string;
}
const { href, ariaLabel, class: className } = Astro.props;
---
<a
href={href}
class={`hover:underline underline-offset-4 decoration-transparent hover:decoration-current transition-colors duration-300 aria-[current]:underline aria-[current]:decoration-current aria-[current]:font-semibold ${className ?? ""}`}
aria-label={ariaLabel}
>
<slot />
</a>

View file

@ -6,4 +6,4 @@ interface Props {
const { title } = Astro.props;
---
<h1 class="text-4xl font-bold mb-8">{title}</h1>
<h1 class="text-2xl font-bold mb-4">{title}</h1>

View file

@ -1,5 +1,5 @@
---
import NavLink from "./NavLink.astro";
import Link from "./Link.astro";
interface Props {
currentPage: number;
@ -13,14 +13,14 @@ const { currentPage, lastPage, getPageUrl } = Astro.props;
<nav aria-label="Pagination Navigation" class="flex py-6">
{
currentPage > 1 && (
<NavLink href={getPageUrl(currentPage - 1)}>« Previous</NavLink>
<Link href={getPageUrl(currentPage - 1)}>« Previous</Link>
)
}
{
currentPage < lastPage && (
<NavLink href={getPageUrl(currentPage + 1)} class="ml-auto">
<Link href={getPageUrl(currentPage + 1)} class="ml-auto">
Next »
</NavLink>
</Link>
)
}
</nav>

View file

@ -0,0 +1,12 @@
---
import { getQuote } from "@/lib/quote";
const { quote, author } = await getQuote();
---
<div class="flex flex-col sticky top-4">
<blockquote class="border-l border-neutral-500 pl-4">
<p class="mb-2">{quote}</p>
<footer class="text-neutral-400 text-sm">{author}</footer>
</blockquote>
<span class="text-neutral-500 py-4">---</span>
</div>

View file

@ -11,7 +11,7 @@ const { href, label, icon: Icon } = Astro.props;
<a
href={href}
aria-label={label}
class="text-zinc-300 hover:text-zinc-100 transition-colors"
class="text-neutral-300 hover:text-neutral-100 transition-colors"
>
<Icon class="size-6" />
<Icon class="size-5" />
</a>

View file

@ -14,7 +14,7 @@ const textSize = isSmall ? "text-sm" : "text-base";
<a href={linkHref}>
<span
class={`${textSize} text-zinc-400 font-semibold rounded-md bg-zinc-800 px-2 py-2 hover:bg-zinc-700`}
class={`${textSize} text-neutral-400 font-semibold bg-neutral-900 px-2 py-2 hover:bg-neutral-700`}
>
#{name}{count !== undefined && <sup>{count}</sup>}
</span>

View file

@ -2,6 +2,7 @@
import "@/styles/global.css";
import Footer from "@/components/Footer.astro";
import Header from "@/components/Header.astro";
import Sidebar from "@/components/Sidebar.astro";
interface Props {
title: string;
@ -21,10 +22,15 @@ const { title, description } = Astro.props;
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
</head>
<body class="h-screen max-w-5xl mx-auto flex flex-col font-sans p-4">
<body class="max-w-5xl mx-auto flex flex-col font-default">
<Header />
<main class="w-full max-w-3xl mx-auto flex-1 py-12">
<slot />
<main class="flex flex-col md:flex-row">
<section class="w-full md:flex-3 p-4 bg-neutral-800">
<slot />
</section>
<aside class="w-full md:flex-1 p-4">
<Sidebar />
</aside>
</main>
<Footer />
</body>

17
src/lib/quote.ts Normal file
View file

@ -0,0 +1,17 @@
let cache: { quote: string; author: string } | null = null;
export async function getQuote() {
if (cache) return cache;
console.log("\nFetching fresh quote...");
const response = await fetch("https://quotes-github-readme.vercel.app/api");
const svgText = await response.text();
cache = {
quote: svgText.match(/<h3>([\s\S]*?)<\/h3>/)?.[1]?.trim() ?? "",
author: svgText.match(/<p>([\s\S]*?)<\/p>/)?.[1]?.trim() ?? "",
};
console.log("Quote fetched: ", cache);
return cache;
}

View file

@ -2,7 +2,7 @@
import type { PaginateFunction, Page } from "astro";
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import BlogPostCard from "@/components/BlogPostCard.astro";
import BlogPostEntry from "@/components/BlogPostEntry.astro";
import PageTitle from "@/components/PageTitle.astro";
import type { CollectionEntry } from "astro:content";
import Pagination from "@/components/Pagination.astro";
@ -30,11 +30,11 @@ const { page } = Astro.props;
description="Read the latest blog posts by Dhemas Nurjaya"
>
<PageTitle title="Posts" />
<ul class="space-y-4">
<ul class="space-y-6">
{
page.data.map((post) => (
<li>
<BlogPostCard
<BlogPostEntry
id={post.id}
title={post.data.title}
date={post.data.date}

View file

@ -6,7 +6,8 @@ import {
type RenderResult,
} from "astro:content";
import Layout from "@/layouts/Layout.astro";
import BlogPost from "@/components/BlogPost.astro";
import Tag from "@/components/Tag.astro";
import Markdown from "@/components/Markdown.astro";
export async function getStaticPaths() {
const posts = await getCollection("blog");
@ -36,12 +37,35 @@ const { post, content } = Astro.props;
title={`${post.data.title} - Dhemas Nurjaya`}
description={post.data.description}
>
<BlogPost
title={post.data.title}
description={post.data.description}
date={post.data.date}
tags={post.data.tags}
>
<content.Content />
</BlogPost>
<article data-pagefind-body>
<header class="mb-12">
<h1 class="text-2xl font-bold mb-2">{post.data.title}</h1>
<h2 class="text-base mb-2">{post.data.description}</h2>
<div class="flex gap-4 text-neutral-500 text-sm">
<time datetime={new Date(post.data.date).toISOString()}>
{
post.data.date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
</time>
</div>
</header>
<Markdown>
<content.Content />
</Markdown>
<ul class="flex flex-wrap gap-2">
{
post.data.tags?.map((tag) => (
<li>
<Tag name={tag} variant="small" />
</li>
))
}
</ul>
</article>
</Layout>

View file

@ -4,7 +4,10 @@ import Layout from "@/layouts/Layout.astro";
import Search from "astro-pagefind/components/Search";
---
<Layout title="Dhemas Nurjaya" description="Welcome to my personal website">
<Layout
title="Search - Dhemas Nurjaya"
description="Search posts on Dhemas Nurjaya's website"
>
<PageTitle title="Search" />
<Search
id="search"
@ -20,37 +23,40 @@ import Search from "astro-pagefind/components/Search";
<style is:global>
.pagefind-ui {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: #a1a1aa; /* zinc-400 */
--pagefind-ui-text: #d4d4d8; /* zinc-300 */
--pagefind-ui-background: #18181b; /* zinc-900 */
--pagefind-ui-border: #3f3f46; /* zinc-700 */
--pagefind-ui-tag: #27272a; /* zinc-800 */
--pagefind-ui-scale: 0.9;
--pagefind-ui-primary: #a3a3a3; /* neutral-400 */
--pagefind-ui-text: #d4d4d8; /* neutral-300 */
--pagefind-ui-background: #171717; /* neutral-900 */
--pagefind-ui-border: #404040; /* neutral-700 */
--pagefind-ui-tag: #262626; /* neutral-800 */
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 0.5rem;
--pagefind-ui-font: "Noto Sans Variable", sans-serif;
--pagefind-ui-border-radius: 0rem;
--pagefind-ui-font: "JetBrains Mono", monospace;
}
.pagefind-ui__search-input {
background: #27272a; /* zinc-800 */
color: #d4d4d8; /* zinc-300 */
background: #262626; /* neutral-800 */
color: #d4d4d8; /* neutral-300 */
border: 1px solid #404040; /* neutral-700 */
}
.pagefind-ui__search-input::placeholder {
color: #71717a; /* zinc-500 */
color: #737373; /* neutral-500 */
}
.pagefind-ui__result {
border-radius: 0.5rem !important;
padding: 1rem !important;
background: #27272a !important; /* zinc-800 */
border: none !important;
margin-bottom: 1rem !important;
border-bottom: 1px solid #404040 !important; /* neutral-700 */
padding: 1rem 0 !important;
background: transparent !important;
border-top: none !important;
border-left: none !important;
border-right: none !important;
margin-bottom: 0 !important;
position: relative !important;
}
.pagefind-ui__result:last-child {
margin-bottom: 0 !important;
border-bottom: none !important;
}
.pagefind-ui__result-inner {
@ -60,8 +66,14 @@ import Search from "astro-pagefind/components/Search";
}
.pagefind-ui__result-link {
color: #d4d4d8; /* zinc-300 */
color: #d4d4d8; /* neutral-300 */
text-decoration: none !important;
font-weight: 500;
}
.pagefind-ui__result-link:hover {
text-decoration: underline !important;
color: #fafafa; /* neutral-50 */
}
.pagefind-ui__result-link::after {
@ -75,15 +87,22 @@ import Search from "astro-pagefind/components/Search";
}
.pagefind-ui__result-title {
color: #fafafa; /* zinc-50 */
color: #fafafa; /* neutral-50 */
}
.pagefind-ui__result-excerpt {
color: #a1a1aa; /* zinc-400 */
color: #a3a3a3; /* neutral-400 */
margin-top: 0.5rem;
}
.pagefind-ui__message {
color: #a1a1aa; /* zinc-400 */
color: #a3a3a3; /* neutral-400 */
padding: 1rem 0;
}
/* Remove default image styling if present, though showImages: false handles most */
.pagefind-ui__result-thumb {
display: none;
}
</style>

View file

@ -1,6 +1,6 @@
---
import { getCollection, type CollectionEntry } from "astro:content";
import BlogPostCard from "@/components/BlogPostCard.astro";
import BlogPostEntry from "@/components/BlogPostEntry.astro";
import Layout from "@/layouts/Layout.astro";
import PageTitle from "@/components/PageTitle.astro";
import EmptyState from "@/components/EmptyState.astro";
@ -44,7 +44,7 @@ const { filteredPosts } = Astro.props;
<ul class="space-y-4">
{filteredPosts.map((post) => (
<li>
<BlogPostCard
<BlogPostEntry
id={post.id}
title={post.data.title}
date={post.data.date}

View file

@ -20,7 +20,7 @@ const tags = Array.from(tagsMap.entries()).sort((a, b) =>
<Layout title="Tags - Dhemas Nurjaya" description="Browse blog posts by tags">
<PageTitle title="Tags" />
<ul class="flex flex-wrap gap-4">
<ul class="flex flex-wrap gap-4 mt-8">
{
tags?.map(([tag, count]) => (
<li class="mb-4">

View file

@ -1,15 +1,13 @@
@import "@fontsource-variable/noto-sans";
@import "@fontsource/jetbrains-mono";
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: "Noto Sans Variable", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-default: "JetBrains Mono", monospace;
}
@layer base {
body {
@apply bg-zinc-900 text-zinc-300;
@apply bg-neutral-900 text-neutral-300;
}
}