+ {(
+ [
+ { key: 'reading' as PostCategory, more: locale === 'en' ? '/en/books/' : '/books/' },
+ { key: 'tech' as PostCategory, more: locale === 'en' ? '/en/archives/' : '/archives/' },
+ ] as const
+ ).map(({ key, more }) => {
+ const rows = posts.filter((post) => post.data.category === key).slice(0, perTrack);
+ if (rows.length === 0) return null;
+ return (
+
+ );
+ })}
+
+) : fold ? (
{[...byYear.entries()].map(([year, list], i) => (
@@ -244,6 +288,28 @@ const byMonth = (list: CollectionEntry<'posts'>[]) => {
}
.more-link:hover { border-bottom-color: var(--accent); }
+ /* ---- 首页分轨形态(perTrack):阅读/技朮两段,各段自带更多出口 ---- */
+ .track-sec + .track-sec { margin-top: var(--space-5); }
+ .track-sec-head {
+ display: flex;
+ align-items: baseline;
+ gap: 0.35rem;
+ justify-content: space-between;
+ margin: 0 0 var(--space-1);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--ink-3);
+ letter-spacing: 0.05em;
+ }
+ .track-sec-head::before {
+ content: '';
+ width: 7px;
+ height: 7px;
+ border-radius: 2px;
+ background: var(--ink-4);
+ margin-right: 0.35rem;
+ }
+
/* ---- 归档折叠形态(fold)---- */
.year-fold { border-top: 1px solid var(--line); }
.year-fold:last-child { border-bottom: 1px solid var(--line); }
diff --git a/src/components/SocialLinks.astro b/src/components/SocialLinks.astro
new file mode 100644
index 0000000..a06c142
--- /dev/null
+++ b/src/components/SocialLinks.astro
@@ -0,0 +1,64 @@
+---
+// 社交链接行(首页名片区):数据唯一事实源 = content/site/home.yaml 的 socials
+// icon 走内置路径注册表;未登记 icon → 纯文字按钮(label 即文字),保证后台随便加
+import { homeSocials } from '../lib/site-content';
+
+// 只收 24×24 viewBox 的 path/d 片段:后台可加新条目但图标集需代码侧扩展,避免 XSS 面
+const ICON_PATHS: Record = {
+ github:
+ 'M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.57 2.34 1.12 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z',
+ rss: 'M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16M5 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z',
+ email: 'M3 7.5A2.5 2.5 0 0 1 5.5 5h13A2.5 2.5 0 0 1 21 7.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5zM4 7.5l8 5.5 8-5.5',
+ bilibili:
+ 'M6 4l2 2h8l2-2M4 8h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2Zm3 4v4m10-4v4',
+ x: 'M4 4l7.5 9.5L4.5 20h2.2l6-6.7 5 6.7H22l-7.7-9.9L21 4h-2.2l-5.4 6L9 4z',
+};
+
+const socials = await homeSocials();
+const iconOf = (icon?: string) => (icon && ICON_PATHS[icon] ? ICON_PATHS[icon] : null);
+---
+
+
+
+
diff --git a/src/components/pages/AboutPage.astro b/src/components/pages/AboutPage.astro
index 341aead..00835f0 100644
--- a/src/components/pages/AboutPage.astro
+++ b/src/components/pages/AboutPage.astro
@@ -3,21 +3,21 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
import IdCard from '../../components/IdCard.astro';
import { t } from '../../i18n/ui';
import type { Locale } from '../../lib/locale';
-import { aboutIntro } from '../../lib/site-content';
+import { aboutIntroMarkdown, renderMarkdown } from '../../lib/site-content';
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
interface Props {
locale?: Locale;
}
const { locale = 'zh' } = Astro.props;
-// 身份陈述段改由 content/site/about.yaml 供给(后台可改),缺值回落默认
-const intro = await aboutIntro(locale);
+// 身份陈述段改由 content/site/about.yaml 供给(多段 markdown,后台可改),缺值回落默认
+const introHtml = await renderMarkdown(await aboutIntroMarkdown(locale));
---
{t('about_title', locale)}
- {intro}
+
@@ -84,6 +84,16 @@ const intro = await aboutIntro(locale);
max-width: var(--measure);
margin-top: var(--space-3);
}
+ /* markdown 正文内部元素(多段/列表/链接)与站点排版对齐 */
+ .intro :global(p) { margin-block: 0 var(--space-2); }
+ .intro :global(ul) {
+ margin: var(--space-1) 0 var(--space-2);
+ padding-left: 1.2em;
+ list-style: disc;
+ }
+ .intro :global(li) { margin-block: 0.2em; }
+ .intro :global(a) { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
+ .intro :global(strong) { color: var(--ink-1); font-weight: 600; }
.sec-title {
font-size: 14px;
font-family: var(--font-mono);
diff --git a/src/content.config.ts b/src/content.config.ts
index 70af175..e38d5a0 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -77,10 +77,28 @@ export const collections = {
// 双语文案用 {zh,en} 对象字段(各自可选):任一语言空/缺字段 → src/lib/site-content.ts 回落默认
siteHome: defineCollection({
loader: glob({ pattern: 'home.yaml', base: './content/site' }),
- schema: z.object({ tagline: bilingual.optional() }),
+ // avatar 走 image():yaml 写 ./assets/xxx.jpg(相对 yaml 文件),构建期入 Astro 图片管线
+ schema: ({ image }) =>
+ z.object({
+ tagline: bilingual.optional(),
+ avatar: image().optional(),
+ /** 站点图标:public 下的路径(.svg/.ico 直出,不经图片管线);缺省回退 /favicon.svg */
+ favicon: z.string().optional(),
+ /** 社交链接,顺序即展示顺序;icon 为内置图标集键(github/rss/email/...) */
+ socials: z
+ .array(
+ z.object({
+ label: z.string(),
+ url: z.string(),
+ icon: z.string().optional(),
+ }),
+ )
+ .optional(),
+ }),
}),
siteAbout: defineCollection({
loader: glob({ pattern: 'about.yaml', base: './content/site' }),
+ // intro = 多段 markdown 正文(页面用 createMarkdownProcessor 渲染),字段级空值回落默认
schema: z.object({ intro: bilingual.optional() }),
}),
siteOpenSource: defineCollection({
diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts
index a2a9486..42bbdbc 100644
--- a/src/i18n/ui.ts
+++ b/src/i18n/ui.ts
@@ -21,6 +21,12 @@ const dict = {
paper_fallback_title: { zh: '无法内嵌展示全文,请用浏览器打开', en: 'Inline preview unavailable — open in a browser' },
paper_wechat_hint: { zh: '微信内请点右上角「···」→ 在浏览器打开', en: 'In WeChat, tap ··· (top right) → Open in Browser' },
paper_open_browser: { zh: '在浏览器中打开 PDF →', en: 'Open the PDF in your browser →' },
+ paper_loading: { zh: '正在加载 PDF…', en: 'Loading PDF…' },
+ paper_prev_page: { zh: '上一页', en: 'Previous page' },
+ paper_next_page: { zh: '下一页', en: 'Next page' },
+ paper_zoom_in: { zh: '放大', en: 'Zoom in' },
+ paper_zoom_out: { zh: '缩小', en: 'Zoom out' },
+ paper_page_label: { zh: '页码', en: 'Page' },
// 折叠批(2026-11-02):首页限 6 篇 + 归档/书架按年折叠
home_more_posts: { zh: '全部文章 →', en: 'All posts →' },
unit_posts: { zh: '篇', en: 'posts' },
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index b42c977..effefe7 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -13,6 +13,7 @@ import Footer from '../components/Footer.astro';
import ThemeScript from '../components/ThemeScript.astro';
import Keyboard from '../components/geek/Keyboard.astro';
import { localeOf, localePath } from '../lib/locale';
+import { siteFavicon } from '../lib/site-content';
import SearchDialog from '../components/SearchDialog.astro';
import BuildInfo from '../components/geek/BuildInfo.astro';
import { ClientRouter } from 'astro:transitions';
@@ -30,6 +31,8 @@ const rawPath = Astro.url.pathname.replace(/\/index\.html$/, '');
const zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath;
const enPath = localePath(rawPath, 'en');
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
+// 站点图标可配置(后台「主页设置 → 站点图标」):缺省回退 /favicon.svg
+const favicon = await siteFavicon();
// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨
const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
---
@@ -39,7 +42,7 @@ const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
-
+
diff --git a/src/lib/site-content.ts b/src/lib/site-content.ts
index 191207a..c536443 100644
--- a/src/lib/site-content.ts
+++ b/src/lib/site-content.ts
@@ -2,6 +2,7 @@
// 兜底纪律:文件缺 → 用默认文案;字段缺/空串 → 用默认文案(构建绝不因文案炸)
// 注意:整文件被删会先在 file() loader 层报错(误删级),此处守的是字段级缺失/空值
import { getEntry } from 'astro:content';
+import type { ImageMetadata } from 'astro';
import type { Locale } from './locale';
export interface Bi {
@@ -72,3 +73,61 @@ export async function openSourceCopy(locale: Locale): Promise {
pinned: pinned.length ? pinned : [...d.pinned],
};
}
+
+// ---- 车道「后台能力扩展」:社交链接 / 头像 / 站点图标 / markdown 正文 ----
+
+export interface SocialLink {
+ label: string;
+ url: string;
+ /** 内置图标键:github / rss / email / bilibili / x;未知或空 → 纯文字按钮 */
+ icon?: string;
+}
+
+export const DEFAULT_SOCIALS: SocialLink[] = [
+ { label: 'GitHub', url: 'https://github.com/wait4xx', icon: 'github' },
+ { label: 'RSS', url: '/rss.xml', icon: 'rss' },
+ { label: '邮件', url: 'mailto:admin@wait4x.online', icon: 'email' },
+];
+
+/** 社交链接(顺序即展示顺序);yaml 空/缺省 → 默认三条(与历史硬编码一致) */
+export async function homeSocials(): Promise {
+ const entry = await getEntry('siteHome', 'home');
+ const list = entry?.data.socials?.filter((s) => s.label?.trim() && s.url?.trim()) ?? [];
+ return list.length ? list : DEFAULT_SOCIALS;
+}
+
+/** 头像(Astro 图片管线的 ImageMetadata);未配置 → null,调用方回退渐变占位 */
+export async function homeAvatar(): Promise {
+ const entry = await getEntry('siteHome', 'home');
+ return entry?.data.avatar ?? null;
+}
+
+/** 站点图标路径:yaml 值非空即采信,否则 /favicon.svg;值不带前导 / 时自动补 */
+export async function siteFavicon(): Promise<{ href: string; type: string }> {
+ const entry = await getEntry('siteHome', 'home');
+ const raw = pick(entry?.data.favicon, '/favicon.svg');
+ const href = raw.startsWith('/') || raw.startsWith('http') ? raw : `/${raw}`;
+ const type = href.endsWith('.svg')
+ ? 'image/svg+xml'
+ : href.endsWith('.ico')
+ ? 'image/x-icon'
+ : href.endsWith('.png')
+ ? 'image/png'
+ : '';
+ return { href, type };
+}
+
+/** markdown 字符串 → HTML(进程内单例处理器,构建期复用) */
+let mdProcessor: Promise | null = null;
+export async function renderMarkdown(md: string): Promise {
+ const { createMarkdownProcessor } = await import('@astrojs/markdown-remark');
+ mdProcessor ??= createMarkdownProcessor();
+ const { code } = await (await mdProcessor).render(md);
+ return code;
+}
+
+/** 关于页正文(markdown,未渲染);空 → 默认文案 */
+export async function aboutIntroMarkdown(locale: Locale): Promise {
+ const entry = await getEntry('siteAbout', 'about');
+ return pickBi(entry?.data.intro, SITE_DEFAULTS.aboutIntro, locale);
+}
diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro
index 8eac2c8..02b859a 100644
--- a/src/pages/en/index.astro
+++ b/src/pages/en/index.astro
@@ -9,6 +9,7 @@ import ProjectWall from '../../components/geek/ProjectWall.astro';
import Heatmap from '../../components/geek/Heatmap.astro';
import { t } from '../../i18n/ui';
import { homeTagline } from '../../lib/site-content';
+import SocialLinks from '../../components/SocialLinks.astro';
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
@@ -25,55 +26,10 @@ const tagline = await homeTagline('en');
{tagline}
-
-
+
+
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 394d4f1..d5ed296 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -9,6 +9,7 @@ import Heatmap from '../components/geek/Heatmap.astro';
import { fetchHeartbeat, agoZh } from '../lib/heartbeat';
import { t } from '../i18n/ui';
import { homeTagline } from '../lib/site-content';
+import SocialLinks from '../components/SocialLinks.astro';
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
@@ -23,55 +24,10 @@ const tagline = await homeTagline('zh');
{tagline}
-
-
+
+
-
+