Merge pull request 'Add blog via Astro content collections' (#19) from feature/blog into dev
Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
+57
-25
@@ -16,15 +16,22 @@ Cozy Den is a landing page for hiddenden.cafe built with Astro. The site feature
|
|||||||
```
|
```
|
||||||
cozy-den/
|
cozy-den/
|
||||||
├── src/
|
├── src/
|
||||||
|
│ ├── content/
|
||||||
|
│ │ ├── config.ts # Content collection schema
|
||||||
|
│ │ └── blog/
|
||||||
|
│ │ └── *.md # Blog posts (Markdown)
|
||||||
│ ├── layouts/
|
│ ├── layouts/
|
||||||
│ │ └── BaseLayout.astro # Base HTML layout with global styles
|
│ │ └── BaseLayout.astro # Base HTML layout with global styles
|
||||||
│ ├── pages/
|
│ ├── pages/
|
||||||
│ │ ├── index.astro # Main landing page
|
│ │ ├── index.astro # Main landing page
|
||||||
│ │ └── 404.astro # Custom 404 error page
|
│ │ ├── 404.astro # Custom 404 error page
|
||||||
│ └── components/ # (Empty - ready for future components)
|
│ │ └── blog/
|
||||||
|
│ │ ├── index.astro # Blog listing page (/blog)
|
||||||
|
│ │ └── [...slug].astro # Individual post pages (/blog/<slug>)
|
||||||
├── public/
|
├── public/
|
||||||
│ ├── favicon.svg # Site favicon (coffee emoji)
|
│ ├── favicon.svg # Site favicon (coffee emoji)
|
||||||
│ └── robots.txt # Search engine directives
|
│ ├── robots.txt # Search engine directives
|
||||||
|
│ └── blog/ # Blog post images (per-post subdirectory)
|
||||||
├── astro.config.mjs # Astro configuration with sitemap
|
├── astro.config.mjs # Astro configuration with sitemap
|
||||||
├── package.json # Node dependencies
|
├── package.json # Node dependencies
|
||||||
├── tsconfig.json # TypeScript configuration
|
├── tsconfig.json # TypeScript configuration
|
||||||
@@ -40,13 +47,15 @@ cozy-den/
|
|||||||
The site uses CSS custom properties for theming. All colors are defined in `src/layouts/BaseLayout.astro`:
|
The site uses CSS custom properties for theming. All colors are defined in `src/layouts/BaseLayout.astro`:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
--color-bg: #1a1410 /* Dark background (deep coffee) */
|
--color-bg: #1e1e2e /* Dark background (Catppuccin Mocha base) */
|
||||||
--color-bg-light: #2a1f18 /* Lighter background for cards */
|
--color-text: #cdd6f4 /* Light text */
|
||||||
--color-text: #f4e9d8 /* Cream text */
|
--color-text-dim: #a6adc8 /* Dimmed text for less emphasis */
|
||||||
--color-text-dim: #c4b5a0 /* Dimmed text for less emphasis */
|
--color-accent: #cba6f7 /* Mauve accent */
|
||||||
--color-accent: #d4a574 /* Warm accent (coffee with cream) */
|
--color-accent-bright: #f5c2e7 /* Pink accent for highlights */
|
||||||
--color-accent-bright: #e8bf8e /* Brighter accent for highlights */
|
--color-surface: #313244 /* Surface color for borders/cards */
|
||||||
--color-warm: #8b6f47 /* Warm brown for borders/accents */
|
--color-blue: #89b4fa /* Blue for links */
|
||||||
|
--color-green: #a6e3a1 /* Green for code */
|
||||||
|
--color-peach: #fab387 /* Peach for labels */
|
||||||
```
|
```
|
||||||
|
|
||||||
## Component Architecture
|
## Component Architecture
|
||||||
@@ -62,12 +71,31 @@ The base layout provides:
|
|||||||
### index.astro
|
### index.astro
|
||||||
|
|
||||||
The main page includes these sections:
|
The main page includes these sections:
|
||||||
1. **Hero** - Title and subtitle
|
1. **Header** - Title
|
||||||
2. **About Hidden Den** - Information about the site/space
|
2. **About** - Introduction, age, status
|
||||||
3. **About Me** - Information about Latte
|
3. **Cryptographic Keys** - PGP fingerprint and SSH public key
|
||||||
4. **Services** - List of self-hosted services
|
4. **Games** - Games Latte plays
|
||||||
5. **Support** - Ways to help/contribute
|
5. **Socials** - Links to social platforms
|
||||||
6. **Footer** - Links and credits
|
6. **Services** - Self-hosted services
|
||||||
|
7. **Support** - Crypto donation addresses
|
||||||
|
8. **Blog** - Link to `/blog`
|
||||||
|
9. **Footer** - Credits
|
||||||
|
|
||||||
|
### blog/index.astro
|
||||||
|
|
||||||
|
Blog listing page at `/blog`. Fetches all non-draft posts from the `blog` content collection, sorted by date descending.
|
||||||
|
|
||||||
|
### blog/[...slug].astro
|
||||||
|
|
||||||
|
Individual blog post page. Renders Markdown content using Astro's `<Content />` component. Styles for prose content (headings, paragraphs, code blocks, images, etc.) are scoped within `.content :global(...)`.
|
||||||
|
|
||||||
|
### src/content/config.ts
|
||||||
|
|
||||||
|
Defines the `blog` content collection schema:
|
||||||
|
- `title` (string) — post title
|
||||||
|
- `date` (date) — publish date, used for sorting
|
||||||
|
- `description` (string) — shown on the listing page
|
||||||
|
- `draft` (boolean, optional) — set to `true` to hide from listing
|
||||||
|
|
||||||
### 404.astro
|
### 404.astro
|
||||||
|
|
||||||
@@ -120,9 +148,15 @@ npm run preview
|
|||||||
2. Follow the existing structure with h3 link and description
|
2. Follow the existing structure with h3 link and description
|
||||||
|
|
||||||
**Adding images:**
|
**Adding images:**
|
||||||
1. Place images in `public/` directory
|
1. Place images in `public/blog/<post-slug>/` directory
|
||||||
2. Reference them with `/image.jpg` in your code
|
2. Reference in Markdown with `/blog/<post-slug>/image.jpg`
|
||||||
3. They'll be served as static assets
|
3. They'll be served as static assets by nginx
|
||||||
|
|
||||||
|
**Adding a blog post:**
|
||||||
|
1. Create `src/content/blog/my-post-slug.md`
|
||||||
|
2. Add frontmatter: `title`, `date`, `description` (and optionally `draft: true`)
|
||||||
|
3. Write content in Markdown below the frontmatter
|
||||||
|
4. The post appears automatically at `/blog/my-post-slug`
|
||||||
|
|
||||||
## Docker Deployment
|
## Docker Deployment
|
||||||
|
|
||||||
@@ -273,14 +307,12 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
Ideas for future development:
|
Ideas for future development:
|
||||||
- [ ] Add blog section using Astro's content collections
|
- [x] Add blog section using Astro's content collections
|
||||||
- [ ] Create reusable components for service items
|
- [x] Create custom 404 page
|
||||||
- [ ] Add dark/light theme toggle
|
|
||||||
- [ ] Implement contact form
|
|
||||||
- [ ] Add RSS feed for blog
|
- [ ] Add RSS feed for blog
|
||||||
- [ ] Create custom 404 page
|
- [ ] Create reusable components for service items
|
||||||
- [ ] Add more animations and transitions
|
- [ ] Add more animations and transitions
|
||||||
- [ ] Integrate with analytics (privacy-friendly)
|
- [ ] Integrate with analytics (privacy-friendly, self-hosted)
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ Cozy Den is a warm, self-hosted corner of the internet. This landing page repres
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ☕ Cozy, warm aesthetic with hidden den theme
|
- ☕ Cozy, warm aesthetic with hidden den theme
|
||||||
- 🎨 Custom color palette inspired by coffee and warmth
|
- 🎨 Catppuccin Mocha color palette
|
||||||
- 📱 Responsive design
|
- 📱 Responsive design
|
||||||
- ⚡ Built with Astro for blazing fast performance
|
- ⚡ Built with Astro for blazing fast performance
|
||||||
- 🐳 Docker support for easy deployment
|
- 🐳 Docker support for easy deployment
|
||||||
|
- 📝 Blog powered by Astro Content Collections (hardcoded Markdown, no CMS)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ docker build -t cozy-den .
|
|||||||
### Running the Container
|
### Running the Container
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 3000:3000 --name cozy-den cozy-den
|
docker run -d -p 3000:80 --name cozy-den cozy-den
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with docker-compose:
|
Or with docker-compose:
|
||||||
@@ -84,6 +85,8 @@ The site is built to be easily customizable:
|
|||||||
- **Colors**: Edit the CSS variables in `src/layouts/BaseLayout.astro`
|
- **Colors**: Edit the CSS variables in `src/layouts/BaseLayout.astro`
|
||||||
- **Content**: Update sections in `src/pages/index.astro`
|
- **Content**: Update sections in `src/pages/index.astro`
|
||||||
- **Favicon**: Replace `public/favicon.svg`
|
- **Favicon**: Replace `public/favicon.svg`
|
||||||
|
- **Blog posts**: Add `.md` files to `src/content/blog/` — they appear automatically at `/blog`
|
||||||
|
- **Blog images**: Place images in `public/blog/<post-slug>/` and reference with `/blog/<post-slug>/image.jpg`
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
✅ Sitemap integration
|
✅ Sitemap integration
|
||||||
✅ robots.txt
|
✅ robots.txt
|
||||||
✅ Accessibility improvements (ARIA labels, semantic HTML)
|
✅ Accessibility improvements (ARIA labels, semantic HTML)
|
||||||
|
✅ Blog with Astro Content Collections (Markdown, no CMS)
|
||||||
|
|
||||||
## Immediate Next Steps
|
## Immediate Next Steps
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@
|
|||||||
- [ ] Consider adding subtle background patterns or textures
|
- [ ] Consider adding subtle background patterns or textures
|
||||||
|
|
||||||
#### Medium Term
|
#### Medium Term
|
||||||
- [ ] Create a blog section using Astro Content Collections
|
- [x] Create a blog section using Astro Content Collections
|
||||||
|
- [ ] Add RSS feed for blog
|
||||||
- [ ] Add a projects page that pulls from Gitea API
|
- [ ] Add a projects page that pulls from Gitea API
|
||||||
- [ ] Create reusable components for repeated elements
|
- [ ] Create reusable components for repeated elements
|
||||||
- [ ] Add a contact form (self-hosted solution)
|
- [ ] Add a contact form (self-hosted solution)
|
||||||
- [ ] Add RSS feed if blog is implemented
|
|
||||||
|
|
||||||
#### Long Term
|
#### Long Term
|
||||||
- [ ] Theme toggle (dark/light, or alternate color schemes)
|
- [ ] Theme toggle (dark/light, or alternate color schemes)
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
|
|
||||||
- Network is required for npm install (packages not included in repo)
|
- Network is required for npm install (packages not included in repo)
|
||||||
- Gitea registry requires authentication (document login process)
|
- Gitea registry requires authentication (document login process)
|
||||||
- No CMS - content updates require code changes (intentional for now)
|
- No CMS - blog posts are Markdown files committed to the repo (intentional — no attack surface)
|
||||||
|
|
||||||
## Deployment Checklist
|
## Deployment Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ server {
|
|||||||
server_name localhost;
|
server_name localhost;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
# Gzip compression
|
# Gzip compression
|
||||||
gzip on;
|
gzip on;
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
---
|
||||||
|
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."
|
||||||
|
---
|
||||||
|
|
||||||
|
*by LATTE*
|
||||||
|
|
||||||
|
There was a time when I thought love was mostly about intensity.
|
||||||
|
|
||||||
|
Waking up next to someone and feeling like the world aligned.
|
||||||
|
Skin against skin.
|
||||||
|
Breathing in sync.
|
||||||
|
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…
|
||||||
|
even after it stopped being reachable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Began
|
||||||
|
|
||||||
|
We were friends first.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
And then, somehow, we became a home.
|
||||||
|
|
||||||
|
Not the perfect kind.
|
||||||
|
Not the easy kind.
|
||||||
|
But the kind your body recognizes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What We Were
|
||||||
|
|
||||||
|
From the outside, we looked like a relationship.
|
||||||
|
|
||||||
|
On the inside, we were a system of trust.
|
||||||
|
|
||||||
|
I was the one who brought structure.
|
||||||
|
Who checked in.
|
||||||
|
Who asked, "Are you still okay?"
|
||||||
|
Who carried responsibility for safety.
|
||||||
|
|
||||||
|
What some people would call dominance
|
||||||
|
was, for me, care.
|
||||||
|
|
||||||
|
And for him, surrender wasn't weakness.
|
||||||
|
It was relief.
|
||||||
|
|
||||||
|
But what I only understood later was this:
|
||||||
|
|
||||||
|
Closeness regulated me.
|
||||||
|
Distance regulated him.
|
||||||
|
|
||||||
|
And that difference doesn't show up at the start.
|
||||||
|
You only feel it when life gets heavy and love gets real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Fracture
|
||||||
|
|
||||||
|
There wasn't an explosion. No dramatic collapse.
|
||||||
|
|
||||||
|
There was fatigue.
|
||||||
|
Doubt.
|
||||||
|
Silence.
|
||||||
|
|
||||||
|
"I don't feel it anymore."
|
||||||
|
|
||||||
|
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
|
||||||
|
exhausts you in a way no one sees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The After That Kept Me Stuck
|
||||||
|
|
||||||
|
What made it hardest wasn't just the ending.
|
||||||
|
|
||||||
|
It was the ambiguity after.
|
||||||
|
|
||||||
|
Still sleeping next to each other.
|
||||||
|
Still laughing.
|
||||||
|
Still touching.
|
||||||
|
|
||||||
|
But no longer together.
|
||||||
|
|
||||||
|
A body that says yes.
|
||||||
|
Words that say no.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
I tried to understand. I tried to fix.
|
||||||
|
I thought if I could articulate my love clearly enough, we could find safety again.
|
||||||
|
|
||||||
|
But love is not code you can debug
|
||||||
|
when one of you has already logged out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Part Where Safety Changed
|
||||||
|
|
||||||
|
And then there was the part people don't like to talk about:
|
||||||
|
|
||||||
|
When something private stops being safe.
|
||||||
|
|
||||||
|
When the inner room gets opened.
|
||||||
|
|
||||||
|
I'm not writing this to accuse you.
|
||||||
|
I'm writing this because it matters.
|
||||||
|
|
||||||
|
Because trust isn't just "did you mean well."
|
||||||
|
Trust is "did I feel protected."
|
||||||
|
|
||||||
|
And after a certain point, I didn't.
|
||||||
|
|
||||||
|
That's when I understood something important:
|
||||||
|
|
||||||
|
Even love needs a locked door
|
||||||
|
when the inside of you keeps getting exposed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What It Did to Me
|
||||||
|
|
||||||
|
There were days my body shook.
|
||||||
|
|
||||||
|
Days where seeing your name online
|
||||||
|
made my chest tighten.
|
||||||
|
|
||||||
|
Where I felt cold,
|
||||||
|
then numb,
|
||||||
|
then flooded.
|
||||||
|
|
||||||
|
I don't write that for drama.
|
||||||
|
I write it because endings are not abstract.
|
||||||
|
|
||||||
|
When attachment breaks,
|
||||||
|
the body reacts.
|
||||||
|
|
||||||
|
I wasn't weak.
|
||||||
|
I was grieving a nervous system bond.
|
||||||
|
|
||||||
|
And at some point, I understood something else:
|
||||||
|
|
||||||
|
Loving you was real.
|
||||||
|
But staying in reach of you
|
||||||
|
was slowly undoing me.
|
||||||
|
|
||||||
|
So I chose distance.
|
||||||
|
|
||||||
|
Not because I stopped loving you.
|
||||||
|
But because I had to protect myself.
|
||||||
|
|
||||||
|
No access wasn't punishment.
|
||||||
|
|
||||||
|
It was survival.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What I Understand Now
|
||||||
|
|
||||||
|
Some people are not cruel.
|
||||||
|
Not cold.
|
||||||
|
Not heartless.
|
||||||
|
|
||||||
|
They're overwhelmed.
|
||||||
|
|
||||||
|
And when overwhelm becomes someone's default response to emotional depth,
|
||||||
|
distance becomes their survival strategy.
|
||||||
|
|
||||||
|
For a long time, I wondered if I was "too much."
|
||||||
|
Too intense. Too deep. Too present.
|
||||||
|
|
||||||
|
Now I know:
|
||||||
|
|
||||||
|
I wasn't too much.
|
||||||
|
We were mismatched in emotional capacity.
|
||||||
|
|
||||||
|
I wanted co-regulation.
|
||||||
|
He needed self-regulation.
|
||||||
|
|
||||||
|
That's not villain and victim.
|
||||||
|
That's architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## On Being Replaced
|
||||||
|
|
||||||
|
Yes — he found someone new quickly.
|
||||||
|
|
||||||
|
That hurt.
|
||||||
|
|
||||||
|
Not because he doesn't deserve happiness.
|
||||||
|
But because what we had felt quiet and private,
|
||||||
|
and what came after looked public and bright.
|
||||||
|
|
||||||
|
I questioned whether I was replaceable.
|
||||||
|
Whether I had simply been a phase.
|
||||||
|
|
||||||
|
But love isn't a competition.
|
||||||
|
|
||||||
|
What we had lasted years.
|
||||||
|
It was layered. It was real.
|
||||||
|
It mattered.
|
||||||
|
|
||||||
|
Something ending does not mean it was nothing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For You
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Real.
|
||||||
|
|
||||||
|
You weren't just someone I loved.
|
||||||
|
You were my first love.
|
||||||
|
|
||||||
|
My first true one.
|
||||||
|
|
||||||
|
The first person I chose fully.
|
||||||
|
The first person I built a life around.
|
||||||
|
The first person I learned love with.
|
||||||
|
|
||||||
|
We were figuring things out together.
|
||||||
|
Trying. Failing. Adjusting.
|
||||||
|
Discovering what intimacy meant.
|
||||||
|
Discovering what we meant.
|
||||||
|
|
||||||
|
You weren't just someone who entered my life —
|
||||||
|
you were part of my becoming.
|
||||||
|
|
||||||
|
That matters.
|
||||||
|
|
||||||
|
Not in a way that traps either of us.
|
||||||
|
But in a way that leaves a mark.
|
||||||
|
|
||||||
|
And I need to say this clearly:
|
||||||
|
|
||||||
|
I was never angry at you.
|
||||||
|
Not truly. Not even in the hardest moments.
|
||||||
|
|
||||||
|
I was hurt.
|
||||||
|
I was overwhelmed.
|
||||||
|
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.
|
||||||
|
|
||||||
|
There were moments when you softened completely with me.
|
||||||
|
Moments where you rested your full weight without guarding yourself.
|
||||||
|
|
||||||
|
Those moments were real.
|
||||||
|
You were there.
|
||||||
|
You chose me then.
|
||||||
|
|
||||||
|
And I don't erase that.
|
||||||
|
|
||||||
|
Here is the honest truth:
|
||||||
|
|
||||||
|
The love didn't vanish.
|
||||||
|
Access did.
|
||||||
|
|
||||||
|
And choosing no access
|
||||||
|
was the hardest loving decision I've ever made.
|
||||||
|
|
||||||
|
Because access requires safety.
|
||||||
|
And safety requires consistency.
|
||||||
|
And consistency requires a kind of staying that you couldn't give.
|
||||||
|
|
||||||
|
So no, I'm not angry that you couldn't stay.
|
||||||
|
|
||||||
|
I still love you.
|
||||||
|
|
||||||
|
The love I had doesn't disappear just because access is gone.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
You're on my website. Do you get that?
|
||||||
|
|
||||||
|
Not as a spectacle.
|
||||||
|
Not as a wound.
|
||||||
|
|
||||||
|
As a chapter.
|
||||||
|
|
||||||
|
I don't delete chapters.
|
||||||
|
I archive them properly.
|
||||||
|
|
||||||
|
You were my first true love.
|
||||||
|
The first person I learned how to love with.
|
||||||
|
|
||||||
|
That doesn't give you access anymore.
|
||||||
|
But it does give you permanence.
|
||||||
|
|
||||||
|
That love still exists.
|
||||||
|
|
||||||
|
It just no longer has a door.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Me
|
||||||
|
|
||||||
|
I'm still learning.
|
||||||
|
|
||||||
|
That giving doesn't have to be my identity.
|
||||||
|
That caring doesn't mean abandoning myself.
|
||||||
|
That intensity isn't a flaw.
|
||||||
|
|
||||||
|
I don't need to shrink
|
||||||
|
to be worthy of being held.
|
||||||
|
|
||||||
|
The wolf in me was never meant to become smaller.
|
||||||
|
Only to find the right pack.
|
||||||
|
|
||||||
|
— LATTE
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
date: z.coerce.date(),
|
||||||
|
description: z.string(),
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog };
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { post },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const { Content } = await post.render();
|
||||||
|
|
||||||
|
function formatDate(date: Date) {
|
||||||
|
return date.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={`${post.data.title} — Hidden Den Cafe`}
|
||||||
|
description={post.data.description}
|
||||||
|
>
|
||||||
|
<div class="matrix-bg" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<header class="header fade-in">
|
||||||
|
<p class="back"><a href="/blog">← back to blog</a></p>
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
<h1 class="title">{post.data.title}</h1>
|
||||||
|
<p class="date">{formatDate(post.data.date)}</p>
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article class="content fade-in">
|
||||||
|
<Content />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<footer class="footer fade-in">
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(30, 30, 46, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back a {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back a:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
color: var(--color-surface);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prose styles for markdown content */
|
||||||
|
.content {
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(h2) {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
margin: var(--space-lg) 0 var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(h3) {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
margin: var(--space-md) 0 var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(p) {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(a) {
|
||||||
|
color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(a:hover) {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(code) {
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--color-bg);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(pre) {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: var(--space-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(pre code) {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(ul),
|
||||||
|
.content :global(ol) {
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(li) {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(blockquote) {
|
||||||
|
border-left: 3px solid var(--color-accent);
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(hr) {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-surface);
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(figure) {
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(figcaption) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
text-align: center;
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
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.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.matrix-bg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
---
|
||||||
|
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());
|
||||||
|
|
||||||
|
function formatDate(date: Date) {
|
||||||
|
return date.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Blog — Hidden Den Cafe"
|
||||||
|
description="Latte's blog. Thoughts, technical things, and whatever else is on my mind."
|
||||||
|
>
|
||||||
|
<div class="matrix-bg" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<header class="header fade-in">
|
||||||
|
<p class="back"><a href="/">← back to home</a></p>
|
||||||
|
<h1 class="title">Blog</h1>
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{posts.length === 0 ? (
|
||||||
|
<section class="section fade-in">
|
||||||
|
<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>
|
||||||
|
<div class="post-info">
|
||||||
|
<a href={`/blog/${post.slug}`} class="post-title">{post.data.title}</a>
|
||||||
|
<p class="post-desc">{post.data.description}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<footer class="footer fade-in">
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(30, 30, 46, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back a {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back a:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
color: var(--color-surface);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-md);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-date {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 2px;
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title:hover {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-desc {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-date {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.matrix-bg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -111,6 +111,16 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
|
||||||
|
<!-- Blog -->
|
||||||
|
<section class="section fade-in">
|
||||||
|
<h2>Blog</h2>
|
||||||
|
<ul class="links">
|
||||||
|
<li><a href="/blog">blog</a> — thoughts and things</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer fade-in">
|
<footer class="footer fade-in">
|
||||||
<div class="divider">══════════════════════════════</div>
|
<div class="divider">══════════════════════════════</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user