Skip to content

Commit 004b8f0

Browse files
committed
feat(blog): add content collections, post layout, dynamic post routes; improve blog and projects lists; add Work Experience; replace footer with Contact section; style tweaks and fixes
1 parent 11d7a3f commit 004b8f0

File tree

11 files changed

+292
-32
lines changed

11 files changed

+292
-32
lines changed

src/components/PostItem.astro

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
interface Props {
3+
title: string;
4+
date: string | Date; // ISO string or Date
5+
link: string;
6+
description?: string;
7+
}
8+
9+
const { title, date, link, description } = Astro.props as Props;
10+
const d = typeof date === 'string' ? new Date(date) : date;
11+
const formatted = d.toLocaleDateString(undefined, {
12+
year: 'numeric',
13+
month: 'short',
14+
day: 'numeric',
15+
});
16+
---
17+
18+
<article class="post">
19+
<h3 class="post-title">
20+
<a href={link} rel="noopener noreferrer">{title}</a>
21+
</h3>
22+
{description && <p class="post-desc">{description}</p>}
23+
<time class="post-date muted" datetime={typeof date === 'string' ? date : d.toISOString()}>{formatted}</time>
24+
</article>

src/components/ProjectItem.astro

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
interface Props {
3+
title: string;
4+
description: string;
5+
link: string;
6+
}
7+
8+
const { title, description, link } = Astro.props as Props;
9+
---
10+
11+
<article class="project">
12+
<h3 class="project-title">
13+
<a href={link} rel="noopener noreferrer">{title}</a>
14+
</h3>
15+
<p class="project-desc">{description}</p>
16+
</article>
17+

src/content/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { defineCollection, z } from 'astro:content';
2+
3+
const posts = defineCollection({
4+
type: 'content',
5+
// Stable, SEO-friendly slugs: YYYY-MM-DD-title
6+
slug: ({ defaultSlug, data }) => {
7+
const custom = (data as any).slug as string | undefined;
8+
if (custom) return custom.toLowerCase();
9+
const d = typeof (data as any).date === 'string' ? new Date((data as any).date) : (data as any).date as Date | undefined;
10+
const iso = d ? d.toISOString().slice(0, 10) : undefined;
11+
return iso ? `${iso}-${defaultSlug}` : defaultSlug;
12+
},
13+
schema: z.object({
14+
title: z.string(),
15+
description: z.string().optional(),
16+
// Accept either a native Date or an ISO string for flexibility
17+
date: z.union([z.date(), z.string()]),
18+
draft: z.boolean().optional().default(false),
19+
}).passthrough(),
20+
});
21+
22+
export const collections = { posts };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Workflow Automation with Coda.io
3+
date: 2025-06-15
4+
description: How I connected Coda, Zoom, and email with n8n to cut manual steps.
5+
draft: false
6+
---
7+
8+
Quick notes from wiring up an internal flow:
9+
10+
1. Trigger on form submit in Coda
11+
2. Look up context from a table
12+
3. Create Zoom meeting and email the invite
13+
14+
Small wins compound — aim for reliability first, then speed.
15+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Building a Procurement Tool
3+
date: 2025-07-01
4+
description: Notes on scoping, pricing logic, and keeping the workflow fast.
5+
---
6+
7+
Starting from a spreadsheet helps validate the core calculations fast. I focused on:
8+
9+
- Clear inputs: base price, fees, taxes, shipping, margin
10+
- Derived outputs: total cost, selling price, breakeven
11+
- One source of truth: persist rows for reuse later
12+
13+
Once stable, the model can move into a small app — same math, better UX.
14+

src/layouts/Base.astro

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const { title = 'Your Name', description = 'Personal site' } = Astro.props;
2424
<main>
2525
<slot />
2626
</main>
27-
<footer class="foot"><div class="wrap"{new Date().getFullYear()} Hidayatullah</div></footer>
2827
</body>
2928

3029
</html>

src/layouts/PostLayout.astro

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
import Base from './Base.astro';
3+
4+
interface Props {
5+
title: string;
6+
date: string | Date; // ISO or Date
7+
description?: string;
8+
}
9+
10+
const { title, date, description } = Astro.props as Props;
11+
const d = typeof date === 'string' ? new Date(date) : date;
12+
const formatted = d.toLocaleDateString(undefined, {
13+
year: 'numeric', month: 'short', day: 'numeric'
14+
});
15+
---
16+
17+
<Base title={`${title} — Hidayatullah`} description={description ?? title}>
18+
<div class="wrap">
19+
<nav aria-label="Breadcrumbs" class="muted">
20+
<a href="/">About</a> / <a href="/blog">Blog</a>
21+
</nav>
22+
<article class="post-article">
23+
<header>
24+
<h1 class="post-h1">{title}</h1>
25+
<time class="post-meta" datetime={typeof date === 'string' ? date : d.toISOString()}>{formatted}</time>
26+
{description && <p class="post-summary">{description}</p>}
27+
</header>
28+
<div class="post-content">
29+
<slot />
30+
</div>
31+
</article>
32+
</div>
33+
</Base>
34+
35+
<style>
36+
.post-article { margin-top: 0.5rem; }
37+
.post-h1 { margin: 0 0 0.25rem; font-size: clamp(1.6rem, 3.6vw, 2.2rem); }
38+
.post-meta { color: var(--muted); font-size: 0.9rem; }
39+
.post-summary { color: color-mix(in oklab, var(--text) 85%, transparent); margin: 0.5rem 0 1rem; }
40+
.post-content :is(p, ul, ol, pre, blockquote) { margin: 0 0 var(--space); }
41+
.post-content code { background: color-mix(in oklab, var(--text) 8%, transparent); padding: 0.05em 0.3em; border-radius: 4px; }
42+
.post-content pre { padding: 0.75rem; border-radius: var(--radius); overflow: auto; }
43+
.post-content blockquote { border-left: 2px solid color-mix(in oklab, var(--text) 20%, transparent); padding-left: 0.75rem; color: var(--muted); }
44+
</style>

src/pages/blog.astro

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
import Base from "../layouts/Base.astro";
3+
import PostItem from "../components/PostItem.astro";
4+
import { getCollection } from 'astro:content';
5+
6+
const all = await getCollection('posts', (p) => !p.data.draft);
7+
const toDate = (d: unknown) => (typeof d === 'string' ? new Date(d) : (d as Date));
8+
const posts = all
9+
.sort((a, b) => toDate(b.data.date).getTime() - toDate(a.data.date).getTime())
10+
.map((p) => ({
11+
title: p.data.title,
12+
date: p.data.date,
13+
description: p.data.description,
14+
link: `/blog/${p.slug}`,
15+
}));
16+
---
17+
18+
<Base title="Blog — Hidayatullah" description="All posts by Hidayatullah">
19+
<div class="wrap">
20+
<nav aria-label="Primary">
21+
<a href="/">About</a> /
22+
<a href="/blog">Blog</a>
23+
</nav>
24+
<h1>Blog</h1>
25+
<div class="posts">
26+
{posts.map((p) => <PostItem {...p} />)}
27+
</div>
28+
</div>
29+
</Base>

src/pages/blog/[slug].astro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import Base from "../../layouts/Base.astro";
3+
import PostLayout from "../../layouts/PostLayout.astro";
4+
import { getCollection } from 'astro:content';
5+
6+
export async function getStaticPaths() {
7+
const posts = await getCollection('posts', (p) => !p.data.draft);
8+
return posts.map((p) => ({
9+
params: { slug: p.slug },
10+
props: { entry: p },
11+
}));
12+
}
13+
14+
const { entry } = Astro.props as { entry: any };
15+
const { Content } = await entry.render();
16+
const data = entry.data;
17+
---
18+
19+
<PostLayout title={data.title} date={data.date} description={data.description}>
20+
<Content />
21+
</PostLayout>

src/pages/index.astro

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
11
---
22
import Base from "../layouts/Base.astro";
3+
import ProjectItem from "../components/ProjectItem.astro";
34
45
const name = "Hidayatullah";
56
const tagline = "Developer, systems builder, problem solver.";
67
const bio = "I like creating practical tools that make work faster, smarter, and easier.";
78
89
const projects = [
9-
{ title: "Project One", description: "Tiny utility that does X swiftly.", link: "#" },
10-
{ title: "Project Two", description: "Minimal experiment exploring Y.", link: "#" },
11-
{ title: "Project Three", description: "A neat thing about Z.", link: "#" },
10+
{
11+
title: "Procurement Cost Calculator",
12+
description:
13+
"Spreadsheet-based tool to calculate total costs and selling prices for procurement items, with data storage for future reference.",
14+
link: "#",
15+
},
16+
{
17+
title: "Order Management System",
18+
description:
19+
"Internal system using Coda.io to manage procurement orders and streamline purchasing processes.",
20+
link: "#",
21+
},
22+
{
23+
title: "Procurement Automation Workflow",
24+
description:
25+
"Automated procurement workflows by integrating Coda.io with Zoom and Email via n8n.",
26+
link: "#",
27+
},
1228
];
1329
1430
const posts = [
15-
{ title: "On simplicity", date: "2025-07-01", link: "#" },
16-
{ title: "Notes on speed", date: "2025-06-10", link: "#" },
17-
{ title: "Quiet design", date: "2025-05-22", link: "#" },
31+
{ title: "Building a Procurement Tool", date: "2025-07-01", link: "#" },
32+
{ title: "Workflow Automation with Coda.io", date: "2025-06-15", link: "#" },
33+
{ title: "Lessons from E-commerce Operations", date: "2025-05-20", link: "#" },
1834
];
1935
---
2036

2137
<Base title={`${name} — ${tagline}`} description={bio}>
2238
<div class="wrap">
2339
<nav aria-label="Primary">
24-
<a href="#about">About</a> /
25-
<a href="#projects">Projects</a> /
26-
<a href="#blog">Blog</a>
40+
<a href="/">About</a> /
41+
<a href="/blog">Blog</a>
2742
</nav>
2843

2944
<section id="about">
@@ -34,25 +49,34 @@ const posts = [
3449

3550
<section id="projects">
3651
<h2>Projects</h2>
37-
<ul class="list">
38-
{projects.map((p) => (
39-
<li class="item">
40-
<strong><a href={p.link} rel="noopener noreferrer">{p.title}</a></strong>
41-
{p.description}
42-
</li>
43-
))}
44-
</ul>
52+
<div class="projects">
53+
{projects.map((p) => <ProjectItem {...p} />)}
54+
</div>
4555
</section>
4656

47-
<section id="blog">
48-
<h2>Blog</h2>
57+
<section id="experience">
58+
<h2>Work Experience</h2>
59+
<article class="experience">
60+
<h3 class="experience-title">
61+
Procurement & Online Operations — PT. Volar Mekanikal Teknologi
62+
<span class="muted"> (2023 – Present)</span>
63+
</h3>
64+
<ul class="list bulleted">
65+
<li class="item">Managed Tokopedia & Shopee stores, including order processing, customer service, and product listing updates.</li>
66+
<li class="item">Handled product sourcing from both local and international suppliers, ensuring quality, cost-effectiveness, and timely delivery.</li>
67+
</ul>
68+
</article>
69+
</section>
70+
71+
72+
73+
<section id="contact">
74+
<h2>Get in touch</h2>
4975
<ul class="list">
50-
{posts.map((post) => (
51-
<li class="item">
52-
<a href={post.link} rel="noopener noreferrer">{post.title}</a>
53-
<span class="muted"> — {new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}</span>
54-
</li>
55-
))}
76+
<li class="item"><a href="#" rel="me noopener noreferrer">Twitter</a></li>
77+
<li class="item"><a href="#" rel="noopener noreferrer">Email</a></li>
78+
<li class="item"><a href="https://github.com/dayatnhbtc" rel="noopener noreferrer">GitHub</a></li>
79+
<li class="item"><a href="#" rel="noopener noreferrer">LinkedIn</a></li>
5680
</ul>
5781
</section>
5882
</div>

0 commit comments

Comments
 (0)