From 077cc06d7519d8beb49d146a76b6c4a0f993c3ef Mon Sep 17 00:00:00 2001 From: Latte Date: Mon, 2 Mar 2026 19:13:30 +0100 Subject: [PATCH] Add blog via Astro content collections Introduce blog support: content collection schema, listing and post routes, and a sample Markdown post. Update docs and TODO; add blog assets dir and adjust color variables in docs. Also set absolute_redirect off in nginx.conf for container routing. --- DEVELOPMENT.md | 82 ++++-- README.md | 7 +- TODO.md | 7 +- nginx.conf | 1 + src/content/blog/love-without-access.md | 336 ++++++++++++++++++++++++ src/content/config.ts | 13 + src/pages/blog/[...slug].astro | 280 ++++++++++++++++++++ src/pages/blog/index.astro | 241 +++++++++++++++++ src/pages/index.astro | 10 + 9 files changed, 947 insertions(+), 30 deletions(-) create mode 100644 src/content/blog/love-without-access.md create mode 100644 src/content/config.ts create mode 100644 src/pages/blog/[...slug].astro create mode 100644 src/pages/blog/index.astro diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 518053d..cb43cfe 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,15 +16,22 @@ Cozy Den is a landing page for hiddenden.cafe built with Astro. The site feature ``` cozy-den/ ├── src/ +│ ├── content/ +│ │ ├── config.ts # Content collection schema +│ │ └── blog/ +│ │ └── *.md # Blog posts (Markdown) │ ├── layouts/ │ │ └── BaseLayout.astro # Base HTML layout with global styles │ ├── pages/ │ │ ├── index.astro # Main landing page -│ │ └── 404.astro # Custom 404 error page -│ └── components/ # (Empty - ready for future components) +│ │ ├── 404.astro # Custom 404 error page +│ │ └── blog/ +│ │ ├── index.astro # Blog listing page (/blog) +│ │ └── [...slug].astro # Individual post pages (/blog/) ├── public/ │ ├── 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 ├── package.json # Node dependencies ├── 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`: ```css ---color-bg: #1a1410 /* Dark background (deep coffee) */ ---color-bg-light: #2a1f18 /* Lighter background for cards */ ---color-text: #f4e9d8 /* Cream text */ ---color-text-dim: #c4b5a0 /* Dimmed text for less emphasis */ ---color-accent: #d4a574 /* Warm accent (coffee with cream) */ ---color-accent-bright: #e8bf8e /* Brighter accent for highlights */ ---color-warm: #8b6f47 /* Warm brown for borders/accents */ +--color-bg: #1e1e2e /* Dark background (Catppuccin Mocha base) */ +--color-text: #cdd6f4 /* Light text */ +--color-text-dim: #a6adc8 /* Dimmed text for less emphasis */ +--color-accent: #cba6f7 /* Mauve accent */ +--color-accent-bright: #f5c2e7 /* Pink accent for highlights */ +--color-surface: #313244 /* Surface color for borders/cards */ +--color-blue: #89b4fa /* Blue for links */ +--color-green: #a6e3a1 /* Green for code */ +--color-peach: #fab387 /* Peach for labels */ ``` ## Component Architecture @@ -62,12 +71,31 @@ The base layout provides: ### index.astro The main page includes these sections: -1. **Hero** - Title and subtitle -2. **About Hidden Den** - Information about the site/space -3. **About Me** - Information about Latte -4. **Services** - List of self-hosted services -5. **Support** - Ways to help/contribute -6. **Footer** - Links and credits +1. **Header** - Title +2. **About** - Introduction, age, status +3. **Cryptographic Keys** - PGP fingerprint and SSH public key +4. **Games** - Games Latte plays +5. **Socials** - Links to social platforms +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 `` 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 @@ -120,9 +148,15 @@ npm run preview 2. Follow the existing structure with h3 link and description **Adding images:** -1. Place images in `public/` directory -2. Reference them with `/image.jpg` in your code -3. They'll be served as static assets +1. Place images in `public/blog//` directory +2. Reference in Markdown with `/blog//image.jpg` +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 @@ -273,14 +307,12 @@ import BaseLayout from '../layouts/BaseLayout.astro'; ## Future Enhancements Ideas for future development: -- [ ] Add blog section using Astro's content collections -- [ ] Create reusable components for service items -- [ ] Add dark/light theme toggle -- [ ] Implement contact form +- [x] Add blog section using Astro's content collections +- [x] Create custom 404 page - [ ] Add RSS feed for blog -- [ ] Create custom 404 page +- [ ] Create reusable components for service items - [ ] Add more animations and transitions -- [ ] Integrate with analytics (privacy-friendly) +- [ ] Integrate with analytics (privacy-friendly, self-hosted) ## Resources diff --git a/README.md b/README.md index 56485d0..868f508 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ Cozy Den is a warm, self-hosted corner of the internet. This landing page repres ## Features - ☕ Cozy, warm aesthetic with hidden den theme -- 🎨 Custom color palette inspired by coffee and warmth +- 🎨 Catppuccin Mocha color palette - 📱 Responsive design - ⚡ Built with Astro for blazing fast performance - 🐳 Docker support for easy deployment +- 📝 Blog powered by Astro Content Collections (hardcoded Markdown, no CMS) ## Setup @@ -55,7 +56,7 @@ docker build -t cozy-den . ### Running the Container ```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: @@ -84,6 +85,8 @@ The site is built to be easily customizable: - **Colors**: Edit the CSS variables in `src/layouts/BaseLayout.astro` - **Content**: Update sections in `src/pages/index.astro` - **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//` and reference with `/blog//image.jpg` ## Technology Stack diff --git a/TODO.md b/TODO.md index dd12a94..725bd05 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,7 @@ ✅ Sitemap integration ✅ robots.txt ✅ Accessibility improvements (ARIA labels, semantic HTML) +✅ Blog with Astro Content Collections (Markdown, no CMS) ## Immediate Next Steps @@ -28,11 +29,11 @@ - [ ] Consider adding subtle background patterns or textures #### 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 - [ ] Create reusable components for repeated elements - [ ] Add a contact form (self-hosted solution) -- [ ] Add RSS feed if blog is implemented #### Long Term - [ ] Theme toggle (dark/light, or alternate color schemes) @@ -45,7 +46,7 @@ - Network is required for npm install (packages not included in repo) - 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 diff --git a/nginx.conf b/nginx.conf index 67a3ead..09dc0aa 100644 --- a/nginx.conf +++ b/nginx.conf @@ -3,6 +3,7 @@ server { server_name localhost; root /usr/share/nginx/html; index index.html; + absolute_redirect off; # Gzip compression gzip on; diff --git a/src/content/blog/love-without-access.md b/src/content/blog/love-without-access.md new file mode 100644 index 0000000..c4c6572 --- /dev/null +++ b/src/content/blog/love-without-access.md @@ -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 diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..a6a1b8e --- /dev/null +++ b/src/content/config.ts @@ -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 }; diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro new file mode 100644 index 0000000..97b664f --- /dev/null +++ b/src/pages/blog/[...slug].astro @@ -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]; +} +--- + + + + diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro new file mode 100644 index 0000000..5e546e2 --- /dev/null +++ b/src/pages/blog/index.astro @@ -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]; +} +--- + + + + +
+
+
+

← back to home

+

Blog

+
══════════════════════════════
+
+ + {posts.length === 0 ? ( +
+

No posts yet. Will get around to it.

+
+ ) : ( +
+
    + {posts.map((post) => ( +
  • + + +
  • + ))} +
+
+ )} + +
+
══════════════════════════════
+

Made with love by Latte

+
+
+
+
+ + diff --git a/src/pages/index.astro b/src/pages/index.astro index 46dd4ff..f1fde1a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -111,6 +111,16 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--; +
══════════════════════════════
+ + +
+

Blog

+ +
+