Add blog post and support pubDate/tags
Add new post "After the Silence". Update content schema to use pubDate and include tags (default empty). Update blog listing, post page and start page to use pubDate, render tag lists, and compute/show up to two related posts by tag overlap. Misc formatting and small display tweaks.
This commit is contained in:
+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>
|
||||
|
||||
Reference in New Issue
Block a user