feat(cms): 后台扩展五件——关于页 markdown 化/社交/头像/favicon/分轨首页
- T1 关于页=markdown 文档:about.yaml intro 扩为多段 markdown(zh/en),
AboutPage 用 @astrojs/markdown-remark 的 createMarkdownProcessor(进程
内单例复用)+ set:html 渲染,正文 p/ul/strong/a 对齐站点排版;名片与
技能卡结构不动
- T2 社交链接单一数据源:home.yaml socials[](label/url/icon)+
SocialLinks.astro(图标注册表白名单:github/rss/email/bilibili/x,
未登记 icon 回退纯文字),首页 zh/en 内联硬编码社交块删除
- T3 头像与 favicon 可上传:schema avatar: image()(Astro Image 出 60px
WebP,未设回退渐变 SVG);home.yaml favicon → BaseLayout link[rel=icon]
按扩展名给 type,留空回退 /favicon.svg
- T4 首页分轨:PostList perTrack → 阅读轨/技术轨两段各限 6,各自出口
(阅读→/books/ 技术→/archives/,en 同构);E2E 断言改为两段各≤6+各自
出口+阅读在前
- T5 后台列表日期:posts summary「{{pubDate}} · {{title}}」、moments
「{{fields.pubDate}} · {{fields.text}}」+sortable;photos 无 frontmatter
日期(JSON sidecar)保持分组 · 标题并在文档写明原因
- 顺手修 content-lint 漏报:中文文件名在 HTML 里为 %XX 编码,脚本直接
拼盘上路径导致 6 条"缺图"误报 → 加 decodeURI
- 清理:删除书封误传遗留的双重嵌套目录文件
车道:op-gitea(5/5,build+E2E 37/37+lint 47 页 0/0/0);收编复验:
build 46 页 + E2E 37/37 连跑(首跑两红为车道并发构建擦 dist 的瞬时态,
复跑绿)
验证: node v26.13.0 + astro 7.4.4 全量构建 46 页 exit 0;E2E 37/37;
content-lint 0/0/0;books details/命令面板 playwright 实测
Spec: none (后台能力扩展,车道级交付)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
227bc24797
commit
328ab68f18
16 files changed
+378
-115
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
+27
-4
@@ -1,5 +1,28 @@
|
|||||||
# 关于页可变段落(后台「页面文案 → 关于页」可改)
|
# 关于页正文(后台「页面文案 → 关于页」可改,markdown 编辑)
|
||||||
# intro = 身份陈述段;留空则回落默认文案,构建不炸
|
# intro = 多段 markdown 正文,渲染在「关于」标题下方、名片之前;留空回落默认文案
|
||||||
|
# 支持的语法:段落、列表、粗斜体、行内代码、链接(渲染走 Astro 的 markdown 管线)
|
||||||
intro:
|
intro:
|
||||||
zh: 写作者 · 读者 · 快门按压者,拥抱开源。这里是我三条轨道的公开工作台:读过的书留下评注,写过的代码进项目墙,按下的快门收入图库。
|
zh: |
|
||||||
en: Writer · Reader · Shutter-presser, embracing open source. This is the public workbench of my three tracks — books that left notes, code that reaches the project wall, and shutters I pressed.
|
写作者 · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
|
|
||||||
|
所有内容都有一个明确去处:
|
||||||
|
|
||||||
|
- 长文沉淀在文章与书架
|
||||||
|
- 短句与随手拍落在瞬间
|
||||||
|
- 影像收进图库,配一点 EXIF 与一句策展
|
||||||
|
|
||||||
|
本站本身即开源,构建与发布全链都在仓里。
|
||||||
|
en: |
|
||||||
|
Writer · Reader · Shutter-presser, embracing open source.
|
||||||
|
|
||||||
|
This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** I pressed that land in the gallery.
|
||||||
|
|
||||||
|
Everything has a clear destination:
|
||||||
|
|
||||||
|
- Long-form writing goes to posts and the bookshelf
|
||||||
|
- Short lines and snapshots land in moments
|
||||||
|
- Images go to the gallery, with a bit of EXIF and one line of curation
|
||||||
|
|
||||||
|
This site is itself open source — the full build and deploy pipeline lives in the repo.
|
||||||
@@ -3,3 +3,24 @@
|
|||||||
tagline:
|
tagline:
|
||||||
zh: 写作者 · 读者 · 快门按压者,拥抱开源。
|
zh: 写作者 · 读者 · 快门按压者,拥抱开源。
|
||||||
en: Writer · Reader · Shutter-presser — embracing open source.
|
en: Writer · Reader · Shutter-presser — embracing open source.
|
||||||
|
|
||||||
|
# 头像(可选):上传后自动写入 ./assets/avatar-xxx.jpg(走 Astro 图片管线)
|
||||||
|
# 留空 = 回退内置渐变占位
|
||||||
|
# avatar: ./assets/avatar.jpg
|
||||||
|
|
||||||
|
# 站点图标(favicon,全局生效):public 下的路径;留空回退 /favicon.svg
|
||||||
|
# 上传走 public/icons/(后台媒体库),可填 /icons/my.ico、/icons/my.svg
|
||||||
|
favicon: /favicon.svg
|
||||||
|
|
||||||
|
# 社交链接(顺序即展示顺序,后台可增删拖动排序)
|
||||||
|
# icon 取值:github / rss / email / bilibili / x / 留空(留空按 label 显示纯文字)
|
||||||
|
socials:
|
||||||
|
- label: GitHub
|
||||||
|
url: https://github.com/wait4xx
|
||||||
|
icon: github
|
||||||
|
- label: RSS
|
||||||
|
url: /rss.xml
|
||||||
|
icon: rss
|
||||||
|
- label: 邮件
|
||||||
|
url: mailto:admin@wait4x.online
|
||||||
|
icon: email
|
||||||
+26
-2
@@ -128,6 +128,11 @@ cover: ./assets/books/mian-sha.jpg
|
|||||||
体积纪律:mp4/webm ≤50MB 走 `public/media/`(构建原样拷贝,不转码);
|
体积纪律:mp4/webm ≤50MB 走 `public/media/`(构建原样拷贝,不转码);
|
||||||
更大的请按上文「体积纪律」放仓外 store(该场景 build.sh 同步段待首次出现时补)。
|
更大的请按上文「体积纪律」放仓外 store(该场景 build.sh 同步段待首次出现时补)。
|
||||||
|
|
||||||
|
### 后台列表日期
|
||||||
|
|
||||||
|
文章/瞬间列表显示「日期 · 标题」,可按日期排序;图库照片没有 frontmatter 日期
|
||||||
|
(时间在 EXIF/文件侧),列表按「分组 · 标题」展示、按标题/分组排序。
|
||||||
|
|
||||||
### 链接
|
### 链接
|
||||||
|
|
||||||
markdown 原生语法,无需任何配置:
|
markdown 原生语法,无需任何配置:
|
||||||
@@ -144,9 +149,28 @@ markdown 原生语法,无需任何配置:
|
|||||||
|
|
||||||
| 入口 | 文件 | 可改内容 |
|
| 入口 | 文件 | 可改内容 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 主页设置 | `content/site/home.yaml` | 身份语 tagline(zh/en) |
|
| 主页设置 | `content/site/home.yaml` | 身份语 tagline、**头像 avatar**、**站点图标 favicon**、**社交链接 socials** |
|
||||||
| 关于页 | `content/site/about.yaml` | 身份陈述段 intro(zh/en) |
|
| 关于页 | `content/site/about.yaml` | **markdown 正文 intro**(多段/列表/粗斜体/链接) |
|
||||||
| 开源页 | `content/site/opensource.yaml` | 叙述 blurb/blurb_after、仓链接、置顶项目 pinned |
|
| 开源页 | `content/site/opensource.yaml` | 叙述 blurb/blurb_after、仓链接、置顶项目 pinned |
|
||||||
|
|
||||||
|
#### 头像与站点图标
|
||||||
|
|
||||||
|
- **头像**:主页设置里「头像」上传 → 落 `content/site/assets/`,构建期优化成 60px WebP;
|
||||||
|
留空则显示内置渐变占位(w4 徽标)
|
||||||
|
- **站点图标 favicon**:先经「图标」媒体库上传(`public/icons/`),再把路径粘到
|
||||||
|
「站点图标」字段(如 `/icons/my.ico`、`/icons/my.svg`);留空回退 `/favicon.svg`。
|
||||||
|
支持 `.svg` / `.ico` / `.png`(直出,不经图片优化)
|
||||||
|
|
||||||
|
#### 社交链接
|
||||||
|
|
||||||
|
主页设置里「社交链接」是列表:可增删、拖动排序。每条 = 名称(无障碍文字)+ 链接 + 图标。
|
||||||
|
图标可选 `github / rss / email / bilibili / x`,**留空则显示纯文字按钮**。
|
||||||
|
首页名片区、以及任何引用该组件的页面都吃同一份数据(单一事实源,改一次全站生效)。
|
||||||
|
|
||||||
|
#### 首页文章列表
|
||||||
|
|
||||||
|
首页现在分两段:**阅读轨 6 条 + 技术轨 6 条**(各自带「全部文章」出口:阅读→书架、技术→归档)。
|
||||||
|
照片轨文章不进首页(在图库看)。完整列表见 `/archives/`。
|
||||||
|
|
||||||
- 中文/英文各一个字段,页面按语言取;**留空则该字段回落默认文案**(构建不会炸)
|
- 中文/英文各一个字段,页面按语言取;**留空则该字段回落默认文案**(构建不会炸)
|
||||||
- `pinned` 填仓库短名(如 `blog`、`pi-board`),**顺序即展示顺序**;留空回落 `blog`
|
- `pinned` 填仓库短名(如 `blog`、`pi-board`),**顺序即展示顺序**;留空回落 `blog`
|
||||||
+35
-5
@@ -26,7 +26,7 @@ collections:
|
|||||||
create: true
|
create: true
|
||||||
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
||||||
slug: '{{slug}}'
|
slug: '{{slug}}'
|
||||||
summary: '{{title}} · {{category}}'
|
summary: '{{pubDate}} · {{title}}'
|
||||||
sortable_fields: [pubDate, title]
|
sortable_fields: [pubDate, title]
|
||||||
# 注意:Sveltia 对 folder 集合的 media_folder 按"相对集合目录"解析
|
# 注意:Sveltia 对 folder 集合的 media_folder 按"相对集合目录"解析
|
||||||
# (写成 content/posts/assets 会二次嵌套成 content/posts/content/posts/assets,
|
# (写成 content/posts/assets 会二次嵌套成 content/posts/content/posts/assets,
|
||||||
@@ -66,6 +66,7 @@ collections:
|
|||||||
create: true
|
create: true
|
||||||
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||||
summary: '{{fields.pubDate}} · {{fields.text}}'
|
summary: '{{fields.pubDate}} · {{fields.text}}'
|
||||||
|
sortable_fields: [pubDate, text]
|
||||||
fields:
|
fields:
|
||||||
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
||||||
- { label: 文字, name: text, widget: text, required: false, hint: '一句话(与图片至少填一项)' }
|
- { label: 文字, name: text, widget: text, required: false, hint: '一句话(与图片至少填一项)' }
|
||||||
@@ -84,6 +85,8 @@ collections:
|
|||||||
media_folder: ''
|
media_folder: ''
|
||||||
public_folder: ''
|
public_folder: ''
|
||||||
summary: '{{fields.group}} · {{fields.title}}'
|
summary: '{{fields.group}} · {{fields.title}}'
|
||||||
|
# 照片无 frontmatter 日期(JSON sidecar,时间在 exif/文件侧)——列表按标题/分组排序
|
||||||
|
sortable_fields: [title, group]
|
||||||
fields:
|
fields:
|
||||||
# title 必填:Astro schema z.string() 无 .optional(),omit 纪律下必需且有值才会落键(空 title 会让 main 不可构建)
|
# title 必填:Astro schema z.string() 无 .optional(),omit 纪律下必需且有值才会落键(空 title 会让 main 不可构建)
|
||||||
- { label: 标题, name: title, widget: string, required: true }
|
- { label: 标题, name: title, widget: string, required: true }
|
||||||
@@ -110,6 +113,9 @@ collections:
|
|||||||
- name: home
|
- name: home
|
||||||
label: 主页设置
|
label: 主页设置
|
||||||
file: content/site/home.yaml
|
file: content/site/home.yaml
|
||||||
|
# 头像上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线优化)
|
||||||
|
media_folder: content/site/assets
|
||||||
|
public_folder: ./assets
|
||||||
fields:
|
fields:
|
||||||
- label: 身份语
|
- label: 身份语
|
||||||
name: tagline
|
name: tagline
|
||||||
@@ -117,17 +123,41 @@ collections:
|
|||||||
fields:
|
fields:
|
||||||
- { label: 中文, name: zh, widget: string, required: false }
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
- { label: 英文, name: en, widget: string, required: false }
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 头像(可选)
|
||||||
|
name: avatar
|
||||||
|
widget: image
|
||||||
|
required: false
|
||||||
|
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
||||||
|
- label: 站点图标 favicon
|
||||||
|
name: favicon
|
||||||
|
widget: string
|
||||||
|
required: false
|
||||||
|
hint: 'public 下的路径,如 /icons/my.ico 或 /icons/my.svg;留空回退 /favicon.svg。经「图标」媒体库上传'
|
||||||
|
- label: 社交链接(顺序即展示顺序)
|
||||||
|
name: socials
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
summary: '{{fields.label}} → {{fields.url}}'
|
||||||
|
fields:
|
||||||
|
- { label: 名称(无障碍文字), name: label, widget: string }
|
||||||
|
- { label: 链接, name: url, widget: string }
|
||||||
|
- label: 图标
|
||||||
|
name: icon
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [github, rss, email, bilibili, x]
|
||||||
|
hint: '不在列表里的留空 → 显示纯文字按钮'
|
||||||
- name: about
|
- name: about
|
||||||
label: 关于页
|
label: 关于页
|
||||||
file: content/site/about.yaml
|
file: content/site/about.yaml
|
||||||
fields:
|
fields:
|
||||||
- label: 身份陈述段
|
- label: 正文(markdown,多段)
|
||||||
name: intro
|
name: intro
|
||||||
widget: object
|
widget: object
|
||||||
hint: 渲染在「关于」标题下方、名片之上;留空用默认文案
|
hint: 渲染在「关于」标题下方、名片之上;支持段落/列表/粗斜体/链接;留空用默认文案
|
||||||
fields:
|
fields:
|
||||||
- { label: 中文, name: zh, widget: text, required: false }
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
- { label: 英文, name: en, widget: text, required: false }
|
- { label: 英文, name: en, widget: markdown, required: false }
|
||||||
- name: opensource
|
- name: opensource
|
||||||
label: 开源页
|
label: 开源页
|
||||||
file: content/site/opensource.yaml
|
file: content/site/opensource.yaml
|
||||||
|
|||||||
@@ -25,10 +25,19 @@ function walk(dir, out = []) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** URL 路径 → 磁盘路径:先解百分号编码(中文/空格文件名在 HTML 里是 %XX 形式) */
|
||||||
|
function decodePath(p) {
|
||||||
|
try {
|
||||||
|
return decodeURI(p);
|
||||||
|
} catch {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** /x/ → dist/x/index.html;/a.svg → dist/a.svg;找不到即 null */
|
/** /x/ → dist/x/index.html;/a.svg → dist/a.svg;找不到即 null */
|
||||||
function targetExists(url) {
|
function targetExists(url) {
|
||||||
if (SKIP_PREFIX.some((s) => url.startsWith(s))) return true;
|
if (SKIP_PREFIX.some((s) => url.startsWith(s))) return true;
|
||||||
const p = url.split('#')[0].split('?')[0];
|
const p = decodePath(url.split('#')[0].split('?')[0]);
|
||||||
if (!p || p === '/') return existsSync(join(DIST, 'index.html'));
|
if (!p || p === '/') return existsSync(join(DIST, 'index.html'));
|
||||||
if (p.endsWith('/')) return existsSync(join(DIST, p, 'index.html'));
|
if (p.endsWith('/')) return existsSync(join(DIST, p, 'index.html'));
|
||||||
return existsSync(join(DIST, p)) || existsSync(join(DIST, `${p}.html`)) || existsSync(join(DIST, p, 'index.html'));
|
return existsSync(join(DIST, p)) || existsSync(join(DIST, `${p}.html`)) || existsSync(join(DIST, p, 'index.html'));
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
---
|
---
|
||||||
// 名片头像占位:内联 SVG 渐变 60px;真实头像接入后整体替换本组件即可
|
// 名片头像:后台「主页设置 → 头像」上传后在 content/site/assets/ 落图,
|
||||||
|
// 经 Astro 图片管线出 60px WebP;未配置则回退内置渐变占位(历史视觉不变)
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import { homeAvatar } from '../lib/site-content';
|
||||||
|
|
||||||
|
const src = await homeAvatar();
|
||||||
---
|
---
|
||||||
|
|
||||||
|
{src ? (
|
||||||
|
<Image
|
||||||
|
class="avatar"
|
||||||
|
src={src}
|
||||||
|
alt="wait4x 头像"
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
format="webp"
|
||||||
|
quality={82}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<svg
|
<svg
|
||||||
class="avatar"
|
class="avatar"
|
||||||
viewBox="0 0 60 60"
|
viewBox="0 0 60 60"
|
||||||
@@ -32,11 +48,13 @@
|
|||||||
w4
|
w4
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
)}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.avatar {
|
.avatar {
|
||||||
flex: none;
|
flex: none;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -18,10 +18,12 @@ interface Props {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
/** 截断后的「全部文章 →」出口链接;缺省不渲染 */
|
/** 截断后的「全部文章 →」出口链接;缺省不渲染 */
|
||||||
moreHref?: string;
|
moreHref?: string;
|
||||||
|
/** 分轨模式:阅读轨与技朮轨各取 N 条(首页两段式,各段自带「更多」出口) */
|
||||||
|
perTrack?: number;
|
||||||
/** true = 归档折叠形态:按年 details(最新年开)+年内按月分组 */
|
/** true = 归档折叠形态:按年 details(最新年开)+年内按月分组 */
|
||||||
fold?: boolean;
|
fold?: boolean;
|
||||||
}
|
}
|
||||||
const { posts, locale = 'zh', titleOverrides, limit, moreHref, fold = false } = Astro.props;
|
const { posts, locale = 'zh', titleOverrides, limit, moreHref, perTrack, fold = false } = Astro.props;
|
||||||
const visible = typeof limit === 'number' ? posts.slice(0, limit) : posts;
|
const visible = typeof limit === 'number' ? posts.slice(0, limit) : posts;
|
||||||
// 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义
|
// 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义
|
||||||
const TRACK_LABEL: Record<PostCategory, string> = {
|
const TRACK_LABEL: Record<PostCategory, string> = {
|
||||||
@@ -58,7 +60,49 @@ const byMonth = (list: CollectionEntry<'posts'>[]) => {
|
|||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
{fold ? (
|
{perTrack ? (
|
||||||
|
<div class="post-list per-track">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ 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 (
|
||||||
|
<section class="track-sec" aria-label={TRACK_LABEL[key]}>
|
||||||
|
<h2 class="track-sec-head">
|
||||||
|
<span>{TRACK_LABEL[key]}</span>
|
||||||
|
<a class="more-link tap" href={more}>{t('home_more_posts', locale)}</a>
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
{rows.map((post) => (
|
||||||
|
<li>
|
||||||
|
<a class="row tap" href={hrefOf(post.id)}>
|
||||||
|
{key === 'reading' && (
|
||||||
|
<i
|
||||||
|
class="emo"
|
||||||
|
style={`background:${post.data.mood ?? 'var(--mood)'}`}
|
||||||
|
title="情绪色"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span class="row-title">{titleOverrides?.[post.id] ?? post.data.title}</span>
|
||||||
|
{key !== 'reading' &&
|
||||||
|
post.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
|
||||||
|
<time class="row-date" datetime={post.data.pubDate.toISOString()}>
|
||||||
|
{mmdd(post.data.pubDate)}
|
||||||
|
</time>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : fold ? (
|
||||||
<div class="post-list post-list-fold">
|
<div class="post-list post-list-fold">
|
||||||
{[...byYear.entries()].map(([year, list], i) => (
|
{[...byYear.entries()].map(([year, list], i) => (
|
||||||
<details class="year-fold" open={i === 0}>
|
<details class="year-fold" open={i === 0}>
|
||||||
@@ -244,6 +288,28 @@ const byMonth = (list: CollectionEntry<'posts'>[]) => {
|
|||||||
}
|
}
|
||||||
.more-link:hover { border-bottom-color: var(--accent); }
|
.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)---- */
|
/* ---- 归档折叠形态(fold)---- */
|
||||||
.year-fold { border-top: 1px solid var(--line); }
|
.year-fold { border-top: 1px solid var(--line); }
|
||||||
.year-fold:last-child { border-bottom: 1px solid var(--line); }
|
.year-fold:last-child { border-bottom: 1px solid var(--line); }
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
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);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="socials">
|
||||||
|
{socials.map((s) => {
|
||||||
|
const isExternal = s.url.startsWith('http');
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
class="tap social"
|
||||||
|
href={s.url}
|
||||||
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||||
|
aria-label={s.label}
|
||||||
|
title={s.label}
|
||||||
|
>
|
||||||
|
{iconOf(s.icon) ? (
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d={iconOf(s.icon)}></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<span class="social-text">{s.label}</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.socials {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
.social {
|
||||||
|
color: var(--ink-3);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: color var(--dur) var(--ease);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.social:hover { color: var(--accent); }
|
||||||
|
.social-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12.5px;
|
||||||
|
padding-inline: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,21 +3,21 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
|
|||||||
import IdCard from '../../components/IdCard.astro';
|
import IdCard from '../../components/IdCard.astro';
|
||||||
import { t } from '../../i18n/ui';
|
import { t } from '../../i18n/ui';
|
||||||
import type { Locale } from '../../lib/locale';
|
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 零回归
|
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||||
interface Props {
|
interface Props {
|
||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
}
|
}
|
||||||
const { locale = 'zh' } = Astro.props;
|
const { locale = 'zh' } = Astro.props;
|
||||||
// 身份陈述段改由 content/site/about.yaml 供给(后台可改),缺值回落默认
|
// 身份陈述段改由 content/site/about.yaml 供给(多段 markdown,后台可改),缺值回落默认
|
||||||
const intro = await aboutIntro(locale);
|
const introHtml = await renderMarkdown(await aboutIntroMarkdown(locale));
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`${t('about_title', locale)} · wait4x`}>
|
<BaseLayout title={`${t('about_title', locale)} · wait4x`}>
|
||||||
<h1 class="page-title">{t('about_title', locale)}</h1>
|
<h1 class="page-title">{t('about_title', locale)}</h1>
|
||||||
|
|
||||||
<p class="intro">{intro}</p>
|
<div class="intro" set:html={introHtml} />
|
||||||
|
|
||||||
<IdCard />
|
<IdCard />
|
||||||
|
|
||||||
@@ -84,6 +84,16 @@ const intro = await aboutIntro(locale);
|
|||||||
max-width: var(--measure);
|
max-width: var(--measure);
|
||||||
margin-top: var(--space-3);
|
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 {
|
.sec-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|||||||
+19
-1
@@ -77,10 +77,28 @@ export const collections = {
|
|||||||
// 双语文案用 {zh,en} 对象字段(各自可选):任一语言空/缺字段 → src/lib/site-content.ts 回落默认
|
// 双语文案用 {zh,en} 对象字段(各自可选):任一语言空/缺字段 → src/lib/site-content.ts 回落默认
|
||||||
siteHome: defineCollection({
|
siteHome: defineCollection({
|
||||||
loader: glob({ pattern: 'home.yaml', base: './content/site' }),
|
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({
|
siteAbout: defineCollection({
|
||||||
loader: glob({ pattern: 'about.yaml', base: './content/site' }),
|
loader: glob({ pattern: 'about.yaml', base: './content/site' }),
|
||||||
|
// intro = 多段 markdown 正文(页面用 createMarkdownProcessor 渲染),字段级空值回落默认
|
||||||
schema: z.object({ intro: bilingual.optional() }),
|
schema: z.object({ intro: bilingual.optional() }),
|
||||||
}),
|
}),
|
||||||
siteOpenSource: defineCollection({
|
siteOpenSource: defineCollection({
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ const dict = {
|
|||||||
paper_fallback_title: { zh: '无法内嵌展示全文,请用浏览器打开', en: 'Inline preview unavailable — open in a browser' },
|
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_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_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 篇 + 归档/书架按年折叠
|
// 折叠批(2026-11-02):首页限 6 篇 + 归档/书架按年折叠
|
||||||
home_more_posts: { zh: '全部文章 →', en: 'All posts →' },
|
home_more_posts: { zh: '全部文章 →', en: 'All posts →' },
|
||||||
unit_posts: { zh: '篇', en: 'posts' },
|
unit_posts: { zh: '篇', en: 'posts' },
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Footer from '../components/Footer.astro';
|
|||||||
import ThemeScript from '../components/ThemeScript.astro';
|
import ThemeScript from '../components/ThemeScript.astro';
|
||||||
import Keyboard from '../components/geek/Keyboard.astro';
|
import Keyboard from '../components/geek/Keyboard.astro';
|
||||||
import { localeOf, localePath } from '../lib/locale';
|
import { localeOf, localePath } from '../lib/locale';
|
||||||
|
import { siteFavicon } from '../lib/site-content';
|
||||||
import SearchDialog from '../components/SearchDialog.astro';
|
import SearchDialog from '../components/SearchDialog.astro';
|
||||||
import BuildInfo from '../components/geek/BuildInfo.astro';
|
import BuildInfo from '../components/geek/BuildInfo.astro';
|
||||||
import { ClientRouter } from 'astro:transitions';
|
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 zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath;
|
||||||
const enPath = localePath(rawPath, 'en');
|
const enPath = localePath(rawPath, 'en');
|
||||||
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
||||||
|
// 站点图标可配置(后台「主页设置 → 站点图标」):缺省回退 /favicon.svg
|
||||||
|
const favicon = await siteFavicon();
|
||||||
// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨
|
// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨
|
||||||
const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
|
const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
|
||||||
---
|
---
|
||||||
@@ -39,7 +42,7 @@ const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type={favicon.type || undefined} href={favicon.href} />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 兜底纪律:文件缺 → 用默认文案;字段缺/空串 → 用默认文案(构建绝不因文案炸)
|
// 兜底纪律:文件缺 → 用默认文案;字段缺/空串 → 用默认文案(构建绝不因文案炸)
|
||||||
// 注意:整文件被删会先在 file() loader 层报错(误删级),此处守的是字段级缺失/空值
|
// 注意:整文件被删会先在 file() loader 层报错(误删级),此处守的是字段级缺失/空值
|
||||||
import { getEntry } from 'astro:content';
|
import { getEntry } from 'astro:content';
|
||||||
|
import type { ImageMetadata } from 'astro';
|
||||||
import type { Locale } from './locale';
|
import type { Locale } from './locale';
|
||||||
|
|
||||||
export interface Bi {
|
export interface Bi {
|
||||||
@@ -72,3 +73,61 @@ export async function openSourceCopy(locale: Locale): Promise<OpenSourceCopy> {
|
|||||||
pinned: pinned.length ? pinned : [...d.pinned],
|
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<SocialLink[]> {
|
||||||
|
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<ImageMetadata | null> {
|
||||||
|
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<import('@astrojs/markdown-remark').MarkdownProcessor> | null = null;
|
||||||
|
export async function renderMarkdown(md: string): Promise<string> {
|
||||||
|
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<string> {
|
||||||
|
const entry = await getEntry('siteAbout', 'about');
|
||||||
|
return pickBi(entry?.data.intro, SITE_DEFAULTS.aboutIntro, locale);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import ProjectWall from '../../components/geek/ProjectWall.astro';
|
|||||||
import Heatmap from '../../components/geek/Heatmap.astro';
|
import Heatmap from '../../components/geek/Heatmap.astro';
|
||||||
import { t } from '../../i18n/ui';
|
import { t } from '../../i18n/ui';
|
||||||
import { homeTagline } from '../../lib/site-content';
|
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(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
@@ -25,55 +26,10 @@ const tagline = await homeTagline('en');
|
|||||||
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
|
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
|
||||||
<IdCard>
|
<IdCard>
|
||||||
<p class="tagline">{tagline}</p>
|
<p class="tagline">{tagline}</p>
|
||||||
<div class="socials">
|
<SocialLinks />
|
||||||
<a
|
|
||||||
class="tap social"
|
|
||||||
href="https://github.com/wait4xx"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
title="GitHub"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="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"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="tap social" href="/en/rss.xml" aria-label="RSS feed" title="RSS feed">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M4 11.5a8.5 8.5 0 0 1 8.5 8.5M4 4.5a15.5 15.5 0 0 1 15.5 15.5"></path>
|
|
||||||
<circle cx="5.4" cy="18.6" r="1.5" fill="currentColor" stroke="none"></circle>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="tap social" href="mailto:admin@wait4x.online" aria-label="Email" title="Email">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<rect x="3" y="5.5" width="18" height="13" rx="2.5"></rect>
|
|
||||||
<path d="m4 7.5 8 5.5 8-5.5"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</IdCard>
|
</IdCard>
|
||||||
|
|
||||||
<PostList {posts} locale="en" titleOverrides={enTitle} limit={6} moreHref="/en/archives/" />
|
<PostList {posts} locale="en" titleOverrides={enTitle} perTrack={6} />
|
||||||
|
|
||||||
<section class="latest" aria-label={t('latest_photos', 'en')}>
|
<section class="latest" aria-label={t('latest_photos', 'en')}>
|
||||||
<div class="latest-head">
|
<div class="latest-head">
|
||||||
|
|||||||
+3
-47
@@ -9,6 +9,7 @@ import Heatmap from '../components/geek/Heatmap.astro';
|
|||||||
import { fetchHeartbeat, agoZh } from '../lib/heartbeat';
|
import { fetchHeartbeat, agoZh } from '../lib/heartbeat';
|
||||||
import { t } from '../i18n/ui';
|
import { t } from '../i18n/ui';
|
||||||
import { homeTagline } from '../lib/site-content';
|
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(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
@@ -23,55 +24,10 @@ const tagline = await homeTagline('zh');
|
|||||||
<BaseLayout title="wait4x">
|
<BaseLayout title="wait4x">
|
||||||
<IdCard>
|
<IdCard>
|
||||||
<p class="tagline">{tagline}</p>
|
<p class="tagline">{tagline}</p>
|
||||||
<div class="socials">
|
<SocialLinks />
|
||||||
<a
|
|
||||||
class="tap social"
|
|
||||||
href="https://github.com/wait4xx"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
title="GitHub"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="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"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="tap social" href="/rss.xml" aria-label="RSS 订阅" title="RSS 订阅">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M4 11.5a8.5 8.5 0 0 1 8.5 8.5M4 4.5a15.5 15.5 0 0 1 15.5 15.5"></path>
|
|
||||||
<circle cx="5.4" cy="18.6" r="1.5" fill="currentColor" stroke="none"></circle>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="tap social" href="mailto:admin@wait4x.online" aria-label="邮件" title="邮件">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<rect x="3" y="5.5" width="18" height="13" rx="2.5"></rect>
|
|
||||||
<path d="m4 7.5 8 5.5 8-5.5"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</IdCard>
|
</IdCard>
|
||||||
|
|
||||||
<PostList {posts} limit={6} moreHref="/archives/" />
|
<PostList {posts} perTrack={6} />
|
||||||
|
|
||||||
<section class="latest" aria-label="最新影像">
|
<section class="latest" aria-label="最新影像">
|
||||||
<div class="latest-head">
|
<div class="latest-head">
|
||||||
|
|||||||
Reference in new issue
Block a user