dev #60
@@ -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"
|
||||
date: 2026-03-01
|
||||
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
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
---
|
||||
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*
|
||||
@@ -16,7 +17,7 @@ Power and surrender that were, underneath it all, just different shapes of trust
|
||||
This isn't a story about blame.
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -25,7 +26,7 @@ even after it stopped being reachable.
|
||||
|
||||
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 talking to tension.
|
||||
From tension to touch.
|
||||
@@ -75,7 +76,7 @@ Silence.
|
||||
|
||||
"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.
|
||||
And carrying someone who no longer carries back
|
||||
@@ -98,7 +99,7 @@ But no longer together.
|
||||
A body that says yes.
|
||||
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.
|
||||
And every time hope collapses, you fracture a little with it.
|
||||
|
||||
@@ -199,7 +200,7 @@ That's architecture.
|
||||
|
||||
## On Being Replaced
|
||||
|
||||
Yes — he found someone new quickly.
|
||||
Yes - he found someone new quickly.
|
||||
|
||||
That hurt.
|
||||
|
||||
@@ -222,7 +223,7 @@ Something ending does not mean it was nothing.
|
||||
|
||||
## 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.
|
||||
Not experimental. Not temporary. Not a placeholder.
|
||||
@@ -243,7 +244,7 @@ Trying. Failing. Adjusting.
|
||||
Discovering what intimacy 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
Moments where you rested your full weight without guarding yourself.
|
||||
@@ -295,7 +296,7 @@ It changes shape.
|
||||
It becomes quieter.
|
||||
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?
|
||||
|
||||
@@ -333,4 +334,4 @@ to be worthy of being held.
|
||||
The wolf in me was never meant to become smaller.
|
||||
Only to find the right pack.
|
||||
|
||||
— LATTE
|
||||
- LATTE
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { z, defineCollection } from 'astro:content';
|
||||
import { z, defineCollection } from "astro:content";
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
description: z.string(),
|
||||
pubDate: z.coerce.date(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
draft: z.boolean().optional().default(false),
|
||||
}),
|
||||
});
|
||||
|
||||
+202
-12
@@ -1,16 +1,43 @@
|
||||
---
|
||||
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() {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
return posts.map((post) => ({
|
||||
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();
|
||||
|
||||
function formatDate(date: Date) {
|
||||
@@ -19,7 +46,7 @@ function formatDate(date: Date) {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${post.data.title} — Hidden Den Cafe`}
|
||||
title={`${post.data.title} - Hidden Den Cafe`}
|
||||
description={post.data.description}
|
||||
>
|
||||
<div class="matrix-bg" aria-hidden="true"></div>
|
||||
@@ -28,14 +55,60 @@ function formatDate(date: Date) {
|
||||
<div class="container">
|
||||
<header class="header fade-in">
|
||||
<h1 class="title">{post.data.title}</h1>
|
||||
<p class="date">{formatDate(post.data.date)}</p>
|
||||
<div class="divider">══════════════════════════════</div>
|
||||
<p class="date">{formatDate(post.data.pubDate)}</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 class="divider">==============================</div>
|
||||
</header>
|
||||
|
||||
<article class="content fade-in">
|
||||
<Content />
|
||||
</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">
|
||||
<p>Made with love by Latte</p>
|
||||
</footer>
|
||||
@@ -60,8 +133,12 @@ function formatDate(date: Date) {
|
||||
}
|
||||
|
||||
@keyframes grid-move {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -107,7 +184,21 @@ function formatDate(date: Date) {
|
||||
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 {
|
||||
color: var(--color-text);
|
||||
line-height: 1.8;
|
||||
@@ -208,6 +299,82 @@ function formatDate(date: Date) {
|
||||
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 {
|
||||
margin-top: var(--space-xl);
|
||||
text-align: center;
|
||||
@@ -223,9 +390,18 @@ function formatDate(date: Date) {
|
||||
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; }
|
||||
.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;
|
||||
}
|
||||
.fade-in:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
@@ -250,6 +426,11 @@ function formatDate(date: Date) {
|
||||
.divider {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.related-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -261,5 +442,14 @@ function formatDate(date: Date) {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+63
-13
@@ -2,8 +2,9 @@
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const posts = (await getCollection("blog", ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toISOString().split("T")[0];
|
||||
@@ -23,25 +24,50 @@ function formatDate(date: Date) {
|
||||
<div class="divider">══════════════════════════════</div>
|
||||
</header>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
{
|
||||
posts.length === 0 ? (
|
||||
<section class="section fade-in">
|
||||
<p class="empty">No posts yet. Will get around to it.</p>
|
||||
<p class="empty">
|
||||
No posts yet. Will get around to it.
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
<section class="section fade-in">
|
||||
<ul class="post-list">
|
||||
{posts.map((post) => (
|
||||
<li class="post-item">
|
||||
<span class="post-date">{formatDate(post.data.date)}</span>
|
||||
<span class="post-date">
|
||||
{formatDate(post.data.pubDate)}
|
||||
</span>
|
||||
<div class="post-info">
|
||||
<a href={`/blog/${post.slug}`} class="post-title">{post.data.title}</a>
|
||||
<p class="post-desc">{post.data.description}</p>
|
||||
<a
|
||||
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">
|
||||
<p>Made with love by Latte</p>
|
||||
@@ -67,8 +93,12 @@ function formatDate(date: Date) {
|
||||
}
|
||||
|
||||
@keyframes grid-move {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -156,6 +186,20 @@ function formatDate(date: Date) {
|
||||
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 {
|
||||
color: var(--color-text-dim);
|
||||
font-style: italic;
|
||||
@@ -176,9 +220,15 @@ function formatDate(date: Date) {
|
||||
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; }
|
||||
.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 {
|
||||
|
||||
+106
-52
@@ -16,7 +16,7 @@ type Project = {
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -28,12 +28,17 @@ const highlightedProjects = highlightedProjectNames
|
||||
.map((name) => typedProjects.find((project) => project.name === name))
|
||||
.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];
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -57,75 +62,95 @@ function getProjectLinkLabel(project: Project) {
|
||||
<h1 class="title">Start Here</h1>
|
||||
<div class="divider">==============================</div>
|
||||
<p class="lead">
|
||||
This page is a small orientation point for first-time visitors. If
|
||||
you have just found Hidden Den, this is the quickest way to get a
|
||||
feel for what kind of place it is and where you might want to wander
|
||||
next.
|
||||
This page is a small orientation point for first-time
|
||||
visitors. If you have just found Hidden Den, this is the
|
||||
quickest way to get a feel for what kind of place it is and
|
||||
where you might want to wander next.
|
||||
</p>
|
||||
</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>
|
||||
<p class="desc">
|
||||
Hidden Den is Latte's personal corner of the internet: part
|
||||
writing space, part workshop, part quiet place to think out loud. It
|
||||
holds projects, experiments, infrastructure-minded notes, and the
|
||||
kind of personal web presence that does not need to behave like a
|
||||
brand.
|
||||
Hidden Den is Latte's personal corner of the internet:
|
||||
part writing space, part workshop, part quiet place to think
|
||||
out loud. It holds projects, experiments,
|
||||
infrastructure-minded notes, and the kind of personal web
|
||||
presence that does not need to behave like a brand.
|
||||
</p>
|
||||
<p class="desc">
|
||||
The site leans toward privacy, ownership, and human-scale spaces
|
||||
online. It is built to feel warm and readable rather than optimized,
|
||||
noisy, or extractive. More cozy tech wizard than cyberpunk hacker.
|
||||
The site leans toward privacy, ownership, and human-scale
|
||||
spaces online. It is built to feel warm and readable rather
|
||||
than optimized, noisy, or extractive. More cozy tech wizard
|
||||
than cyberpunk hacker.
|
||||
</p>
|
||||
<div class="note">
|
||||
<p>
|
||||
If you want the short version: this is a personal site for
|
||||
writing, building, self-hosting, and keeping a small piece of the
|
||||
web genuinely personal.
|
||||
If you want the short version: this is a personal site
|
||||
for writing, building, self-hosting, and keeping a small
|
||||
piece of the web genuinely personal.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="recommended-reading">
|
||||
<section
|
||||
class="section fade-in"
|
||||
aria-labelledby="recommended-reading"
|
||||
>
|
||||
<div class="section-head">
|
||||
<h2 id="recommended-reading">Recommended Reading</h2>
|
||||
<a class="section-link" href="/blog">See all posts</a>
|
||||
</div>
|
||||
<p class="desc section-intro">
|
||||
A few good starting points from the blog. These give a feel for the
|
||||
den so far without asking you to dig through everything first.
|
||||
A few good starting points from the blog. These give a feel
|
||||
for the den so far without asking you to dig through
|
||||
everything first.
|
||||
</p>
|
||||
|
||||
{recommendedPosts.length === 0 ? (
|
||||
<p class="empty">The den is still quiet on the writing front.</p>
|
||||
{
|
||||
recommendedPosts.length === 0 ? (
|
||||
<p class="empty">
|
||||
The den is still quiet on the writing front.
|
||||
</p>
|
||||
) : (
|
||||
<ul class="reading-list">
|
||||
{recommendedPosts.map((post) => (
|
||||
<li class="reading-item">
|
||||
<article class="content-card">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">posted</span>
|
||||
<time datetime={formatDate(post.data.date)}>
|
||||
{formatDate(post.data.date)}
|
||||
<span class="meta-label">
|
||||
posted
|
||||
</span>
|
||||
<time
|
||||
datetime={formatDate(
|
||||
post.data.pubDate,
|
||||
)}
|
||||
>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
{post.data.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p>{post.data.description}</p>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="explore-the-site">
|
||||
<h2 id="explore-the-site">Explore The Site</h2>
|
||||
<p class="desc section-intro">
|
||||
If you would rather browse by section, these are the best places to
|
||||
continue.
|
||||
If you would rather browse by section, these are the best
|
||||
places to continue.
|
||||
</p>
|
||||
|
||||
<div class="explore-grid">
|
||||
@@ -133,9 +158,9 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">About</span>
|
||||
<h3>Meet the person behind the den</h3>
|
||||
<p>
|
||||
A fuller introduction to Latte, the philosophy behind the
|
||||
site, and the kind of internet this place is trying to make
|
||||
room for.
|
||||
A fuller introduction to Latte, the philosophy
|
||||
behind the site, and the kind of internet this place
|
||||
is trying to make room for.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -143,8 +168,9 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">Blog</span>
|
||||
<h3>Read the writing</h3>
|
||||
<p>
|
||||
Thoughts, personal pieces, and technical reflections gathered
|
||||
in one place without feeds, tracking, or platform noise.
|
||||
Thoughts, personal pieces, and technical reflections
|
||||
gathered in one place without feeds, tracking, or
|
||||
platform noise.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -152,36 +178,47 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">Projects</span>
|
||||
<h3>See what is being built</h3>
|
||||
<p>
|
||||
Bots, tools, experiments, and den-adjacent systems that show
|
||||
the practical side of the site.
|
||||
Bots, tools, experiments, and den-adjacent systems
|
||||
that show the practical side of the site.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="project-highlights">
|
||||
<section
|
||||
class="section fade-in"
|
||||
aria-labelledby="project-highlights"
|
||||
>
|
||||
<div class="section-head">
|
||||
<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>
|
||||
<p class="desc section-intro">
|
||||
A small curated subset, just enough to sketch the shape of the work
|
||||
without turning this page into a full catalog.
|
||||
A small curated subset, just enough to sketch the shape of
|
||||
the work without turning this page into a full catalog.
|
||||
</p>
|
||||
|
||||
<ul class="project-list">
|
||||
{highlightedProjects.map((project) => (
|
||||
{
|
||||
highlightedProjects.map((project) => (
|
||||
<li class="project-item">
|
||||
<article class="content-card project-card">
|
||||
<div class="project-topline">
|
||||
<h3>{project.name}</h3>
|
||||
<span class={`status status-${project.status}`}>
|
||||
<span
|
||||
class={`status status-${project.status}`}
|
||||
>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
<p>{project.description}</p>
|
||||
<div class="project-footer">
|
||||
<div class="tag-list" aria-label={`${project.name} tags`}>
|
||||
<div
|
||||
class="tag-list"
|
||||
aria-label={`${project.name} tags`}
|
||||
>
|
||||
{project.tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
@@ -189,15 +226,28 @@ function getProjectLinkLabel(project: Project) {
|
||||
<a
|
||||
class="project-link"
|
||||
href={getProjectLink(project)}
|
||||
target={getProjectLink(project).startsWith("http") ? "_blank" : undefined}
|
||||
rel={getProjectLink(project).startsWith("http") ? "noopener noreferrer" : undefined}
|
||||
target={
|
||||
getProjectLink(
|
||||
project,
|
||||
).startsWith("http")
|
||||
? "_blank"
|
||||
: undefined
|
||||
}
|
||||
rel={
|
||||
getProjectLink(
|
||||
project,
|
||||
).startsWith("http")
|
||||
? "noopener noreferrer"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{getProjectLinkLabel(project)}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -337,7 +387,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
.note {
|
||||
margin-top: 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:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
@@ -365,7 +416,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
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);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
}
|
||||
|
||||
.content-card h3 {
|
||||
@@ -405,7 +457,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
display: block;
|
||||
padding: var(--space-md);
|
||||
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);
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
@@ -416,7 +469,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
.explore-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);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.card-kicker {
|
||||
|
||||
Reference in New Issue
Block a user