Add blog series support and first post
- Add optional series field to blog collection schema (name, part) - Update blog route to collect and sort series posts, boost related scoring when series matches, and render a series section with styles and responsive tweaks - Add "Coffee & Code #1" blog post markdown for the new series
This commit is contained in:
@@ -11,13 +11,38 @@ export async function getStaticPaths() {
|
||||
params: { slug: post.slug },
|
||||
props: {
|
||||
post,
|
||||
seriesPosts: post.data.series
|
||||
? posts
|
||||
.filter(
|
||||
(candidate) =>
|
||||
candidate.data.series?.name ===
|
||||
post.data.series?.name,
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const partDifference =
|
||||
(a.data.series?.part ?? Number.MAX_SAFE_INTEGER) -
|
||||
(b.data.series?.part ?? Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (partDifference !== 0) return partDifference;
|
||||
|
||||
return (
|
||||
a.data.pubDate.valueOf() -
|
||||
b.data.pubDate.valueOf()
|
||||
);
|
||||
})
|
||||
: [],
|
||||
relatedPosts: posts
|
||||
.filter((candidate) => candidate.slug !== post.slug)
|
||||
.map((candidate) => ({
|
||||
post: candidate,
|
||||
score: candidate.data.tags.filter((tag) =>
|
||||
post.data.tags.includes(tag),
|
||||
).length,
|
||||
score:
|
||||
(candidate.data.series?.name &&
|
||||
candidate.data.series.name === post.data.series?.name
|
||||
? 10
|
||||
: 0) +
|
||||
candidate.data.tags.filter((tag) =>
|
||||
post.data.tags.includes(tag),
|
||||
).length,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
@@ -33,8 +58,13 @@ export async function getStaticPaths() {
|
||||
}));
|
||||
}
|
||||
|
||||
const { post, relatedPosts = [] } = Astro.props as {
|
||||
const {
|
||||
post,
|
||||
seriesPosts = [],
|
||||
relatedPosts = [],
|
||||
} = Astro.props as {
|
||||
post: BlogPost;
|
||||
seriesPosts: BlogPost[];
|
||||
relatedPosts: BlogPost[];
|
||||
};
|
||||
|
||||
@@ -54,6 +84,13 @@ function formatDate(date: Date) {
|
||||
<main class="main">
|
||||
<div class="container">
|
||||
<header class="header fade-in">
|
||||
{
|
||||
post.data.series && (
|
||||
<p class="series-label">
|
||||
{post.data.series.name} #{post.data.series.part}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<h1 class="title">{post.data.title}</h1>
|
||||
<p class="date">{formatDate(post.data.pubDate)}</p>
|
||||
{
|
||||
@@ -75,6 +112,60 @@ function formatDate(date: Date) {
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
{
|
||||
post.data.series && (
|
||||
<section
|
||||
class="series fade-in"
|
||||
aria-labelledby="series-posts"
|
||||
>
|
||||
<div class="series-head">
|
||||
<h2 id="series-posts">{post.data.series.name}</h2>
|
||||
<a href="/blog" class="series-link">
|
||||
Browse the blog
|
||||
</a>
|
||||
</div>
|
||||
<p class="series-note">
|
||||
This post is part of an ongoing series about
|
||||
building warm, intentional places on the web.
|
||||
</p>
|
||||
<ol class="series-list">
|
||||
{seriesPosts.map((seriesPost) => (
|
||||
<li
|
||||
class:list={[
|
||||
"series-item",
|
||||
{
|
||||
current:
|
||||
seriesPost.slug === post.slug,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{seriesPost.slug === post.slug ? (
|
||||
<span class="series-current">
|
||||
#{seriesPost.data.series?.part}{" "}
|
||||
{seriesPost.data.title}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={`/blog/${seriesPost.slug}`}
|
||||
class="series-entry"
|
||||
>
|
||||
<span class="series-part">
|
||||
#{seriesPost.data.series?.part}
|
||||
</span>
|
||||
<span>{seriesPost.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p class="series-footnote">
|
||||
Future Coffee & Code entries will show up here as
|
||||
they are written.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
relatedPosts.length > 0 && (
|
||||
<section
|
||||
@@ -164,6 +255,14 @@ function formatDate(date: Date) {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.series-label {
|
||||
color: var(--color-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: var(--color-surface);
|
||||
text-align: center;
|
||||
@@ -299,6 +398,96 @@ function formatDate(date: Date) {
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.series {
|
||||
margin-top: var(--space-xl);
|
||||
padding: var(--space-lg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-accent) 28%, transparent);
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--color-accent) 9%, transparent),
|
||||
transparent 70%
|
||||
),
|
||||
color-mix(in srgb, var(--color-bg-light) 84%, transparent);
|
||||
}
|
||||
|
||||
.series-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.series-head h2 {
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.series-link {
|
||||
color: var(--color-blue);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.series-link:hover {
|
||||
color: var(--color-accent-bright);
|
||||
}
|
||||
|
||||
.series-note,
|
||||
.series-footnote {
|
||||
color: var(--color-text-dim);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.series-note {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.series-footnote {
|
||||
margin-top: var(--space-md);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.series-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.series-item {
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
.series-entry,
|
||||
.series-current {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.series-entry {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.series-entry:hover {
|
||||
color: var(--color-accent-bright);
|
||||
}
|
||||
|
||||
.series-current {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.series-part {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.related {
|
||||
margin-top: var(--space-xl);
|
||||
padding-top: var(--space-lg);
|
||||
@@ -431,6 +620,11 @@ function formatDate(date: Date) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.series-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
Reference in New Issue
Block a user