Add blog utilities and tag pages #54
@@ -0,0 +1,17 @@
|
|||||||
|
export function formatBlogDate(date: Date) {
|
||||||
|
return date.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slugifyTag(tag: string) {
|
||||||
|
return tag
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/&/g, " and ")
|
||||||
|
.replace(/[^a-z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagHref(tag: string) {
|
||||||
|
return `/blog/tag/${slugifyTag(tag)}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export function getReadingTime(content: string) {
|
||||||
|
const normalized = content
|
||||||
|
.replace(/```[\s\S]*?```/g, " ")
|
||||||
|
.replace(/`[^`]*`/g, " ")
|
||||||
|
.replace(/!\[[^\]]*\]\([^)]+\)/g, " ")
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/<[^>]+>/g, " ")
|
||||||
|
.replace(/[#>*_~-]/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const wordCount = normalized ? normalized.split(" ").length : 0;
|
||||||
|
const minutes = Math.max(1, Math.ceil(wordCount / 200));
|
||||||
|
|
||||||
|
return {
|
||||||
|
minutes,
|
||||||
|
text: `${minutes} min read`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import { getCollection, type CollectionEntry } from "astro:content";
|
import { getCollection, type CollectionEntry } from "astro:content";
|
||||||
|
import { formatBlogDate, getTagHref } from "../../lib/blog";
|
||||||
|
import { getReadingTime } from "../../lib/readingTime";
|
||||||
|
|
||||||
type BlogPost = CollectionEntry<"blog">;
|
type BlogPost = CollectionEntry<"blog">;
|
||||||
|
|
||||||
@@ -69,10 +71,7 @@ const {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
|
const readingTime = getReadingTime(post.body);
|
||||||
function formatDate(date: Date) {
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -92,7 +91,16 @@ function formatDate(date: Date) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
<h1 class="title">{post.data.title}</h1>
|
<h1 class="title">{post.data.title}</h1>
|
||||||
<p class="date">{formatDate(post.data.pubDate)}</p>
|
<div class="meta-row">
|
||||||
|
<time
|
||||||
|
class="date"
|
||||||
|
datetime={formatBlogDate(post.data.pubDate)}
|
||||||
|
>
|
||||||
|
{formatBlogDate(post.data.pubDate)}
|
||||||
|
</time>
|
||||||
|
<span class="meta-sep" aria-hidden="true">·</span>
|
||||||
|
<span class="reading-time">{readingTime.text}</span>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
post.data.tags.length > 0 && (
|
post.data.tags.length > 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -100,7 +108,9 @@ function formatDate(date: Date) {
|
|||||||
aria-label={`${post.data.title} tags`}
|
aria-label={`${post.data.title} tags`}
|
||||||
>
|
>
|
||||||
{post.data.tags.map((tag) => (
|
{post.data.tags.map((tag) => (
|
||||||
<span class="tag">{tag}</span>
|
<a class="tag" href={getTagHref(tag)}>
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -185,11 +195,26 @@ function formatDate(date: Date) {
|
|||||||
href={`/blog/${relatedPost.slug}`}
|
href={`/blog/${relatedPost.slug}`}
|
||||||
class="related-card"
|
class="related-card"
|
||||||
>
|
>
|
||||||
|
<div class="related-meta">
|
||||||
<span class="related-date">
|
<span class="related-date">
|
||||||
{formatDate(
|
{formatBlogDate(
|
||||||
relatedPost.data.pubDate,
|
relatedPost.data.pubDate,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="related-sep"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<span class="related-reading-time">
|
||||||
|
{
|
||||||
|
getReadingTime(
|
||||||
|
relatedPost.body,
|
||||||
|
).text
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<h3>{relatedPost.data.title}</h3>
|
<h3>{relatedPost.data.title}</h3>
|
||||||
<p>{relatedPost.data.description}</p>
|
<p>{relatedPost.data.description}</p>
|
||||||
</a>
|
</a>
|
||||||
@@ -283,6 +308,22 @@ function formatDate(date: Date) {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-sep {
|
||||||
|
color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-time {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.tag-list {
|
.tag-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -296,6 +337,12 @@ function formatDate(date: Date) {
|
|||||||
border: 1px solid var(--color-surface);
|
border: 1px solid var(--color-surface);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 45%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -547,12 +594,26 @@ function formatDate(date: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.related-date {
|
.related-date {
|
||||||
display: inline-block;
|
|
||||||
color: var(--color-text-dim);
|
color: var(--color-text-dim);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
margin-bottom: var(--space-xs);
|
margin-bottom: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.related-sep {
|
||||||
|
color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-reading-time {
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
.related-card h3 {
|
.related-card h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--color-accent-bright);
|
color: var(--color-accent-bright);
|
||||||
@@ -625,6 +686,11 @@ function formatDate(date: Date) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-row,
|
||||||
|
.related-meta {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
+44
-10
@@ -1,14 +1,17 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
import { formatBlogDate, getTagHref } from "../../lib/blog";
|
||||||
|
import { getReadingTime } from "../../lib/readingTime";
|
||||||
|
|
||||||
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
);
|
);
|
||||||
|
|
||||||
function formatDate(date: Date) {
|
const previewPosts = posts.map((post) => ({
|
||||||
return date.toISOString().split("T")[0];
|
...post,
|
||||||
}
|
readingTime: getReadingTime(post.body),
|
||||||
|
}));
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -34,11 +37,21 @@ function formatDate(date: Date) {
|
|||||||
) : (
|
) : (
|
||||||
<section class="section fade-in">
|
<section class="section fade-in">
|
||||||
<ul class="post-list">
|
<ul class="post-list">
|
||||||
{posts.map((post) => (
|
{previewPosts.map((post) => (
|
||||||
<li class="post-item">
|
<li class="post-item">
|
||||||
<span class="post-date">
|
<div class="post-meta-column">
|
||||||
{formatDate(post.data.pubDate)}
|
<time
|
||||||
|
class="post-date"
|
||||||
|
datetime={formatBlogDate(
|
||||||
|
post.data.pubDate,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatBlogDate(post.data.pubDate)}
|
||||||
|
</time>
|
||||||
|
<span class="post-reading-time">
|
||||||
|
{post.readingTime.text}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<div class="post-info">
|
<div class="post-info">
|
||||||
<a
|
<a
|
||||||
href={`/blog/${post.slug}`}
|
href={`/blog/${post.slug}`}
|
||||||
@@ -55,9 +68,12 @@ function formatDate(date: Date) {
|
|||||||
aria-label={`${post.data.title} tags`}
|
aria-label={`${post.data.title} tags`}
|
||||||
>
|
>
|
||||||
{post.data.tags.map((tag) => (
|
{post.data.tags.map((tag) => (
|
||||||
<span class="tag">
|
<a
|
||||||
|
class="tag"
|
||||||
|
href={getTagHref(tag)}
|
||||||
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -156,12 +172,24 @@ function formatDate(date: Date) {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-meta-column {
|
||||||
|
min-width: 110px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.post-date {
|
.post-date {
|
||||||
color: var(--color-text-dim);
|
color: var(--color-text-dim);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
min-width: 90px;
|
}
|
||||||
|
|
||||||
|
.post-reading-time {
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-info {
|
.post-info {
|
||||||
@@ -198,6 +226,12 @@ function formatDate(date: Date) {
|
|||||||
border: 1px solid var(--color-surface);
|
border: 1px solid var(--color-surface);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 45%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
@@ -259,7 +293,7 @@ function formatDate(date: Date) {
|
|||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-date {
|
.post-meta-column {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||||
|
import { getCollection, type CollectionEntry } from "astro:content";
|
||||||
|
import { formatBlogDate, getTagHref, slugifyTag } from "../../../lib/blog";
|
||||||
|
import { getReadingTime } from "../../../lib/readingTime";
|
||||||
|
|
||||||
|
type BlogPost = CollectionEntry<"blog">;
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||||
|
const tagMap = new Map<string, { tag: string; posts: BlogPost[] }>();
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
for (const tag of post.data.tags) {
|
||||||
|
const slug = slugifyTag(tag);
|
||||||
|
const existing = tagMap.get(slug);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.posts.push(post);
|
||||||
|
} else {
|
||||||
|
tagMap.set(slug, { tag, posts: [post] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...tagMap.entries()].map(([slug, entry]) => ({
|
||||||
|
params: { tag: slug },
|
||||||
|
props: {
|
||||||
|
tag: entry.tag,
|
||||||
|
posts: entry.posts.sort(
|
||||||
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag, posts } = Astro.props as {
|
||||||
|
tag: string;
|
||||||
|
posts: BlogPost[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewPosts = posts.map((post) => ({
|
||||||
|
...post,
|
||||||
|
readingTime: getReadingTime(post.body),
|
||||||
|
}));
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={`Tag: ${tag} - Hidden Den Cafe`}
|
||||||
|
description={`Blog posts tagged ${tag} on Hidden Den.`}
|
||||||
|
>
|
||||||
|
<div class="matrix-bg" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<header class="header fade-in">
|
||||||
|
<p class="eyebrow">Blog tag</p>
|
||||||
|
<h1 class="title">{tag}</h1>
|
||||||
|
<div class="divider">==============================</div>
|
||||||
|
<p class="lead">
|
||||||
|
Posts filed under <span class="highlight">{tag}</span>. A
|
||||||
|
small cluster of related thoughts from the den.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section fade-in" aria-labelledby="tag-posts">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2 id="tag-posts">Posts</h2>
|
||||||
|
<a class="section-link" href="/blog">Back to blog</a>
|
||||||
|
</div>
|
||||||
|
<ul class="post-list">
|
||||||
|
{previewPosts.map((post) => (
|
||||||
|
<li class="post-item">
|
||||||
|
<div class="post-meta-column">
|
||||||
|
<time
|
||||||
|
class="post-date"
|
||||||
|
datetime={formatBlogDate(post.data.pubDate)}
|
||||||
|
>
|
||||||
|
{formatBlogDate(post.data.pubDate)}
|
||||||
|
</time>
|
||||||
|
<span class="post-reading-time">
|
||||||
|
{post.readingTime.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="post-info">
|
||||||
|
<a
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
class="post-title"
|
||||||
|
>
|
||||||
|
{post.data.title}
|
||||||
|
</a>
|
||||||
|
<p class="post-desc">{post.data.description}</p>
|
||||||
|
{post.data.tags.length > 0 && (
|
||||||
|
<div
|
||||||
|
class="tag-list"
|
||||||
|
aria-label={`${post.data.title} tags`}
|
||||||
|
>
|
||||||
|
{post.data.tags.map((entryTag) => (
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
"tag",
|
||||||
|
{
|
||||||
|
active:
|
||||||
|
entryTag === tag,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
href={getTagHref(entryTag)}
|
||||||
|
>
|
||||||
|
{entryTag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</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: 700px;
|
||||||
|
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-dim);
|
||||||
|
line-height: 1.8;
|
||||||
|
max-width: 38rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link {
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link:hover {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-meta-column {
|
||||||
|
min-width: 110px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-date {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-reading-time {
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-accent);
|
||||||
|
border: 1px solid var(--color-surface);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover,
|
||||||
|
.tag.active {
|
||||||
|
color: var(--color-accent-bright);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 45%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta-column {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.matrix-bg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user