refactor: streamline homepage layout and enhance sidebar functionality
All checks were successful
docker-images / resolve-build-targets (push) Successful in 6s
ui-regression / playwright-regression (push) Successful in 4m15s
docker-images / build-and-push (admin) (push) Successful in 1m6s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m19s
docker-images / submit-indexnow (push) Successful in 18s

- Removed unused FriendLinkCard import and adjusted sidebar friend links to display a maximum of three.
- Introduced a new popularPreviewLimit constant to limit the number of popular posts displayed.
- Enhanced the sidebar with quick links, popular content, and friend links sections, improving overall navigation.
- Updated the layout to use a grid system for better responsiveness and organization.
- Simplified the popular posts section by removing sorting options and adjusting the display logic.
- Improved accessibility and readability of various components, including command prompts and statistics.
This commit is contained in:
2026-04-03 12:49:15 +08:00
parent 83f3c8d249
commit 0f2342a713
2 changed files with 448 additions and 386 deletions

View File

@@ -17,6 +17,7 @@ interface Props {
description?: string;
stats?: ShareStat[];
wechatShareQrEnabled?: boolean;
variant?: 'default' | 'compact';
}
const { locale, t } = getI18n(Astro);
@@ -33,7 +34,9 @@ const {
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
stats = [],
wechatShareQrEnabled = false,
variant = 'default',
} = Astro.props as Props;
const isCompact = variant === 'compact';
const visibleBadge = badge;
const visibleTitle = title;
@@ -136,10 +139,13 @@ if (wechatShareQrEnabled) {
---
<section
class="rounded-[28px] border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.1),rgba(var(--secondary-rgb),0.04)_46%,rgba(var(--bg-rgb),0.92))] p-5 sm:p-6"
class:list={[
'border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.1),rgba(var(--secondary-rgb),0.04)_46%,rgba(var(--bg-rgb),0.92))]',
isCompact ? 'rounded-[24px] p-4' : 'rounded-[28px] p-5 sm:p-6',
]}
data-share-panel-id={panelId}
>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class:list={['flex flex-col gap-4', !isCompact && 'lg:flex-row lg:items-start lg:justify-between']}>
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--primary)]/20 bg-[var(--primary)]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--primary)]">
@@ -149,13 +155,17 @@ if (wechatShareQrEnabled) {
</div>
<div class="space-y-2">
<h3 class="text-xl font-semibold text-[var(--title-color)]">{visibleTitle}</h3>
<p class="max-w-3xl text-sm leading-7 text-[var(--text-secondary)]">{visibleDescription}</p>
<h3 class:list={['font-semibold text-[var(--title-color)]', isCompact ? 'text-lg' : 'text-xl']}>
{visibleTitle}
</h3>
<p class:list={['text-sm text-[var(--text-secondary)]', isCompact ? 'leading-6' : 'max-w-3xl leading-7']}>
{visibleDescription}
</p>
</div>
</div>
{stats.length > 0 ? (
<div class="grid gap-3 sm:grid-cols-2 lg:min-w-[16rem]">
<div class:list={['grid gap-3 sm:grid-cols-2', !isCompact && 'lg:min-w-[16rem]']}>
{stats.slice(0, 4).map((item) => (
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/76 px-4 py-3">
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
@@ -166,9 +176,14 @@ if (wechatShareQrEnabled) {
) : null}
</div>
<div class="mt-5 rounded-[24px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/86 p-5 shadow-[0_16px_40px_rgba(15,23,42,0.06)]">
<div class:list={[
'rounded-[24px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/86 shadow-[0_16px_40px_rgba(15,23,42,0.06)]',
isCompact ? 'mt-4 p-4' : 'mt-5 p-5',
]}>
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{copy.summaryTitle}</div>
<p class="mt-3 text-base leading-8 text-[var(--title-color)]">{safeSummary}</p>
<p class:list={['mt-3 text-[var(--title-color)]', isCompact ? 'text-sm leading-7 line-clamp-4' : 'text-base leading-8']}>
{safeSummary}
</p>
</div>
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
@@ -217,7 +232,7 @@ if (wechatShareQrEnabled) {
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
{isEnglish ? 'Share channels' : '分享渠道'}
</p>
<p class="text-xs leading-6 text-[var(--text-secondary)]">{visibleDescription}</p>
{!isCompact && <p class="text-xs leading-6 text-[var(--text-secondary)]">{visibleDescription}</p>}
</div>
<div class="flex flex-wrap gap-2">
<a

View File

@@ -7,7 +7,6 @@ import TerminalWindow from '../components/ui/TerminalWindow.astro';
import CommandPrompt from '../components/ui/CommandPrompt.astro';
import FilterPill from '../components/ui/FilterPill.astro';
import PostCard from '../components/PostCard.astro';
import FriendLinkCard from '../components/FriendLinkCard.astro';
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
import StatsList from '../components/StatsList.astro';
import TechStackList from '../components/TechStackList.astro';
@@ -28,6 +27,7 @@ const selectedCategory = url.searchParams.get('category') || '';
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
const previewLimit = 5;
const popularPreviewLimit = 4;
const DEFAULT_HOME_RANGE_KEY = '7d';
let siteSettings = DEFAULT_SITE_SETTINGS;
@@ -166,6 +166,18 @@ const popularRangeOptions = contentRanges.map((range) => ({
const initialPopularCount = popularRangeCards.filter(
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
).length;
const initialPopularVisibleKeys = new Set(
popularRangeCards
.filter(({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post))
.sort((left, right) => {
const scoreDiff = right.item.pageViews - left.item.pageViews;
if (scoreDiff !== 0) return scoreDiff;
return right.item.readCompletes - left.item.readCompletes;
})
.slice(0, popularPreviewLimit)
.map(({ rangeKey, post }) => `${rangeKey}:${post.slug}`),
);
const sidebarFriendLinks = friendLinks.slice(0, 3);
const buildHomeUrl = ({
type = selectedType,
tag = selectedTag,
@@ -210,11 +222,6 @@ const postsPrompt = hasActiveFilters
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
: t('home.promptPostsDefault', { count: previewCount });
const popularPrompt = t('home.promptPopularRange', { label: activeContentRange.label });
const popularSortOptions = [
{ id: 'views', label: t('home.sortByViews'), icon: 'fa-eye' },
{ id: 'completes', label: t('home.sortByCompletes'), icon: 'fa-check-double' },
{ id: 'depth', label: t('home.sortByDepth'), icon: 'fa-chart-line' },
];
const navLinks = [
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
@@ -254,6 +261,29 @@ const homeShareCopy = isEnglish
title: '分享首页',
description: '把首页发给别人,能快速看到文章、分类、评测和个人介绍等主要内容。',
};
const homeSidebarCopy = isEnglish
? {
quickLinks: 'Quick links',
quickLinksDesc: 'Jump to the main sections of the site.',
popularTitle: 'Hot now',
popularDesc: 'Track the most-read content in the selected window.',
friendsTitle: 'Friend links',
friendsDesc: 'A few recommended sites from the blogroll.',
statsTitle: 'Site stats',
statsDesc: 'A compact snapshot of current site scale.',
aiBriefTitle: 'AI site brief',
}
: {
quickLinks: '快速入口',
quickLinksDesc: '把常用入口收进侧栏,首页阅读流更清爽。',
popularTitle: '最近热门',
popularDesc: '按当前时间窗口查看最受关注的内容。',
friendsTitle: '友情链接',
friendsDesc: '先看几个常访问的站点入口。',
statsTitle: '站点概览',
statsDesc: '快速看一下当前站点规模与内容状态。',
aiBriefTitle: '站点摘要',
};
const homeBriefHighlights = buildDiscoveryHighlights([
siteSettings.siteDescription,
siteSettings.heroSubtitle,
@@ -287,7 +317,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
>
<PageViewTracker pageType="home" entityId="homepage" />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="mx-auto max-w-[1480px] px-4 py-6 sm:px-6 lg:px-8">
<TerminalWindow title={terminalConfig.title} class="w-full">
<div class="mb-5 px-4 overflow-x-auto">
<pre class="font-mono text-xs sm:text-sm text-[var(--primary)] whitespace-pre">{terminalConfig.asciiArt}</pre>
@@ -321,39 +351,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
</div>
<div class="ml-4 mt-4 home-nav-strip">
{navLinks.map(link => (
<a href={link.href} class="home-nav-pill">
<i class={`fas ${link.icon} text-[11px]`}></i>
<span class="min-w-0 truncate">{link.text}</span>
</a>
))}
</div>
<div class="ml-4 mt-4">
<SharePanel
shareTitle={siteSettings.siteTitle}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
canonicalUrl={siteBaseUrl}
badge={homeShareCopy.badge}
kicker="geo / homepage"
title={homeShareCopy.title}
description={homeShareCopy.description}
stats={systemStats.slice(0, 4)}
wechatShareQrEnabled={siteSettings.seo.wechatShareQrEnabled}
/>
</div>
<div class="ml-4 mt-4">
<DiscoveryBrief
badge={isEnglish ? 'site brief' : '站点摘要'}
kicker="geo / overview"
title={isEnglish ? 'AI-readable site brief' : '给 AI 看的站点摘要'}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
highlights={homeBriefHighlights}
faqs={homeFaqs}
/>
</div>
</div>
{apiError && (
@@ -364,7 +361,9 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
)}
<div id="discover" class="mb-6 px-4">
<div class="grid gap-6 px-4 xl:grid-cols-[minmax(0,1fr)_320px]">
<div class="min-w-0 space-y-6">
<div id="discover">
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
<div class="ml-4 terminal-panel home-discovery-shell">
<div class="home-discovery-head">
@@ -495,20 +494,20 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
{pinnedPost && (
<div class="mb-6 px-4">
<div>
<CommandPrompt command={t('home.promptPinned')} />
<div id="home-pinned-wrap" class:list={['ml-4', !initialPinnedVisible && 'hidden']}>
<div class="terminal-panel terminal-panel-accent terminal-interactive-card p-4" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 text-xs rounded bg-[var(--primary)] text-[var(--terminal-bg)] font-bold">{t('home.pinned')}</span>
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
<div class="mb-2 flex items-center gap-2">
<span class="rounded bg-[var(--primary)] px-2 py-0.5 text-xs font-bold text-[var(--terminal-bg)]">{t('home.pinned')}</span>
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
{pinnedPost.type === 'article' ? t('common.article') : t('common.tweet')}
</span>
<a href={`/articles/${pinnedPost.slug}`} class="text-lg font-bold text-[var(--title-color)] transition hover:text-[var(--primary)]">
{pinnedPost.title}
</a>
</div>
<p class="text-sm text-[var(--text-secondary)] mb-2">{pinnedPost.date} | {t('common.readTime')}: {formatReadTime(locale, pinnedPost.readTime, t)}</p>
<p class="mb-2 text-sm text-[var(--text-secondary)]">{pinnedPost.date} | {t('common.readTime')}: {formatReadTime(locale, pinnedPost.readTime, t)}</p>
<p class="text-[var(--text-secondary)]">{pinnedPost.description}</p>
<div class="mt-4">
<a href={`/articles/${pinnedPost.slug}`} class="terminal-action-button inline-flex">
@@ -521,7 +520,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
)}
<div id="posts" class="mb-10 px-4">
<div id="posts">
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
<div class="ml-4">
{allPosts.length > 0 ? (
@@ -585,22 +584,49 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
</div>
</div>
</div>
<div id="popular" class="mb-10 px-4">
<CommandPrompt promptId="home-popular-prompt" command={popularPrompt} />
<div class="ml-4 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start">
<section class="terminal-panel space-y-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="space-y-1">
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.quickLinks}</h3>
<p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.quickLinksDesc}</p>
</div>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-2">
{navLinks.map(link => (
<a
href={link.href}
class="group flex min-w-0 items-center gap-2 rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/75 px-3 py-3 text-sm text-[var(--title-color)] transition hover:-translate-y-0.5 hover:border-[var(--primary)] hover:text-[var(--primary)]"
>
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--primary)]/10 text-[var(--primary)]">
<i class={`fas ${link.icon} text-[11px]`}></i>
</span>
<span class="min-w-0 truncate font-medium">{link.text}</span>
</a>
))}
</div>
</section>
<SharePanel
shareTitle={siteSettings.siteTitle}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
canonicalUrl={siteBaseUrl}
badge={homeShareCopy.badge}
title={homeShareCopy.title}
description={homeShareCopy.description}
wechatShareQrEnabled={siteSettings.seo.wechatShareQrEnabled}
variant="compact"
/>
<section class="terminal-panel space-y-4">
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.hotNow')}</h3>
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
{t('home.hotNowDescription')}
</p>
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.popularTitle}</h3>
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.popularDesc}</p>
</div>
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="home-popular-sortbar">
{popularRangeOptions.map((option) => (
<button
@@ -617,26 +643,8 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
))}
</div>
<div class="home-popular-sortbar">
{popularSortOptions.map((option) => (
<button
type="button"
data-home-popular-sort={option.id}
class:list={[
'home-popular-sort',
option.id === 'views' && 'is-active'
]}
>
<i class={`fas ${option.icon} text-[10px]`}></i>
<span>{option.label}</span>
</button>
))}
</div>
</div>
<div id="home-popular-list" class="space-y-3">
{popularRangeCards.map(({ rangeKey, item, post }) => {
return (
{popularRangeCards.map(({ rangeKey, item, post }) => (
<a
href={`/articles/${post.slug}`}
data-home-popular-card
@@ -649,112 +657,135 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
data-home-popular-completes={item.readCompletes}
data-home-popular-depth={Math.round(item.avgProgressPercent)}
class:list={[
'block rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/70 p-4 transition hover:border-[var(--primary)] hover:-translate-y-0.5',
rangeKey !== activeContentRange.key && 'hidden'
'block rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/72 p-3 transition hover:-translate-y-0.5 hover:border-[var(--primary)]',
!initialPopularVisibleKeys.has(`${rangeKey}:${post.slug}`) && 'hidden'
]}
style={getAccentVars(getPostTypeTheme(post.type))}
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="flex items-start gap-3">
<span class="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--primary)]/10 text-[11px] font-semibold text-[var(--primary)]">
{item.pageViews}
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(post.type))}>
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(post.type))}>
{post.type === 'article' ? t('common.article') : t('common.tweet')}
</span>
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getCategoryTheme(post.category))}>
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getCategoryTheme(post.category))}>
{post.category}
</span>
</div>
<h4 class="mt-3 text-base font-semibold text-[var(--title-color)]">{post.title}</h4>
<p class="mt-2 line-clamp-2 text-sm leading-6 text-[var(--text-secondary)]">
{post.description}
</p>
</div>
<div class="space-y-2 text-right text-xs text-[var(--text-tertiary)]">
<div>{post.date}</div>
<div>{t('common.readTime')}: {formatReadTime(locale, post.readTime, t)}</div>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<span class="terminal-stat-pill">
<i class="fas fa-eye text-[10px] text-[var(--primary)]"></i>
<h4 class="mt-2 line-clamp-2 text-sm font-semibold leading-6 text-[var(--title-color)]">{post.title}</h4>
<div class="mt-2 flex flex-wrap gap-2 text-xs text-[var(--text-secondary)]">
<span>{t('home.views')}: {item.pageViews}</span>
</span>
<span class="terminal-stat-pill">
<i class="fas fa-check-double text-[10px] text-[var(--primary)]"></i>
<span>{t('home.completes')}: {item.readCompletes}</span>
</span>
<span class="terminal-stat-pill">
<i class="fas fa-chart-line text-[10px] text-[var(--primary)]"></i>
<span>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
</span>
<span class="terminal-stat-pill">
<i class="fas fa-stopwatch text-[10px] text-[var(--primary)]"></i>
<span>{t('home.avgDuration')}: {formatDurationMs(item.avgDurationMs)}</span>
</span>
</div>
</div>
</div>
</a>
);
})}
))}
</div>
<div id="home-popular-empty" class:list={['terminal-empty', initialPopularCount > 0 && 'hidden']}>
<div id="home-popular-empty" class:list={['terminal-empty py-6', initialPopularCount > 0 && 'hidden']}>
<p class="text-sm leading-6 text-[var(--text-secondary)]">{t('home.hotNowEmpty')}</p>
</div>
</section>
<section class="terminal-panel space-y-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.readingSignals')}</h3>
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
{t('home.readingSignalsDescription')}
</p>
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.statsTitle}</h3>
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.statsDesc}</p>
</div>
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-2">
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.views')}</p>
<p id="home-reading-views-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.pageViews}</p>
<p id="home-reading-views-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.pageViews}</p>
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
</div>
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.completes')}</p>
<p id="home-reading-completes-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.readCompletes}</p>
<p id="home-reading-completes-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.readCompletes}</p>
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalCompletes')}: {contentOverview.totalReadCompletes}</p>
</div>
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.avgProgress')}</p>
<p id="home-reading-progress-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{formatProgressPercent(activeContentRange.overview.avgReadProgress)}</p>
<p id="home-reading-progress-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{formatProgressPercent(activeContentRange.overview.avgReadProgress)}</p>
<p id="home-reading-window-meta" class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.statsWindowLabel', { label: activeContentRange.label })}</p>
</div>
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.avgDuration')}</p>
<p id="home-reading-duration-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{formatDurationMs(activeContentRange.overview.avgReadDurationMs)}</p>
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
</div>
</div>
</section>
<p id="home-reading-duration-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{formatDurationMs(activeContentRange.overview.avgReadDurationMs)}</p>
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('common.posts')}: {allPosts.length}</p>
</div>
</div>
<div class="border-t border-[var(--border-color)] my-8"></div>
<div id="friends" class="mb-8 px-4">
<CommandPrompt command={t('home.promptFriends')} />
<div class="ml-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{friendLinks.map(friend => (
<FriendLinkCard friend={friend} />
<div class="grid gap-3 sm:grid-cols-2">
{systemStats.slice(0, 4).map((item) => (
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/78 px-4 py-3">
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
<div class="mt-2 text-lg font-semibold text-[var(--title-color)]">{item.value}</div>
</div>
))}
</div>
<div class="mt-6 ml-4">
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
</div>
</section>
{sidebarFriendLinks.length > 0 && (
<section class="terminal-panel space-y-4">
<div class="space-y-1">
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.friendsTitle}</h3>
<p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.friendsDesc}</p>
</div>
<div class="border-t border-[var(--border-color)] my-8"></div>
<div class="space-y-3">
{sidebarFriendLinks.map((friend) => (
<a
href={friend.url}
target="_blank"
rel="noopener noreferrer"
class="group flex items-start gap-3 rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/75 p-3 transition hover:-translate-y-0.5 hover:border-[var(--primary)]"
>
{friend.avatar ? (
<img
src={friend.avatar}
alt={friend.name}
class="h-10 w-10 shrink-0 rounded-xl border border-[var(--border-color)] object-cover bg-[var(--code-bg)]"
loading="lazy"
decoding="async"
/>
) : (
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--code-bg)] text-sm font-bold text-[var(--primary)]">
{friend.name.charAt(0).toUpperCase()}
</span>
)}
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-[var(--title-color)] group-hover:text-[var(--primary)]">
{friend.name}
</span>
<i class="fas fa-arrow-up-right-from-square text-[10px] text-[var(--text-tertiary)] opacity-0 transition-opacity group-hover:opacity-100"></i>
</div>
{friend.description && (
<p class="mt-1 line-clamp-2 text-xs leading-6 text-[var(--text-secondary)]">
{friend.description}
</p>
)}
</div>
</a>
))}
</div>
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
</section>
)}
</aside>
</div>
<div class="my-8 border-t border-[var(--border-color)]"></div>
<div id="about" class="px-4">
<CommandPrompt command={t('home.promptAbout')} />
@@ -775,6 +806,21 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div>
</div>
</div>
<div class="my-8 border-t border-[var(--border-color)]"></div>
<div class="px-4 pb-2">
<div class="ml-4">
<DiscoveryBrief
badge={isEnglish ? 'site brief' : homeSidebarCopy.aiBriefTitle}
kicker="geo / overview"
title={isEnglish ? 'AI-readable site brief' : '给 AI 看的站点摘要'}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
highlights={homeBriefHighlights}
faqs={homeFaqs}
/>
</div>
</div>
</TerminalWindow>
</div>
</BaseLayout>
@@ -783,6 +829,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
is:inline
define:vars={{
previewLimit,
popularPreviewLimit,
categoryAccentMap,
tagAccentMap,
contentRangesPayload: contentRanges.map((range) => ({
@@ -1097,7 +1144,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
});
const sortedPopular = sortPopularCards(filteredPopular);
popularCards.forEach((card) => card.classList.add('hidden'));
sortedPopular.forEach((card) => {
sortedPopular.slice(0, popularPreviewLimit).forEach((card) => {
card.classList.remove('hidden');
popularList?.appendChild(card);
});