bd0dc30f9e
Introduce a Library page and wire it into the main nav. Add blog frontmatter fields: category, featuredEssay (default false), and readingOrder (positive integer). Update several posts to mark featured essays and assign readingOrder for the recommended path.
611 lines
20 KiB
Plaintext
611 lines
20 KiB
Plaintext
---
|
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
import { getCollection } from "astro:content";
|
|
import { formatBlogDate } from "../lib/blog";
|
|
import { getReadingTime } from "../lib/readingTime";
|
|
|
|
const featuredEssays = (
|
|
await getCollection("blog", ({ data }) => !data.draft && data.featuredEssay)
|
|
).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
|
|
|
const curatedEssays = featuredEssays.map((post) => ({
|
|
...post,
|
|
readingTime: getReadingTime(post.body),
|
|
}));
|
|
|
|
const recommendedPath = curatedEssays
|
|
.filter((post) => typeof post.data.readingOrder === "number")
|
|
.sort(
|
|
(a, b) =>
|
|
(a.data.readingOrder ?? Number.MAX_SAFE_INTEGER) -
|
|
(b.data.readingOrder ?? Number.MAX_SAFE_INTEGER),
|
|
);
|
|
|
|
const categoryMap = new Map<string, (typeof curatedEssays)[number][]>();
|
|
|
|
for (const post of curatedEssays) {
|
|
if (!post.data.category) continue;
|
|
|
|
const current = categoryMap.get(post.data.category) ?? [];
|
|
current.push(post);
|
|
categoryMap.set(post.data.category, current);
|
|
}
|
|
|
|
const categoryGroups = [...categoryMap.entries()].sort(([a], [b]) =>
|
|
a.localeCompare(b),
|
|
);
|
|
|
|
function formatCategory(category: string) {
|
|
return category.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
}
|
|
---
|
|
|
|
<BaseLayout
|
|
title="Library - Hidden Den Cafe"
|
|
description="A quiet reading room for deeper writing, reflections, and longer posts from Hidden Den."
|
|
>
|
|
<div class="matrix-bg" aria-hidden="true"></div>
|
|
|
|
<main class="main">
|
|
<div class="container">
|
|
<header class="header fade-in">
|
|
<p class="eyebrow">Deeper writing</p>
|
|
<h1 class="title">Library</h1>
|
|
<div class="divider">==============================</div>
|
|
<p class="lead">
|
|
This is the quieter shelf of the den: a place for longer
|
|
posts, reflective essays, and writing worth revisiting. The
|
|
blog keeps the full archive. The library is a smaller,
|
|
curated reading room.
|
|
</p>
|
|
</header>
|
|
|
|
{
|
|
curatedEssays.length === 0 ? (
|
|
<section class="section fade-in">
|
|
<div class="note">
|
|
<p>
|
|
The shelf is still being arranged. For now, the
|
|
deeper writing still lives in the blog until
|
|
more posts are marked for the library.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
) : (
|
|
<>
|
|
<section
|
|
class="section fade-in"
|
|
aria-labelledby="highlighted-essays"
|
|
>
|
|
<div class="section-head">
|
|
<h2 id="highlighted-essays">
|
|
Highlighted Essays
|
|
</h2>
|
|
<a class="section-link" href="/blog">
|
|
Browse all posts
|
|
</a>
|
|
</div>
|
|
<p class="section-intro">
|
|
A small curated shelf of the more substantial
|
|
writing on the site.
|
|
</p>
|
|
<ul class="essay-list">
|
|
{curatedEssays.map((post) => (
|
|
<li class="essay-item">
|
|
<article class="essay-card">
|
|
<div class="card-topline">
|
|
<div class="meta-row">
|
|
<time
|
|
datetime={formatBlogDate(
|
|
post.data.pubDate,
|
|
)}
|
|
>
|
|
{formatBlogDate(
|
|
post.data.pubDate,
|
|
)}
|
|
</time>
|
|
<span
|
|
class="meta-sep"
|
|
aria-hidden="true"
|
|
>
|
|
·
|
|
</span>
|
|
<span>
|
|
{post.readingTime.text}
|
|
</span>
|
|
</div>
|
|
{post.data.category && (
|
|
<span class="category-pill">
|
|
{formatCategory(
|
|
post.data.category,
|
|
)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h3>
|
|
<a href={`/blog/${post.slug}`}>
|
|
{post.data.title}
|
|
</a>
|
|
</h3>
|
|
<p>{post.data.description}</p>
|
|
<a
|
|
class="essay-link"
|
|
href={`/blog/${post.slug}`}
|
|
>
|
|
Read essay
|
|
</a>
|
|
</article>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{recommendedPath.length >= 2 && (
|
|
<section
|
|
class="section fade-in"
|
|
aria-labelledby="reading-path"
|
|
>
|
|
<h2 id="reading-path">
|
|
Recommended Reading Path
|
|
</h2>
|
|
<p class="section-intro">
|
|
If you want a gentle path through the longer
|
|
writing, start here.
|
|
</p>
|
|
<ol class="path-list">
|
|
{recommendedPath.map((post) => (
|
|
<li class="path-item">
|
|
<span class="path-number">
|
|
{post.data.readingOrder}
|
|
</span>
|
|
<article class="path-card">
|
|
<h3>
|
|
<a
|
|
href={`/blog/${post.slug}`}
|
|
>
|
|
{post.data.title}
|
|
</a>
|
|
</h3>
|
|
<p>{post.data.description}</p>
|
|
<div class="meta-row path-meta">
|
|
<span>
|
|
{formatBlogDate(
|
|
post.data.pubDate,
|
|
)}
|
|
</span>
|
|
<span
|
|
class="meta-sep"
|
|
aria-hidden="true"
|
|
>
|
|
·
|
|
</span>
|
|
<span>
|
|
{post.readingTime.text}
|
|
</span>
|
|
</div>
|
|
</article>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</section>
|
|
)}
|
|
|
|
{categoryGroups.length > 0 && (
|
|
<section
|
|
class="section fade-in"
|
|
aria-labelledby="browse-by-theme"
|
|
>
|
|
<h2 id="browse-by-theme">Browse By Theme</h2>
|
|
<p class="section-intro">
|
|
The same shelf, grouped more loosely by what
|
|
each piece leans toward.
|
|
</p>
|
|
<div class="theme-grid">
|
|
{categoryGroups.map(([category, posts]) => (
|
|
<section
|
|
class="theme-group"
|
|
aria-labelledby={`theme-${category}`}
|
|
>
|
|
<h3 id={`theme-${category}`}>
|
|
{formatCategory(category)}
|
|
</h3>
|
|
<ul class="theme-list">
|
|
{posts.map((post) => (
|
|
<li>
|
|
<a
|
|
href={`/blog/${post.slug}`}
|
|
>
|
|
{post.data.title}
|
|
</a>
|
|
<span class="theme-meta">
|
|
{formatBlogDate(
|
|
post.data
|
|
.pubDate,
|
|
)}{" "}
|
|
·{" "}
|
|
{
|
|
post.readingTime
|
|
.text
|
|
}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
<footer class="footer fade-in">
|
|
<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);
|
|
padding-top: calc(var(--space-lg) + 3rem);
|
|
}
|
|
|
|
.container {
|
|
max-width: 760px;
|
|
width: 100%;
|
|
background: var(--color-glass);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid var(--color-surface);
|
|
border-radius: 8px;
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
.eyebrow {
|
|
color: var(--color-text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.24em;
|
|
font-size: 0.75rem;
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
.title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.divider {
|
|
color: var(--color-surface);
|
|
text-align: center;
|
|
font-size: 0.75rem;
|
|
margin: var(--space-md) 0;
|
|
user-select: none;
|
|
}
|
|
|
|
.lead {
|
|
color: var(--color-text);
|
|
line-height: 1.8;
|
|
max-width: 40rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.section {
|
|
margin: var(--space-xl) 0;
|
|
}
|
|
|
|
.section h2 {
|
|
font-size: 1rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
margin-bottom: var(--space-sm);
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.section-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
.section-head h2 {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.section-link {
|
|
color: var(--color-blue);
|
|
font-size: 0.85rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.section-link:hover {
|
|
color: var(--color-accent-bright);
|
|
}
|
|
|
|
.section-intro {
|
|
color: var(--color-text-dim);
|
|
line-height: 1.7;
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
|
|
.essay-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.essay-card,
|
|
.path-card,
|
|
.theme-group {
|
|
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);
|
|
}
|
|
|
|
.card-topline {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-sm);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
color: var(--color-text-dim);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.meta-sep {
|
|
color: var(--color-surface);
|
|
}
|
|
|
|
.category-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--color-accent);
|
|
border: 1px solid
|
|
color-mix(in srgb, var(--color-surface) 80%, transparent);
|
|
padding: 1px 8px;
|
|
border-radius: 999px;
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
}
|
|
|
|
.essay-card h3,
|
|
.path-card h3,
|
|
.theme-group h3 {
|
|
font-size: 1rem;
|
|
margin-bottom: var(--space-xs);
|
|
color: var(--color-accent-bright);
|
|
}
|
|
|
|
.essay-card p,
|
|
.path-card p {
|
|
color: var(--color-text-dim);
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.essay-link {
|
|
display: inline-flex;
|
|
margin-top: var(--space-sm);
|
|
color: var(--color-blue);
|
|
}
|
|
|
|
.essay-link:hover,
|
|
.essay-card h3 a:hover,
|
|
.path-card h3 a:hover,
|
|
.theme-list a:hover {
|
|
color: var(--color-accent-bright);
|
|
}
|
|
|
|
.note {
|
|
padding: var(--space-md);
|
|
border: 1px solid
|
|
color-mix(in srgb, var(--color-accent) 30%, transparent);
|
|
background:
|
|
linear-gradient(
|
|
135deg,
|
|
color-mix(in srgb, var(--color-accent) 12%, transparent),
|
|
transparent 70%
|
|
),
|
|
color-mix(in srgb, var(--color-bg-light) 84%, transparent);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.note p {
|
|
color: var(--color-text);
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.path-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.path-item {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: var(--space-md);
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.path-number {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 50%;
|
|
border: 1px solid
|
|
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
|
color: var(--color-accent);
|
|
font-size: 0.9rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.path-meta {
|
|
margin-top: var(--space-sm);
|
|
}
|
|
|
|
.theme-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.theme-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.theme-list li {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.theme-meta {
|
|
color: var(--color-text-dim);
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.footer {
|
|
margin-top: var(--space-xl);
|
|
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;
|
|
}
|
|
|
|
.fade-in:nth-child(4) {
|
|
animation-delay: 0.4s;
|
|
}
|
|
|
|
.fade-in:nth-child(5) {
|
|
animation-delay: 0.5s;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
.theme-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.container {
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.title {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.divider {
|
|
font-size: 0.6rem;
|
|
}
|
|
|
|
.lead {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.section-head {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.path-item {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.matrix-bg {
|
|
animation: none;
|
|
}
|
|
|
|
.fade-in {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|