Refactor SEO and JSON-LD handling; improve layout and styles
All checks were successful
docker-images / resolve-build-targets (push) Successful in 5s
ui-regression / playwright-regression (push) Successful in 3m51s
docker-images / build-and-push (admin) (push) Successful in 4s
docker-images / build-and-push (backend) (push) Successful in 3s
docker-images / build-and-push (frontend) (push) Successful in 1m10s
docker-images / submit-indexnow (push) Successful in 19s
All checks were successful
docker-images / resolve-build-targets (push) Successful in 5s
ui-regression / playwright-regression (push) Successful in 3m51s
docker-images / build-and-push (admin) (push) Successful in 4s
docker-images / build-and-push (backend) (push) Successful in 3s
docker-images / build-and-push (frontend) (push) Successful in 1m10s
docker-images / submit-indexnow (push) Successful in 19s
- Introduced `compactJsonLd` utility to filter out falsy values from JSON-LD arrays. - Updated various pages to utilize `compactJsonLd` for cleaner JSON-LD handling. - Refactored music playlist configuration in Header component. - Enhanced BaseLayout with inline script for JSON-LD and removed unnecessary media attributes from stylesheets. - Improved error handling in category and tag pages by simplifying response logic. - Added new styles for home hero section and sidebar components to enhance UI. - Adjusted layout components for better responsiveness and visual consistency.
This commit is contained in:
@@ -17,7 +17,8 @@ const {
|
||||
const { locale, t, buildLocaleUrl } = getI18n(Astro);
|
||||
const aiEnabled = Boolean(Astro.props.siteSettings?.ai?.enabled);
|
||||
const musicEnabled = Astro.props.siteSettings?.musicEnabled ?? true;
|
||||
const musicPlaylist = (musicEnabled ? Astro.props.siteSettings?.musicPlaylist : []).filter(
|
||||
const configuredMusicPlaylist = Astro.props.siteSettings?.musicPlaylist ?? [];
|
||||
const musicPlaylist = (musicEnabled ? configuredMusicPlaylist : []).filter(
|
||||
(item) => item?.title?.trim() && item?.url?.trim()
|
||||
);
|
||||
const musicPlaylistPayload = JSON.stringify(musicPlaylist);
|
||||
|
||||
@@ -150,7 +150,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>{title}</title>
|
||||
{jsonLd && <script type="application/ld+json" set:html={jsonLd}></script>}
|
||||
{jsonLd && <script is:inline type="application/ld+json" set:html={jsonLd}></script>}
|
||||
<slot name="head" />
|
||||
|
||||
<style is:inline>
|
||||
@@ -467,8 +467,6 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
@@ -478,8 +476,6 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
>
|
||||
<noscript>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface DiscoveryFaqOptions {
|
||||
signals?: string[];
|
||||
}
|
||||
|
||||
export type JsonLdObject = Record<string, unknown>;
|
||||
|
||||
function normalizeWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
@@ -280,6 +282,12 @@ export function buildFaqJsonLd(faqs: ArticleFaqItem[]) {
|
||||
};
|
||||
}
|
||||
|
||||
export function compactJsonLd<T extends JsonLdObject>(
|
||||
items: Array<T | null | undefined | false>,
|
||||
): T[] {
|
||||
return items.filter((item): item is T => Boolean(item));
|
||||
}
|
||||
|
||||
export function buildPostItemList(posts: Post[], siteUrl: string) {
|
||||
return posts.map((post, index) => ({
|
||||
'@type': 'ListItem',
|
||||
|
||||
@@ -9,7 +9,7 @@ import StatsList from '../../components/StatsList.astro';
|
||||
import TechStackList from '../../components/TechStackList.astro';
|
||||
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -130,7 +130,7 @@ const aboutJsonLd = [
|
||||
title={`${t('about.pageTitle')} - ${siteSettings.siteShortName}`}
|
||||
description={siteSettings.siteDescription}
|
||||
siteSettings={siteSettings}
|
||||
jsonLd={aboutJsonLd.filter(Boolean)}
|
||||
jsonLd={compactJsonLd(aboutJsonLd)}
|
||||
>
|
||||
<PageViewTracker pageType="about" entityId="about" />
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
buildArticleHighlights,
|
||||
buildArticlePreviewParagraphs,
|
||||
buildArticleSynopsis,
|
||||
compactJsonLd,
|
||||
resolvePostUpdatedAt,
|
||||
} from '../../lib/seo';
|
||||
import type { PopularPostHighlight } from '../../lib/types';
|
||||
@@ -45,7 +46,6 @@ let post = null;
|
||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||
const analyticsEndpoint = `${resolvePublicApiBaseUrl(Astro.url)}/analytics/content`;
|
||||
let homeData: Awaited<ReturnType<typeof apiClient.getHomePageData>> | null = null;
|
||||
let postLookupFailed = false;
|
||||
|
||||
const [postResult, siteSettingsResult, homeDataResult] = await Promise.allSettled([
|
||||
apiClient.getPostBySlug(slug ?? ''),
|
||||
@@ -56,7 +56,6 @@ const [postResult, siteSettingsResult, homeDataResult] = await Promise.allSettle
|
||||
if (postResult.status === 'fulfilled') {
|
||||
post = postResult.value;
|
||||
} else {
|
||||
postLookupFailed = true;
|
||||
console.error('API Error:', postResult.reason);
|
||||
}
|
||||
|
||||
@@ -77,8 +76,8 @@ if (homeDataResult.status === 'fulfilled') {
|
||||
|
||||
if (!post) {
|
||||
return new Response(null, {
|
||||
status: postLookupFailed ? 503 : 404,
|
||||
headers: postLookupFailed ? { 'Retry-After': '120' } : undefined,
|
||||
status: postResult.status !== 'fulfilled' ? 503 : 404,
|
||||
headers: postResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,7 +370,7 @@ const breadcrumbJsonLd = {
|
||||
ogImage={ogImage}
|
||||
ogType="article"
|
||||
twitterCard="summary_large_image"
|
||||
jsonLd={[articleJsonLd, breadcrumbJsonLd, faqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([articleJsonLd, breadcrumbJsonLd, faqJsonLd])}
|
||||
>
|
||||
<Fragment slot="head">
|
||||
<meta property="article:published_time" content={publishedAt} />
|
||||
|
||||
@@ -9,7 +9,7 @@ import FilterPill from '../../components/ui/FilterPill.astro';
|
||||
import PostCard from '../../components/PostCard.astro';
|
||||
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, buildPostItemList } from '../../lib/seo';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, buildPostItemList, compactJsonLd } from '../../lib/seo';
|
||||
import type { Category, Post, Tag } from '../../lib/types';
|
||||
import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../../lib/utils';
|
||||
|
||||
@@ -193,7 +193,7 @@ const buildArticlesUrl = ({
|
||||
siteSettings={siteSettings}
|
||||
canonical={canonicalUrl}
|
||||
noindex={hasActiveFilters}
|
||||
jsonLd={[...jsonLd, articleIndexFaqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([...jsonLd, articleIndexFaqJsonLd])}
|
||||
>
|
||||
<PageViewTracker pageType="articles" entityId={`articles-page-${currentPage}`} />
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
@@ -6,7 +6,7 @@ import SharePanel from '../../components/seo/SharePanel.astro';
|
||||
import CommandPrompt from '../../components/ui/CommandPrompt.astro';
|
||||
import { api, DEFAULT_SITE_SETTINGS, resolvePublicApiBaseUrl } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -104,7 +104,7 @@ const askFaqJsonLd = buildFaqJsonLd(askFaqs);
|
||||
title={`${t('ask.pageTitle')} | ${siteSettings.siteShortName}`}
|
||||
description={t('ask.pageDescription', { siteName: siteSettings.siteName })}
|
||||
siteSettings={siteSettings}
|
||||
jsonLd={[...askJsonLd, askFaqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([...askJsonLd, askFaqJsonLd])}
|
||||
>
|
||||
<PageViewTracker pageType="ask" entityId="ask" />
|
||||
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
||||
@@ -20,33 +20,27 @@ const isEnglish = locale.startsWith('en');
|
||||
let categories: Category[] = [];
|
||||
let posts: Post[] = [];
|
||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||
let categoriesFailed = false;
|
||||
|
||||
try {
|
||||
const [categoriesResult, postsResult, settingsResult] = await Promise.allSettled([
|
||||
api.getCategories(),
|
||||
api.getPosts(),
|
||||
api.getSiteSettings(),
|
||||
]);
|
||||
const [categoriesResult, postsResult, settingsResult] = await Promise.allSettled([
|
||||
api.getCategories(),
|
||||
api.getPosts(),
|
||||
api.getSiteSettings(),
|
||||
]);
|
||||
|
||||
if (categoriesResult.status === 'fulfilled') {
|
||||
categories = categoriesResult.value;
|
||||
} else {
|
||||
categoriesFailed = true;
|
||||
console.error('Failed to fetch categories:', categoriesResult.reason);
|
||||
}
|
||||
if (categoriesResult.status === 'fulfilled') {
|
||||
categories = categoriesResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch categories:', categoriesResult.reason);
|
||||
}
|
||||
|
||||
if (postsResult.status === 'fulfilled') {
|
||||
posts = postsResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch category posts:', postsResult.reason);
|
||||
}
|
||||
if (postsResult.status === 'fulfilled') {
|
||||
posts = postsResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch category posts:', postsResult.reason);
|
||||
}
|
||||
|
||||
if (settingsResult.status === 'fulfilled') {
|
||||
siteSettings = settingsResult.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch category detail data:', error);
|
||||
if (settingsResult.status === 'fulfilled') {
|
||||
siteSettings = settingsResult.value;
|
||||
}
|
||||
|
||||
const requested = decodeURIComponent(slug || '').trim().toLowerCase();
|
||||
@@ -59,8 +53,8 @@ const category =
|
||||
|
||||
if (!category) {
|
||||
return new Response(null, {
|
||||
status: categoriesFailed ? 503 : 404,
|
||||
headers: categoriesFailed ? { 'Retry-After': '120' } : undefined,
|
||||
status: categoriesResult.status !== 'fulfilled' ? 503 : 404,
|
||||
headers: categoriesResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import FriendLinkCard from '../../components/FriendLinkCard.astro';
|
||||
import FriendLinkApplication from '../../components/FriendLinkApplication.astro';
|
||||
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
|
||||
import type { AppFriendLink } from '../../lib/api/client';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -120,7 +120,7 @@ const friendsFaqJsonLd = buildFaqJsonLd(friendsFaqs);
|
||||
title={`${t('friends.pageTitle')} - ${siteSettings.siteShortName}`}
|
||||
description={t('friends.pageDescription', { siteName: siteSettings.siteName })}
|
||||
siteSettings={siteSettings}
|
||||
jsonLd={[...friendsJsonLd, friendsFaqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([...friendsJsonLd, friendsFaqJsonLd])}
|
||||
>
|
||||
<PageViewTracker pageType="friends" entityId="friends-index" />
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 } from '../lib/seo';
|
||||
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';
|
||||
@@ -119,6 +119,11 @@ const systemStats = [
|
||||
{ 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<string, number>();
|
||||
@@ -153,8 +158,9 @@ const activeContentRange =
|
||||
const popularRangeCards = contentRanges.flatMap((range) =>
|
||||
range.popularPosts
|
||||
.filter((item): item is PopularPostHighlight & { post: Post } => Boolean(item.post))
|
||||
.map((item) => ({
|
||||
.map((item, index) => ({
|
||||
rangeKey: range.key,
|
||||
rank: index + 1,
|
||||
item,
|
||||
post: item.post,
|
||||
})),
|
||||
@@ -221,7 +227,6 @@ const discoverPrompt = hasActiveFilters
|
||||
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 navLinks = [
|
||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||
@@ -314,7 +319,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
siteSettings={siteSettings}
|
||||
canonical="/"
|
||||
noindex={hasActiveFilters}
|
||||
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([...homeJsonLd, homeFaqJsonLd])}
|
||||
>
|
||||
<PageViewTracker pageType="home" entityId="homepage" />
|
||||
<div class="mx-auto max-w-[1480px] px-4 py-6 sm:px-6 lg:px-8">
|
||||
@@ -325,10 +330,28 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
|
||||
<div class="mb-6 px-4">
|
||||
<CommandPrompt command={t('home.promptWelcome')} />
|
||||
<div class="ml-4 home-hero-shell">
|
||||
<div class="min-w-0">
|
||||
<div class="ml-4 home-hero-shell home-hero-shell--panel">
|
||||
<div class="home-hero-copy">
|
||||
<span class="terminal-kicker w-fit">
|
||||
<i class="fas fa-terminal"></i>
|
||||
home / overview
|
||||
</span>
|
||||
<p class="mb-1 text-lg font-bold text-[var(--primary)]">{siteSettings.heroTitle}</p>
|
||||
<p class="text-sm leading-6 text-[var(--text-secondary)]">{siteSettings.heroSubtitle}</p>
|
||||
|
||||
<div class="home-hero-glance">
|
||||
{heroGlanceStats.map((item) => (
|
||||
<div class="home-hero-glance-item">
|
||||
<span class="home-hero-glance-icon">
|
||||
<i class={`fas ${item.icon} text-[11px]`}></i>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||
<div class="text-sm font-semibold text-[var(--title-color)]">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-hero-meta">
|
||||
@@ -361,7 +384,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="grid gap-6 px-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div class="grid gap-6 px-4 xl:grid-cols-[minmax(0,1fr)_320px] 2xl:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div class="min-w-0 space-y-6">
|
||||
<div id="discover">
|
||||
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
|
||||
@@ -586,22 +609,27 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start">
|
||||
<section class="terminal-panel space-y-4">
|
||||
<aside class="home-sidebar-stack xl:sticky xl:top-24 xl:self-start">
|
||||
<section class="terminal-panel home-sidebar-card space-y-4">
|
||||
<div class="space-y-1">
|
||||
<span class="terminal-kicker w-fit">
|
||||
<i class="fas fa-compass"></i>
|
||||
side nav
|
||||
</span>
|
||||
<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">
|
||||
<div class="home-sidebar-grid">
|
||||
{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)]"
|
||||
class="home-sidebar-link group flex min-w-0 items-center gap-3 rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/75 px-3 py-2.5 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)]">
|
||||
<span class="home-sidebar-link__icon">
|
||||
<i class={`fas ${link.icon} text-[11px]`}></i>
|
||||
</span>
|
||||
<span class="min-w-0 truncate font-medium">{link.text}</span>
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{link.text}</span>
|
||||
<i class="fas fa-arrow-right text-[10px] text-[var(--text-tertiary)] transition-transform duration-200 group-hover:translate-x-0.5 group-hover:text-[var(--primary)]"></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -618,22 +646,26 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
variant="compact"
|
||||
/>
|
||||
|
||||
<section class="terminal-panel space-y-4">
|
||||
<section class="terminal-panel home-sidebar-card space-y-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<span class="terminal-kicker w-fit">
|
||||
<i class="fas fa-fire"></i>
|
||||
traffic
|
||||
</span>
|
||||
<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="home-popular-sortbar">
|
||||
<div class="home-popular-rangebar">
|
||||
{popularRangeOptions.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
data-home-popular-range={option.key}
|
||||
class:list={[
|
||||
'home-popular-sort',
|
||||
'home-popular-range',
|
||||
option.key === activeContentRange.key && 'is-active'
|
||||
]}
|
||||
>
|
||||
@@ -644,7 +676,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
|
||||
<div id="home-popular-list" class="space-y-3">
|
||||
{popularRangeCards.map(({ rangeKey, item, post }) => (
|
||||
{popularRangeCards.map(({ rangeKey, rank, item, post }) => (
|
||||
<a
|
||||
href={`/articles/${post.slug}`}
|
||||
data-home-popular-card
|
||||
@@ -655,16 +687,15 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
data-home-slug={post.slug}
|
||||
data-home-popular-views={item.pageViews}
|
||||
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)]/72 p-3 transition hover:-translate-y-0.5 hover:border-[var(--primary)]',
|
||||
'home-sidebar-popular 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 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 class="home-sidebar-popular__rank">
|
||||
<span data-home-popular-rank-label>{rank}</span>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -676,7 +707,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</span>
|
||||
</div>
|
||||
<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)]">
|
||||
<div class="home-sidebar-popular__metrics">
|
||||
<span>{t('home.views')}: {item.pageViews}</span>
|
||||
<span>{t('home.completes')}: {item.readCompletes}</span>
|
||||
<span>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
|
||||
@@ -692,9 +723,13 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="terminal-panel space-y-4">
|
||||
<section class="terminal-panel home-sidebar-card space-y-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<span class="terminal-kicker w-fit">
|
||||
<i class="fas fa-chart-simple"></i>
|
||||
stats
|
||||
</span>
|
||||
<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>
|
||||
@@ -702,41 +737,45 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
|
||||
<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="home-sidebar-stat home-sidebar-stat--metric 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-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">
|
||||
<div class="home-sidebar-stat home-sidebar-stat--metric 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-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">
|
||||
<div class="home-sidebar-stat home-sidebar-stat--metric 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-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">
|
||||
<div class="home-sidebar-stat home-sidebar-stat--metric 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-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="grid gap-3 sm:grid-cols-2">
|
||||
<div class="home-sidebar-meta-list">
|
||||
{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 class="home-sidebar-meta-item">
|
||||
<span class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</span>
|
||||
<span class="text-sm font-semibold text-[var(--title-color)]">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{sidebarFriendLinks.length > 0 && (
|
||||
<section class="terminal-panel space-y-4">
|
||||
<section class="terminal-panel home-sidebar-card space-y-4">
|
||||
<div class="space-y-1">
|
||||
<span class="terminal-kicker w-fit">
|
||||
<i class="fas fa-link"></i>
|
||||
network
|
||||
</span>
|
||||
<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>
|
||||
@@ -747,7 +786,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
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)]"
|
||||
class="home-sidebar-friend 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
|
||||
@@ -874,7 +913,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
const popularList = document.getElementById('home-popular-list');
|
||||
const popularCount = document.getElementById('home-popular-count');
|
||||
const popularRangeButtons = Array.from(document.querySelectorAll('[data-home-popular-range]'));
|
||||
const popularSortButtons = Array.from(document.querySelectorAll('[data-home-popular-sort]'));
|
||||
const readingWindowPill = document.getElementById('home-stats-window-pill');
|
||||
const readingViewsValue = document.getElementById('home-reading-views-value');
|
||||
const readingCompletesValue = document.getElementById('home-reading-completes-value');
|
||||
@@ -902,7 +940,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
category: initialHomeState.category || '',
|
||||
tag: initialHomeState.tag || '',
|
||||
range: initialHomeState.range || '7d',
|
||||
popularSort: 'views',
|
||||
};
|
||||
|
||||
function getActiveRange() {
|
||||
@@ -928,37 +965,20 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
});
|
||||
}
|
||||
|
||||
function syncPopularSortButtons() {
|
||||
popularSortButtons.forEach((button) => {
|
||||
button.classList.toggle(
|
||||
'is-active',
|
||||
(button.getAttribute('data-home-popular-sort') || 'views') === state.popularSort
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function sortPopularCards(cards) {
|
||||
const metricKey =
|
||||
state.popularSort === 'completes'
|
||||
? 'data-home-popular-completes'
|
||||
: state.popularSort === 'depth'
|
||||
? 'data-home-popular-depth'
|
||||
: 'data-home-popular-views';
|
||||
|
||||
return [...cards].sort((left, right) => {
|
||||
const leftValue = Number(left.getAttribute(metricKey) || '0');
|
||||
const rightValue = Number(right.getAttribute(metricKey) || '0');
|
||||
|
||||
if (rightValue !== leftValue) {
|
||||
return rightValue - leftValue;
|
||||
}
|
||||
|
||||
const leftViews = Number(left.getAttribute('data-home-popular-views') || '0');
|
||||
const rightViews = Number(right.getAttribute('data-home-popular-views') || '0');
|
||||
if (rightViews !== leftViews) {
|
||||
return rightViews - leftViews;
|
||||
}
|
||||
|
||||
const leftCompletes = Number(left.getAttribute('data-home-popular-completes') || '0');
|
||||
const rightCompletes = Number(right.getAttribute('data-home-popular-completes') || '0');
|
||||
if (rightCompletes !== leftCompletes) {
|
||||
return rightCompletes - leftCompletes;
|
||||
}
|
||||
|
||||
return String(left.getAttribute('data-home-slug') || '').localeCompare(
|
||||
String(right.getAttribute('data-home-slug') || '')
|
||||
);
|
||||
@@ -975,18 +995,14 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
|
||||
function syncPromptText(total) {
|
||||
const tokens = getActiveTokens();
|
||||
const activeRange = getActiveRange();
|
||||
const discoverCommand = tokens.length
|
||||
? t('home.promptDiscoverFiltered', { filters: tokens.join(' · ') })
|
||||
: t('home.promptDiscoverDefault');
|
||||
const postsCommand = tokens.length
|
||||
? t('home.promptPostsFiltered', { count: Math.min(total, previewLimit), filters: tokens.join(' · ') })
|
||||
: t('home.promptPostsDefault', { count: Math.min(total, previewLimit) });
|
||||
const popularCommand = t('home.promptPopularRange', { label: activeRange.label || state.range });
|
||||
|
||||
promptApi?.set?.('home-discover-prompt', discoverCommand, { typing: false });
|
||||
promptApi?.set?.('home-posts-prompt', postsCommand, { typing: false });
|
||||
promptApi?.set?.('home-popular-prompt', popularCommand, { typing: false });
|
||||
}
|
||||
|
||||
function syncRangeMetrics(filteredPopularCount) {
|
||||
@@ -1144,8 +1160,12 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
});
|
||||
const sortedPopular = sortPopularCards(filteredPopular);
|
||||
popularCards.forEach((card) => card.classList.add('hidden'));
|
||||
sortedPopular.slice(0, popularPreviewLimit).forEach((card) => {
|
||||
sortedPopular.slice(0, popularPreviewLimit).forEach((card, index) => {
|
||||
card.classList.remove('hidden');
|
||||
const rankLabel = card.querySelector('[data-home-popular-rank-label]');
|
||||
if (rankLabel) {
|
||||
rankLabel.textContent = String(index + 1);
|
||||
}
|
||||
popularList?.appendChild(card);
|
||||
});
|
||||
|
||||
@@ -1172,7 +1192,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
syncActiveSummary();
|
||||
syncPostTagSelection();
|
||||
syncPopularRangeButtons();
|
||||
syncPopularSortButtons();
|
||||
syncRangeMetrics(filteredPopular.length);
|
||||
syncPromptText(total);
|
||||
|
||||
@@ -1224,13 +1243,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
});
|
||||
});
|
||||
|
||||
popularSortButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
state.popularSort = button.getAttribute('data-home-popular-sort') || 'views';
|
||||
applyHomeFilters(false);
|
||||
});
|
||||
});
|
||||
|
||||
postsRoot?.addEventListener('click', (event) => {
|
||||
const target = event.target instanceof Element ? event.target.closest('a[href*="tag="]') : null;
|
||||
if (!target) return;
|
||||
|
||||
@@ -9,7 +9,7 @@ import FilterPill from '../../components/ui/FilterPill.astro';
|
||||
import ResponsiveImage from '../../components/ui/ResponsiveImage.astro';
|
||||
import { apiClient, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo';
|
||||
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
|
||||
import { parseReview, type ParsedReview, type ReviewStatus } from '../../lib/reviews';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -286,7 +286,7 @@ const reviewFaqJsonLd = buildFaqJsonLd(reviewFaqs);
|
||||
siteSettings={siteSettings}
|
||||
canonical={hasActiveFilters ? '/reviews' : undefined}
|
||||
noindex={hasActiveFilters}
|
||||
jsonLd={[...reviewsJsonLd, reviewFaqJsonLd].filter(Boolean)}
|
||||
jsonLd={compactJsonLd([...reviewsJsonLd, reviewFaqJsonLd])}
|
||||
>
|
||||
<PageViewTracker pageType="reviews" entityId="reviews-index" />
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
@@ -20,33 +20,27 @@ const isEnglish = locale.startsWith('en');
|
||||
let tags: Tag[] = [];
|
||||
let posts: Post[] = [];
|
||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||
let tagsFailed = false;
|
||||
|
||||
try {
|
||||
const [tagsResult, postsResult, settingsResult] = await Promise.allSettled([
|
||||
apiClient.getTags(),
|
||||
apiClient.getPosts(),
|
||||
apiClient.getSiteSettings(),
|
||||
]);
|
||||
const [tagsResult, postsResult, settingsResult] = await Promise.allSettled([
|
||||
apiClient.getTags(),
|
||||
apiClient.getPosts(),
|
||||
apiClient.getSiteSettings(),
|
||||
]);
|
||||
|
||||
if (tagsResult.status === 'fulfilled') {
|
||||
tags = tagsResult.value;
|
||||
} else {
|
||||
tagsFailed = true;
|
||||
console.error('Failed to fetch tags:', tagsResult.reason);
|
||||
}
|
||||
if (tagsResult.status === 'fulfilled') {
|
||||
tags = tagsResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch tags:', tagsResult.reason);
|
||||
}
|
||||
|
||||
if (postsResult.status === 'fulfilled') {
|
||||
posts = postsResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch tag posts:', postsResult.reason);
|
||||
}
|
||||
if (postsResult.status === 'fulfilled') {
|
||||
posts = postsResult.value;
|
||||
} else {
|
||||
console.error('Failed to fetch tag posts:', postsResult.reason);
|
||||
}
|
||||
|
||||
if (settingsResult.status === 'fulfilled') {
|
||||
siteSettings = settingsResult.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tag detail data:', error);
|
||||
if (settingsResult.status === 'fulfilled') {
|
||||
siteSettings = settingsResult.value;
|
||||
}
|
||||
|
||||
const requested = decodeURIComponent(slug || '').trim().toLowerCase();
|
||||
@@ -59,8 +53,8 @@ const tag =
|
||||
|
||||
if (!tag) {
|
||||
return new Response(null, {
|
||||
status: tagsFailed ? 503 : 404,
|
||||
headers: tagsFailed ? { 'Retry-After': '120' } : undefined,
|
||||
status: tagsResult.status !== 'fulfilled' ? 503 : 404,
|
||||
headers: tagsResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -621,8 +621,40 @@ html.dark {
|
||||
@apply flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between;
|
||||
}
|
||||
|
||||
.home-hero-shell--panel {
|
||||
@apply rounded-[28px] border px-5 py-5 sm:px-6;
|
||||
border-color: color-mix(in oklab, var(--primary) 10%, var(--border-color));
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(var(--primary-rgb), 0.1), transparent 28%),
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--terminal-bg) 98%, transparent), color-mix(in oklab, var(--header-bg) 92%, transparent));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.34),
|
||||
0 18px 42px rgba(var(--text-rgb), 0.04);
|
||||
}
|
||||
|
||||
.home-hero-copy {
|
||||
@apply min-w-0 space-y-3;
|
||||
}
|
||||
|
||||
.home-hero-glance {
|
||||
@apply grid gap-2 pt-1 sm:grid-cols-3;
|
||||
}
|
||||
|
||||
.home-hero-glance-item {
|
||||
@apply flex items-center gap-3 rounded-2xl border px-3 py-3;
|
||||
border-color: color-mix(in oklab, var(--primary) 8%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 88%, transparent);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
.home-hero-glance-icon {
|
||||
@apply flex h-9 w-9 shrink-0 items-center justify-center rounded-xl;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
.home-hero-meta {
|
||||
@apply flex flex-wrap items-center gap-2;
|
||||
@apply flex flex-wrap items-center gap-2 lg:max-w-[18rem] lg:justify-end;
|
||||
}
|
||||
|
||||
.home-nav-strip {
|
||||
@@ -777,25 +809,25 @@ html.dark {
|
||||
color: color-mix(in oklab, var(--accent-color, var(--primary)) 78%, var(--title-color));
|
||||
}
|
||||
|
||||
.home-popular-sortbar {
|
||||
.home-popular-rangebar {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
|
||||
.home-popular-sort {
|
||||
.home-popular-range {
|
||||
@apply inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-mono transition-all duration-200;
|
||||
border-color: color-mix(in oklab, var(--primary) 12%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 96%, transparent);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.home-popular-sort:hover {
|
||||
.home-popular-range:hover {
|
||||
border-color: color-mix(in oklab, var(--primary) 22%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--primary) 6%, var(--terminal-bg));
|
||||
color: var(--title-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.home-popular-sort.is-active {
|
||||
.home-popular-range.is-active {
|
||||
border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
|
||||
color: var(--title-color);
|
||||
@@ -804,6 +836,92 @@ html.dark {
|
||||
0 10px 24px rgba(var(--text-rgb), 0.04);
|
||||
}
|
||||
|
||||
.home-sidebar-stack {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.home-sidebar-grid {
|
||||
@apply grid gap-2 sm:grid-cols-2 xl:grid-cols-2;
|
||||
}
|
||||
|
||||
.home-sidebar-card {
|
||||
@apply rounded-[26px] border;
|
||||
border-color: color-mix(in oklab, var(--primary) 9%, var(--border-color));
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(var(--primary-rgb), 0.08), transparent 30%),
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--terminal-bg) 98%, transparent), color-mix(in oklab, var(--header-bg) 92%, transparent));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.34),
|
||||
0 16px 36px rgba(var(--text-rgb), 0.04);
|
||||
}
|
||||
|
||||
.home-sidebar-link {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.home-sidebar-link__icon {
|
||||
@apply flex h-8 w-8 shrink-0 items-center justify-center rounded-xl;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
.home-sidebar-link:hover {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
||||
0 14px 28px rgba(var(--text-rgb), 0.05);
|
||||
}
|
||||
|
||||
.home-sidebar-popular {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.home-sidebar-popular__rank {
|
||||
@apply inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
.home-sidebar-popular__metrics {
|
||||
@apply mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11px];
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.home-sidebar-popular:hover {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
||||
0 14px 30px rgba(var(--text-rgb), 0.05);
|
||||
}
|
||||
|
||||
.home-sidebar-stat {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
.home-sidebar-stat--metric {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--terminal-bg) 92%, transparent), color-mix(in oklab, var(--header-bg) 92%, transparent)),
|
||||
linear-gradient(135deg, rgba(var(--primary-rgb), 0.035), transparent 70%);
|
||||
}
|
||||
|
||||
.home-sidebar-meta-list {
|
||||
@apply divide-y rounded-2xl border;
|
||||
border-color: color-mix(in oklab, var(--primary) 9%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 78%, transparent);
|
||||
}
|
||||
|
||||
.home-sidebar-meta-item {
|
||||
@apply flex items-center justify-between gap-3 px-4 py-3;
|
||||
}
|
||||
|
||||
.home-sidebar-friend {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
.home-sidebar-friend:hover {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
0 12px 28px rgba(var(--text-rgb), 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.home-tag-cloud__item:hover {
|
||||
transform: translateY(-1px);
|
||||
|
||||
Reference in New Issue
Block a user