feat: add SharePanel component for social sharing with QR code support
Some checks failed
docker-images / resolve-build-targets (push) Successful in 7s
ui-regression / playwright-regression (push) Failing after 13m47s
docker-images / build-and-push (push) Failing after 7s
docker-images / submit-indexnow (push) Has been skipped

- Implemented SharePanel component in `SharePanel.astro` for sharing content on social media platforms.
- Integrated QR code generation for WeChat sharing using the `qrcode` library.
- Added localization support for English and Chinese languages.
- Created utility functions in `seo.ts` for building article summaries and FAQs.
- Introduced API routes for serving IndexNow key and generating full LLM catalog and summaries.
- Enhanced SEO capabilities with structured data for articles and pages.
This commit is contained in:
2026-04-02 14:15:21 +08:00
parent a516be2e91
commit 3628a46ed1
53 changed files with 4390 additions and 91 deletions

View File

@@ -1,5 +1,8 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import DiscoveryBrief from '../components/seo/DiscoveryBrief.astro';
import PageViewTracker from '../components/seo/PageViewTracker.astro';
import SharePanel from '../components/seo/SharePanel.astro';
import TerminalWindow from '../components/ui/TerminalWindow.astro';
import CommandPrompt from '../components/ui/CommandPrompt.astro';
import FilterPill from '../components/ui/FilterPill.astro';
@@ -12,6 +15,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 type { AppFriendLink } from '../lib/api/client';
import type { ContentOverview, ContentWindowHighlight, PopularPostHighlight, Post } from '../lib/types';
import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../lib/utils';
@@ -63,6 +67,7 @@ let contentOverview: ContentOverview = {
};
let apiError: string | null = null;
const { locale, t } = getI18n(Astro);
const isEnglish = locale.startsWith('en');
const formatDurationMs = (value: number | undefined) => {
if (!value || value <= 0) return locale === 'en' ? 'N/A' : '暂无';
@@ -221,9 +226,68 @@ const navLinks = [
{ icon: 'fa-user-secret', text: t('nav.about'), href: '/about' },
...(siteSettings.ai.enabled ? [{ icon: 'fa-robot', text: t('nav.ask'), href: '/ask' }] : []),
];
const siteBaseUrl = (siteSettings.siteUrl || new URL(Astro.request.url).origin).replace(/\/$/, '');
const homeJsonLd = [
{
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: siteSettings.siteTitle,
description: siteSettings.siteDescription,
url: siteBaseUrl,
inLanguage: locale,
},
{
'@context': 'https://schema.org',
'@type': 'ItemList',
name: `${siteSettings.siteName} recent posts`,
itemListElement: buildPostItemList(recentPosts, siteBaseUrl),
},
];
const homeShareCopy = isEnglish
? {
badge: 'site entry',
title: 'Share the homepage',
description:
'Use the homepage as the canonical top-level entry for people and AI search to branch into articles, taxonomies, reviews, and profile context.',
}
: {
badge: '站点入口',
title: '分享首页总入口',
description: '把首页当成站点的规范总入口分发出去,方便用户和 AI 搜索继续进入文章、分类、评测和个人介绍等核心页面。',
};
const homeBriefHighlights = buildDiscoveryHighlights([
siteSettings.siteDescription,
siteSettings.heroSubtitle,
siteSettings.ownerBio,
`${t('common.posts')}: ${allPosts.length}`,
`${t('common.categories')}: ${categories.length}`,
`${t('common.tags')}: ${tags.length}`,
]);
const homeFaqs = buildPageFaqs({
locale,
pageTitle: siteSettings.siteTitle,
summary: siteSettings.heroSubtitle || siteSettings.siteDescription,
primaryLabel: isEnglish ? 'homepage' : '首页',
primaryUrl: siteBaseUrl,
relatedLinks: [
{ label: t('nav.articles'), url: `${siteBaseUrl}/articles` },
{ label: t('nav.categories'), url: `${siteBaseUrl}/categories` },
{ label: t('nav.about'), url: `${siteBaseUrl}/about` },
],
signals: homeBriefHighlights,
});
const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
---
<BaseLayout title={siteSettings.siteTitle} description={siteSettings.siteDescription} siteSettings={siteSettings}>
<BaseLayout
title={siteSettings.siteTitle}
description={siteSettings.siteDescription}
siteSettings={siteSettings}
canonical="/"
noindex={hasActiveFilters}
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
>
<PageViewTracker pageType="home" entityId="homepage" />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<TerminalWindow title={terminalConfig.title} class="w-full">
<div class="mb-5 px-4 overflow-x-auto">
@@ -266,6 +330,31 @@ const navLinks = [
</a>
))}
</div>
<div class="ml-4 mt-4">
<SharePanel
shareTitle={siteSettings.siteTitle}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
canonicalUrl={siteBaseUrl}
badge={homeShareCopy.badge}
kicker="geo / homepage"
title={homeShareCopy.title}
description={homeShareCopy.description}
stats={systemStats.slice(0, 4)}
wechatShareQrEnabled={siteSettings.seo.wechatShareQrEnabled}
/>
</div>
<div class="ml-4 mt-4">
<DiscoveryBrief
badge={isEnglish ? 'site brief' : '站点摘要'}
kicker="geo / overview"
title={isEnglish ? 'AI-readable site brief' : '给 AI 看的站点摘要'}
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
highlights={homeBriefHighlights}
faqs={homeFaqs}
/>
</div>
</div>
{apiError && (