Add Coffee Notes collection and page
CI / ci (push) Successful in 29s
CI / ci (pull_request) Successful in 28s
Docker / docker (pull_request) Successful in 17s

This commit is contained in:
2026-03-07 16:41:46 +01:00
parent 2ae98e8857
commit faea831bef
7 changed files with 372 additions and 1 deletions
+1
View File
@@ -10,6 +10,7 @@ const links = [
{ href: "/now", label: "now" }, { href: "/now", label: "now" },
{ href: "/uses", label: "uses" }, { href: "/uses", label: "uses" },
{ href: "/blog", label: "blog" }, { href: "/blog", label: "blog" },
{ href: "/coffee", label: "coffee" },
{ href: "/changelog", label: "changelog" }, { href: "/changelog", label: "changelog" },
]; ];
+11
View File
@@ -0,0 +1,11 @@
---
date: 2026-03-07T21:30:00Z
title: Late-night systems
draft: false
---
There is a very specific kind of peace in checking on a quiet system late at
night and finding that everything is simply humming along.
No alerts. No drama. Just warm coffee, a few container logs, and the feeling
that the machines are behaving themselves for once.
@@ -0,0 +1,11 @@
---
date: 2026-03-05T20:40:00Z
title: Learning by breaking
draft: false
---
One of the nicest things about a homelab is that it gives you permission to
learn by breaking things in a place that still feels like yours.
Rebuild, retry, take notes, improve the backup story, and understand the system
a little better than you did the night before.
+12
View File
@@ -0,0 +1,12 @@
---
date: 2026-03-06T23:10:00Z
title: Privacy as care
draft: false
---
Privacy is often framed like fear, but most of the time I experience it more as
care.
Care for my own data. Care for other people's attention. Care for building
systems that do not assume they are entitled to watch everyone who walks
through the door.
@@ -0,0 +1,12 @@
---
date: 2026-03-07T18:05:00Z
title: Small sites still matter
draft: false
---
I still think small personal websites matter because they let a person sound
like themselves again.
Not every thought needs to be flattened into a post that fights for reach. Some
things are better when they can just quietly exist on a page someone made on
purpose.
+10 -1
View File
@@ -17,4 +17,13 @@ const blog = defineCollection({
}), }),
}); });
export const collections = { blog }; const coffee = defineCollection({
type: "content",
schema: z.object({
title: z.string().optional(),
date: z.coerce.date(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = { blog, coffee };
+315
View File
@@ -0,0 +1,315 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
const coffeeEntries = (await getCollection("coffee", ({ data }) => !data.draft))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const renderedEntries = await Promise.all(
coffeeEntries.map(async (entry) => {
const { Content } = await entry.render();
return {
...entry,
Content,
};
}),
);
function formatTimestamp(date: Date) {
const iso = date.toISOString();
return `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
}
---
<BaseLayout
title="Coffee Notes - Hidden Den Cafe"
description="Short notes from inside the den: quick reflections on building, privacy, life, and the quieter parts of the internet."
>
<div class="matrix-bg" aria-hidden="true"></div>
<main class="main">
<div class="container">
<header class="header fade-in">
<p class="eyebrow">Small reflections</p>
<h1 class="title">Coffee Notes</h1>
<div class="divider">==============================</div>
<p class="lead">
These are shorter notes than the blog. Small thoughts,
building notes, privacy reflections, and quiet little things
written down while the coffee is still warm.
</p>
</header>
{
renderedEntries.length === 0 ? (
<section class="section fade-in">
<p class="empty">
No notes yet. The mug is here, the notebook is just
still blank.
</p>
</section>
) : (
<section class="section fade-in" aria-labelledby="coffee-notes">
<h2 id="coffee-notes" class="sr-only">
Coffee Notes entries
</h2>
<ol class="note-list">
{renderedEntries.map((entry) => (
<li class="note-item">
<time
class="note-time"
datetime={entry.data.date.toISOString()}
>
{formatTimestamp(entry.data.date)}
</time>
<article
class="note-body"
aria-label={`Coffee note from ${formatTimestamp(entry.data.date)}`}
>
{entry.data.title && (
<h3 class="note-title">
{entry.data.title}
</h3>
)}
<div class="note-content">
<entry.Content />
</div>
</article>
</li>
))}
</ol>
</section>
)
}
<footer class="footer fade-in">
<p>Made with love by Latte</p>
</footer>
</div>
</main>
</BaseLayout>
<style>
.matrix-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.03;
background:
linear-gradient(var(--color-accent) 1px, transparent 1px),
linear-gradient(90deg, var(--color-accent) 1px, transparent 1px);
background-size: 50px 50px;
animation: grid-move 20s linear infinite;
}
@keyframes grid-move {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
.main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
}
.container {
max-width: 700px;
width: 100%;
background: var(--color-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--color-surface);
border-radius: 8px;
padding: var(--space-xl);
}
.header {
margin-bottom: var(--space-lg);
text-align: center;
}
.eyebrow {
color: var(--color-text-dim);
text-transform: uppercase;
letter-spacing: 0.24em;
font-size: 0.75rem;
margin-bottom: var(--space-sm);
}
.title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 2px;
}
.divider {
color: var(--color-surface);
text-align: center;
font-size: 0.75rem;
margin: var(--space-md) 0;
user-select: none;
}
.lead {
color: var(--color-text-dim);
line-height: 1.8;
max-width: 38rem;
margin: 0 auto;
}
.section {
margin: var(--space-md) 0;
}
.note-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.note-item {
display: flex;
gap: var(--space-md);
align-items: flex-start;
}
.note-time {
color: var(--color-text-dim);
font-size: 0.8rem;
white-space: nowrap;
padding-top: 0.15rem;
min-width: 126px;
}
.note-body {
flex: 1;
padding: 0.1rem 0 0.2rem var(--space-md);
border-left: 1px solid color-mix(in srgb, var(--color-surface) 78%, transparent);
}
.note-title {
color: var(--color-accent-bright);
font-size: 1rem;
margin-bottom: var(--space-xs);
}
.note-content :global(p) {
color: var(--color-text-dim);
line-height: 1.8;
margin-bottom: var(--space-sm);
}
.note-content :global(p:last-child) {
margin-bottom: 0;
}
.note-content :global(a) {
color: var(--color-blue);
}
.note-content :global(a:hover) {
color: var(--color-accent-bright);
}
.note-content :global(strong) {
color: var(--color-text);
}
.empty {
color: var(--color-text-dim);
font-style: italic;
}
.footer {
margin-top: var(--space-xl);
text-align: center;
}
.footer p {
color: var(--color-text-dim);
font-size: 0.85rem;
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
opacity: 0;
}
.fade-in:nth-child(1) {
animation-delay: 0.1s;
}
.fade-in:nth-child(2) {
animation-delay: 0.2s;
}
.fade-in:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 600px) {
.container {
padding: var(--space-md);
}
.title {
font-size: 1.5rem;
}
.divider {
font-size: 0.6rem;
}
.lead {
font-size: 0.95rem;
}
.note-item {
flex-direction: column;
gap: var(--space-xs);
}
.note-time {
min-width: unset;
}
.note-body {
width: 100%;
padding-left: var(--space-sm);
}
}
@media (prefers-reduced-motion: reduce) {
.matrix-bg {
animation: none;
}
.fade-in {
animation: none;
opacity: 1;
}
}
</style>