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

- 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:
2026-04-03 13:46:08 +08:00
parent 0f2342a713
commit 1df179c327
13 changed files with 269 additions and 147 deletions

View File

@@ -17,7 +17,8 @@ const {
const { locale, t, buildLocaleUrl } = getI18n(Astro); const { locale, t, buildLocaleUrl } = getI18n(Astro);
const aiEnabled = Boolean(Astro.props.siteSettings?.ai?.enabled); const aiEnabled = Boolean(Astro.props.siteSettings?.ai?.enabled);
const musicEnabled = Astro.props.siteSettings?.musicEnabled ?? true; 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() (item) => item?.title?.trim() && item?.url?.trim()
); );
const musicPlaylistPayload = JSON.stringify(musicPlaylist); const musicPlaylistPayload = JSON.stringify(musicPlaylist);

View File

@@ -150,7 +150,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title>{title}</title> <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" /> <slot name="head" />
<style is:inline> <style is:inline>
@@ -467,8 +467,6 @@ const i18nPayload = JSON.stringify({ locale, messages });
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
media="print"
onload="this.media='all'"
/> />
<noscript> <noscript>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> <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 <link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" 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" rel="stylesheet"
media="print"
onload="this.media='all'"
> >
<noscript> <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"> <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">

View File

@@ -19,6 +19,8 @@ export interface DiscoveryFaqOptions {
signals?: string[]; signals?: string[];
} }
export type JsonLdObject = Record<string, unknown>;
function normalizeWhitespace(value: string): string { function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, ' ').trim(); 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) { export function buildPostItemList(posts: Post[], siteUrl: string) {
return posts.map((post, index) => ({ return posts.map((post, index) => ({
'@type': 'ListItem', '@type': 'ListItem',

View File

@@ -9,7 +9,7 @@ import StatsList from '../../components/StatsList.astro';
import TechStackList from '../../components/TechStackList.astro'; import TechStackList from '../../components/TechStackList.astro';
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client'; import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
import { getI18n } from '../../lib/i18n'; import { getI18n } from '../../lib/i18n';
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo'; import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
export const prerender = false; export const prerender = false;
@@ -130,7 +130,7 @@ const aboutJsonLd = [
title={`${t('about.pageTitle')} - ${siteSettings.siteShortName}`} title={`${t('about.pageTitle')} - ${siteSettings.siteShortName}`}
description={siteSettings.siteDescription} description={siteSettings.siteDescription}
siteSettings={siteSettings} siteSettings={siteSettings}
jsonLd={aboutJsonLd.filter(Boolean)} jsonLd={compactJsonLd(aboutJsonLd)}
> >
<PageViewTracker pageType="about" entityId="about" /> <PageViewTracker pageType="about" entityId="about" />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@@ -23,6 +23,7 @@ import {
buildArticleHighlights, buildArticleHighlights,
buildArticlePreviewParagraphs, buildArticlePreviewParagraphs,
buildArticleSynopsis, buildArticleSynopsis,
compactJsonLd,
resolvePostUpdatedAt, resolvePostUpdatedAt,
} from '../../lib/seo'; } from '../../lib/seo';
import type { PopularPostHighlight } from '../../lib/types'; import type { PopularPostHighlight } from '../../lib/types';
@@ -45,7 +46,6 @@ let post = null;
let siteSettings = DEFAULT_SITE_SETTINGS; let siteSettings = DEFAULT_SITE_SETTINGS;
const analyticsEndpoint = `${resolvePublicApiBaseUrl(Astro.url)}/analytics/content`; const analyticsEndpoint = `${resolvePublicApiBaseUrl(Astro.url)}/analytics/content`;
let homeData: Awaited<ReturnType<typeof apiClient.getHomePageData>> | null = null; let homeData: Awaited<ReturnType<typeof apiClient.getHomePageData>> | null = null;
let postLookupFailed = false;
const [postResult, siteSettingsResult, homeDataResult] = await Promise.allSettled([ const [postResult, siteSettingsResult, homeDataResult] = await Promise.allSettled([
apiClient.getPostBySlug(slug ?? ''), apiClient.getPostBySlug(slug ?? ''),
@@ -56,7 +56,6 @@ const [postResult, siteSettingsResult, homeDataResult] = await Promise.allSettle
if (postResult.status === 'fulfilled') { if (postResult.status === 'fulfilled') {
post = postResult.value; post = postResult.value;
} else { } else {
postLookupFailed = true;
console.error('API Error:', postResult.reason); console.error('API Error:', postResult.reason);
} }
@@ -77,8 +76,8 @@ if (homeDataResult.status === 'fulfilled') {
if (!post) { if (!post) {
return new Response(null, { return new Response(null, {
status: postLookupFailed ? 503 : 404, status: postResult.status !== 'fulfilled' ? 503 : 404,
headers: postLookupFailed ? { 'Retry-After': '120' } : undefined, headers: postResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
}); });
} }
@@ -371,7 +370,7 @@ const breadcrumbJsonLd = {
ogImage={ogImage} ogImage={ogImage}
ogType="article" ogType="article"
twitterCard="summary_large_image" twitterCard="summary_large_image"
jsonLd={[articleJsonLd, breadcrumbJsonLd, faqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([articleJsonLd, breadcrumbJsonLd, faqJsonLd])}
> >
<Fragment slot="head"> <Fragment slot="head">
<meta property="article:published_time" content={publishedAt} /> <meta property="article:published_time" content={publishedAt} />

View File

@@ -9,7 +9,7 @@ import FilterPill from '../../components/ui/FilterPill.astro';
import PostCard from '../../components/PostCard.astro'; import PostCard from '../../components/PostCard.astro';
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client'; import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
import { getI18n } from '../../lib/i18n'; 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 type { Category, Post, Tag } from '../../lib/types';
import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../../lib/utils'; import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../../lib/utils';
@@ -193,7 +193,7 @@ const buildArticlesUrl = ({
siteSettings={siteSettings} siteSettings={siteSettings}
canonical={canonicalUrl} canonical={canonicalUrl}
noindex={hasActiveFilters} noindex={hasActiveFilters}
jsonLd={[...jsonLd, articleIndexFaqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([...jsonLd, articleIndexFaqJsonLd])}
> >
<PageViewTracker pageType="articles" entityId={`articles-page-${currentPage}`} /> <PageViewTracker pageType="articles" entityId={`articles-page-${currentPage}`} />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@@ -6,7 +6,7 @@ import SharePanel from '../../components/seo/SharePanel.astro';
import CommandPrompt from '../../components/ui/CommandPrompt.astro'; import CommandPrompt from '../../components/ui/CommandPrompt.astro';
import { api, DEFAULT_SITE_SETTINGS, resolvePublicApiBaseUrl } from '../../lib/api/client'; import { api, DEFAULT_SITE_SETTINGS, resolvePublicApiBaseUrl } from '../../lib/api/client';
import { getI18n } from '../../lib/i18n'; import { getI18n } from '../../lib/i18n';
import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs } from '../../lib/seo'; import { buildDiscoveryHighlights, buildFaqJsonLd, buildPageFaqs, compactJsonLd } from '../../lib/seo';
export const prerender = false; export const prerender = false;
@@ -104,7 +104,7 @@ const askFaqJsonLd = buildFaqJsonLd(askFaqs);
title={`${t('ask.pageTitle')} | ${siteSettings.siteShortName}`} title={`${t('ask.pageTitle')} | ${siteSettings.siteShortName}`}
description={t('ask.pageDescription', { siteName: siteSettings.siteName })} description={t('ask.pageDescription', { siteName: siteSettings.siteName })}
siteSettings={siteSettings} siteSettings={siteSettings}
jsonLd={[...askJsonLd, askFaqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([...askJsonLd, askFaqJsonLd])}
> >
<PageViewTracker pageType="ask" entityId="ask" /> <PageViewTracker pageType="ask" entityId="ask" />
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">

View File

@@ -20,33 +20,27 @@ const isEnglish = locale.startsWith('en');
let categories: Category[] = []; let categories: Category[] = [];
let posts: Post[] = []; let posts: Post[] = [];
let siteSettings = DEFAULT_SITE_SETTINGS; let siteSettings = DEFAULT_SITE_SETTINGS;
let categoriesFailed = false;
try { const [categoriesResult, postsResult, settingsResult] = await Promise.allSettled([
const [categoriesResult, postsResult, settingsResult] = await Promise.allSettled([ api.getCategories(),
api.getCategories(), api.getPosts(),
api.getPosts(), api.getSiteSettings(),
api.getSiteSettings(), ]);
]);
if (categoriesResult.status === 'fulfilled') { if (categoriesResult.status === 'fulfilled') {
categories = categoriesResult.value; categories = categoriesResult.value;
} else { } else {
categoriesFailed = true; console.error('Failed to fetch categories:', categoriesResult.reason);
console.error('Failed to fetch categories:', categoriesResult.reason); }
}
if (postsResult.status === 'fulfilled') { if (postsResult.status === 'fulfilled') {
posts = postsResult.value; posts = postsResult.value;
} else { } else {
console.error('Failed to fetch category posts:', postsResult.reason); console.error('Failed to fetch category posts:', postsResult.reason);
} }
if (settingsResult.status === 'fulfilled') { if (settingsResult.status === 'fulfilled') {
siteSettings = settingsResult.value; siteSettings = settingsResult.value;
}
} catch (error) {
console.error('Failed to fetch category detail data:', error);
} }
const requested = decodeURIComponent(slug || '').trim().toLowerCase(); const requested = decodeURIComponent(slug || '').trim().toLowerCase();
@@ -59,8 +53,8 @@ const category =
if (!category) { if (!category) {
return new Response(null, { return new Response(null, {
status: categoriesFailed ? 503 : 404, status: categoriesResult.status !== 'fulfilled' ? 503 : 404,
headers: categoriesFailed ? { 'Retry-After': '120' } : undefined, headers: categoriesResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
}); });
} }

View File

@@ -9,7 +9,7 @@ import FriendLinkCard from '../../components/FriendLinkCard.astro';
import FriendLinkApplication from '../../components/FriendLinkApplication.astro'; import FriendLinkApplication from '../../components/FriendLinkApplication.astro';
import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client'; import { api, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
import { getI18n } from '../../lib/i18n'; 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'; import type { AppFriendLink } from '../../lib/api/client';
export const prerender = false; export const prerender = false;
@@ -120,7 +120,7 @@ const friendsFaqJsonLd = buildFaqJsonLd(friendsFaqs);
title={`${t('friends.pageTitle')} - ${siteSettings.siteShortName}`} title={`${t('friends.pageTitle')} - ${siteSettings.siteShortName}`}
description={t('friends.pageDescription', { siteName: siteSettings.siteName })} description={t('friends.pageDescription', { siteName: siteSettings.siteName })}
siteSettings={siteSettings} siteSettings={siteSettings}
jsonLd={[...friendsJsonLd, friendsFaqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([...friendsJsonLd, friendsFaqJsonLd])}
> >
<PageViewTracker pageType="friends" entityId="friends-index" /> <PageViewTracker pageType="friends" entityId="friends-index" />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@@ -13,7 +13,7 @@ import TechStackList from '../components/TechStackList.astro';
import { terminalConfig } from '../lib/config/terminal'; import { terminalConfig } from '../lib/config/terminal';
import { api, DEFAULT_SITE_SETTINGS } from '../lib/api/client'; import { api, DEFAULT_SITE_SETTINGS } from '../lib/api/client';
import { formatReadTime, getI18n } from '../lib/i18n'; 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 { AppFriendLink } from '../lib/api/client';
import type { ContentOverview, ContentWindowHighlight, PopularPostHighlight, Post } from '../lib/types'; import type { ContentOverview, ContentWindowHighlight, PopularPostHighlight, Post } from '../lib/types';
import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../lib/utils'; 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.categories'), value: String(categories.length) },
{ label: t('common.friends'), value: String(friendLinks.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 techStack = siteSettings.techStack.map(name => ({ name }));
const tagFrequency = new Map<string, number>(); const tagFrequency = new Map<string, number>();
@@ -153,8 +158,9 @@ const activeContentRange =
const popularRangeCards = contentRanges.flatMap((range) => const popularRangeCards = contentRanges.flatMap((range) =>
range.popularPosts range.popularPosts
.filter((item): item is PopularPostHighlight & { post: Post } => Boolean(item.post)) .filter((item): item is PopularPostHighlight & { post: Post } => Boolean(item.post))
.map((item) => ({ .map((item, index) => ({
rangeKey: range.key, rangeKey: range.key,
rank: index + 1,
item, item,
post: item.post, post: item.post,
})), })),
@@ -221,7 +227,6 @@ const discoverPrompt = hasActiveFilters
const postsPrompt = hasActiveFilters 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 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' },
@@ -314,7 +319,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
siteSettings={siteSettings} siteSettings={siteSettings}
canonical="/" canonical="/"
noindex={hasActiveFilters} noindex={hasActiveFilters}
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([...homeJsonLd, homeFaqJsonLd])}
> >
<PageViewTracker pageType="home" entityId="homepage" /> <PageViewTracker pageType="home" entityId="homepage" />
<div class="mx-auto max-w-[1480px] px-4 py-6 sm:px-6 lg:px-8"> <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"> <div class="mb-6 px-4">
<CommandPrompt command={t('home.promptWelcome')} /> <CommandPrompt command={t('home.promptWelcome')} />
<div class="ml-4 home-hero-shell"> <div class="ml-4 home-hero-shell home-hero-shell--panel">
<div class="min-w-0"> <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="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> <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>
<div class="home-hero-meta"> <div class="home-hero-meta">
@@ -361,7 +384,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div> </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 class="min-w-0 space-y-6">
<div id="discover"> <div id="discover">
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} /> <CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
@@ -586,22 +609,27 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div> </div>
</div> </div>
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start"> <aside class="home-sidebar-stack xl:sticky xl:top-24 xl:self-start">
<section class="terminal-panel space-y-4"> <section class="terminal-panel home-sidebar-card space-y-4">
<div class="space-y-1"> <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> <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> <p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.quickLinksDesc}</p>
</div> </div>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-2"> <div class="home-sidebar-grid">
{navLinks.map(link => ( {navLinks.map(link => (
<a <a
href={link.href} 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> <i class={`fas ${link.icon} text-[11px]`}></i>
</span> </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> </a>
))} ))}
</div> </div>
@@ -618,22 +646,26 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
variant="compact" 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 class="flex items-start justify-between gap-3">
<div> <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> <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> <p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.popularDesc}</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="home-popular-sortbar"> <div class="home-popular-rangebar">
{popularRangeOptions.map((option) => ( {popularRangeOptions.map((option) => (
<button <button
type="button" type="button"
data-home-popular-range={option.key} data-home-popular-range={option.key}
class:list={[ class:list={[
'home-popular-sort', 'home-popular-range',
option.key === activeContentRange.key && 'is-active' option.key === activeContentRange.key && 'is-active'
]} ]}
> >
@@ -644,7 +676,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</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, rank, item, post }) => (
<a <a
href={`/articles/${post.slug}`} href={`/articles/${post.slug}`}
data-home-popular-card data-home-popular-card
@@ -655,16 +687,15 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
data-home-slug={post.slug} data-home-slug={post.slug}
data-home-popular-views={item.pageViews} data-home-popular-views={item.pageViews}
data-home-popular-completes={item.readCompletes} data-home-popular-completes={item.readCompletes}
data-home-popular-depth={Math.round(item.avgProgressPercent)}
class:list={[ 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' !initialPopularVisibleKeys.has(`${rangeKey}:${post.slug}`) && 'hidden'
]} ]}
style={getAccentVars(getPostTypeTheme(post.type))} style={getAccentVars(getPostTypeTheme(post.type))}
> >
<div class="flex items-start 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)]"> <span class="home-sidebar-popular__rank">
{item.pageViews} <span data-home-popular-rank-label>{rank}</span>
</span> </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">
@@ -676,7 +707,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</span> </span>
</div> </div>
<h4 class="mt-2 line-clamp-2 text-sm font-semibold leading-6 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>
<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.views')}: {item.pageViews}</span>
<span>{t('home.completes')}: {item.readCompletes}</span> <span>{t('home.completes')}: {item.readCompletes}</span>
<span>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span> <span>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
@@ -692,9 +723,13 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div> </div>
</section> </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 class="flex items-start justify-between gap-3">
<div> <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> <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> <p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.statsDesc}</p>
</div> </div>
@@ -702,41 +737,45 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
</div> </div>
<div class="grid gap-3 sm:grid-cols-2 xl: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="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 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 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="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 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 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="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 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-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="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 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 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> <p class="mt-2 text-xs text-[var(--text-secondary)]">{t('common.posts')}: {allPosts.length}</p>
</div> </div>
</div> </div>
<div class="grid gap-3 sm:grid-cols-2"> <div class="home-sidebar-meta-list">
{systemStats.slice(0, 4).map((item) => ( {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="home-sidebar-meta-item">
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div> <span class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</span>
<div class="mt-2 text-lg font-semibold text-[var(--title-color)]">{item.value}</div> <span class="text-sm font-semibold text-[var(--title-color)]">{item.value}</span>
</div> </div>
))} ))}
</div> </div>
</section> </section>
{sidebarFriendLinks.length > 0 && ( {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"> <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> <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> <p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.friendsDesc}</p>
</div> </div>
@@ -747,7 +786,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
href={friend.url} href={friend.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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 ? ( {friend.avatar ? (
<img <img
@@ -874,7 +913,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
const popularList = document.getElementById('home-popular-list'); const popularList = document.getElementById('home-popular-list');
const popularCount = document.getElementById('home-popular-count'); const popularCount = document.getElementById('home-popular-count');
const popularRangeButtons = Array.from(document.querySelectorAll('[data-home-popular-range]')); 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 readingWindowPill = document.getElementById('home-stats-window-pill');
const readingViewsValue = document.getElementById('home-reading-views-value'); const readingViewsValue = document.getElementById('home-reading-views-value');
const readingCompletesValue = document.getElementById('home-reading-completes-value'); const readingCompletesValue = document.getElementById('home-reading-completes-value');
@@ -902,7 +940,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
category: initialHomeState.category || '', category: initialHomeState.category || '',
tag: initialHomeState.tag || '', tag: initialHomeState.tag || '',
range: initialHomeState.range || '7d', range: initialHomeState.range || '7d',
popularSort: 'views',
}; };
function getActiveRange() { 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) { 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) => { 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 leftViews = Number(left.getAttribute('data-home-popular-views') || '0');
const rightViews = Number(right.getAttribute('data-home-popular-views') || '0'); const rightViews = Number(right.getAttribute('data-home-popular-views') || '0');
if (rightViews !== leftViews) { if (rightViews !== leftViews) {
return 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( return String(left.getAttribute('data-home-slug') || '').localeCompare(
String(right.getAttribute('data-home-slug') || '') String(right.getAttribute('data-home-slug') || '')
); );
@@ -975,18 +995,14 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
function syncPromptText(total) { function syncPromptText(total) {
const tokens = getActiveTokens(); const tokens = getActiveTokens();
const activeRange = getActiveRange();
const discoverCommand = tokens.length const discoverCommand = tokens.length
? t('home.promptDiscoverFiltered', { filters: tokens.join(' · ') }) ? t('home.promptDiscoverFiltered', { filters: tokens.join(' · ') })
: t('home.promptDiscoverDefault'); : t('home.promptDiscoverDefault');
const postsCommand = tokens.length const postsCommand = tokens.length
? t('home.promptPostsFiltered', { count: Math.min(total, previewLimit), filters: tokens.join(' · ') }) ? t('home.promptPostsFiltered', { count: Math.min(total, previewLimit), filters: tokens.join(' · ') })
: t('home.promptPostsDefault', { count: Math.min(total, previewLimit) }); : 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-discover-prompt', discoverCommand, { typing: false });
promptApi?.set?.('home-posts-prompt', postsCommand, { typing: false }); promptApi?.set?.('home-posts-prompt', postsCommand, { typing: false });
promptApi?.set?.('home-popular-prompt', popularCommand, { typing: false });
} }
function syncRangeMetrics(filteredPopularCount) { function syncRangeMetrics(filteredPopularCount) {
@@ -1144,8 +1160,12 @@ 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.slice(0, popularPreviewLimit).forEach((card) => { sortedPopular.slice(0, popularPreviewLimit).forEach((card, index) => {
card.classList.remove('hidden'); card.classList.remove('hidden');
const rankLabel = card.querySelector('[data-home-popular-rank-label]');
if (rankLabel) {
rankLabel.textContent = String(index + 1);
}
popularList?.appendChild(card); popularList?.appendChild(card);
}); });
@@ -1172,7 +1192,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
syncActiveSummary(); syncActiveSummary();
syncPostTagSelection(); syncPostTagSelection();
syncPopularRangeButtons(); syncPopularRangeButtons();
syncPopularSortButtons();
syncRangeMetrics(filteredPopular.length); syncRangeMetrics(filteredPopular.length);
syncPromptText(total); 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) => { postsRoot?.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target.closest('a[href*="tag="]') : null; const target = event.target instanceof Element ? event.target.closest('a[href*="tag="]') : null;
if (!target) return; if (!target) return;

View File

@@ -9,7 +9,7 @@ import FilterPill from '../../components/ui/FilterPill.astro';
import ResponsiveImage from '../../components/ui/ResponsiveImage.astro'; import ResponsiveImage from '../../components/ui/ResponsiveImage.astro';
import { apiClient, DEFAULT_SITE_SETTINGS } from '../../lib/api/client'; import { apiClient, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
import { getI18n } from '../../lib/i18n'; 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'; import { parseReview, type ParsedReview, type ReviewStatus } from '../../lib/reviews';
export const prerender = false; export const prerender = false;
@@ -286,7 +286,7 @@ const reviewFaqJsonLd = buildFaqJsonLd(reviewFaqs);
siteSettings={siteSettings} siteSettings={siteSettings}
canonical={hasActiveFilters ? '/reviews' : undefined} canonical={hasActiveFilters ? '/reviews' : undefined}
noindex={hasActiveFilters} noindex={hasActiveFilters}
jsonLd={[...reviewsJsonLd, reviewFaqJsonLd].filter(Boolean)} jsonLd={compactJsonLd([...reviewsJsonLd, reviewFaqJsonLd])}
> >
<PageViewTracker pageType="reviews" entityId="reviews-index" /> <PageViewTracker pageType="reviews" entityId="reviews-index" />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@@ -20,33 +20,27 @@ const isEnglish = locale.startsWith('en');
let tags: Tag[] = []; let tags: Tag[] = [];
let posts: Post[] = []; let posts: Post[] = [];
let siteSettings = DEFAULT_SITE_SETTINGS; let siteSettings = DEFAULT_SITE_SETTINGS;
let tagsFailed = false;
try { const [tagsResult, postsResult, settingsResult] = await Promise.allSettled([
const [tagsResult, postsResult, settingsResult] = await Promise.allSettled([ apiClient.getTags(),
apiClient.getTags(), apiClient.getPosts(),
apiClient.getPosts(), apiClient.getSiteSettings(),
apiClient.getSiteSettings(), ]);
]);
if (tagsResult.status === 'fulfilled') { if (tagsResult.status === 'fulfilled') {
tags = tagsResult.value; tags = tagsResult.value;
} else { } else {
tagsFailed = true; console.error('Failed to fetch tags:', tagsResult.reason);
console.error('Failed to fetch tags:', tagsResult.reason); }
}
if (postsResult.status === 'fulfilled') { if (postsResult.status === 'fulfilled') {
posts = postsResult.value; posts = postsResult.value;
} else { } else {
console.error('Failed to fetch tag posts:', postsResult.reason); console.error('Failed to fetch tag posts:', postsResult.reason);
} }
if (settingsResult.status === 'fulfilled') { if (settingsResult.status === 'fulfilled') {
siteSettings = settingsResult.value; siteSettings = settingsResult.value;
}
} catch (error) {
console.error('Failed to fetch tag detail data:', error);
} }
const requested = decodeURIComponent(slug || '').trim().toLowerCase(); const requested = decodeURIComponent(slug || '').trim().toLowerCase();
@@ -59,8 +53,8 @@ const tag =
if (!tag) { if (!tag) {
return new Response(null, { return new Response(null, {
status: tagsFailed ? 503 : 404, status: tagsResult.status !== 'fulfilled' ? 503 : 404,
headers: tagsFailed ? { 'Retry-After': '120' } : undefined, headers: tagsResult.status !== 'fulfilled' ? { 'Retry-After': '120' } : undefined,
}); });
} }

View File

@@ -621,8 +621,40 @@ html.dark {
@apply flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between; @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 { .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 { .home-nav-strip {
@@ -777,25 +809,25 @@ html.dark {
color: color-mix(in oklab, var(--accent-color, var(--primary)) 78%, var(--title-color)); 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; @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; @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)); border-color: color-mix(in oklab, var(--primary) 12%, var(--border-color));
background: color-mix(in oklab, var(--terminal-bg) 96%, transparent); background: color-mix(in oklab, var(--terminal-bg) 96%, transparent);
color: var(--text-secondary); color: var(--text-secondary);
} }
.home-popular-sort:hover { .home-popular-range:hover {
border-color: color-mix(in oklab, var(--primary) 22%, var(--border-color)); border-color: color-mix(in oklab, var(--primary) 22%, var(--border-color));
background: color-mix(in oklab, var(--primary) 6%, var(--terminal-bg)); background: color-mix(in oklab, var(--primary) 6%, var(--terminal-bg));
color: var(--title-color); color: var(--title-color);
transform: translateY(-1px); 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)); border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg)); background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
color: var(--title-color); color: var(--title-color);
@@ -804,6 +836,92 @@ html.dark {
0 10px 24px rgba(var(--text-rgb), 0.04); 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) { @media (max-width: 640px) {
.home-tag-cloud__item:hover { .home-tag-cloud__item:hover {
transform: translateY(-1px); transform: translateY(-1px);