--- import BaseLayout from '../layouts/BaseLayout.astro'; import DiscoveryBrief from '../components/seo/DiscoveryBrief.astro'; import PageViewTracker from '../components/seo/PageViewTracker.astro'; import SharePanel from '../components/seo/SharePanel.astro'; 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 ViewMoreLink from '../components/ui/ViewMoreLink.astro'; import StatsList from '../components/StatsList.astro'; import TechStackList from '../components/TechStackList.astro'; import { terminalConfig } from '../lib/config/terminal'; import { api, DEFAULT_SITE_SETTINGS } from '../lib/api/client'; import { formatReadTime, getI18n } from '../lib/i18n'; import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, buildPostItemList, compactJsonLd } from '../lib/seo'; import type { AppFriendLink } from '../lib/api/client'; import type { ContentOverview, ContentWindowHighlight, PopularPostHighlight, Post } from '../lib/types'; import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../lib/utils'; export const prerender = false; const url = new URL(Astro.request.url); const selectedType = url.searchParams.get('type') || 'all'; const selectedTag = url.searchParams.get('tag') || ''; 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; let allPosts: Post[] = []; let recentPosts: Post[] = []; let filteredPostsCount = 0; let pinnedPost: Post | null = null; let tags: string[] = []; let friendLinks: AppFriendLink[] = []; let categories: Awaited> = []; const createEmptyContentRange = (key: string, label: string, days: number): ContentWindowHighlight => ({ key, label, days, overview: { pageViews: 0, readCompletes: 0, avgReadProgress: 0, avgReadDurationMs: undefined, }, popularPosts: [], }); let contentRanges: ContentWindowHighlight[] = [ createEmptyContentRange('24h', '24h', 1), createEmptyContentRange('7d', '7d', 7), createEmptyContentRange('30d', '30d', 30), ]; let contentOverview: ContentOverview = { totalPageViews: 0, pageViewsLast24h: 0, pageViewsLast7d: 0, totalReadCompletes: 0, readCompletesLast7d: 0, avgReadProgressLast7d: 0, avgReadDurationMsLast7d: undefined, }; let apiError: string | null = null; const { locale, t } = getI18n(Astro); const isEnglish = locale.startsWith('en'); const formatDurationMs = (value: number | undefined) => { if (!value || value <= 0) return locale === 'en' ? 'N/A' : '暂无'; if (value < 1000) return `${Math.round(value)} ms`; const seconds = value / 1000; if (seconds < 60) { return locale === 'en' ? `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s` : `${seconds.toFixed(seconds >= 10 ? 0 : 1)} 秒`; } const minutes = Math.floor(seconds / 60); const restSeconds = Math.round(seconds % 60); return locale === 'en' ? `${minutes}m ${restSeconds}s` : `${minutes} 分 ${restSeconds} 秒`; }; const formatProgressPercent = (value: number) => `${Math.round(value)}%`; try { const homeData = await api.getHomePageData(); siteSettings = homeData.siteSettings; allPosts = homeData.posts; tags = homeData.tags.map(tag => tag.name); friendLinks = homeData.friendLinks.filter(friend => friend.status === 'approved'); categories = homeData.categories; contentRanges = homeData.contentRanges; contentOverview = homeData.contentOverview; const filteredPosts = allPosts.filter(post => { const normalizedCategory = post.category?.trim().toLowerCase() || ''; if (selectedType !== 'all' && post.type !== selectedType) return false; if (selectedCategory && normalizedCategory !== normalizedSelectedCategory) return false; if (selectedTag && !post.tags.some(tag => tag.trim().toLowerCase() === normalizedSelectedTag)) return false; return true; }); recentPosts = filteredPosts.slice(0, previewLimit); filteredPostsCount = filteredPosts.length; pinnedPost = allPosts.find(post => post.pinned) || null; } catch (error) { apiError = error instanceof Error ? error.message : t('common.apiUnavailable'); console.error('API Error:', error); } const systemStats = [ { label: t('common.posts'), value: String(allPosts.length) }, { label: t('common.tags'), value: String(tags.length) }, { label: t('common.categories'), value: String(categories.length) }, { label: t('common.friends'), value: String(friendLinks.length) }, ]; const heroGlanceStats = [ { label: t('common.posts'), value: String(allPosts.length), icon: 'fa-file-alt' }, { label: t('common.categories'), value: String(categories.length), icon: 'fa-folder-open' }, { label: t('common.tags'), value: String(tags.length), icon: 'fa-hashtag' }, ]; const techStack = siteSettings.techStack.map(name => ({ name })); const tagFrequency = new Map(); for (const post of allPosts) { for (const tag of post.tags) { tagFrequency.set(tag, (tagFrequency.get(tag) ?? 0) + 1); } } const tagEntries = tags .map(name => ({ name, count: tagFrequency.get(name) ?? 0, })) .sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)); const getTagCloudStyle = (name: string) => { return getAccentVars(getTagTheme(name)); }; const matchesSelectedFilters = (post: Post) => { const normalizedCategory = post.category?.trim().toLowerCase() || ''; if (selectedType !== 'all' && post.type !== selectedType) return false; if (selectedCategory && normalizedCategory !== normalizedSelectedCategory) return false; if (selectedTag && !post.tags.some(tag => tag.trim().toLowerCase() === normalizedSelectedTag)) return false; return true; }; const activeContentRange = contentRanges.find((range) => range.key === DEFAULT_HOME_RANGE_KEY) ?? contentRanges[0] ?? createEmptyContentRange(DEFAULT_HOME_RANGE_KEY, DEFAULT_HOME_RANGE_KEY, 7); const popularRangeCards = contentRanges.flatMap((range) => range.popularPosts .filter((item): item is PopularPostHighlight & { post: Post } => Boolean(item.post)) .map((item, index) => ({ rangeKey: range.key, rank: index + 1, item, post: item.post, })), ); const popularRangeOptions = contentRanges.map((range) => ({ key: range.key, label: range.label, })); 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, category = selectedCategory, }: { type?: string; tag?: string; category?: string; }) => { const params = new URLSearchParams(); if (type && type !== 'all') params.set('type', type); if (category) params.set('category', category); if (tag) params.set('tag', tag); const query = params.toString(); return query ? `/?${query}` : '/'; }; const homeTagHrefPrefix = `${buildHomeUrl({ tag: '' })}${buildHomeUrl({ tag: '' }).includes('?') ? '&' : '?'}tag=`; const hasActiveFilters = selectedType !== 'all' || Boolean(selectedCategory) || Boolean(selectedTag); const previewCount = Math.min(filteredPostsCount, previewLimit); const categoryAccentMap = Object.fromEntries( categories.map((category) => [category.name.trim().toLowerCase(), getAccentVars(getCategoryTheme(category.name))]) ); const tagAccentMap = Object.fromEntries( tagEntries.map((tag) => [tag.name.trim().toLowerCase(), getAccentVars(getTagTheme(tag.name))]) ); const initialPinnedVisible = pinnedPost ? matchesSelectedFilters(pinnedPost) : false; const postTypeFilters = [ { id: 'all', name: t('common.all'), icon: 'fa-stream' }, { id: 'article', name: t('common.article'), icon: 'fa-file-alt' }, { id: 'tweet', name: t('common.tweet'), icon: 'fa-comment-dots' } ]; const activeFilterLabels = [ selectedType !== 'all' ? `type=${selectedType}` : '', selectedCategory ? `category=${selectedCategory}` : '', selectedTag ? `tag=${selectedTag}` : '', ].filter(Boolean); const discoverPrompt = hasActiveFilters ? t('home.promptDiscoverFiltered', { filters: activeFilterLabels.join(' · ') }) : t('home.promptDiscoverDefault'); const postsPrompt = hasActiveFilters ? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') }) : t('home.promptPostsDefault', { count: previewCount }); const siteBaseUrl = (siteSettings.siteUrl || new URL(Astro.request.url).origin).replace(/\/$/, ''); const homeJsonLd = [ { '@context': 'https://schema.org', '@type': 'CollectionPage', name: siteSettings.siteTitle, description: siteSettings.siteDescription, url: siteBaseUrl, inLanguage: locale, }, { '@context': 'https://schema.org', '@type': 'ItemList', name: `${siteSettings.siteName} recent posts`, itemListElement: buildPostItemList(recentPosts, siteBaseUrl), }, ]; const homeShareCopy = isEnglish ? { badge: 'site entry', title: 'Share the homepage', description: 'Use the homepage as the canonical top-level entry for people and AI search to branch into articles, taxonomies, reviews, and profile context.', } : { badge: '入口页', title: '把首页甩出去', description: '不知道发什么时,先发这个入口。轻松、不剧透,还挺省心。', }; const homeShareSummary = isEnglish ? 'A light entry point for curious visitors. Click around and let the rest reveal itself.' : '这是一个适合顺手转发的小入口,先逛逛,细节留到点开再说。'; const homeSidebarCopy = isEnglish ? { quickLinks: 'Quick links', quickLinksDesc: 'Keep the main channels pinned on the right so you can switch context without losing reading flow.', quickLinksMore: 'More channels', 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: '常用入口都放这儿,手别忙,点就行。', quickLinksMore: '更多频道', popularTitle: '最近热门', popularDesc: '看看最近是谁在悄悄抢镜。', friendsTitle: '友情链接', friendsDesc: '隔壁摊位也许也有好东西。', statsTitle: '站点概览', statsDesc: '轻量围观一下站内气氛,不必知道太多。', aiBriefTitle: '站点便签', }; const primaryQuickLinks = [ { icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' }, { icon: 'fa-folder', text: t('nav.categories'), href: '/categories' }, { icon: 'fa-user-secret', text: t('nav.about'), href: '/about' }, ...(siteSettings.ai.enabled ? [{ icon: 'fa-robot', text: t('nav.ask'), href: '/ask' }] : []), ]; const secondaryQuickLinks = [ { icon: 'fa-tags', text: t('nav.tags'), href: '/tags' }, { icon: 'fa-stream', text: t('nav.timeline'), href: '/timeline' }, { icon: 'fa-star', text: t('nav.reviews'), href: '/reviews' }, { icon: 'fa-link', text: t('nav.friends'), href: '/friends' }, ]; const homeBriefHighlights = buildDiscoveryHighlights([ siteSettings.siteDescription, siteSettings.heroSubtitle, siteSettings.ownerBio, `${t('common.posts')}: ${allPosts.length}`, `${t('common.categories')}: ${categories.length}`, `${t('common.tags')}: ${tags.length}`, ]); const homeFaqs = buildPageFaqs({ locale, pageTitle: siteSettings.siteTitle, summary: siteSettings.heroSubtitle || siteSettings.siteDescription, primaryLabel: isEnglish ? 'homepage' : '首页', primaryUrl: siteBaseUrl, relatedLinks: [ { label: t('nav.articles'), url: `${siteBaseUrl}/articles` }, { label: t('nav.categories'), url: `${siteBaseUrl}/categories` }, { label: t('nav.about'), url: `${siteBaseUrl}/about` }, ], signals: homeBriefHighlights, }); const homeFaqJsonLd = buildFaqJsonLd(homeFaqs); ---
{terminalConfig.asciiArt}
home / overview

{siteSettings.heroTitle}

{siteSettings.heroSubtitle}

{heroGlanceStats.map((item) => (
{item.label}
{item.value}
))}
{t('common.resultsCount', { count: filteredPostsCount })} {siteSettings.subscriptions.popupEnabled && ( )} {siteSettings.ai.enabled && ( {t('nav.ask')} )}
{apiError && (
{apiError}
)}
home filters

{t('common.browsePosts')}

{t('common.categoriesCount', { count: categories.length })} · {t('common.tagsCount', { count: tags.length })} · {t('common.resultsCount', { count: filteredPostsCount })}

{postTypeFilters.map(filter => ( {filter.name} ))}
item.id === selectedType)?.icon || 'fa-stream'} text-[10px]`}> {postTypeFilters.find((item) => item.id === selectedType)?.name || selectedType} {selectedCategory} {selectedTag}

{t('nav.categories')}

{categories.length}
{categories.map(category => (

{category.name}

{category.count ?? 0}
))}

{t('nav.tags')}

{tags.length}
{tagEntries.map((tag) => ( # {tag.name} {tag.count} ))}
{pinnedPost && (
{t('home.pinned')} {pinnedPost.type === 'article' ? t('common.article') : t('common.tweet')} {pinnedPost.title}

{pinnedPost.date} | {t('common.readTime')}: {formatReadTime(locale, pinnedPost.readTime, t)}

{pinnedPost.description}

)}
{allPosts.length > 0 ? (
{allPosts.map((post) => { const matchesCurrentFilter = matchesSelectedFilters(post); const filteredIndex = matchesCurrentFilter ? allPosts.filter(matchesSelectedFilters).findIndex((item) => item.slug === post.slug) : -1; const isVisible = matchesCurrentFilter && filteredIndex < previewLimit; return (
tag.trim().toLowerCase()).join('|')} data-home-pinned={post.pinned ? 'true' : 'false'} data-home-slug={post.slug} class:list={[!isVisible && 'hidden']} >
); })}
) : (

{t('articlesPage.emptyTitle')}

{t('articlesPage.emptyDescription')}

)} {allPosts.length > 0 && (
0 && 'hidden']}>

{t('articlesPage.emptyTitle')}

{t('articlesPage.emptyDescription')}

)}

{t('home.about')}

{siteSettings.ownerBio}

{t('home.techStack')}

{t('home.systemStatus')}