Skip to content

Commit dcbc108

Browse files
committed
blog scale: fix nested-a render bug in cards, paginate index (10/page), tag-list limit + /blog/tags/, sidebar 5-per-cat with overflow link
1 parent caa8b77 commit dcbc108

5 files changed

Lines changed: 253 additions & 105 deletions

File tree

src/components/BlogList.astro

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ const CATEGORY_COLOR: Record<string, string> = {
77
release: 'var(--ok)',
88
research: 'var(--accent-soft)',
99
tutorial: 'var(--pending)',
10-
news: 'var(--fg-muted)'
10+
news: 'var(--fg-muted)',
1111
};
1212
---
1313
<ul class="post-list">
1414
{posts.map(p => (
15-
<li>
16-
<a href={`/blog/${p.id}/`} class="post-card">
17-
<div class="meta">
18-
<span class="cat" style={`--cat: ${CATEGORY_COLOR[p.data.category]}`}>{p.data.category}</span>
19-
<time datetime={p.data.publishedAt.toISOString()}>{p.data.publishedAt.toISOString().slice(0,10)}</time>
20-
</div>
21-
<h3>{p.data.title}</h3>
22-
<p class="excerpt">{p.data.description}</p>
23-
{p.data.tags.length > 0 && (
24-
<div class="tags">{p.data.tags.map(t => <TagPill tag={t} href={`/blog/tags/${t}/`} />)}</div>
25-
)}
26-
</a>
15+
<li class="post-card">
16+
<div class="meta">
17+
<a class="cat" style={`--cat: ${CATEGORY_COLOR[p.data.category]}`} href={`/blog/categories/${p.data.category}/`}>{p.data.category}</a>
18+
<time datetime={p.data.publishedAt.toISOString()}>{p.data.publishedAt.toISOString().slice(0,10)}</time>
19+
</div>
20+
<h3><a href={`/blog/${p.id}/`}>{p.data.title}</a></h3>
21+
<p class="excerpt">{p.data.description}</p>
22+
{p.data.tags.length > 0 && (
23+
<div class="tags">{p.data.tags.map(t => <TagPill tag={t} href={`/blog/tags/${t}/`} />)}</div>
24+
)}
2725
</li>
2826
))}
2927
</ul>
@@ -34,13 +32,21 @@ const CATEGORY_COLOR: Record<string, string> = {
3432
border: 1px solid var(--border);
3533
border-radius: var(--radius-md);
3634
background: var(--bg-elevated);
37-
text-decoration: none; color: var(--fg);
38-
transition: border-color 120ms, transform 120ms;
35+
color: var(--fg);
36+
transition: border-color 120ms;
3937
}
40-
.post-card:hover { border-color: var(--accent); transform: translateY(-1px); }
41-
.meta { display: flex; gap: 12px; font-family: var(--font-mono); font-size: 11px; color: var(--fg-muted); margin-bottom: 6px; }
42-
.cat { color: var(--cat); text-transform: lowercase; }
43-
h3 { font-family: var(--font-mono); margin: 0 0 6px; color: var(--fg-strong); font-size: 18px; }
38+
.post-card:hover { border-color: var(--accent); }
39+
.meta { display: flex; gap: 12px; font-family: var(--font-mono); font-size: 11px; color: var(--fg-muted); margin-bottom: 6px; align-items: center; }
40+
.meta .cat {
41+
color: var(--cat); text-transform: lowercase;
42+
text-decoration: none;
43+
border: 1px solid color-mix(in oklab, var(--cat) 35%, transparent);
44+
padding: 1px 8px; border-radius: var(--radius-pill);
45+
}
46+
.meta .cat:hover { background: color-mix(in oklab, var(--cat) 12%, transparent); }
47+
h3 { font-family: var(--font-mono); margin: 0 0 6px; font-size: 18px; }
48+
h3 a { color: var(--fg-strong); text-decoration: none; }
49+
h3 a:hover { color: var(--accent-soft); }
4450
.excerpt { margin: 0 0 10px; color: var(--fg-muted); font-size: 14px; }
4551
.tags { display: flex; gap: 6px; flex-wrap: wrap; }
4652
</style>

src/components/BlogSidebar.astro

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,31 @@ const CATEGORY_LABEL: Record<typeof CATEGORY_ORDER[number], string> = {
1111
tutorial: 'Tutorials',
1212
news: 'News',
1313
};
14+
const ITEMS_PER_CATEGORY = 5;
1415
1516
const posts = (await getCollection('blog', ({ data }) => !data.draft || import.meta.env.DEV))
1617
.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());
1718
18-
const grouped = CATEGORY_ORDER.map(cat => ({
19-
category: cat,
20-
label: CATEGORY_LABEL[cat],
21-
items: posts.filter(p => p.data.category === cat),
22-
})).filter(g => g.items.length > 0);
19+
const grouped = CATEGORY_ORDER.map(cat => {
20+
const all = posts.filter(p => p.data.category === cat);
21+
return {
22+
category: cat,
23+
label: CATEGORY_LABEL[cat],
24+
items: all.slice(0, ITEMS_PER_CATEGORY),
25+
overflow: Math.max(0, all.length - ITEMS_PER_CATEGORY),
26+
};
27+
}).filter(g => g.items.length > 0);
2328
---
2429
<aside class="blog-sidebar" data-open="false" aria-label="Blog posts">
2530
<button class="sidebar-toggle" type="button" aria-expanded="false" aria-controls="blog-sidebar-nav">
26-
<span class="label">All posts</span>
31+
<span class="label">Blog</span>
2732
<span class="count">{posts.length}</span>
2833
<span class="chevron" aria-hidden="true">▾</span>
2934
</button>
3035
<nav id="blog-sidebar-nav">
36+
<section class="head-row">
37+
<a href="/blog/" class={currentHref === '/blog/' ? 'all-link current' : 'all-link'}>All posts ({posts.length})</a>
38+
</section>
3139
{grouped.map(group => (
3240
<section>
3341
<h3>{group.label}</h3>
@@ -44,9 +52,15 @@ const grouped = CATEGORY_ORDER.map(cat => ({
4452
);
4553
})}
4654
</ul>
55+
{group.overflow > 0 && (
56+
<a class="more" href={`/blog/categories/${group.category}/`}>
57+
+{group.overflow} more in {group.label}
58+
</a>
59+
)}
4760
</section>
4861
))}
4962
<section class="rss-row">
63+
<a href="/blog/tags/">Browse all tags →</a>
5064
<a href="/blog/rss.xml">RSS feed →</a>
5165
</section>
5266
</nav>
@@ -81,8 +95,17 @@ const grouped = CATEGORY_ORDER.map(cat => ({
8195

8296
.blog-sidebar nav { padding: 12px 0; }
8397
.blog-sidebar section { padding: 8px 16px; }
84-
.blog-sidebar section.rss-row { padding-top: 16px; border-top: 1px dashed var(--border); margin-top: 12px; }
85-
.blog-sidebar section.rss-row a { color: var(--accent-soft); font-size: 12px; }
98+
.blog-sidebar section.head-row { padding-bottom: 4px; }
99+
.all-link {
100+
display: block; padding: 6px 8px;
101+
color: var(--fg-strong); text-decoration: none;
102+
border-radius: var(--radius-sm);
103+
border: 1px solid var(--border);
104+
font-weight: 600;
105+
}
106+
.all-link.current { color: var(--accent-soft); border-color: var(--accent); background: var(--accent-bg); }
107+
.all-link:hover { border-color: var(--accent); color: var(--accent-soft); }
108+
86109
.blog-sidebar h3 {
87110
font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
88111
color: var(--fg-muted); margin: 6px 0 8px;
@@ -108,9 +131,21 @@ const grouped = CATEGORY_ORDER.map(cat => ({
108131
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
109132
line-clamp: 2;
110133
}
111-
.blog-sidebar li time {
112-
color: var(--fg-muted); font-size: 11px;
134+
.blog-sidebar li time { color: var(--fg-muted); font-size: 11px; }
135+
136+
.blog-sidebar .more {
137+
display: block; margin-top: 6px;
138+
color: var(--accent-soft); font-size: 11px;
139+
text-decoration: none; padding: 4px 8px;
140+
}
141+
.blog-sidebar .more:hover { text-decoration: underline; }
142+
143+
.blog-sidebar section.rss-row {
144+
padding-top: 16px; border-top: 1px dashed var(--border); margin-top: 12px;
145+
display: grid; gap: 4px;
113146
}
147+
.blog-sidebar section.rss-row a { color: var(--accent-soft); font-size: 12px; text-decoration: none; }
148+
.blog-sidebar section.rss-row a:hover { text-decoration: underline; }
114149

115150
@media (max-width: 960px) {
116151
.blog-sidebar { border-right: 0; border-bottom: 1px solid var(--border); padding-bottom: 16px; }

src/pages/blog/[...page].astro

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
import BaseLayout from '~/layouts/BaseLayout.astro';
3+
import BlogList from '~/components/BlogList.astro';
4+
import PrevNext from '~/components/PrevNext.astro';
5+
import { getCollection } from 'astro:content';
6+
import type { GetStaticPathsOptions } from 'astro';
7+
8+
const TAG_CHIPS_LIMIT = 12;
9+
10+
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
11+
const allPosts = (await getCollection('blog', ({ data }) => !data.draft || import.meta.env.DEV))
12+
.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());
13+
14+
// Collect aggregate filter data once and pass via props to every page.
15+
const categoryCounts = new Map<string, number>();
16+
const tagCounts = new Map<string, number>();
17+
for (const p of allPosts) {
18+
categoryCounts.set(p.data.category, (categoryCounts.get(p.data.category) ?? 0) + 1);
19+
for (const t of p.data.tags) tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1);
20+
}
21+
const categories = Array.from(categoryCounts.entries()).sort((a, b) => b[1] - a[1]);
22+
const tags = Array.from(tagCounts.entries()).sort((a, b) => b[1] - a[1]);
23+
24+
return paginate(allPosts, {
25+
pageSize: 10,
26+
props: { categories, tags, totalPosts: allPosts.length },
27+
});
28+
}
29+
30+
const { page, categories, tags, totalPosts } = Astro.props as {
31+
page: import('astro').Page<import('astro:content').CollectionEntry<'blog'>>;
32+
categories: [string, number][];
33+
tags: [string, number][];
34+
totalPosts: number;
35+
};
36+
const visibleTags = tags.slice(0, TAG_CHIPS_LIMIT);
37+
const hiddenTagsCount = Math.max(0, tags.length - TAG_CHIPS_LIMIT);
38+
---
39+
<BaseLayout
40+
title={page.currentPage === 1 ? 'Blog — nSealr' : `Blog · page ${page.currentPage} — nSealr`}
41+
description="Release notes, research, tutorials, and news from the nSealr program."
42+
>
43+
<section class="container">
44+
<p class="eyebrow">// blog</p>
45+
<h1>Notes on the program.</h1>
46+
<p class="lead">Release notes, research, tutorials, and news from nSealr signers, companion, and specs.</p>
47+
48+
{page.currentPage === 1 && (
49+
<>
50+
<div class="filter-block">
51+
<p class="filter-label">Filter by category</p>
52+
<div class="filters">
53+
<a href="/blog/" class="active">all <span class="n">{totalPosts}</span></a>
54+
{categories.map(([c, n]) => (
55+
<a href={`/blog/categories/${c}/`}>{c} <span class="n">{n}</span></a>
56+
))}
57+
</div>
58+
</div>
59+
60+
{tags.length > 0 && (
61+
<div class="filter-block">
62+
<p class="filter-label">Filter by tag</p>
63+
<div class="filters">
64+
{visibleTags.map(([t, n]) => (
65+
<a href={`/blog/tags/${t}/`}>#{t} <span class="n">{n}</span></a>
66+
))}
67+
{hiddenTagsCount > 0 && (
68+
<a class="see-more" href="/blog/tags/">+{hiddenTagsCount} more →</a>
69+
)}
70+
</div>
71+
</div>
72+
)}
73+
</>
74+
)}
75+
76+
<BlogList posts={page.data} />
77+
78+
<PrevNext
79+
prev={page.url.prev ? { href: page.url.prev, title: `Page ${page.currentPage - 1}`, label: 'Newer posts' } : undefined}
80+
next={page.url.next ? { href: page.url.next, title: `Page ${page.currentPage + 1}`, label: 'Older posts' } : undefined}
81+
/>
82+
83+
<p class="rss-row"><a href="/blog/rss.xml">RSS feed →</a></p>
84+
</section>
85+
</BaseLayout>
86+
87+
<style>
88+
.container { padding-block: 60px; max-width: 880px; }
89+
h1 { font-family: var(--font-mono); color: var(--fg-strong); font-size: clamp(28px, 4vw, 44px); margin: 8px 0 12px; }
90+
.lead { color: var(--fg-muted); font-size: 17px; max-width: 64ch; margin: 0 0 22px; }
91+
.filter-block { margin-bottom: 16px; }
92+
.filter-label {
93+
font-family: var(--font-mono); font-size: 11px;
94+
color: var(--fg-muted);
95+
text-transform: uppercase; letter-spacing: 0.06em;
96+
margin: 0 0 6px;
97+
}
98+
.filters { display: flex; gap: 8px; flex-wrap: wrap; font-family: var(--font-mono); font-size: 12px; }
99+
.filters a {
100+
display: inline-flex; align-items: center; gap: 6px;
101+
color: var(--fg-muted); border: 1px solid var(--border);
102+
padding: 4px 10px; border-radius: var(--radius-pill);
103+
text-decoration: none;
104+
transition: color 120ms, border-color 120ms;
105+
}
106+
.filters a:hover { color: var(--accent-soft); border-color: var(--accent); }
107+
.filters a.active { color: var(--accent-soft); border-color: var(--accent); background: var(--accent-bg); }
108+
.filters a .n { color: var(--fg-muted); font-size: 10px; }
109+
.filters a:hover .n, .filters a.active .n { color: var(--accent-soft); }
110+
.filters a.see-more { color: var(--accent-soft); border-style: dashed; }
111+
.rss-row { margin-top: 28px; font-family: var(--font-mono); font-size: 12px; }
112+
.rss-row a { color: var(--accent-soft); }
113+
</style>

src/pages/blog/index.astro

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)