@windcoder/astro-theme-orchid 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/CategoryBar.astro +1 -1
- package/src/components/PostCard.astro +6 -0
- package/src/routes/categories/[category]/[...page].astro +36 -25
- package/src/routes/friends.astro +4 -0
- package/src/routes/moments/[...page].astro +7 -2
- package/src/routes/posts/[...slug].astro +6 -0
- package/src/routes/tags/[tag]/[...page].astro +34 -23
- package/src/schema.ts +3 -2
- package/src/scripts/app.ts +21 -14
- package/src/utils.ts +7 -5
package/package.json
CHANGED
|
@@ -30,6 +30,11 @@ const { cover: rawCover, retries } = resolveCover(
|
|
|
30
30
|
下)构建期解析为产物 URL,public / 外链原样直通 */
|
|
31
31
|
const resolvedCover = rawCover ? await resolveSiteImage(rawCover) : rawCover;
|
|
32
32
|
const cover = config.postList.showCover ? resolvedCover : null;
|
|
33
|
+
/* §4.114:普通封面图失效的客户端兜底(api 文章 retries 末尾已含 fallbackCover,无需重复) */
|
|
34
|
+
const coverFallback =
|
|
35
|
+
!retries && config.postList.fallbackCover
|
|
36
|
+
? await resolveSiteImage(config.postList.fallbackCover)
|
|
37
|
+
: "";
|
|
33
38
|
/* §4.106:列表封面用缩略图(外链/未命中自动回退原图;点击进文章页看正文原图) */
|
|
34
39
|
const thumb = cover ? getThumbInfo(cover) : null;
|
|
35
40
|
const classes = [
|
|
@@ -59,6 +64,7 @@ const excerpt = d.excerpt || d.description;
|
|
|
59
64
|
loading="lazy"
|
|
60
65
|
data-lqip
|
|
61
66
|
data-api-retries={retries ? JSON.stringify(retries) : undefined}
|
|
67
|
+
data-cover-fallback={coverFallback || undefined}
|
|
62
68
|
/>
|
|
63
69
|
</a>
|
|
64
70
|
)
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
3
|
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
4
|
import Pagination from "virtual:theme/components/Pagination.astro";
|
|
5
|
+
import PostCard from "virtual:theme/components/PostCard.astro";
|
|
6
|
+
import Sidebar from "virtual:theme/components/Sidebar.astro";
|
|
5
7
|
import config from "virtual:theme/config";
|
|
6
8
|
import { t } from "../../../i18n/index.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
formatDate,
|
|
10
|
-
getSortedPosts,
|
|
11
|
-
} from "../../../utils.js";
|
|
9
|
+
import { collectCategories, getSortedPosts } from "../../../utils.js";
|
|
10
|
+
import { hydratePosts } from "../../_hydrate.js";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* 分类详情(§4.87 分页):每分类按 postList.pageSize 分页,第一页
|
|
15
|
-
* /categories/<category>/(page 段省略),后续 /categories/<category>/
|
|
16
|
-
*
|
|
14
|
+
* /categories/<category>/(page 段省略),后续 /categories/<category>/page/N/。
|
|
15
|
+
* 注意:分页段必须是 page/N/(§4.113)——Pagination 组件按此约定生成 href,
|
|
16
|
+
* paginate() 默认产出的 /N/ 会与组件链接错位导致 404,这里统一 remap。
|
|
17
|
+
* 布局与首页同构(§4.113):PostCard 列表 + Sidebar,替代原单列窄卡片。
|
|
17
18
|
*/
|
|
18
19
|
export async function getStaticPaths({ paginate }: { paginate: any }) {
|
|
19
20
|
const all = await getCollection("posts", ({ data }) =>
|
|
@@ -24,30 +25,40 @@ export async function getStaticPaths({ paginate }: { paginate: any }) {
|
|
|
24
25
|
return [...collectCategories(sorted).entries()].flatMap(([category, posts]) =>
|
|
25
26
|
paginate(posts, {
|
|
26
27
|
params: { category },
|
|
27
|
-
props: { category },
|
|
28
|
+
props: { category, sorted },
|
|
28
29
|
pageSize: config.postList.pageSize,
|
|
29
|
-
}),
|
|
30
|
+
}).map((entry: any, i: number) => ({
|
|
31
|
+
params: { category, page: i > 0 ? `page/${i + 1}` : undefined },
|
|
32
|
+
props: entry.props,
|
|
33
|
+
})),
|
|
30
34
|
);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
const { page, category } = Astro.props;
|
|
34
|
-
const
|
|
37
|
+
const { page, category, sorted } = Astro.props;
|
|
38
|
+
const pagePosts = await hydratePosts(page.data);
|
|
35
39
|
---
|
|
36
40
|
|
|
37
41
|
<BaseLayout title={t('categoryTitle', { category })} page="categories">
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
<div class="home-layout">
|
|
43
|
+
{/* 文章列表:与首页同一容器约定,供 app.ts 恢复与切换网格/列表形态 */}
|
|
44
|
+
<div
|
|
45
|
+
class="post-list"
|
|
46
|
+
id="postList"
|
|
47
|
+
data-allow-switch={config.postList.allowSwitch ? "true" : undefined}
|
|
48
|
+
data-default-layout={config.postList.defaultLayout}
|
|
49
|
+
data-grid-col={config.postList.gridColumnWidth}
|
|
50
|
+
>
|
|
51
|
+
<header class="archive-head reveal">
|
|
52
|
+
<h1 class="page-title">{category}</h1>
|
|
53
|
+
<p class="page-sub">{t('totalPosts', { n: page.total })}</p>
|
|
54
|
+
</header>
|
|
55
|
+
{
|
|
56
|
+
pagePosts.map((post: any) => (
|
|
57
|
+
<PostCard post={post} featured={post.data.pinned} />
|
|
58
|
+
))
|
|
59
|
+
}
|
|
60
|
+
<Pagination page={page.currentPage} total={page.lastPage} basePath={`/categories/${category}/`} />
|
|
61
|
+
</div>
|
|
62
|
+
<Sidebar posts={sorted} />
|
|
51
63
|
</div>
|
|
52
|
-
<Pagination page={page.currentPage} total={page.lastPage} basePath={`/categories/${category}/`} />
|
|
53
64
|
</BaseLayout>
|
package/src/routes/friends.astro
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getEntry, render } from "astro:content";
|
|
3
3
|
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
|
+
import Comment from "virtual:theme/components/Comment.astro";
|
|
4
5
|
import config from "virtual:theme/config";
|
|
5
6
|
import { t } from "../i18n/index.js";
|
|
6
7
|
import { resolveSiteImage } from "../utils/image.js";
|
|
@@ -88,4 +89,7 @@ const apply = config.friends.apply;
|
|
|
88
89
|
</section>
|
|
89
90
|
)
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
<!-- §4.113:友链页评论区,支持直接留言申请友链(provider 未启用时自动 no-op) -->
|
|
94
|
+
<Comment path="/friends" />
|
|
91
95
|
</BaseLayout>
|
|
@@ -8,7 +8,9 @@ import { formatDate } from "../../utils.js";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* 说说页(§4.87):moments collection 时间流,按 published 新→旧,
|
|
11
|
-
* 每页 20 条(第一页 /moments/,后续 /moments/
|
|
11
|
+
* 每页 20 条(第一页 /moments/,后续 /moments/page/N/)。
|
|
12
|
+
* 分页段必须是 page/N/(§4.113)——Pagination 组件按此约定生成 href,
|
|
13
|
+
* paginate() 默认产出的 /N/ 会与组件链接错位导致 404,这里统一 remap。
|
|
12
14
|
* 正文走主题统一 markdown processor(全部增强语法可用);配图 images 网格
|
|
13
15
|
* 展示并接入通用灯箱(data-lightbox-group 组浏览)。
|
|
14
16
|
*/
|
|
@@ -19,7 +21,10 @@ export async function getStaticPaths({ paginate }: { paginate: any }) {
|
|
|
19
21
|
const sorted = [...all].sort(
|
|
20
22
|
(a, b) => b.data.published.valueOf() - a.data.published.valueOf(),
|
|
21
23
|
);
|
|
22
|
-
return paginate(sorted, { pageSize: 20 })
|
|
24
|
+
return paginate(sorted, { pageSize: 20 }).map((entry: any, i: number) => ({
|
|
25
|
+
params: { page: i > 0 ? `page/${i + 1}` : undefined },
|
|
26
|
+
props: entry.props,
|
|
27
|
+
}));
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const { page } = Astro.props;
|
|
@@ -111,6 +111,11 @@ const { cover: rawArticleCover, retries: coverRetries } = resolveCover(
|
|
|
111
111
|
const articleCover = rawArticleCover
|
|
112
112
|
? await resolveSiteImage(rawArticleCover)
|
|
113
113
|
: rawArticleCover;
|
|
114
|
+
/* §4.114:普通封面图失效的客户端兜底(api 文章 retries 末尾已含 fallbackCover,无需重复) */
|
|
115
|
+
const coverFallback =
|
|
116
|
+
!coverRetries && config.postList.fallbackCover
|
|
117
|
+
? await resolveSiteImage(config.postList.fallbackCover)
|
|
118
|
+
: "";
|
|
114
119
|
const { Content, headings, remarkPluginFrontmatter } = await render(post);
|
|
115
120
|
const { words, minutes } = remarkPluginFrontmatter as {
|
|
116
121
|
words?: number;
|
|
@@ -240,6 +245,7 @@ const jsonLd = {
|
|
|
240
245
|
loading="eager"
|
|
241
246
|
data-lqip
|
|
242
247
|
data-api-retries={coverRetries ? JSON.stringify(coverRetries) : undefined}
|
|
248
|
+
data-cover-fallback={coverFallback || undefined}
|
|
243
249
|
/>
|
|
244
250
|
</figure>
|
|
245
251
|
)
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
3
|
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
4
|
import Pagination from "virtual:theme/components/Pagination.astro";
|
|
5
|
+
import PostCard from "virtual:theme/components/PostCard.astro";
|
|
6
|
+
import Sidebar from "virtual:theme/components/Sidebar.astro";
|
|
5
7
|
import config from "virtual:theme/config";
|
|
6
8
|
import { t } from "../../../i18n/index.js";
|
|
7
|
-
import { collectTags,
|
|
9
|
+
import { collectTags, getSortedPosts } from "../../../utils.js";
|
|
10
|
+
import { hydratePosts } from "../../_hydrate.js";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* 标签详情(§4.87 分页):每标签按 postList.pageSize 分页,第一页
|
|
11
|
-
* /tags/<tag>/(page 段省略),后续 /tags/<tag>/
|
|
14
|
+
* /tags/<tag>/(page 段省略),后续 /tags/<tag>/page/N/。结构与分类详情页一致。
|
|
15
|
+
* 分页段 page/N/ 与布局同构改造见分类详情页注释(§4.113)。
|
|
12
16
|
*/
|
|
13
17
|
export async function getStaticPaths({ paginate }: { paginate: any }) {
|
|
14
18
|
const all = await getCollection("posts", ({ data }) =>
|
|
@@ -19,33 +23,40 @@ export async function getStaticPaths({ paginate }: { paginate: any }) {
|
|
|
19
23
|
return [...collectTags(sorted).entries()].flatMap(([tag, posts]) =>
|
|
20
24
|
paginate(posts, {
|
|
21
25
|
params: { tag },
|
|
22
|
-
props: { tag },
|
|
26
|
+
props: { tag, sorted },
|
|
23
27
|
pageSize: config.postList.pageSize,
|
|
24
|
-
}),
|
|
28
|
+
}).map((entry: any, i: number) => ({
|
|
29
|
+
params: { tag, page: i > 0 ? `page/${i + 1}` : undefined },
|
|
30
|
+
props: entry.props,
|
|
31
|
+
})),
|
|
25
32
|
);
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
const { page, tag } = Astro.props;
|
|
29
|
-
const
|
|
35
|
+
const { page, tag, sorted } = Astro.props;
|
|
36
|
+
const pagePosts = await hydratePosts(page.data);
|
|
30
37
|
---
|
|
31
38
|
|
|
32
39
|
<BaseLayout title={t('tagTitle', { tag })} page="tags">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
<div class="home-layout">
|
|
41
|
+
{/* 文章列表:与首页同一容器约定,供 app.ts 恢复与切换网格/列表形态 */}
|
|
42
|
+
<div
|
|
43
|
+
class="post-list"
|
|
44
|
+
id="postList"
|
|
45
|
+
data-allow-switch={config.postList.allowSwitch ? "true" : undefined}
|
|
46
|
+
data-default-layout={config.postList.defaultLayout}
|
|
47
|
+
data-grid-col={config.postList.gridColumnWidth}
|
|
48
|
+
>
|
|
49
|
+
<header class="archive-head reveal">
|
|
50
|
+
<h1 class="page-title">{tag}</h1>
|
|
51
|
+
<p class="page-sub">{t('totalPosts', { n: page.total })}</p>
|
|
52
|
+
</header>
|
|
53
|
+
{
|
|
54
|
+
pagePosts.map((post: any) => (
|
|
55
|
+
<PostCard post={post} featured={post.data.pinned} />
|
|
56
|
+
))
|
|
57
|
+
}
|
|
58
|
+
<Pagination page={page.currentPage} total={page.lastPage} basePath={`/tags/${tag}/`} />
|
|
59
|
+
</div>
|
|
60
|
+
<Sidebar posts={sorted} />
|
|
49
61
|
</div>
|
|
50
|
-
<Pagination page={page.currentPage} total={page.lastPage} basePath={`/tags/${tag}/`} />
|
|
51
62
|
</BaseLayout>
|
package/src/schema.ts
CHANGED
|
@@ -368,9 +368,10 @@ export const themeConfigSchema = z.object({
|
|
|
368
368
|
fallbackCover: z.string().optional(),
|
|
369
369
|
/**
|
|
370
370
|
* 随机封面图源(§4.33):apis 非空时,frontmatter 显式 image:"api" 的文章
|
|
371
|
-
*
|
|
371
|
+
* 按 slug 确定性轮转选一条(§4.113,可传本地 public 路径列表做多源随机;
|
|
372
|
+
* 单条时恒为第一条)。URL 支持 {slug} 占位符(替换为文章 id),
|
|
372
373
|
* 并自动追加 ?v=<hash>(Java hashCode(slug)),同 slug 每次构建稳定同图。
|
|
373
|
-
*
|
|
374
|
+
* 客户端加载失败时按其余图源顺序依次重试,全部失败显示本地 fallbackCover。
|
|
374
375
|
*/
|
|
375
376
|
randomCover: z
|
|
376
377
|
.object({
|
package/src/scripts/app.ts
CHANGED
|
@@ -1110,8 +1110,9 @@ doc.addEventListener("click", (e) => {
|
|
|
1110
1110
|
}
|
|
1111
1111
|
});
|
|
1112
1112
|
|
|
1113
|
-
/*
|
|
1114
|
-
|
|
1113
|
+
/* 封面加载失败兜底(§4.33 随机重试 + §4.114 普通图兜底):img error 不冒泡,捕获阶段
|
|
1114
|
+
document 级委托(bundled 一次性执行,VT 换页后仍有效)。优先按 data-api-retries
|
|
1115
|
+
顺序换源;无重试列表的普通封面图按 data-cover-fallback 换兜底图(仅试一次);
|
|
1115
1116
|
全部失败加 cover-broken 藏 img,露出底层 LQIP 渐变占位。 */
|
|
1116
1117
|
doc.addEventListener(
|
|
1117
1118
|
"error",
|
|
@@ -1119,20 +1120,26 @@ doc.addEventListener(
|
|
|
1119
1120
|
const img = e.target as HTMLImageElement | null;
|
|
1120
1121
|
if (img?.tagName !== "IMG") return;
|
|
1121
1122
|
const raw = img.dataset.apiRetries;
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1123
|
+
if (raw) {
|
|
1124
|
+
try {
|
|
1125
|
+
const list: string[] = JSON.parse(raw);
|
|
1126
|
+
const idx = Number(img.dataset.retryIdx ?? "1");
|
|
1127
|
+
if (idx >= 0 && idx < list.length) {
|
|
1128
|
+
img.dataset.retryIdx = String(idx + 1);
|
|
1129
|
+
img.src = list[idx];
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
} catch {
|
|
1133
|
+
/* 列表损坏则落到兜底分支 */
|
|
1134
|
+
}
|
|
1128
1135
|
}
|
|
1129
|
-
const
|
|
1130
|
-
if (
|
|
1131
|
-
img.dataset.
|
|
1132
|
-
img.src =
|
|
1133
|
-
|
|
1134
|
-
img.classList.add("cover-broken");
|
|
1136
|
+
const fb = img.dataset.coverFallback;
|
|
1137
|
+
if (fb && !img.dataset.fallbackTried) {
|
|
1138
|
+
img.dataset.fallbackTried = "1";
|
|
1139
|
+
img.src = fb;
|
|
1140
|
+
return;
|
|
1135
1141
|
}
|
|
1142
|
+
if (raw || fb) img.classList.add("cover-broken");
|
|
1136
1143
|
},
|
|
1137
1144
|
true,
|
|
1138
1145
|
);
|
package/src/utils.ts
CHANGED
|
@@ -212,8 +212,9 @@ export function randomCoverUrl(template: string, slug: string): string {
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* 解析文章封面(§4.33):frontmatter image 为 "api" 且配置了图源时,
|
|
215
|
-
* 返回 { cover, retries }——cover
|
|
216
|
-
*
|
|
215
|
+
* 返回 { cover, retries }——cover 按 slug 确定性轮转选一条(§4.113,
|
|
216
|
+
* 支持本地图片列表做多源随机;单图源退化为恒取第一条,行为不变),
|
|
217
|
+
* retries 为客户端加载失败时的依次重试列表(其余图源 + fallbackCover 兜底)。
|
|
217
218
|
* 非 "api" 或未配置图源时原样返回,行为与之前一致。
|
|
218
219
|
*/
|
|
219
220
|
export function resolveCover(
|
|
@@ -226,8 +227,9 @@ export function resolveCover(
|
|
|
226
227
|
if (image !== "api" || apis.length === 0) {
|
|
227
228
|
return { cover: image || firstImage || fallbackCover, retries: undefined };
|
|
228
229
|
}
|
|
229
|
-
const
|
|
230
|
-
const
|
|
230
|
+
const urls = apis.map((u) => randomCoverUrl(u, slug));
|
|
231
|
+
const idx = seedHash(slug) % urls.length;
|
|
232
|
+
const retries = [...urls.slice(idx + 1), ...urls.slice(0, idx)];
|
|
231
233
|
if (fallbackCover) retries.push(fallbackCover);
|
|
232
|
-
return { cover, retries };
|
|
234
|
+
return { cover: urls[idx], retries };
|
|
233
235
|
}
|