Add blog post and support pubDate/tags #46
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
title: "After the Silence"
|
||||||
|
description: "Reflections on what remains after a meaningful relationship ends, and how love can transform without disappearing."
|
||||||
|
pubDate: 2026-03-04
|
||||||
|
tags: ["love", "reflection", "healing", "relationships", "personal"]
|
||||||
|
---
|
||||||
|
|
||||||
|
When a relationship ends, the world does not become quiet immediately.
|
||||||
|
|
||||||
|
At first there is noise.
|
||||||
|
|
||||||
|
Questions that circle endlessly.
|
||||||
|
Memories that appear without warning.
|
||||||
|
Moments where the absence of someone feels louder than their presence ever was.
|
||||||
|
|
||||||
|
But eventually something changes.
|
||||||
|
|
||||||
|
The storm that once lived inside your mind slowly settles.
|
||||||
|
|
||||||
|
And what remains is something different.
|
||||||
|
|
||||||
|
A silence that is not empty - but reflective.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Quiet After Love
|
||||||
|
|
||||||
|
The silence that follows a meaningful relationship is unlike any other.
|
||||||
|
|
||||||
|
It is not the silence of indifference.
|
||||||
|
Nor the silence of forgetting.
|
||||||
|
|
||||||
|
It is the quiet that appears when two lives that once moved together begin to move separately.
|
||||||
|
|
||||||
|
Routines shift.
|
||||||
|
Spaces feel different.
|
||||||
|
The future you once imagined slowly dissolves into something undefined.
|
||||||
|
|
||||||
|
At first, that silence can feel unsettling.
|
||||||
|
|
||||||
|
But over time it becomes something else.
|
||||||
|
|
||||||
|
A place where reflection becomes possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Love Does Not Always Disappear
|
||||||
|
|
||||||
|
One of the more confusing realizations after a relationship ends is that love does not always vanish with it.
|
||||||
|
|
||||||
|
Even when distance becomes necessary, care can remain.
|
||||||
|
|
||||||
|
This can feel contradictory at first.
|
||||||
|
|
||||||
|
We often expect emotional closure to mean the disappearance of feeling.
|
||||||
|
|
||||||
|
But meaningful connections rarely dissolve that neatly.
|
||||||
|
|
||||||
|
Sometimes love simply changes form.
|
||||||
|
|
||||||
|
It moves from something shared into something carried quietly.
|
||||||
|
|
||||||
|
And that does not make it less real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Quiet Transformation
|
||||||
|
|
||||||
|
Time has a way of reshaping emotional intensity.
|
||||||
|
|
||||||
|
What once felt overwhelming slowly becomes more understandable.
|
||||||
|
|
||||||
|
Moments that once caused pain begin to look different when viewed from a distance.
|
||||||
|
|
||||||
|
Not because the past changes.
|
||||||
|
|
||||||
|
But because you do.
|
||||||
|
|
||||||
|
Perspective grows.
|
||||||
|
|
||||||
|
The urgency fades.
|
||||||
|
|
||||||
|
And what once felt like chaos becomes something you can hold with calm understanding.
|
||||||
|
|
||||||
|
This transformation rarely happens suddenly.
|
||||||
|
|
||||||
|
It arrives slowly, through reflection, distance, and the quiet work of healing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Carrying Without Clinging
|
||||||
|
|
||||||
|
There is a difference between remembering someone and remaining attached to them.
|
||||||
|
|
||||||
|
Clinging keeps you in the past.
|
||||||
|
|
||||||
|
Carrying allows you to move forward.
|
||||||
|
|
||||||
|
When you carry something, you acknowledge its importance without letting it control your direction.
|
||||||
|
|
||||||
|
You recognize the place it had in your life.
|
||||||
|
|
||||||
|
You respect the impact it left.
|
||||||
|
|
||||||
|
But you also accept that some chapters belong where they ended.
|
||||||
|
|
||||||
|
In that sense, remembering can become an act of peace rather than longing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Remains
|
||||||
|
|
||||||
|
When the emotional storm has passed, something meaningful remains.
|
||||||
|
|
||||||
|
Not the loss.
|
||||||
|
|
||||||
|
Not the confusion.
|
||||||
|
|
||||||
|
But the understanding.
|
||||||
|
|
||||||
|
You understand more about the kind of connection that matters to you.
|
||||||
|
|
||||||
|
You understand more about your own capacity to care deeply.
|
||||||
|
|
||||||
|
And perhaps most importantly, you understand that love itself was never the mistake.
|
||||||
|
|
||||||
|
Even when relationships end, the love that existed still shapes who we become.
|
||||||
|
|
||||||
|
It leaves behind lessons, perspective, and a deeper awareness of what it means to connect with another human being.
|
||||||
|
|
||||||
|
And sometimes, what remains after the silence is not emptiness.
|
||||||
|
|
||||||
|
But growth.
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Welcome to the Den"
|
title: "Welcome to the Den"
|
||||||
date: 2026-03-01
|
|
||||||
description: "First proper post. Why I built this site, what it runs on, and what to expect."
|
description: "First proper post. Why I built this site, what it runs on, and what to expect."
|
||||||
|
pubDate: 2026-03-01
|
||||||
|
tags: ["self-hosting", "privacy", "personal-web", "infrastructure"]
|
||||||
draft: false
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ So I finally got around to setting up a proper blog. Welcome.
|
|||||||
|
|
||||||
I wanted a place to write that wasn't owned by a corporation. No Medium, no Substack, no algorithm deciding who sees what. Just markdown files on my own server, served by nginx from a Docker container I control.
|
I wanted a place to write that wasn't owned by a corporation. No Medium, no Substack, no algorithm deciding who sees what. Just markdown files on my own server, served by nginx from a Docker container I control.
|
||||||
|
|
||||||
That's the whole point of the den — owning your own space on the internet.
|
That's the whole point of the den - owning your own space on the internet.
|
||||||
|
|
||||||
## What It Runs On
|
## What It Runs On
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Love Without Access"
|
title: "Love Without Access"
|
||||||
date: 2026-03-01
|
description: "A reflection on a first love - what it meant, what it cost, and why distance was the most loving thing left."
|
||||||
description: "A reflection on a first love — what it meant, what it cost, and why distance was the most loving thing left."
|
pubDate: 2026-03-01
|
||||||
|
tags: ["love", "reflection", "healing", "relationships", "personal"]
|
||||||
---
|
---
|
||||||
|
|
||||||
*by LATTE*
|
*by LATTE*
|
||||||
@@ -16,7 +17,7 @@ Power and surrender that were, underneath it all, just different shapes of trust
|
|||||||
This isn't a story about blame.
|
This isn't a story about blame.
|
||||||
And it's not a story about anger.
|
And it's not a story about anger.
|
||||||
|
|
||||||
It's a story about something that stayed real…
|
It's a story about something that stayed real...
|
||||||
even after it stopped being reachable.
|
even after it stopped being reachable.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -25,7 +26,7 @@ even after it stopped being reachable.
|
|||||||
|
|
||||||
We were friends first.
|
We were friends first.
|
||||||
|
|
||||||
Maybe that's what made it so deep. It didn't explode into existence — it grew. Slowly. Safely.
|
Maybe that's what made it so deep. It didn't explode into existence - it grew. Slowly. Safely.
|
||||||
From gaming together to talking for hours.
|
From gaming together to talking for hours.
|
||||||
From talking to tension.
|
From talking to tension.
|
||||||
From tension to touch.
|
From tension to touch.
|
||||||
@@ -75,7 +76,7 @@ Silence.
|
|||||||
|
|
||||||
"I don't feel it anymore."
|
"I don't feel it anymore."
|
||||||
|
|
||||||
And something in me went very quiet — and very loud — at the same time.
|
And something in me went very quiet - and very loud - at the same time.
|
||||||
|
|
||||||
Because I could still feel myself holding on.
|
Because I could still feel myself holding on.
|
||||||
And carrying someone who no longer carries back
|
And carrying someone who no longer carries back
|
||||||
@@ -98,7 +99,7 @@ But no longer together.
|
|||||||
A body that says yes.
|
A body that says yes.
|
||||||
Words that say no.
|
Words that say no.
|
||||||
|
|
||||||
That contradiction doesn't just hurt emotionally — it destabilizes you.
|
That contradiction doesn't just hurt emotionally - it destabilizes you.
|
||||||
Hope becomes a reflex.
|
Hope becomes a reflex.
|
||||||
And every time hope collapses, you fracture a little with it.
|
And every time hope collapses, you fracture a little with it.
|
||||||
|
|
||||||
@@ -199,7 +200,7 @@ That's architecture.
|
|||||||
|
|
||||||
## On Being Replaced
|
## On Being Replaced
|
||||||
|
|
||||||
Yes — he found someone new quickly.
|
Yes - he found someone new quickly.
|
||||||
|
|
||||||
That hurt.
|
That hurt.
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ Something ending does not mean it was nothing.
|
|||||||
|
|
||||||
## For You
|
## For You
|
||||||
|
|
||||||
If you ever read this —
|
If you ever read this -
|
||||||
|
|
||||||
I want you to know that what we had was real to me.
|
I want you to know that what we had was real to me.
|
||||||
Not experimental. Not temporary. Not a placeholder.
|
Not experimental. Not temporary. Not a placeholder.
|
||||||
@@ -243,7 +244,7 @@ Trying. Failing. Adjusting.
|
|||||||
Discovering what intimacy meant.
|
Discovering what intimacy meant.
|
||||||
Discovering what we meant.
|
Discovering what we meant.
|
||||||
|
|
||||||
You weren't just someone who entered my life —
|
You weren't just someone who entered my life -
|
||||||
you were part of my becoming.
|
you were part of my becoming.
|
||||||
|
|
||||||
That matters.
|
That matters.
|
||||||
@@ -262,7 +263,7 @@ I was trying to hold something I didn't yet know how to let go of.
|
|||||||
|
|
||||||
But even then, I wasn't against you.
|
But even then, I wasn't against you.
|
||||||
|
|
||||||
I saw you as someone struggling — not someone malicious.
|
I saw you as someone struggling - not someone malicious.
|
||||||
|
|
||||||
There were moments when you softened completely with me.
|
There were moments when you softened completely with me.
|
||||||
Moments where you rested your full weight without guarding yourself.
|
Moments where you rested your full weight without guarding yourself.
|
||||||
@@ -295,7 +296,7 @@ It changes shape.
|
|||||||
It becomes quieter.
|
It becomes quieter.
|
||||||
It becomes something I carry instead of something I reach for.
|
It becomes something I carry instead of something I reach for.
|
||||||
|
|
||||||
And yes — I'm going to be a little playful about it:
|
And yes - I'm going to be a little playful about it:
|
||||||
|
|
||||||
You're on my website. Do you get that?
|
You're on my website. Do you get that?
|
||||||
|
|
||||||
@@ -333,4 +334,4 @@ to be worthy of being held.
|
|||||||
The wolf in me was never meant to become smaller.
|
The wolf in me was never meant to become smaller.
|
||||||
Only to find the right pack.
|
Only to find the right pack.
|
||||||
|
|
||||||
— LATTE
|
- LATTE
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from "astro:content";
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
type: 'content',
|
type: "content",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
date: z.coerce.date(),
|
description: z.string(),
|
||||||
description: z.string(),
|
pubDate: z.coerce.date(),
|
||||||
draft: z.boolean().optional().default(false),
|
tags: z.array(z.string()).default([]),
|
||||||
}),
|
draft: z.boolean().optional().default(false),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { blog };
|
export const collections = { blog };
|
||||||
|
|||||||
+202
-12
@@ -1,16 +1,43 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection, type CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
type BlogPost = CollectionEntry<"blog">;
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||||
|
|
||||||
return posts.map((post) => ({
|
return posts.map((post) => ({
|
||||||
params: { slug: post.slug },
|
params: { slug: post.slug },
|
||||||
props: { post },
|
props: {
|
||||||
|
post,
|
||||||
|
relatedPosts: posts
|
||||||
|
.filter((candidate) => candidate.slug !== post.slug)
|
||||||
|
.map((candidate) => ({
|
||||||
|
post: candidate,
|
||||||
|
score: candidate.data.tags.filter((tag) =>
|
||||||
|
post.data.tags.includes(tag),
|
||||||
|
).length,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (b.score !== a.score) return b.score - a.score;
|
||||||
|
return (
|
||||||
|
b.post.data.pubDate.valueOf() -
|
||||||
|
a.post.data.pubDate.valueOf()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter((candidate) => candidate.score > 0)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((candidate) => candidate.post),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post } = Astro.props;
|
const { post, relatedPosts = [] } = Astro.props as {
|
||||||
|
post: BlogPost;
|
||||||
|
relatedPosts: BlogPost[];
|
||||||
|
};
|
||||||
|
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
|
|
||||||
function formatDate(date: Date) {
|
function formatDate(date: Date) {
|
||||||
@@ -19,7 +46,7 @@ function formatDate(date: Date) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={`${post.data.title} — Hidden Den Cafe`}
|
title={`${post.data.title} - Hidden Den Cafe`}
|
||||||
description={post.data.description}
|
description={post.data.description}
|
||||||
>
|
>
|
||||||
<div class="matrix-bg" aria-hidden="true"></div>
|
<div class="matrix-bg" aria-hidden="true"></div>
|
||||||
@@ -28,14 +55,60 @@ function formatDate(date: Date) {
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header fade-in">
|
<header class="header fade-in">
|
||||||
<h1 class="title">{post.data.title}</h1>
|
<h1 class="title">{post.data.title}</h1>
|
||||||
<p class="date">{formatDate(post.data.date)}</p>
|
<p class="date">{formatDate(post.data.pubDate)}</p>
|
||||||
<div class="divider">══════════════════════════════</div>
|
{
|
||||||
|
post.data.tags.length > 0 && (
|
||||||
|
<div
|
||||||
|
class="tag-list"
|
||||||
|
aria-label={`${post.data.title} tags`}
|
||||||
|
>
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<span class="tag">{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="divider">==============================</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<article class="content fade-in">
|
<article class="content fade-in">
|
||||||
<Content />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{
|
||||||
|
relatedPosts.length > 0 && (
|
||||||
|
<section
|
||||||
|
class="related fade-in"
|
||||||
|
aria-labelledby="related-posts"
|
||||||
|
>
|
||||||
|
<div class="related-head">
|
||||||
|
<h2 id="related-posts">Related Reading</h2>
|
||||||
|
<a href="/blog" class="related-link">
|
||||||
|
Back to blog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul class="related-list">
|
||||||
|
{relatedPosts.map((relatedPost) => (
|
||||||
|
<li class="related-item">
|
||||||
|
<a
|
||||||
|
href={`/blog/${relatedPost.slug}`}
|
||||||
|
class="related-card"
|
||||||
|
>
|
||||||
|
<span class="related-date">
|
||||||
|
{formatDate(
|
||||||
|
relatedPost.data.pubDate,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<h3>{relatedPost.data.title}</h3>
|
||||||
|
<p>{relatedPost.data.description}</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<footer class="footer fade-in">
|
<footer class="footer fade-in">
|
||||||
<p>Made with love by Latte</p>
|
<p>Made with love by Latte</p>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -60,8 +133,12 @@ function formatDate(date: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes grid-move {
|
@keyframes grid-move {
|
||||||
0% { transform: translate(0, 0); }
|
0% {
|
||||||
100% { transform: translate(50px, 50px); }
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(50px, 50px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
@@ -107,7 +184,21 @@ function formatDate(date: Date) {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prose styles for markdown content */
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-accent);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
@@ -208,6 +299,82 @@ function formatDate(date: Date) {
|
|||||||
margin-top: var(--space-xs);
|
margin-top: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.related {
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
padding-top: var(--space-lg);
|
||||||
|
border-top: 1px solid var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-head h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-link {
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-link:hover {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-md);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||||
|
border: 1px solid
|
||||||
|
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0 0 1px
|
||||||
|
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-date {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card p {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: var(--space-xl);
|
margin-top: var(--space-xl);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -223,9 +390,18 @@ function formatDate(date: Date) {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in:nth-child(1) { animation-delay: 0.1s; }
|
.fade-in:nth-child(1) {
|
||||||
.fade-in:nth-child(2) { animation-delay: 0.2s; }
|
animation-delay: 0.1s;
|
||||||
.fade-in:nth-child(3) { animation-delay: 0.3s; }
|
}
|
||||||
|
.fade-in:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.fade-in:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
.fade-in:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
@@ -250,6 +426,11 @@ function formatDate(date: Date) {
|
|||||||
.divider {
|
.divider {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.related-head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@@ -261,5 +442,14 @@ function formatDate(date: Date) {
|
|||||||
animation: none;
|
animation: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+76
-26
@@ -2,8 +2,9 @@
|
|||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
const posts = (await getCollection("blog", ({ data }) => !data.draft))
|
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
|
);
|
||||||
|
|
||||||
function formatDate(date: Date) {
|
function formatDate(date: Date) {
|
||||||
return date.toISOString().split("T")[0];
|
return date.toISOString().split("T")[0];
|
||||||
@@ -23,25 +24,50 @@ function formatDate(date: Date) {
|
|||||||
<div class="divider">══════════════════════════════</div>
|
<div class="divider">══════════════════════════════</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{posts.length === 0 ? (
|
{
|
||||||
<section class="section fade-in">
|
posts.length === 0 ? (
|
||||||
<p class="empty">No posts yet. Will get around to it.</p>
|
<section class="section fade-in">
|
||||||
</section>
|
<p class="empty">
|
||||||
) : (
|
No posts yet. Will get around to it.
|
||||||
<section class="section fade-in">
|
</p>
|
||||||
<ul class="post-list">
|
</section>
|
||||||
{posts.map((post) => (
|
) : (
|
||||||
<li class="post-item">
|
<section class="section fade-in">
|
||||||
<span class="post-date">{formatDate(post.data.date)}</span>
|
<ul class="post-list">
|
||||||
<div class="post-info">
|
{posts.map((post) => (
|
||||||
<a href={`/blog/${post.slug}`} class="post-title">{post.data.title}</a>
|
<li class="post-item">
|
||||||
<p class="post-desc">{post.data.description}</p>
|
<span class="post-date">
|
||||||
</div>
|
{formatDate(post.data.pubDate)}
|
||||||
</li>
|
</span>
|
||||||
))}
|
<div class="post-info">
|
||||||
</ul>
|
<a
|
||||||
</section>
|
href={`/blog/${post.slug}`}
|
||||||
)}
|
class="post-title"
|
||||||
|
>
|
||||||
|
{post.data.title}
|
||||||
|
</a>
|
||||||
|
<p class="post-desc">
|
||||||
|
{post.data.description}
|
||||||
|
</p>
|
||||||
|
{post.data.tags.length > 0 && (
|
||||||
|
<div
|
||||||
|
class="tag-list"
|
||||||
|
aria-label={`${post.data.title} tags`}
|
||||||
|
>
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<span class="tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<footer class="footer fade-in">
|
<footer class="footer fade-in">
|
||||||
<p>Made with love by Latte</p>
|
<p>Made with love by Latte</p>
|
||||||
@@ -67,8 +93,12 @@ function formatDate(date: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes grid-move {
|
@keyframes grid-move {
|
||||||
0% { transform: translate(0, 0); }
|
0% {
|
||||||
100% { transform: translate(50px, 50px); }
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(50px, 50px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
@@ -156,6 +186,20 @@ function formatDate(date: Date) {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-accent);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: var(--color-text-dim);
|
color: var(--color-text-dim);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -176,9 +220,15 @@ function formatDate(date: Date) {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in:nth-child(1) { animation-delay: 0.1s; }
|
.fade-in:nth-child(1) {
|
||||||
.fade-in:nth-child(2) { animation-delay: 0.2s; }
|
animation-delay: 0.1s;
|
||||||
.fade-in:nth-child(3) { animation-delay: 0.3s; }
|
}
|
||||||
|
.fade-in:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.fade-in:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
|
|||||||
+142
-88
@@ -16,7 +16,7 @@ type Project = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const recommendedPosts = posts.slice(0, 3);
|
const recommendedPosts = posts.slice(0, 3);
|
||||||
@@ -28,12 +28,17 @@ const highlightedProjects = highlightedProjectNames
|
|||||||
.map((name) => typedProjects.find((project) => project.name === name))
|
.map((name) => typedProjects.find((project) => project.name === name))
|
||||||
.filter((project): project is Project => Boolean(project));
|
.filter((project): project is Project => Boolean(project));
|
||||||
|
|
||||||
function formatDate(date: CollectionEntry<"blog">["data"]["date"]) {
|
function formatDate(date: CollectionEntry<"blog">["data"]["pubDate"]) {
|
||||||
return date.toISOString().split("T")[0];
|
return date.toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectLink(project: Project) {
|
function getProjectLink(project: Project) {
|
||||||
return project.links.site || project.links.gitea || project.links.github || "/projects";
|
return (
|
||||||
|
project.links.site ||
|
||||||
|
project.links.gitea ||
|
||||||
|
project.links.github ||
|
||||||
|
"/projects"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectLinkLabel(project: Project) {
|
function getProjectLinkLabel(project: Project) {
|
||||||
@@ -57,75 +62,95 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
<h1 class="title">Start Here</h1>
|
<h1 class="title">Start Here</h1>
|
||||||
<div class="divider">==============================</div>
|
<div class="divider">==============================</div>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
This page is a small orientation point for first-time visitors. If
|
This page is a small orientation point for first-time
|
||||||
you have just found Hidden Den, this is the quickest way to get a
|
visitors. If you have just found Hidden Den, this is the
|
||||||
feel for what kind of place it is and where you might want to wander
|
quickest way to get a feel for what kind of place it is and
|
||||||
next.
|
where you might want to wander next.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="section fade-in" aria-labelledby="what-this-place-is">
|
<section
|
||||||
|
class="section fade-in"
|
||||||
|
aria-labelledby="what-this-place-is"
|
||||||
|
>
|
||||||
<h2 id="what-this-place-is">What Hidden Den Is</h2>
|
<h2 id="what-this-place-is">What Hidden Den Is</h2>
|
||||||
<p class="desc">
|
<p class="desc">
|
||||||
Hidden Den is Latte's personal corner of the internet: part
|
Hidden Den is Latte's personal corner of the internet:
|
||||||
writing space, part workshop, part quiet place to think out loud. It
|
part writing space, part workshop, part quiet place to think
|
||||||
holds projects, experiments, infrastructure-minded notes, and the
|
out loud. It holds projects, experiments,
|
||||||
kind of personal web presence that does not need to behave like a
|
infrastructure-minded notes, and the kind of personal web
|
||||||
brand.
|
presence that does not need to behave like a brand.
|
||||||
</p>
|
</p>
|
||||||
<p class="desc">
|
<p class="desc">
|
||||||
The site leans toward privacy, ownership, and human-scale spaces
|
The site leans toward privacy, ownership, and human-scale
|
||||||
online. It is built to feel warm and readable rather than optimized,
|
spaces online. It is built to feel warm and readable rather
|
||||||
noisy, or extractive. More cozy tech wizard than cyberpunk hacker.
|
than optimized, noisy, or extractive. More cozy tech wizard
|
||||||
|
than cyberpunk hacker.
|
||||||
</p>
|
</p>
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<p>
|
<p>
|
||||||
If you want the short version: this is a personal site for
|
If you want the short version: this is a personal site
|
||||||
writing, building, self-hosting, and keeping a small piece of the
|
for writing, building, self-hosting, and keeping a small
|
||||||
web genuinely personal.
|
piece of the web genuinely personal.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section fade-in" aria-labelledby="recommended-reading">
|
<section
|
||||||
|
class="section fade-in"
|
||||||
|
aria-labelledby="recommended-reading"
|
||||||
|
>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2 id="recommended-reading">Recommended Reading</h2>
|
<h2 id="recommended-reading">Recommended Reading</h2>
|
||||||
<a class="section-link" href="/blog">See all posts</a>
|
<a class="section-link" href="/blog">See all posts</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="desc section-intro">
|
<p class="desc section-intro">
|
||||||
A few good starting points from the blog. These give a feel for the
|
A few good starting points from the blog. These give a feel
|
||||||
den so far without asking you to dig through everything first.
|
for the den so far without asking you to dig through
|
||||||
|
everything first.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{recommendedPosts.length === 0 ? (
|
{
|
||||||
<p class="empty">The den is still quiet on the writing front.</p>
|
recommendedPosts.length === 0 ? (
|
||||||
) : (
|
<p class="empty">
|
||||||
<ul class="reading-list">
|
The den is still quiet on the writing front.
|
||||||
{recommendedPosts.map((post) => (
|
</p>
|
||||||
<li class="reading-item">
|
) : (
|
||||||
<article class="content-card">
|
<ul class="reading-list">
|
||||||
<div class="meta-row">
|
{recommendedPosts.map((post) => (
|
||||||
<span class="meta-label">posted</span>
|
<li class="reading-item">
|
||||||
<time datetime={formatDate(post.data.date)}>
|
<article class="content-card">
|
||||||
{formatDate(post.data.date)}
|
<div class="meta-row">
|
||||||
</time>
|
<span class="meta-label">
|
||||||
</div>
|
posted
|
||||||
<h3>
|
</span>
|
||||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
<time
|
||||||
</h3>
|
datetime={formatDate(
|
||||||
<p>{post.data.description}</p>
|
post.data.pubDate,
|
||||||
</article>
|
)}
|
||||||
</li>
|
>
|
||||||
))}
|
{formatDate(post.data.pubDate)}
|
||||||
</ul>
|
</time>
|
||||||
)}
|
</div>
|
||||||
|
<h3>
|
||||||
|
<a href={`/blog/${post.slug}`}>
|
||||||
|
{post.data.title}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p>{post.data.description}</p>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section fade-in" aria-labelledby="explore-the-site">
|
<section class="section fade-in" aria-labelledby="explore-the-site">
|
||||||
<h2 id="explore-the-site">Explore The Site</h2>
|
<h2 id="explore-the-site">Explore The Site</h2>
|
||||||
<p class="desc section-intro">
|
<p class="desc section-intro">
|
||||||
If you would rather browse by section, these are the best places to
|
If you would rather browse by section, these are the best
|
||||||
continue.
|
places to continue.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="explore-grid">
|
<div class="explore-grid">
|
||||||
@@ -133,9 +158,9 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
<span class="card-kicker">About</span>
|
<span class="card-kicker">About</span>
|
||||||
<h3>Meet the person behind the den</h3>
|
<h3>Meet the person behind the den</h3>
|
||||||
<p>
|
<p>
|
||||||
A fuller introduction to Latte, the philosophy behind the
|
A fuller introduction to Latte, the philosophy
|
||||||
site, and the kind of internet this place is trying to make
|
behind the site, and the kind of internet this place
|
||||||
room for.
|
is trying to make room for.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -143,8 +168,9 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
<span class="card-kicker">Blog</span>
|
<span class="card-kicker">Blog</span>
|
||||||
<h3>Read the writing</h3>
|
<h3>Read the writing</h3>
|
||||||
<p>
|
<p>
|
||||||
Thoughts, personal pieces, and technical reflections gathered
|
Thoughts, personal pieces, and technical reflections
|
||||||
in one place without feeds, tracking, or platform noise.
|
gathered in one place without feeds, tracking, or
|
||||||
|
platform noise.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -152,52 +178,76 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
<span class="card-kicker">Projects</span>
|
<span class="card-kicker">Projects</span>
|
||||||
<h3>See what is being built</h3>
|
<h3>See what is being built</h3>
|
||||||
<p>
|
<p>
|
||||||
Bots, tools, experiments, and den-adjacent systems that show
|
Bots, tools, experiments, and den-adjacent systems
|
||||||
the practical side of the site.
|
that show the practical side of the site.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section fade-in" aria-labelledby="project-highlights">
|
<section
|
||||||
|
class="section fade-in"
|
||||||
|
aria-labelledby="project-highlights"
|
||||||
|
>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2 id="project-highlights">Projects Worth Seeing</h2>
|
<h2 id="project-highlights">Projects Worth Seeing</h2>
|
||||||
<a class="section-link" href="/projects">Browse all projects</a>
|
<a class="section-link" href="/projects"
|
||||||
|
>Browse all projects</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="desc section-intro">
|
<p class="desc section-intro">
|
||||||
A small curated subset, just enough to sketch the shape of the work
|
A small curated subset, just enough to sketch the shape of
|
||||||
without turning this page into a full catalog.
|
the work without turning this page into a full catalog.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul class="project-list">
|
<ul class="project-list">
|
||||||
{highlightedProjects.map((project) => (
|
{
|
||||||
<li class="project-item">
|
highlightedProjects.map((project) => (
|
||||||
<article class="content-card project-card">
|
<li class="project-item">
|
||||||
<div class="project-topline">
|
<article class="content-card project-card">
|
||||||
<h3>{project.name}</h3>
|
<div class="project-topline">
|
||||||
<span class={`status status-${project.status}`}>
|
<h3>{project.name}</h3>
|
||||||
{project.status}
|
<span
|
||||||
</span>
|
class={`status status-${project.status}`}
|
||||||
</div>
|
>
|
||||||
<p>{project.description}</p>
|
{project.status}
|
||||||
<div class="project-footer">
|
</span>
|
||||||
<div class="tag-list" aria-label={`${project.name} tags`}>
|
|
||||||
{project.tags.map((tag) => (
|
|
||||||
<span class="tag">{tag}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<p>{project.description}</p>
|
||||||
class="project-link"
|
<div class="project-footer">
|
||||||
href={getProjectLink(project)}
|
<div
|
||||||
target={getProjectLink(project).startsWith("http") ? "_blank" : undefined}
|
class="tag-list"
|
||||||
rel={getProjectLink(project).startsWith("http") ? "noopener noreferrer" : undefined}
|
aria-label={`${project.name} tags`}
|
||||||
>
|
>
|
||||||
{getProjectLinkLabel(project)}
|
{project.tags.map((tag) => (
|
||||||
</a>
|
<span class="tag">{tag}</span>
|
||||||
</div>
|
))}
|
||||||
</article>
|
</div>
|
||||||
</li>
|
<a
|
||||||
))}
|
class="project-link"
|
||||||
|
href={getProjectLink(project)}
|
||||||
|
target={
|
||||||
|
getProjectLink(
|
||||||
|
project,
|
||||||
|
).startsWith("http")
|
||||||
|
? "_blank"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rel={
|
||||||
|
getProjectLink(
|
||||||
|
project,
|
||||||
|
).startsWith("http")
|
||||||
|
? "noopener noreferrer"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getProjectLinkLabel(project)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -337,7 +387,8 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
.note {
|
.note {
|
||||||
margin-top: var(--space-md);
|
margin-top: var(--space-md);
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
|
border: 1px solid
|
||||||
|
color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
@@ -365,7 +416,8 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||||
border: 1px solid color-mix(in srgb, var(--color-surface) 70%, transparent);
|
border: 1px solid
|
||||||
|
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-card h3 {
|
.content-card h3 {
|
||||||
@@ -405,7 +457,8 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
display: block;
|
display: block;
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid color-mix(in srgb, var(--color-surface) 70%, transparent);
|
border: 1px solid
|
||||||
|
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||||
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s ease,
|
border-color 0.2s ease,
|
||||||
@@ -416,7 +469,8 @@ function getProjectLinkLabel(project: Project) {
|
|||||||
.explore-card:hover {
|
.explore-card:hover {
|
||||||
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
|
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 35%, transparent);
|
box-shadow: 0 0 0 1px
|
||||||
|
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-kicker {
|
.card-kicker {
|
||||||
|
|||||||
Reference in New Issue
Block a user