feat(listing): portfolio-first archive, case-study hero, row action links
/posts/ and /zh/posts/ become hybrid: three fixed-slug flagship cards above the complete year/month archive, with the flagship block collapsing (inert, h=0) as soon as the reader filters. /categories/case-studies/ becomes a portfolio: compact two-column hero with a 21:9 deterministic topology banner, flagship ordering from lib/portfolio.ts instead of pubDate, and one measured pill per card. Also fixes what the screenshot review turned up: ZH archive rows and category chips were printing raw English slugs, the two chip strips were unlabelled and read as one list with a duplicated 'devops', card meta rows broke rhythm past four tags, the nav had no current-section indicator, and mobile title links were under the 44px tap minimum. scripts/probe-layout.mjs is the repeatable gate: 72/72 page x viewport checks and the collapse test.
This commit is contained in:
1 parent
68771101ea
commit
041471d25e
17 files changed
+1632
-76
No files matched your search
@@ -9,9 +9,14 @@ interface Props {
|
||||
posts: CollectionEntry<'posts'>[];
|
||||
limit?: number;
|
||||
lang?: 'en' | 'zh';
|
||||
/**
|
||||
* Measured headline per base slug (see lib/portfolio.ts) — rendered as a pill on a
|
||||
* portfolio card. Omitted on the home page and tag pages, where a bare list is right.
|
||||
*/
|
||||
metrics?: Record<string, string>;
|
||||
}
|
||||
|
||||
const { posts, limit, lang = 'en' } = Astro.props;
|
||||
const { posts, limit, lang = 'en', metrics } = Astro.props;
|
||||
const shown = limit ? posts.slice(0, limit) : posts;
|
||||
const isZh = lang === 'zh';
|
||||
|
||||
@@ -45,6 +50,20 @@ const thumb = (post: CollectionEntry<'posts'>, w: 400 | 800) => {
|
||||
const base = post.slug.replace(/^zh\//, '');
|
||||
return `/og/thumb/${base}-${w}.webp`;
|
||||
};
|
||||
|
||||
// A case study is a portfolio artefact, so the row's action says so. The same rule is
|
||||
// applied in Archive.astro for the /posts/ Featured block.
|
||||
const ctaLabel = (category: string) => {
|
||||
if (category === 'case-studies') return isZh ? '阅读案例复盘' : 'Read case study';
|
||||
return isZh ? '阅读文章' : 'Read article';
|
||||
};
|
||||
const metricFor = (post: CollectionEntry<'posts'>) =>
|
||||
metrics?.[post.slug.replace(/^zh\//, '')];
|
||||
|
||||
// A card that lists every tag wraps to two metadata lines on some rows and one on
|
||||
// others, so the grid loses its rhythm (and the summary/CTA drift). Show four and
|
||||
// fold the rest into a "+N" chip whose tooltip names them.
|
||||
const TAG_LIMIT = 4;
|
||||
---
|
||||
|
||||
<ul class="post-list">
|
||||
@@ -76,13 +95,30 @@ const thumb = (post: CollectionEntry<'posts'>, w: 400 | 800) => {
|
||||
)}
|
||||
<span class="sep">·</span>
|
||||
<a class="tag" href={catHref(post.data.category)}>{categoryLabel(post.data.category, lang)}</a>
|
||||
{post.data.tags.map((t) => {
|
||||
{post.data.tags.slice(0, TAG_LIMIT).map((t) => {
|
||||
const href = tagLink(t, counts, lang);
|
||||
return href ? <a class="tag" href={href}>{t}</a> : <span class="tag">{t}</span>;
|
||||
})}
|
||||
{post.data.tags.length > TAG_LIMIT && (
|
||||
<span class="tag tag-more" title={post.data.tags.slice(TAG_LIMIT).join(', ')}>
|
||||
+{post.data.tags.length - TAG_LIMIT}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p>{post.data.description}</p>
|
||||
<div class="row-foot">
|
||||
{/* A second visual target for the same destination — deliberately out of the
|
||||
tab order so keyboard users get one stop per card, not two. */}
|
||||
<a class="row-cta" href={`/posts/${post.slug}/`} tabindex="-1" aria-hidden="true">
|
||||
{ctaLabel(post.data.category)} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
{metricFor(post) && (
|
||||
<span class="row-metric">
|
||||
<span class="pulse-dot" aria-hidden="true"></span>{metricFor(post)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
Reference in new issue
Block a user