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
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:
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
stats?: ShareStat[];
|
stats?: ShareStat[];
|
||||||
wechatShareQrEnabled?: boolean;
|
wechatShareQrEnabled?: boolean;
|
||||||
|
variant?: 'default' | 'compact';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locale, t } = getI18n(Astro);
|
const { locale, t } = getI18n(Astro);
|
||||||
@@ -33,7 +34,9 @@ const {
|
|||||||
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
|
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
|
||||||
stats = [],
|
stats = [],
|
||||||
wechatShareQrEnabled = false,
|
wechatShareQrEnabled = false,
|
||||||
|
variant = 'default',
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
|
const isCompact = variant === 'compact';
|
||||||
|
|
||||||
const visibleBadge = badge;
|
const visibleBadge = badge;
|
||||||
const visibleTitle = title;
|
const visibleTitle = title;
|
||||||
@@ -136,10 +139,13 @@ if (wechatShareQrEnabled) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<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}
|
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="space-y-3">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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)]">
|
<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>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h3 class="text-xl font-semibold text-[var(--title-color)]">{visibleTitle}</h3>
|
<h3 class:list={['font-semibold text-[var(--title-color)]', isCompact ? 'text-lg' : 'text-xl']}>
|
||||||
<p class="max-w-3xl text-sm leading-7 text-[var(--text-secondary)]">{visibleDescription}</p>
|
{visibleTitle}
|
||||||
|
</h3>
|
||||||
|
<p class:list={['text-sm text-[var(--text-secondary)]', isCompact ? 'leading-6' : 'max-w-3xl leading-7']}>
|
||||||
|
{visibleDescription}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.length > 0 ? (
|
{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) => (
|
{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="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>
|
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||||
@@ -166,9 +176,14 @@ if (wechatShareQrEnabled) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
<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>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
<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)]">
|
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||||
{isEnglish ? 'Share channels' : '分享渠道'}
|
{isEnglish ? 'Share channels' : '分享渠道'}
|
||||||
</p>
|
</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>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import TerminalWindow from '../components/ui/TerminalWindow.astro';
|
|||||||
import CommandPrompt from '../components/ui/CommandPrompt.astro';
|
import CommandPrompt from '../components/ui/CommandPrompt.astro';
|
||||||
import FilterPill from '../components/ui/FilterPill.astro';
|
import FilterPill from '../components/ui/FilterPill.astro';
|
||||||
import PostCard from '../components/PostCard.astro';
|
import PostCard from '../components/PostCard.astro';
|
||||||
import FriendLinkCard from '../components/FriendLinkCard.astro';
|
|
||||||
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
||||||
import StatsList from '../components/StatsList.astro';
|
import StatsList from '../components/StatsList.astro';
|
||||||
import TechStackList from '../components/TechStackList.astro';
|
import TechStackList from '../components/TechStackList.astro';
|
||||||
@@ -28,6 +27,7 @@ const selectedCategory = url.searchParams.get('category') || '';
|
|||||||
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
|
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
|
||||||
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
|
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
|
||||||
const previewLimit = 5;
|
const previewLimit = 5;
|
||||||
|
const popularPreviewLimit = 4;
|
||||||
const DEFAULT_HOME_RANGE_KEY = '7d';
|
const DEFAULT_HOME_RANGE_KEY = '7d';
|
||||||
|
|
||||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||||
@@ -166,6 +166,18 @@ const popularRangeOptions = contentRanges.map((range) => ({
|
|||||||
const initialPopularCount = popularRangeCards.filter(
|
const initialPopularCount = popularRangeCards.filter(
|
||||||
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
|
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
|
||||||
).length;
|
).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 = ({
|
const buildHomeUrl = ({
|
||||||
type = selectedType,
|
type = selectedType,
|
||||||
tag = selectedTag,
|
tag = selectedTag,
|
||||||
@@ -210,11 +222,6 @@ const postsPrompt = hasActiveFilters
|
|||||||
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
|
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
|
||||||
: t('home.promptPostsDefault', { count: previewCount });
|
: t('home.promptPostsDefault', { count: previewCount });
|
||||||
const popularPrompt = t('home.promptPopularRange', { label: activeContentRange.label });
|
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 = [
|
const navLinks = [
|
||||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||||
@@ -254,6 +261,29 @@ const homeShareCopy = isEnglish
|
|||||||
title: '分享首页',
|
title: '分享首页',
|
||||||
description: '把首页发给别人,能快速看到文章、分类、评测和个人介绍等主要内容。',
|
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([
|
const homeBriefHighlights = buildDiscoveryHighlights([
|
||||||
siteSettings.siteDescription,
|
siteSettings.siteDescription,
|
||||||
siteSettings.heroSubtitle,
|
siteSettings.heroSubtitle,
|
||||||
@@ -287,7 +317,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
||||||
>
|
>
|
||||||
<PageViewTracker pageType="home" entityId="homepage" />
|
<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">
|
<TerminalWindow title={terminalConfig.title} class="w-full">
|
||||||
<div class="mb-5 px-4 overflow-x-auto">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{apiError && (
|
{apiError && (
|
||||||
@@ -364,7 +361,9 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</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} />
|
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
|
||||||
<div class="ml-4 terminal-panel home-discovery-shell">
|
<div class="ml-4 terminal-panel home-discovery-shell">
|
||||||
<div class="home-discovery-head">
|
<div class="home-discovery-head">
|
||||||
@@ -495,20 +494,20 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pinnedPost && (
|
{pinnedPost && (
|
||||||
<div class="mb-6 px-4">
|
<div>
|
||||||
<CommandPrompt command={t('home.promptPinned')} />
|
<CommandPrompt command={t('home.promptPinned')} />
|
||||||
<div id="home-pinned-wrap" class:list={['ml-4', !initialPinnedVisible && 'hidden']}>
|
<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="terminal-panel terminal-panel-accent terminal-interactive-card p-4" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="mb-2 flex items-center gap-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="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 text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
<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')}
|
{pinnedPost.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||||
</span>
|
</span>
|
||||||
<a href={`/articles/${pinnedPost.slug}`} class="text-lg font-bold text-[var(--title-color)] transition hover:text-[var(--primary)]">
|
<a href={`/articles/${pinnedPost.slug}`} class="text-lg font-bold text-[var(--title-color)] transition hover:text-[var(--primary)]">
|
||||||
{pinnedPost.title}
|
{pinnedPost.title}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
<p class="text-[var(--text-secondary)]">{pinnedPost.description}</p>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href={`/articles/${pinnedPost.slug}`} class="terminal-action-button inline-flex">
|
<a href={`/articles/${pinnedPost.slug}`} class="terminal-action-button inline-flex">
|
||||||
@@ -521,7 +520,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div id="posts" class="mb-10 px-4">
|
<div id="posts">
|
||||||
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
|
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
{allPosts.length > 0 ? (
|
{allPosts.length > 0 ? (
|
||||||
@@ -585,22 +584,49 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="popular" class="mb-10 px-4">
|
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start">
|
||||||
<CommandPrompt promptId="home-popular-prompt" command={popularPrompt} />
|
|
||||||
<div class="ml-4 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
|
||||||
<section class="terminal-panel space-y-4">
|
<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>
|
<div>
|
||||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.hotNow')}</h3>
|
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.popularTitle}</h3>
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.popularDesc}</p>
|
||||||
{t('home.hotNowDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
|
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div class="home-popular-sortbar">
|
<div class="home-popular-sortbar">
|
||||||
{popularRangeOptions.map((option) => (
|
{popularRangeOptions.map((option) => (
|
||||||
<button
|
<button
|
||||||
@@ -617,26 +643,8 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div id="home-popular-list" class="space-y-3">
|
||||||
{popularRangeCards.map(({ rangeKey, item, post }) => {
|
{popularRangeCards.map(({ rangeKey, item, post }) => (
|
||||||
return (
|
|
||||||
<a
|
<a
|
||||||
href={`/articles/${post.slug}`}
|
href={`/articles/${post.slug}`}
|
||||||
data-home-popular-card
|
data-home-popular-card
|
||||||
@@ -649,112 +657,135 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
data-home-popular-completes={item.readCompletes}
|
data-home-popular-completes={item.readCompletes}
|
||||||
data-home-popular-depth={Math.round(item.avgProgressPercent)}
|
data-home-popular-depth={Math.round(item.avgProgressPercent)}
|
||||||
class:list={[
|
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',
|
'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)]',
|
||||||
rangeKey !== activeContentRange.key && 'hidden'
|
!initialPopularVisibleKeys.has(`${rangeKey}:${post.slug}`) && 'hidden'
|
||||||
]}
|
]}
|
||||||
style={getAccentVars(getPostTypeTheme(post.type))}
|
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="min-w-0 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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')}
|
{post.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||||
</span>
|
</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}
|
{post.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mt-3 text-base font-semibold text-[var(--title-color)]">{post.title}</h4>
|
<h4 class="mt-2 line-clamp-2 text-sm font-semibold leading-6 text-[var(--title-color)]">{post.title}</h4>
|
||||||
<p class="mt-2 line-clamp-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<div class="mt-2 flex flex-wrap gap-2 text-xs 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>
|
|
||||||
<span>{t('home.views')}: {item.pageViews}</span>
|
<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>{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>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
|
||||||
</span>
|
</div>
|
||||||
<span class="terminal-stat-pill">
|
</div>
|
||||||
<i class="fas fa-stopwatch text-[10px] text-[var(--primary)]"></i>
|
|
||||||
<span>{t('home.avgDuration')}: {formatDurationMs(item.avgDurationMs)}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</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>
|
<p class="text-sm leading-6 text-[var(--text-secondary)]">{t('home.hotNowEmpty')}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="terminal-panel space-y-4">
|
<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>
|
<div>
|
||||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.readingSignals')}</h3>
|
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.statsTitle}</h3>
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.statsDesc}</p>
|
||||||
{t('home.readingSignalsDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
|
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
|
||||||
</div>
|
</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">
|
<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 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>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalCompletes')}: {contentOverview.totalReadCompletes}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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>
|
<p id="home-reading-window-meta" class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.statsWindowLabel', { label: activeContentRange.label })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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 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('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('common.posts')}: {allPosts.length}</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-[var(--border-color)] my-8"></div>
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
{systemStats.slice(0, 4).map((item) => (
|
||||||
<div id="friends" class="mb-8 px-4">
|
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/78 px-4 py-3">
|
||||||
<CommandPrompt command={t('home.promptFriends')} />
|
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||||
<div class="ml-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="mt-2 text-lg font-semibold text-[var(--title-color)]">{item.value}</div>
|
||||||
{friendLinks.map(friend => (
|
</div>
|
||||||
<FriendLinkCard friend={friend} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 ml-4">
|
</section>
|
||||||
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
|
|
||||||
</div>
|
{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>
|
||||||
|
|
||||||
<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">
|
<div id="about" class="px-4">
|
||||||
<CommandPrompt command={t('home.promptAbout')} />
|
<CommandPrompt command={t('home.promptAbout')} />
|
||||||
@@ -775,6 +806,21 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</TerminalWindow>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -783,6 +829,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
is:inline
|
is:inline
|
||||||
define:vars={{
|
define:vars={{
|
||||||
previewLimit,
|
previewLimit,
|
||||||
|
popularPreviewLimit,
|
||||||
categoryAccentMap,
|
categoryAccentMap,
|
||||||
tagAccentMap,
|
tagAccentMap,
|
||||||
contentRangesPayload: contentRanges.map((range) => ({
|
contentRangesPayload: contentRanges.map((range) => ({
|
||||||
@@ -1097,7 +1144,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
});
|
});
|
||||||
const sortedPopular = sortPopularCards(filteredPopular);
|
const sortedPopular = sortPopularCards(filteredPopular);
|
||||||
popularCards.forEach((card) => card.classList.add('hidden'));
|
popularCards.forEach((card) => card.classList.add('hidden'));
|
||||||
sortedPopular.forEach((card) => {
|
sortedPopular.slice(0, popularPreviewLimit).forEach((card) => {
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
popularList?.appendChild(card);
|
popularList?.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user