Add blog via Astro content collections
CI / ci (push) Successful in 26s
CI / ci (pull_request) Successful in 27s
Docker / docker (pull_request) Successful in 15s
Enterprise AI Code Review / ai-review (pull_request) Successful in 1m53s
Security / security (pull_request) Successful in 6s

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.
This commit is contained in:
2026-03-02 19:13:30 +01:00
parent 7fd3a59c3a
commit 077cc06d75
9 changed files with 947 additions and 30 deletions
+280
View File
@@ -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>