初始化

This commit is contained in:
RiseForever
2026-08-13 00:26:08 +08:00
parent c382cd642a
commit 8ae7b2a60e
27 changed files with 1084 additions and 331 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+5 -19
View File
@@ -1,20 +1,12 @@
![Astro](https://img.shields.io/badge/Built%20with-Astro-dee2e6?logo=astro&logoColor=white&style=for-the-badge) ![Eidolon](https://img.shields.io/badge/theme-eidolon-yellow?style=for-the-badge) Eidolon 是一款轻量、简洁且美观的 Astro 主题。基于 MIT License 开源。
--- [GitHub](https://github.com/virelyx258/Astro-Theme-Eidolon) | [云仓](https://src.luming.cool/riseforever2026/Eidolon)
这里是 **⌈路明笔记⌋** 的代码仓库。
[GitHub](https://github.com/virelyx258/Blog) | [云仓](https://src.luming.cool/riseforever2026/Blog)
之所以在自己的服务器上托管一份,是因为 GitHub 直连很不稳定,导致我无法愉快地更新博客。自托管后,无需挂代理即可轻松更新。
两个仓库互为镜像关系,同步更新。 两个仓库互为镜像关系,同步更新。
## 主题 本主题的设计思想参照了 Typecho 主题 [Mirages](https://get233.com/archives/mirages-intro.html)。
Eidolon,基于 [Astro](https://astro.build) 进行构建的主题,设计思想参照了 Typecho 主题 [Mirages](https://get233.com/archives/mirages-intro.html) 我自己曾是 Mirages 主题用户,后为追求极致速度,便产生了移植主题到 Astro 的想法
我曾是 Mirages 主题用户,后为追求极致速度,便产生了移植主题到 Astro 的想法。
一款外观相似、免费的主题,如果开源,可能会对原付费主题的销售造成影响。但细想想,我们其实不在一个赛道。喜欢使用 Typecho 博客系统、追求简约的用户,一定还会选择 Mirages 的。本主题的开源,只是为 Astro 用户提供另一种选择。 一款外观相似、免费的主题,如果开源,可能会对原付费主题的销售造成影响。但细想想,我们其实不在一个赛道。喜欢使用 Typecho 博客系统、追求简约的用户,一定还会选择 Mirages 的。本主题的开源,只是为 Astro 用户提供另一种选择。
@@ -28,10 +20,4 @@ Eidolon,基于 [Astro](https://astro.build) 进行构建的主题,设计思
- 支持多样的短代码; - 支持多样的短代码;
- 原生支持友链卡片。 - 原生支持友链卡片。
欢迎使用。如果能帮助到你,欢迎去 [GitHub 仓库](https://github.com/virelyx258/astro_theme_) 给我点个 Star。 欢迎使用。如果能帮助到你,欢迎去 [GitHub 仓库](https://github.com/virelyx258/Astro-Theme-Eidolon) 给我点个 Star。
## 版权协议
本站的文章基于 [CC-BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans) 协议公开。
转载请注明原文链接。[协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans)不允许对本站文章的再混合、转换、或者二次创作。若认为有必要二创,请邮件沟通。
+17 -3
View File
@@ -6,9 +6,23 @@ import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import shortcodes from './src/lib/shortcodes.ts'; import shortcodes from './src/lib/shortcodes.ts';
import externalLinks from './src/lib/external-links.ts'; import externalLinks from './src/lib/external-links.ts';
import responsiveTables from './src/lib/responsive-tables.ts';
import noticeShortcode from './src/lib/notice-shortcode.ts';
import { siteConfig } from './src/site.config.ts'; import { siteConfig } from './src/site.config.ts';
import { readFileSync } from 'node:fs'; import { copyFileSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const rootGif = resolve(process.cwd(), '88x31.gif');
const copyRootGif = {
name: 'copy-root-gif',
hooks: {
'astro:build:done': ({ dir }) => {
copyFileSync(rootGif, resolve(fileURLToPath(dir), '88x31.gif'));
}
}
};
let githubData = {}; let githubData = {};
try { try {
@@ -20,7 +34,7 @@ try {
export default defineConfig({ export default defineConfig({
site: siteConfig.site.url, site: siteConfig.site.url,
integrations: [sitemap()], integrations: [sitemap(), copyRootGif],
vite: { vite: {
plugins: [tailwindcss()] plugins: [tailwindcss()]
}, },
@@ -34,7 +48,7 @@ export default defineConfig({
}, },
processor: unified({ processor: unified({
remarkPlugins: [remarkMath, [shortcodes, { githubData, siteUrl: siteConfig.site.url }]], remarkPlugins: [remarkMath, [shortcodes, { githubData, siteUrl: siteConfig.site.url }]],
rehypePlugins: [rehypeKatex, [externalLinks, { siteUrl: siteConfig.site.url }]] rehypePlugins: [rehypeKatex, noticeShortcode, responsiveTables, [externalLinks, { siteUrl: siteConfig.site.url }]]
}) })
} }
}); });
+2
View File
@@ -0,0 +1,2 @@
RiseForever
RSV
+2
View File
@@ -0,0 +1,2 @@
RiseForever
RSV
+2
View File
@@ -0,0 +1,2 @@
RiseForever
RSV
+48 -3
View File
@@ -1,17 +1,33 @@
--- ---
import { siteConfig } from '@/site.config'; import { siteConfig } from '@/site.config';
import lightSticker from '@/images/Written-By-Human-white.svg';
import darkSticker from '@/images/Written-By-Human-black.svg';
const containsChinese = (value: string) => /[\u3400-\u9fff]/.test(value); const containsChinese = (value: string) => /[\u3400-\u9fff]/.test(value);
const blogDomain = new URL(siteConfig.site.url).hostname;
const blogsClubBadge = `https://www.blogsclub.org/badge/${blogDomain}/theme`;
--- ---
<footer class="w-full border-t border-line bg-raised px-5 py-5 text-center font-mono text-xs leading-6 text-subtle" data-pagefind-ignore="all"> <footer class="w-full border-t border-line bg-raised px-5 py-5 text-center font-mono text-xs leading-6 text-subtle" data-pagefind-ignore="all">
<div class="mx-auto max-w-[73.125rem]"> <div class="mx-auto max-w-[73.125rem]">
<!-- 页脚标签 Start
<div class="footer-badges">
<a href="https://notbyai.fyi" target="_blank" rel="noreferrer" aria-label="Not By AI">
<img class="footer-badge-image" src={lightSticker.src} data-light-src={lightSticker.src} data-dark-src={darkSticker.src} width="131" height="42" alt="Not By AI" />
</a>
<a href="https://www.blogsclub.org/rank.html" target="_blank" rel="noreferrer" aria-label="博阅榜">
<img class="footer-badge-image blogs-club-badge" src={`${blogsClubBadge}/simple`} data-light-src={`${blogsClubBadge}/simple`} data-dark-src={`${blogsClubBadge}/simple-dark`} height="42" alt="博阅榜" loading="lazy" />
</a>
</div>
-->
{siteConfig.footer.links.length > 0 && <nav aria-label="页脚导航">{siteConfig.footer.links.map((item, index) => <Fragment>{index > 0 && ' · '}<a data-text-link href={item.href} target={item.external ? '_blank' : undefined} rel={item.external ? 'noreferrer' : undefined}><span class:list={{ 'footer-chinese': containsChinese(item.label) }} lang={containsChinese(item.label) ? 'zh-CN' : undefined}>{item.label}</span></a></Fragment>)}</nav>} {siteConfig.footer.links.length > 0 && <nav aria-label="页脚导航">{siteConfig.footer.links.map((item, index) => <Fragment>{index > 0 && ' · '}<a data-text-link href={item.href} target={item.external ? '_blank' : undefined} rel={item.external ? 'noreferrer' : undefined}><span class:list={{ 'footer-chinese': containsChinese(item.label) }} lang={containsChinese(item.label) ? 'zh-CN' : undefined}>{item.label}</span></a></Fragment>)}</nav>}
{siteConfig.footer.copyright && <p class="m-0">{siteConfig.footer.copyright}</p>} <p class="m-0">Powered By Astro · Theme <a data-text-link href="https://src.luming.cool/riseforever2026/Eidolon">Eidolon</a></p>
{siteConfig.footer.copyright && <p class="m-0">{siteConfig.footer.copyright}</p>}
{siteConfig.footer.note && <p class="footer-chinese m-0" lang="zh-CN">{siteConfig.footer.note}</p>} {siteConfig.footer.note && <p class="footer-chinese m-0" lang="zh-CN">{siteConfig.footer.note}</p>}
{siteConfig.footer.theme && <p class="m-0"><a data-text-link href={siteConfig.footer.theme.href} target={siteConfig.footer.theme.external ? '_blank' : undefined} rel={siteConfig.footer.theme.external ? 'noreferrer' : undefined}>{siteConfig.footer.theme.label}</a></p>} {siteConfig.footer.theme && <p class="m-0"><a data-text-link href={siteConfig.footer.theme.href} target={siteConfig.footer.theme.external ? '_blank' : undefined} rel={siteConfig.footer.theme.external ? 'noreferrer' : undefined}>{siteConfig.footer.theme.label}</a></p>}
<p class="m-0">Powered By Astro. Theme <a data-text-link href="https://src.luming.cool/riseforever2026/Eidolon">Eidolon</a></p> </div>
</div>
</footer> </footer>
<style> <style>
@@ -19,4 +35,33 @@ const containsChinese = (value: string) => /[\u3400-\u9fff]/.test(value);
font-family: var(--mirages-font-sans); font-family: var(--mirages-font-sans);
} }
.footer-badges {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.footer-badges a { line-height: 0; }
.footer-badge-image { display: block; }
.blogs-club-badge { height: 42px !important; width: auto; }
</style> </style>
<script>
const root = document.documentElement;
const systemTheme = matchMedia('(prefers-color-scheme: dark)');
const syncBadges = () => {
const isDark = root.dataset.theme === 'dark' || (root.dataset.theme === 'auto' && systemTheme.matches);
document.querySelectorAll<HTMLImageElement>('.footer-badge-image').forEach((badge) => {
badge.src = isDark ? badge.dataset.darkSrc ?? badge.src : badge.dataset.lightSrc ?? badge.src;
});
};
syncBadges();
systemTheme.addEventListener('change', syncBadges);
const themeObserver = new MutationObserver(syncBadges);
themeObserver.observe(root, { attributes: true, attributeFilter: ['data-theme'] });
document.addEventListener('astro:page-load', syncBadges);
</script>
+3 -3
View File
@@ -122,8 +122,8 @@ const position = banner?.position ?? siteConfig.banner.position;
.masthead-separator { font-family: Consolas, Menlo, Monaco, 'lucida_console', 'Liberation_Mono', 'Courier_New', 'andale_mono', monospaceX, monospace, sans-serif; } .masthead-separator { font-family: Consolas, Menlo, Monaco, 'lucida_console', 'Liberation_Mono', 'Courier_New', 'andale_mono', monospaceX, monospace, sans-serif; }
.masthead-date strong { font-family: var(--mirages-font-ui); font-weight: 400; } .masthead-date strong { font-family: var(--mirages-font-ui); font-weight: 400; }
.masthead-category { font-family: var(--mirages-font-ui); } .masthead-category { font-family: var(--mirages-font-ui); }
.about-avatar { animation: avatar-rotate 8s ease-in-out infinite alternate; } .about-avatar { transform: rotate(0deg); transition: transform 0.5s cubic-bezier(0.33, 1, 0.68, 1); }
@keyframes avatar-rotate { from { transform: rotate(-2deg); } to { transform: rotate(2deg); } } .about-avatar:hover { transform: rotate(-32deg); }
@media (min-width: 48rem) { section { height: var(--desktop-height); } } @media (min-width: 48rem) { section { height: var(--desktop-height); } }
@media (prefers-reduced-motion: reduce) { .about-avatar { animation: none; transform: none; } } @media (prefers-reduced-motion: reduce) { .about-avatar { transition: none; transform: none; } .about-avatar:hover { transform: none; } }
</style> </style>
-1
View File
@@ -168,7 +168,6 @@ const { overlay = false } = Astro.props;
.navbar-over-banner { .navbar-over-banner {
border-color: color-mix(in srgb, var(--color-border) 70%, transparent); border-color: color-mix(in srgb, var(--color-border) 70%, transparent);
background: color-mix(in srgb, var(--color-bg) 82%, transparent); background: color-mix(in srgb, var(--color-bg) 82%, transparent);
box-shadow: 0 2px 10px color-mix(in srgb, var(--color-text) 12%, transparent);
} }
.navbar-over-banner [data-navbar-tools] > [data-toolbar-item] > a:hover, .navbar-over-banner [data-navbar-tools] > [data-toolbar-item] > a:hover,
-1
View File
@@ -114,7 +114,6 @@
min-height: 3px; min-height: 3px;
max-height: 3px; max-height: 3px;
background: var(--color-accent); background: var(--color-accent);
box-shadow: 0 3px 8px rgb(0 0 0 / 35%);
transform: scaleX(0); transform: scaleX(0);
transform-origin: left center; transform-origin: left center;
transition: transform 240ms linear; transition: transform 240ms linear;
+1 -1
View File
@@ -187,7 +187,7 @@ const totalWordsLabel = totalWords >= 10_000
const availableCellSize = (heatmapScroll.clientWidth - 3.25 * baseRootFontSize) / 53; const availableCellSize = (heatmapScroll.clientWidth - 3.25 * baseRootFontSize) / 53;
if (scale <= 1.01 || baseCellSize === 0) { if (scale <= 1.01 || baseCellSize === 0) {
baseCellSize = Math.min(12, Math.max(8, availableCellSize)); baseCellSize = Math.max(8, availableCellSize);
} }
heatmapGrid.style.setProperty('--heatmap-cell-size', `${baseCellSize * scale}px`); heatmapGrid.style.setProperty('--heatmap-cell-size', `${baseCellSize * scale}px`);
heatmapViewport?.classList.toggle('has-overflow', heatmapGrid.getBoundingClientRect().width > heatmapScroll.clientWidth + 1); heatmapViewport?.classList.toggle('has-overflow', heatmapGrid.getBoundingClientRect().width > heatmapScroll.clientWidth + 1);
+188
View File
@@ -0,0 +1,188 @@
---
cover: "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80"
title: "关于"
pubDate: "2026-07-22T01:28:00.000Z"
updatedDate: "2026-07-27T05:26:00.000Z"
categories:
tags:
slug: "about"
comments: false
---
## 关于⌈我⌋
我是 RiseForever,生于河南。
准高二了。喜欢开发软件、科技数码。是一名 Xiaomi Vela 快应用开发者。
性格偏多疑敏感,善于察觉身边人的微小情绪。平时喜欢独处,不喜欢与任何人一起逛街、看电影,因为我感觉受到束缚。
如果你想知道我是一个怎样的人,请随便翻翻[我的文章](/archives),里面写的比我说的更清楚。
### 联系
电子邮箱:hi[at]riseforever.cn(请将“[at]”替换为“@”)
GitHub[virelyx258](https://github.com/virelyx258)
请不要试图添加我的即时通讯账户。无论有什么想探讨的,随时欢迎电邮交流。
### 使用的工具
[tools title = "设计"]
[Canva](https://www.canva.cn)+(https://image.luming.cool/i/2026/08/07/6a75ba231df9b.webp)/(图像设计)
[即时设计](https://js.design)+(https://image.luming.cool/i/2026/08/07/6a75c0b9bbf29.webp)/(UI 设计)
[Microsoft 365](https://m365.cloud.microsoft/)+(https://image.luming.cool/i/2026/08/07/6a75bd0318e8e.webp)/(文档处理)
[剪映](https://www.capcut.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bf6a337d1.webp)/(视频剪辑)
[/tools]
[tools title = "生产力"]
[Chrome](https://google.cn/chrome/)+(https://image.luming.cool/i/2026/08/07/6a75c029d170b.webp)/(网页浏览)
[Edge](https://explore.microsoft.com/zh-cn/edge?ep=2185&form=MA14LR&es=375&cs=2324297850)+(https://image.luming.cool/i/2026/08/07/6a75c06af3bd2.webp)/(网页浏览)
[Typora](https://typoraio.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bfad4d232.webp)/(Markdown 编辑)
[/tools]
[tools title = "开发"]
[AIoT IDE](https://iot.mi.com/vela/quickapp/)+(https://image.luming.cool/i/2026/08/07/6a75bd500971b.webp)/(快应用编写 & 调试)
[VS Code](https://code.visualstudio.com/)+(https://image.luming.cool/i/2026/08/07/6a75c0ba31e58.webp)/(全能 IDE)
[记事本](#)+(https://image.luming.cool/i/2026/08/07/6a75c16e34a58.webp)/(你懂的,真的便捷)
[/tools]
[tools title = "环境"]
[Windows 11](https://news.microsoft.com/windows11-general-availability/)+(https://image.luming.cool/i/2026/08/07/6a75c1d50d47d.webp)/(主力系统)
[Debian 13](https://www.debian.org/index.zh-cn.html)+(https://image.luming.cool/i/2026/08/07/6a75c2db6a6e9.webp)/(备用系统)
[/tools]
## 关于⌈这个网站⌋
严格来说,这个网站的建立时间是在 2024 年 10 月。因为我在那时开始转型记录生活。
有些话我没法对任何人说,于是就在这里写下。
### 域名
起初用的是 riseforever.cn,因为初中时我很想要一个有美好寓意的域名,想了很多,结果只有这个域名可注册。于是我选了它,网名也跟着取成了“RiseForever”。
初三时,我觉得 riseforever.cn 太长了,且 .CN 当时仍不支持隐私保护,便想着换个域名。当时朋友买了一个“mnb.cool”,让我看到了 .cool 这个后缀。过了几天,我在学校灵机一动——用自己的名字拼写“luming”做前缀,使用“.cool”后缀,这样搭配在视觉上非常和谐。
那周周末,我回家一搜 luming.cool ——未注册,于是赶紧注册了它。
| 注册商 | 帝思普(腾讯云) |
| -------------: | :------------------------------------------------------------------------------------------------------------- |
| 首次购入价格 | ¥48 |
| 续费价格 | ¥65 |
| 域名年龄 | ![域名年龄徽章](https://yisi.yun/api/badge/domain-age/luming.cool?theme=blue&lang=zh&size=medium&mode=light) |
[collapse title="riseforever.cn 信息"]
| 注册商 | 云讯(腾讯云) |
| -------------: | :---------------------------------------------------------------------------------------------------------------- |
| 首次购入价格 | ¥33 |
| 续费价格 | ¥38 |
| 域名年龄 | ![域名年龄徽章](https://yisi.yun/api/badge/domain-age/riseforever.cn?theme=blue&lang=zh&size=medium&mode=light) |
[/collapse]
目前保持着 riseforever.cn 和 luming.cool 双域名持有,每年续费大约需要¥103。
### 服务器
于 2026 年 7 月 21 日搬迁至 007IDC 的香港 BGP 线路服务器,经实测晚高峰也能跑满带宽 100 Mbps,三网回程经过优化,表现优异,河南联通 ping 延迟仅 31ms。
首月 9 元,后续叠加优惠,¥17.5/月。
| 厂商 | [007IDC](https://www.007idc.cn/aff/ZFIKNXMX) |
| -----: | :--------------------------------------------- |
| CPU | 2H Platinum 8272CL |
| 内存 | 2G DDR4 |
| 硬盘 | 30GB SSD |
| 带宽 | 100 Mbps(峰值) |
[collapse title="2026 年 1 月 - 6 月"]
7iNet 香港特惠秒杀机,¥9.9/月,炸夸响。后因其性能太过诡异而被更换。
| CPU | 2H E5-2698 v4 |
| -----: | :-------------------------------------- |
| 内存 | 2G |
| 硬盘 | 40GB SSD(写入 43MB/s,读取 156MB/s |
| 带宽 | 10 Mbps(不限流量) |
[/collapse]
[collapse title="2026 年以前"]
腾讯云新加坡服务器,¥99/年,买了两年,参加活动又送了三个月。后因 IP 线路更换导致延迟过高而被更换。
| CPU | 2H Platinum 8255C |
| -----: | :--------------------------------------- |
| 内存 | 2G |
| 硬盘 | 50GB SSD(写入 338MB/s,读取 6.2GB/s |
| 带宽 | 30 Mbps(月限流 1TB |
[/collapse]
### CDN
使用[毅融盾](https://rcdn.hydun.com/)进行图床加速。
毅融盾是融合了网宿与白山 CDN 资源的企业级 CDN,针对未备案域名进行特调,提供卓越的中国大陆访问速度。且没有三六九等的套餐配置,仅按流量计费,出售的是流量包。
毅融盾是我所在公司的产品,内部团队可免费使用,不限流量。如果你是外部用户,也可以通过[赞助计划](https://blog.hydun.com/9.html)免费获取 CDN 服务。
### 主题
使用自己制作的 Astro 主题 Eidolon。
它极大程度上复刻了 Typecho 主题 Mirages 的风格和功能,又在此基础上使用了更先进的 TailWind CSS,使得站点响应速度更加优秀。
~~因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。~~
仔细想想,发现我们其实不是同一个赛道。我做的是 Astro 主题,受众是 Astro 用户,不耽误 Typecho 主题的销售。况且,并不是所有 Typecho 用户都能无感切换到静态博客。
所以,本主题准备开源。
[collapse title="在此之前"]
使用 [Mirages](https://get233.com/archives/mirages-intro.html),一款阅读体验优秀的单栏 Typecho 主题。
我是在[白熊阿丸的小屋](https://blog.bxaw.name)首次体验这款主题,发现它支持用户手动切换衬线/非衬线字体、字体大小,这能使不同口味的用户都获得最适合自己的阅读体验。并且,这款主题在我的 MacBook Pro 2013 上表现得非常流畅。
于是在 2026 年 4 月,我选择购入并使用它。
[/collapse]
### 使用的第三方服务
使用 [WeAvatar](https://weavatar.com/) 作为评论区头像系统。
使用 [LUM ALBUM](https://image.luming.cool) 作为图床。(~~这也算第三方服务嘛~~)
### 备案
本站永远都不会考虑备案。
## 关于⌈我的作品⌋
[tools title = "Xiaomi Vela 快应用"]
[真智学](https://astrobox.online/open?source=resv2&id=cn.seedsoft.realzhixue&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c6781929d.webp)/(免费 · 智学网客户端)
[极物流](https://astrobox.online/open?source=resv2&id=cn.seedsoft.express&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c6910d03b.webp)/(订阅制 · 快递查询工具)
[Poster](https://astrobox.online/open?source=resv2&id=com.virelyx258.poster&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c7021f80e.webp)/(免费 · 网络请求工具)
[/tools]
[tools title = "开源项目"]
[RStatus](https://github.com/virelyx258/RStatus)+(https://image.luming.cool/i/2026/08/08/6a7729950adcd.webp)/(公开你的互联网在线状态)
[火苗调试器](https://github.com/virelyx258/Flame-Debugger)+(https://image.luming.cool/i/2026/08/08/6a772a0218e74.webp)/(适用于网课软件的调试工具)
[/tools]
## 关于⌈版权协议⌋
如果没有特殊说明,本站文章都是原创,基于 [CC-BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans) 协议公开。
转载请注明原文链接。[协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans)原则上不允许对本站文章的再混合、转换、或者二次创作。如果有必要二创,请邮件沟通。
+11
View File
@@ -0,0 +1,11 @@
---
title: "友人"
description: "本站收藏的一些精品中文博客"
pubDate: "2026-07-22T01:28:00.000Z"
cover: "https://images.unsplash.com/photo-1490730141103-6cac27aaab94?auto=format&fit=crop&w=1600&q=80"
slug: "links"
---
## 单向链接
[路明笔记](https://www.luming.cool)+(https://image.luming.cool/i/2026/05/10/6a001b4129893.png)/(一名高中生的技术和生活博客。)
-20
View File
@@ -1,20 +0,0 @@
---
title: "测试页面"
description: "主题初始化后创建的测试页面。"
pubDate: "2026-08-11T00:00:00.000Z"
categories: []
tags: []
slug: "test-page"
draft: false
comments: false
toc: true
---
## 测试页面
这是主题初始化后创建的测试页面。
你可以在这里检查独立页面的标题、正文、链接和列表排版:
- [Markdown 排版测试文章](/posts/markdown-test/)
- [Astro 官方网站](https://astro.build/)
+254
View File
@@ -0,0 +1,254 @@
---
title: "Eidolon 主题基本使用教程"
pubDate: "2026-08-13T00:25:27.180Z"
categories: ['主题']
tags: ['主题']
draft: false
---
欢迎使用 Eidolon,本文将带着您快速入手,使用 Eidolon 轻松定制您的个人博客。
---
## 站点基础信息
欲修改站点基础信息(如站点标题、Banner 大标题、副标题、背景图等),您需要编辑 `/src/site.config.ts`
其中,`site`块代表站点总体配置。示例格式如下:
```ts
site: {
title: 'Eidolon',
description: '一款轻量、简洁且美观的 Astro 主题',
author: {
name: 'Eidolon',
avatar: 'https://weavatar.com/avatar/302380667bdaf4e1390800e62494d4af?s=512&r=G',
bio: '一款轻量、简洁且美观的 Astro 主题',
rotateAvatar: true
},
locale: 'zh-CN',
url: 'https://www.luming.cool',
favicon: 'https://image.luming.cool/i/2026/05/10/6a001b4129893.png'
},
```
每个字段的含义如下表:
| 字段 | 含义 | 类型 |
| :------------------: | :----------------------------------------------: | :----: |
| title | 站点名称,也作为显示在浏览器标签页的标题 | 文本型 |
| description | 站点简介,也用于 SEO 描述 | 文本型 |
| author: name | 博主昵称 | 文本型 |
| author: avatar | 博主头像,填写图片链接 | 文本型 |
| author: bio | 博主的个性签名 | 文本型 |
| author: rotateAvatar | 是否开启关于页面的头像旋转动画 | 布尔型 |
| locale | 站点语言 | 文本型 |
| url | 站点的域名 | 文本型 |
| favicon | 站点图标,同时作为浏览器标签页图标,填写图片链接 | 文本型 |
## 导航栏设置
`navigation`块代表导航项目。示例格式如下:
```ts
navigation: [
{ label: '首页', href: '/', external: false },
{ label: '归档', href: '/archives/', external: false },
{ label: '友人', href: '/links/', external: false },
{ label: '关于', href: '/about/', external: false }
],
```
每个字段的含义如下表:
| 字段 | 含义 | 类型 |
| :------: | :----------------------------------------------: | :----: |
| label | 对外显示的导航名称 | 文本型 |
| href | 点击后跳转的链接,支持站内相对地址和站外绝对地址 | 文本型 |
| external | 是否为外部链接,它决定链接是否在新标签页打开 | 布尔型 |
[hint type="info" title="提示"]无论是顶栏还是侧栏,都有一个“分类”选项,以展示博客里存在的所有分类。该项目无法删除。[/hint]
## Pjax 无刷新加载
`pjax`块代表无刷新加载开关。示例代码:
```ts
pjax: {
enabled: true
},
```
本主题默认开启 Pjax 以获得更好的浏览体验。如果存在兼容性问题,可将 `enabled` 属性设置为 false 以关闭。
## 颜色设置
`appearance`块代表着主题的样式设置。包括深浅色模式和强调色。
示例代码如下:
```ts
appearance: {
defaultTheme: 'auto',
accentColor: '#1abc9c'
},
```
每个字段的含义如下表:
| 字段 | 含义 | 类型 |
| :----------: | :----------------------------------------------------------: | :-----------------: |
| defaultTheme | 用户首次访问博客时,默认采用的主题类型。`auto`代表自动,`light`代表浅色主题,`sunset`代表日落主题,`dark`代表深色主题。 | 文本型 |
| accentColor | 强调色。会影响部分手机浏览器,决定系统状态栏的氛围颜色。 | HEX颜色值(文本型) |
## Banner 设置
`Banner`块代表首页的 Banner 设置。示例代码如下:
```ts
banner: {
enabled: true,
title: 'Eidolon',
subtitle: '一款轻量、简洁且美观的 Astro 主题',
image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2000&q=85',
position: 'center center',
desktopHeightVh: 55,
mobileHeightVh: 40,
overlay: 0.25,
textTone: 'auto'
},
```
各字段含义如下表:
| 字段 | 含义 | 类型 |
| :-------------: | :----------------------------------------------------------: | :----: |
| enabled | 是否启用 Banner。如果设为 false,则下方所有项目都不起作用。 | 布尔型 |
| title | 大标题 | 文本型 |
| subtitle | 小标题 | 文本型 |
| image | 背景图片,填写图片链接 | 文本型 |
| position | 图片的对齐方式,填写格式为 “方向 方向”(方向 空格 方向) | 文本型 |
| desktopHeightVh | 桌面端 Banner 高度占比,譬如上例代表 Banner 高度占页面可视高度的 55% | 数值型 |
| mobileHeightVh | 移动端 Banner 高度占比 | 数值型 |
| overlay | 横幅上方黑色遮罩的透明度。此功能是为了防止 Banner 图过亮导致遮挡文字。0 为完全没有遮罩,1 为暗到纯黑。 | 小数型 |
| textTone | Banner 文字的色调。推荐保持 `auto`,这样 Banner 文字就能够根据图片的亮暗来调整文字的颜色。如果实在需要,可选 `light``Dark`。 | 文本型 |
## 文章卡片默认封面图
`cards`块代表文章卡片设置。示例代码如下:
```ts
cards: {
defaultCovers: ['https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80']
},
```
其中,defaultCovers 是一个 JSON 数组,你可以往里面填写一个多个图片的地址。当有文章未设置封面,且文章里没有图片可被自动设为封面时,系统会从 defaultCovers 里随机抽选一个图片作为封面。
## 页脚
`footer`块代表页脚部分。示例代码如下:
```ts
footer: {
copyright: `© 2023-${new Date().getFullYear()} 你的名字`, // 记得把"你的名字"改成你自己的名字。
// 外链(如果有)
links: [
{ label: 'BlogsClub', href: 'https://blogs.club', external: true }
]
},
```
首先,你需要把 Copyright 中的“你的名字”改为你自己的名字。
其次,如果你有外链需求(比如加入了某博客组织),你可以在 links 这个 JSON 数组里添加外链。`external`代表是否在新标签页打开。
## 评论系统
Eidolon 支持使用 Twikoo 作为评论系统。你只需要自行部署 Twikoo 云函数并获取 envID,将其填写到 comments: envID 中即可。
如果你不需要评论系统,只需将 comments: provider 改为 `none`,然后删除 comments 里剩余的子项即可。
## 顶栏/侧栏按钮自定义
Eidolon 支持自定义按钮。该按钮项目会在顶栏和侧栏同步显示。
示例代码如下:
```ts
toolbarItems: [
{ type: 'search', icon: 'search', name: '搜索文章' },
{ type: 'rss', icon: 'rss', name: 'RSS 订阅', href: '/rss.xml' },
{ type: 'settings', icon: 'settings', name: '阅读设置' }
]
```
其中,`search``rss``settings`这三个 type 都是系统自带的按钮类型,分别对应搜索按钮、RSS 按钮、阅读设置按钮。其分别具有独立的功能,不建议删除。
如果你要新增带图标的外链按钮,需要经历以下这几步:
### 选择图标
Eidolon 使用 [Lucide](https://lucide.dev/icons/) 图标库。先前往该站点选择你要使用的图标,并将其名称复制下来。
### 注册图标名称
编辑 `src/types/config.ts`,第 18 行,有一个
```ts
export type ToolbarIcon = 'search' | 'rss' | 'settings' | 'link' | 'tram-front';
```
只需要将你要添加的图标名称,规范地加在 ToolbarIcon 后面即可。
譬如我新增了一个名为 check 的图标,那么我修改后的代码就是:
```ts
export type ToolbarIcon = 'search' | 'rss' | 'settings' | 'link' | 'tram-front' | 'check';
```
### 导入并建立映射
编辑 `/src/components/ToolbarIcon.astro`,第 2 行
```ts
import { Link, Rss, Search, TramFront, Type } from '@lucide/astro';
```
在这个数组的最前方添加你的图标名称。同上例,如果我要添加名为 check 的图标,那么实际代码就是:
```ts
import { Check, Link, Rss, Search, TramFront, Type } from '@lucide/astro';
```
是的,在第 2 行添加图标名时,需要将图标名首字母大写。
接下来看到第 7 行:
```ts
const icons = { link: Link, rss: Rss, search: Search, settings: Type, 'tram-front': TramFront } as const;
```
只需要在数组的末端加入`图标名(首字母小写): 图标名(首字母大写)`,即可。
同上例,加入 check 这个图标后,代码应为:
```ts
const icons = { link: Link, rss: Rss, search: Search, settings: Type, 'tram-front': TramFront, check: Check } as const;
```
### 添加按钮
回到 site.config.ts,在 toolbarItems 里加入自定义链接按钮。
譬如我添加的是开往,图标名是`tram-front`,那么我的代码就是这样的:
```json
{ type: 'link', icon: 'tram-front', name: '', href: 'https://www.travellings.cn/plain.html', external: true },
```
其中,type: 'link' 定义了这是一个自定义链接;icon 制定了我想要的图标;name 就是鼠标悬浮之上时显示的气泡标题;href 即自定义外链跳转的的地址;external 即是否在新标签页中打开。
---
至此,Eidolon 的自定义教程结束。
-167
View File
@@ -1,167 +0,0 @@
---
title: "Markdown 排版测试"
description: "用于检查主题中常见 Markdown 元素的完整排版效果。"
pubDate: "2026-08-11T00:00:00.000Z"
categories: ['测试分类']
tags: ['测试标签']
slug: "markdown-test"
draft: false
comments: false
math: true
mermaid: true
toc: true
---
# 一级标题
这是一篇用于检查主题排版的测试文章,涵盖常用 Markdown、GFM、数学公式和 Mermaid 图表。
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
## 文本样式
普通文本、**粗体文本**、*斜体文本*、***粗斜体文本***、~~删除线文本~~、`行内代码`
这是第一行,行末使用两个空格强制换行。
这是第二行。
特殊字符可以转义:\*星号\*、\# 井号、\[方括号\]。
## 链接与图片
这是[行内链接](https://astro.build/),这是使用引用的[参考链接][astro],也可以自动识别 <https://astro.build/> 和 <mail@example.com>。
![Astro 标志测试图](/favicon.svg "图片标题")
[astro]: https://astro.build/ "Astro"
## 引用
> 这是一段引用。
>
> > 这是嵌套引用,其中包含 **粗体** 和 `代码`。
## 列表
- 无序列表第一项
- 无序列表第二项
- 二级列表
- 三级列表
1. 有序列表第一项
2. 有序列表第二项
1. 二级有序列表
2. 另一项
- [x] 已完成任务
- [ ] 未完成任务
## 代码
行内代码示例:`const message = 'Hello, Astro';`
```ts
interface Post {
title: string;
published: boolean;
}
const post: Post = {
title: 'Markdown 排版测试',
published: true
};
console.log(post.title);
```
```diff
- const theme = 'old';
+ const theme = 'Eidolon';
```
这是使用四个空格缩进的代码块。
## 表格
| 左对齐 | 居中 | 右对齐 |
| :--- | :---: | ---: |
| 文本 | **粗体** | 100 |
| `code` | [链接](https://astro.build/) | 200 |
## 分隔线
---
分隔线前后的正文用于检查垂直间距。
## 脚注
这句话包含一个脚注。[^note]
[^note]: 这是脚注内容,其中也可以包含 **Markdown**
## 数学公式
行内公式:$E = mc^2$。
块级公式:
$$
\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}
$$
## Mermaid 图表
```mermaid
flowchart LR
A[Markdown] --> B{渲染成功?}
B -->|是| C[完成]
B -->|否| D[检查样式]
```
## HTML 元素
<details>
<summary>展开原生 HTML 内容</summary>
<p>这是一段位于 details 元素中的内容。</p>
</details>
<kbd>Ctrl</kbd> + <kbd>K</kbd>
## 主题短代码
[hint type="info" title="信息提示"]
这是一个信息提示块,包含 **Markdown 文本**
[/hint]
[collapse title="折叠内容"]
这里是折叠区域中的内容。
- 列表项目
- 另一项目
[/collapse]
[tabs title="选项卡测试"]
[tab name="选项一" selected]
第一个选项卡的内容。
[/tab]
[tab name="选项二"]
第二个选项卡的内容。
[/tab]
[/tabs]
[button href="https://astro.build/"]访问 Astro[/button]
[tag type="success"]成功标签[/tag]
## 段落收尾
最后一段用于观察文章底部留白、正文行高以及中英文混排效果。The quick brown fox jumps over the lazy dog. 0123456789。
+30 -6
View File
@@ -10,12 +10,11 @@ import NavigationProgress from '@/components/NavigationProgress.astro';
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';
import '../styles/global.css'; import '../styles/global.css';
interface Props { title?: string; description?: string; image?: string; type?: 'website' | 'article'; noindex?: boolean; math?: boolean; navbarOverlay?: boolean; } interface Props { title?: string; description?: string; image?: string; type?: 'website' | 'article'; noindex?: boolean; math?: boolean; navbarOverlay?: boolean; hasMasthead?: boolean; }
const { title, description, image, type, noindex, math, navbarOverlay = false } = Astro.props; const { title, description, image, type, noindex, math, navbarOverlay = false, hasMasthead = Astro.slots.has('masthead') } = Astro.props;
const hasMasthead = Astro.slots.has('masthead');
const appearanceScript = `(function(){var r=document.documentElement,t='mirages-theme',s='mirages-font-scale',f='mirages-font-family',themes=['auto','light','sunset','dark'],fonts=['serif','sans'],stored=Number(localStorage.getItem(s)),scale=stored>=.8&&stored<=1.5?stored*100:stored>=80&&stored<=150?stored:100,theme=localStorage.getItem(t),font=localStorage.getItem(f);scale=Math.round(scale/5)*5;r.dataset.theme=themes.indexOf(theme)>-1?theme:'${siteConfig.appearance.defaultTheme}';r.dataset.font=fonts.indexOf(font)>-1?font:'sans';r.dataset.platform=/Win/.test(navigator.userAgentData?.platform||navigator.platform)?'windows':'other';r.style.fontSize=scale+'%'})();`; const appearanceScript = `(function(){var r=document.documentElement,t='mirages-theme',s='mirages-font-scale',f='mirages-font-family',themes=['auto','light','sunset','dark'],fonts=['serif','sans'],stored=Number(localStorage.getItem(s)),scale=stored>=.8&&stored<=1.5?stored*100:stored>=80&&stored<=150?stored:100,theme=localStorage.getItem(t),font=localStorage.getItem(f);scale=Math.round(scale/5)*5;r.dataset.theme=themes.indexOf(theme)>-1?theme:'${siteConfig.appearance.defaultTheme}';r.dataset.font=fonts.indexOf(font)>-1?font:'sans';r.dataset.platform=/Win/.test(navigator.userAgentData?.platform||navigator.platform)?'windows':'other';r.style.fontSize=scale+'%'})();`;
const tabsScript = `document.addEventListener('click',function(event){var button=event.target.closest('[data-shortcode-tab]');if(!button)return;var group=button.closest('[data-shortcode-tabs]');var id=button.dataset.shortcodeTab;group.querySelectorAll('[data-shortcode-tab="'+id+'"]').forEach(function(tab){var active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;var panel=group.querySelector('#'+tab.getAttribute('aria-controls'));if(panel)panel.hidden=!active;});});`; const tabsScript = `if(!window.__tabsInit){window.__tabsInit=true;document.addEventListener('click',function(event){var button=event.target.closest('[data-shortcode-tab]');if(!button)return;var group=button.closest('[data-shortcode-tabs]');var id=button.dataset.shortcodeTab;group.querySelectorAll('[data-shortcode-tab=\"'+id+'\"]').forEach(function(tab){var active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;var panel=group.querySelector('#'+tab.getAttribute('aria-controls'));if(panel)panel.hidden=!active;});});}`;
const collapseScript = `document.querySelectorAll('.shortcode-collapse').forEach(function(details){details.dataset.collapseReady='';details.classList.toggle('is-expanded',details.open);var body=details.querySelector(':scope>.shortcode-body');if(body instanceof HTMLElement&&details.open)body.style.height='auto';});document.addEventListener('click',function(event){var summary=event.target.closest('.shortcode-collapse>summary');if(!summary)return;var details=summary.parentElement;if(!(details instanceof HTMLDetailsElement))return;event.preventDefault();var body=details.querySelector(':scope>.shortcode-body');if(!(body instanceof HTMLElement))return;var expand=!details.classList.contains('is-expanded');details.classList.toggle('is-expanded',expand);if(matchMedia('(prefers-reduced-motion: reduce)').matches){details.open=expand;body.style.height=expand?'auto':'';delete details.dataset.animating;return;}var height=body.getBoundingClientRect().height;details.open=true;body.style.height=height+'px';void body.offsetHeight;details.dataset.animating='true';body.style.height=(expand?body.scrollHeight:0)+'px';});document.addEventListener('transitionend',function(event){var body=event.target;if(!(body instanceof HTMLElement)||!body.classList.contains('shortcode-body')||event.propertyName!=='height')return;var details=body.parentElement;if(!(details instanceof HTMLDetailsElement))return;var expanded=details.classList.contains('is-expanded');details.open=expanded;body.style.height=expanded?'auto':'';delete details.dataset.animating;});`; const collapseScript = `function initCollapse(){document.querySelectorAll('.shortcode-collapse').forEach(function(details){details.dataset.collapseReady='';details.classList.toggle('is-expanded',details.open);var body=details.querySelector(':scope>.shortcode-body');if(body instanceof HTMLElement&&details.open)body.style.height='auto';});}initCollapse();document.addEventListener('astro:page-load',initCollapse);document.addEventListener('click',function(event){var summary=event.target.closest('.shortcode-collapse>summary');if(!summary)return;var details=summary.parentElement;if(!(details instanceof HTMLDetailsElement))return;event.preventDefault();var body=details.querySelector(':scope>.shortcode-body');if(!(body instanceof HTMLElement))return;var expand=!details.classList.contains('is-expanded');details.classList.toggle('is-expanded',expand);if(matchMedia('(prefers-reduced-motion: reduce)').matches){details.open=expand;body.style.height=expand?'auto':'';delete details.dataset.animating;return;}var height=body.getBoundingClientRect().height;details.open=true;body.style.height=height+'px';void body.offsetHeight;details.dataset.animating='true';body.style.height=(expand?body.scrollHeight:0)+'px';});document.addEventListener('transitionend',function(event){var body=event.target;if(!(body instanceof HTMLElement)||!body.classList.contains('shortcode-body')||event.propertyName!=='height')return;var details=body.parentElement;if(!(details instanceof HTMLDetailsElement))return;var expanded=details.classList.contains('is-expanded');details.open=expanded;body.style.height=expanded?'auto':'';delete details.dataset.animating;});`;
--- ---
<!doctype html> <!doctype html>
@@ -51,10 +50,35 @@ const hasMasthead = Astro.slots.has('masthead');
<script> <script>
import pangu from 'pangu/browser'; import pangu from 'pangu/browser';
const applyPanguSpacing = () => pangu.spacingPage(); const applyPanguSpacing = () => {
document.querySelectorAll('pre, code, .line').forEach((el) => el.classList.add('no-pangu-spacing'));
document.querySelectorAll('.prose p, .prose li, .prose blockquote, .prose h1, .prose h2, .prose h3, .prose h4').forEach((el) => {
pangu.spacingNode(el);
});
};
applyPanguSpacing(); applyPanguSpacing();
document.addEventListener('astro:page-load', applyPanguSpacing); document.addEventListener('astro:page-load', applyPanguSpacing);
const initCodeLineNumbers = () => {
document.querySelectorAll<HTMLElement>('.prose .astro-code').forEach((block) => {
if (block.querySelector(':scope > .code-line-numbers')) return;
const lines = block.querySelectorAll(':scope > code > .line');
if (!lines.length) return;
const gutter = document.createElement('span');
gutter.className = 'code-line-numbers';
gutter.setAttribute('aria-hidden', 'true');
for (let index = 1; index <= lines.length; index += 1) {
const number = document.createElement('span');
number.textContent = String(index);
gutter.append(number);
}
block.append(gutter);
});
};
initCodeLineNumbers();
document.addEventListener('astro:page-load', initCodeLineNumbers);
</script> </script>
</body> </body>
</html> </html>
+52
View File
@@ -0,0 +1,52 @@
const NOTICE_ICON = {
type: 'element',
tagName: 'span',
properties: { className: ['shortcode-notice'], ariaLabel: '警告' },
children: [{
type: 'element',
tagName: 'svg',
properties: {
ariaHidden: 'true',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
},
children: [
{ type: 'element', tagName: 'circle', properties: { cx: 12, cy: 12, r: 10 }, children: [] },
{ type: 'element', tagName: 'path', properties: { d: 'M12 8v4' }, children: [] },
{ type: 'element', tagName: 'path', properties: { d: 'M12 16h.01' }, children: [] },
],
}],
};
function cloneNoticeIcon() {
return structuredClone(NOTICE_ICON);
}
export function applyNoticeShortcodes(tree: any) {
const transform = (node: any) => {
if (!Array.isArray(node.children) || node.tagName === 'code' || node.tagName === 'pre') return;
const output: any[] = [];
for (const child of node.children) {
if (child.type !== 'text' || !child.value.includes('[!/]')) {
transform(child);
output.push(child);
continue;
}
const parts = child.value.split('[!/]');
parts.forEach((part: string, index: number) => {
if (part) output.push({ type: 'text', value: part });
if (index < parts.length - 1) output.push({ type: 'text', value: '\u00a0' }, cloneNoticeIcon(), { type: 'text', value: '\u00a0' });
});
}
node.children = output;
};
transform(tree);
}
export default function noticeShortcode() {
return (tree: any) => applyNoticeShortcodes(tree);
}
+25
View File
@@ -0,0 +1,25 @@
import { SKIP, visit } from 'unist-util-visit';
export function applyResponsiveTables(tree: any) {
visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {
if (node.tagName !== 'table' || index === undefined || !parent) return;
const classes = Array.isArray(node.properties?.className) ? node.properties.className : [];
if (classes.includes('responsive-table')) return;
const section = node.children?.find((child: any) => child.tagName === 'thead' || child.tagName === 'tbody');
const row = section?.children?.find((child: any) => child.tagName === 'tr');
const columns = Math.max(1, row?.children?.filter((child: any) => child.tagName === 'th' || child.tagName === 'td').length ?? 1);
node.properties = { ...node.properties, className: [...classes, 'responsive-table'] };
parent.children[index] = {
type: 'element',
tagName: 'div',
properties: { className: ['table-scroll'], style: `--table-columns:${columns}` },
children: [node],
};
return SKIP;
});
}
export default function responsiveTables() {
return (tree: any) => applyResponsiveTables(tree);
}
+81 -18
View File
@@ -5,10 +5,18 @@ import { toHast } from 'mdast-util-to-hast';
import { gfm } from 'micromark-extension-gfm'; import { gfm } from 'micromark-extension-gfm';
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import { applyExternalLinks } from './external-links'; import { applyExternalLinks } from './external-links';
import { applyResponsiveTables } from './responsive-tables';
import { applyNoticeShortcodes } from './notice-shortcode';
const URL_SCHEME = /^(https?:|mailto:|tel:)/i; const URL_SCHEME = /^(https?:|mailto:|tel:)/i;
const TAG_TYPES = new Set(['primary', 'success', 'warning', 'danger', 'info', 'default']); const TAG_TYPES = new Set(['primary', 'success', 'warning', 'danger', 'info', 'default']);
const HINT_TYPES = new Set(['warn', 'warning', 'error', 'danger', 'success', 'info']); const HINT_TYPES = new Set(['warn', 'warning', 'error', 'danger', 'success', 'info']);
const HINT_ICONS: Record<string, string> = {
info: '<circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path>',
warning: '<circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path>',
danger: '<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 8.54 21h6.92a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path>',
success: '<path d="m9 12 2 2 4-4"></path><circle cx="12" cy="12" r="10"></circle>',
};
const BLOCK_NAMES = new Set(['hint', 'tip', 'collapse', 'tabs', 'tools']); const BLOCK_NAMES = new Set(['hint', 'tip', 'collapse', 'tabs', 'tools']);
const INLINE_NAMES = new Set(['button', 'btn', 'file', 'tag', 'label']); const INLINE_NAMES = new Set(['button', 'btn', 'file', 'tag', 'label']);
type GithubData = { stars?: string; forks?: string; description?: string; commitDate?: string; htmlUrl?: string; defaultBranch?: string }; type GithubData = { stars?: string; forks?: string; description?: string; commitDate?: string; htmlUrl?: string; defaultBranch?: string };
@@ -61,12 +69,44 @@ function normalizeMarkdownHeadings(source: string): string {
}).join('\n'); }).join('\n');
} }
function markdown(source: string, preserveHtml = false): string { function markdown(source: string, preserveHtml = true, lineNumbers = true): string {
const normalized = normalizeMarkdownHeadings(source); const normalized = normalizeMarkdownHeadings(source);
const tree = fromMarkdown(normalized, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] }); const tree = fromMarkdown(normalized, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
transform(tree, normalized); transform(tree, normalized);
if (!preserveHtml) visit(tree, 'html', (node: any) => { if (!node.data?.shortcode) node.value = ''; }); if (!preserveHtml) visit(tree, 'html', (node: any) => { if (!node.data?.shortcode) node.value = ''; });
const hast = toHast(tree, { allowDangerousHtml: true }) as any; const hast = toHast(tree, { allowDangerousHtml: true }) as any;
if (lineNumbers) {
visit(hast, 'element', (node: any) => {
if (node.tagName !== 'pre') return;
const code = node.children?.find((child: any) => child.type === 'element' && child.tagName === 'code');
if (!code) return;
const classes = Array.isArray(node.properties?.className) ? node.properties.className : [];
if (!classes.includes('astro-code')) classes.push('astro-code');
if (!classes.includes('shortcode-code')) classes.push('shortcode-code');
if (!classes.includes('no-pangu-spacing')) classes.push('no-pangu-spacing');
node.properties = { ...node.properties, className: classes };
if (code.children?.every((child: any) => child.type === 'text')) {
const value = code.children.map((child: any) => child.value).join('');
const text = value.endsWith('\n') ? value.slice(0, -1) : value;
const lines = text.split('\n');
code.children = lines.map((line: string) => ({
type: 'element',
tagName: 'span',
properties: { className: ['line'] },
children: [
{
type: 'element',
tagName: 'span',
properties: { className: ['line-content'] },
children: line ? [{ type: 'text', value: line }] : [{ type: 'text', value: '\n' }],
}
],
}));
}
});
}
applyNoticeShortcodes(hast);
applyResponsiveTables(hast);
if (siteUrl) applyExternalLinks(hast, siteUrl); if (siteUrl) applyExternalLinks(hast, siteUrl);
return toHtml(hast, { allowDangerousHtml: true }); return toHtml(hast, { allowDangerousHtml: true });
} }
@@ -77,33 +117,41 @@ export function renderMarkdown(source: string): string {
function renderBlock(name: string, args: Record<string, string>, body = ''): string { function renderBlock(name: string, args: Record<string, string>, body = ''): string {
const label = args.title ?? args.name ?? args.text ?? body.trim().split('\n')[0] ?? ''; const label = args.title ?? args.name ?? args.text ?? body.trim().split('\n')[0] ?? '';
if (name === 'button' || name === 'btn' || name === 'file') { if (name === 'button' || name === 'btn') {
const url = safeUrl(args.href ?? args.url ?? args.link); const url = safeUrl(args.href ?? args.url ?? args.link);
if (!url) return `<span class="shortcode-invalid">${escape(label || '链接不可用')}</span>`; if (!url) return `<span class="shortcode-invalid">${escape(label || '链接不可用')}</span>`;
return `<a class="shortcode-button shortcode-${name}" href="${escape(url)}" data-no-text-link>${inline(label || url)}</a>`; return `<a class="shortcode-button shortcode-${name}" href="${escape(url)}" data-no-text-link>${inline(label || url)}</a>`;
} }
if (name === 'file') {
const url = safeUrl(args.href ?? args.url ?? args.link);
if (!url) return `<span class="shortcode-invalid">${escape(label || '文件链接不可用')}</span>`;
const icon = '<svg class="content-file-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M10 12v-1"></path><path d="M10 18v-2"></path><path d="M10 7v1"></path><path d="M10 15h.01"></path></svg>';
return `<a class="shortcode-file content-file" href="${escape(url)}" target="_blank" rel="noopener noreferrer" data-no-text-link>${icon}<span class="content-file-filename">${inline(label || url)}</span></a>`;
}
if (name === 'tag' || name === 'label') { if (name === 'tag' || name === 'label') {
const type = TAG_TYPES.has(args.type ?? '') ? args.type : 'default'; const type = TAG_TYPES.has(args.type ?? '') ? args.type : 'default';
const outline = args.outline !== undefined ? ' shortcode-tag-outline' : ''; const outline = args.outline !== undefined ? ' shortcode-tag-outline' : '';
return `<span class="shortcode-tag shortcode-tag-${type}${outline}">${inline(label)}</span>`; return `<span class="shortcode-tag shortcode-tag-${type}${outline}">${inline(label)}</span>`;
} }
if (name === 'hint' || name === 'tip') { if (name === 'hint' || name === 'tip') {
const type = HINT_TYPES.has(args.type ?? '') ? args.type : 'info'; const requestedType = args.type ?? [...HINT_TYPES].find((candidate) => args[candidate] !== undefined);
const title = args.title ? `<strong>${inline(args.title)}</strong>` : ''; const type = requestedType === 'warn' ? 'warning' : requestedType === 'error' ? 'danger' : HINT_TYPES.has(requestedType ?? '') ? requestedType : 'info';
return `<aside class="shortcode-hint shortcode-hint-${type}" role="note">${title}${markdown(body)}</aside>`; const icon = `<svg class="content-hint-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${HINT_ICONS[type]}</svg>`;
const heading = args.title ? `<div class="content-hint-heading">${icon}<p class="content-hint-title">${inline(args.title)}</p></div>` : icon;
return `<aside class="shortcode-hint content-hint shortcode-${name} shortcode-hint-${type} hint-${type}${args.title ? ' content-hint-has-title' : ''}" role="note">${heading}${markdown(body, true, true)}</aside>`;
} }
if (name === 'collapse') { if (name === 'collapse') {
const open = args.open !== undefined || args.expanded !== undefined ? ' open' : ''; const open = args.open !== undefined || args.expanded !== undefined ? ' open' : '';
return `<details class="shortcode-collapse"${open}><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body, true)}</div></div></details>`; return `<details class="shortcode-collapse"${open}><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body, true, true)}</div></div></details>`;
} }
if (name === 'tabs') { if (name === 'tabs') {
const parsed = parseBlocks(body); const parsed = parseBlocks(body);
const tabs = parsed.filter((item) => item.name === 'tab'); const tabs = parsed.filter((item) => item.name === 'tab');
if (!tabs.length) return `<details class="shortcode-collapse"><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body)}</div></div></details>`; if (!tabs.length) return `<details class="shortcode-collapse"><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body, true, true)}</div></div></details>`;
const id = `shortcode-tabs-${tabs.length}-${Math.random().toString(36).slice(2, 8)}`; const id = `shortcode-tabs-${tabs.length}-${Math.random().toString(36).slice(2, 8)}`;
const selected = Math.max(0, tabs.findIndex((tab) => tab.args.selected !== undefined)); const selected = Math.max(0, tabs.findIndex((tab) => tab.args.selected !== undefined));
const buttons = tabs.map((tab, index) => `<button type="button" role="tab" aria-controls="${id}-panel-${index}" aria-selected="${index === selected}" id="${id}-tab-${index}" data-shortcode-tab="${id}"${index === selected ? '' : ' tabindex="-1"'}>${inline(tab.args.name ?? tab.args.title ?? `选项 ${index + 1}`)}</button>`).join(''); const buttons = tabs.map((tab, index) => `<button type="button" role="tab" aria-controls="${id}-panel-${index}" aria-selected="${index === selected}" id="${id}-tab-${index}" data-shortcode-tab="${id}"${index === selected ? '' : ' tabindex="-1"'}>${inline(tab.args.name ?? tab.args.title ?? `选项 ${index + 1}`)}</button>`).join('');
const panels = tabs.map((tab, index) => `<div role="tabpanel" id="${id}-panel-${index}" aria-labelledby="${id}-tab-${index}"${index === selected ? '' : ' hidden'}>${markdown(tab.body)}</div>`).join(''); const panels = tabs.map((tab, index) => `<div role="tabpanel" id="${id}-panel-${index}" aria-labelledby="${id}-tab-${index}"${index === selected ? '' : ' hidden'}>${markdown(tab.body, true, true)}</div>`).join('');
return `<section class="shortcode-tabs" data-shortcode-tabs="${id}"><div class="shortcode-tab-list" role="tablist" aria-label="${escape(label || '选项卡')}">${buttons}</div>${panels}</section>`; return `<section class="shortcode-tabs" data-shortcode-tabs="${id}"><div class="shortcode-tab-list" role="tablist" aria-label="${escape(label || '选项卡')}">${buttons}</div>${panels}</section>`;
} }
if (name === 'tools') { if (name === 'tools') {
@@ -151,8 +199,28 @@ function renderBlock(name: string, args: Record<string, string>, body = ''): str
function parseBlocks(source: string): Array<{ name: string; args: Record<string, string>; body: string }> { function parseBlocks(source: string): Array<{ name: string; args: Record<string, string>; body: string }> {
const result: Array<{ name: string; args: Record<string, string>; body: string }> = []; const result: Array<{ name: string; args: Record<string, string>; body: string }> = [];
const pattern = /^\[tab([^\]]*)\]\s*\n?([\s\S]*?)^\[\/tab\]\s*$/gm; let fence: string | null = null;
for (const match of source.matchAll(pattern)) result.push({ name: 'tab', args: attrs(match[1]), body: match[2] }); let opening: { args: Record<string, string>; bodyStart: number } | null = null;
for (const line of source.matchAll(/^[^\n]*(?:\n|$)/gm)) {
const text = line[0].trim();
const start = line.index!;
const fenceMatch = text.match(/^(`{3,}|~{3,})/);
if (fenceMatch) {
if (!fence) fence = fenceMatch[1][0];
else if (fenceMatch[1][0] === fence) fence = null;
continue;
}
if (fence) continue;
const closing = text.match(/^\[\/tab\]\s*$/i);
if (closing && opening) {
result.push({ name: 'tab', args: opening.args, body: source.slice(opening.bodyStart, start) });
opening = null;
continue;
}
const openingMatch = text.match(/^\[tab([^\]]*)\]\s*$/i);
if (openingMatch && !opening) opening = { args: attrs(openingMatch[1]), bodyStart: start + line[0].length };
}
return result; return result;
} }
@@ -220,15 +288,9 @@ function findBlocks(source: string): Array<{ name: string; args: Record<string,
} }
function replaceInline(source: string): string | null { function replaceInline(source: string): string | null {
const noticePattern = /^\[!(?:\/\s*)?\]\s*(.+)$/gm; const shortcode = /\[(button|btn|file|tag|label|hint|tip)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi;
let replacedNotice = false;
source = source.replace(noticePattern, (_match, body) => {
replacedNotice = true;
return `<span class="shortcode-notice" role="note">${inline(body)}</span>`;
});
const shortcode = /\[(button|btn|file|tag|label)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi;
const replacedShortcode = shortcode.test(source); const replacedShortcode = shortcode.test(source);
if (!replacedNotice && !replacedShortcode) return null; if (!replacedShortcode) return null;
return source.replace(shortcode, (_match, name, rawArgs, body) => renderBlock(name.toLowerCase(), attrs(rawArgs), body)); return source.replace(shortcode, (_match, name, rawArgs, body) => renderBlock(name.toLowerCase(), attrs(rawArgs), body));
} }
@@ -268,6 +330,7 @@ function transform(tree: any, raw = '') {
const output: any[] = []; const output: any[] = [];
for (const node of root.children) { for (const node of root.children) {
if (node.type !== 'paragraph' && node.type !== 'html') { output.push(node); continue; } if (node.type !== 'paragraph' && node.type !== 'html') { output.push(node); continue; }
if (node.type === 'html' && node.data?.shortcode) { output.push(node); continue; }
const source = node.type === 'html' ? node.value : node.position ? raw.slice(node.position.start.offset, node.position.end.offset) : ''; const source = node.type === 'html' ? node.value : node.position ? raw.slice(node.position.start.offset, node.position.end.offset) : '';
const replaced = replaceInline(source.trim()); const replaced = replaceInline(source.trim());
output.push(replaced ? { type: 'html', value: replaced, data: { shortcode: true } } : node); output.push(replaced ? { type: 'html', value: replaced, data: { shortcode: true } } : node);
+1 -1
View File
@@ -12,7 +12,7 @@ const tags = deriveTaxonomy(posts, 'tags')
const visibleTags = tagLimit === 0 ? tags : tags.slice(0, Math.max(0, tagLimit)); const visibleTags = tagLimit === 0 ? tags : tags.slice(0, Math.max(0, tagLimit));
--- ---
<BaseLayout title="归档" description="按日期和主题整理的文章。"> <BaseLayout title="归档" description="按日期和主题整理的文章。">
<Fragment slot="masthead"><Masthead banner={{ title: '归档', subtitle: '按日期和主题整理的文章。',image: 'https://image.luming.cool/i/2026/06/06/6a23f222a1641.webp', textTone: 'auto' }} /></Fragment> <Fragment slot="masthead"><Masthead banner={{ title: '归档', subtitle: '按日期和主题整理的文章。',image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2000&q=85', textTone: 'auto' }} /></Fragment>
<div class="article-shell archives-shell pt-8 pb-14 md:pt-10 md:pb-20"> <div class="article-shell archives-shell pt-8 pb-14 md:pt-10 md:pb-20">
<WritingHeatmap posts={posts} /> <WritingHeatmap posts={posts} />
<section class="archives-tags" aria-labelledby="tag-cloud-title" data-pagefind-ignore="all"> <section class="archives-tags" aria-labelledby="tag-cloud-title" data-pagefind-ignore="all">
+2 -2
View File
@@ -8,7 +8,7 @@ const posts = await getPublicPosts();
const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE)); const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
--- ---
<BaseLayout navbarOverlay={Boolean(siteConfig.banner.image)}> <BaseLayout hasMasthead={siteConfig.banner.enabled} navbarOverlay={siteConfig.banner.enabled && Boolean(siteConfig.banner.image)}>
<Fragment slot="masthead"><Masthead /></Fragment> {siteConfig.banner.enabled && <Fragment slot="masthead"><Masthead /></Fragment>}
<PostList posts={posts.slice(0, POSTS_PER_PAGE)} total={total} basePath="" /> <PostList posts={posts.slice(0, POSTS_PER_PAGE)} total={total} basePath="" />
</BaseLayout> </BaseLayout>
+90
View File
@@ -0,0 +1,90 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import { renderMarkdown } from '@/lib/shortcodes';
const page = (await getCollection('pages')).find((entry) => entry.data.slug === 'links');
if (!page) throw new Error('Missing links page content.');
type Link = { name: string; url: string; avatar: string; description: string };
type Section = { name: string; links: Link[]; contentLines: string[] };
const sections: Section[] = [{ name: '其他', links: [], contentLines: [] }];
let section = sections[0];
let shortcodeDepth = 0;
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]!);
const renderNestedCard = (link: Link) => `<div class="links-nested-card"><article class="link-card"><img class="link-avatar" src="${escapeHtml(link.avatar)}" alt="" width="32" height="32" loading="lazy"><div class="link-copy"><h3><a class="link-name" data-text-link href="${escapeHtml(link.url)}" target="_blank" rel="noreferrer">${escapeHtml(link.name)}</a></h3><p class="link-description">${escapeHtml(link.description)}</p></div></article></div>`;
for (const line of (page.body ?? '').split(/\r?\n/)) {
if (/^\s*\[(?:collapse|hint|tip|tabs)\b[^\]]*\]\s*$/i.test(line)) {
shortcodeDepth += 1;
section.contentLines.push(line);
continue;
}
if (/^\s*\[\/(?:collapse|hint|tip|tabs)\]\s*$/i.test(line)) {
shortcodeDepth = Math.max(0, shortcodeDepth - 1);
section.contentLines.push(line);
continue;
}
const heading = line.match(/^##\s+(.+?)\s*#*$/);
if (heading && shortcodeDepth === 0) {
section = { name: heading[1].trim(), links: [], contentLines: [] };
sections.push(section);
continue;
}
const link = line.match(/^\s*\[([^\]]+)\]\(([^)]+)\)\+\(([^)]+)\)\/\((.*)\)\s*$/)
?? line.match(/^\s*\[([^\]]+)\]\(([^)]+)\)\+\(([^)]+)\)\/(.*?)\s*\)?$/);
if (link && shortcodeDepth === 0) {
section.links.push({ name: link[1], url: link[2], avatar: link[3], description: link[4] });
continue;
}
section.contentLines.push(shortcodeDepth > 0 && link ? renderNestedCard({ name: link[1], url: link[2], avatar: link[3], description: link[4] }) : line);
}
if (!sections[0].links.length && !sections[0].contentLines.some((line) => line.trim())) sections.shift();
---
<BaseLayout title={page.data.title} description={page.data.description} image={page.data.hero ?? page.data.cover} navbarOverlay={Boolean(page.data.hero ?? page.data.cover)}>
<Fragment slot="masthead"><Masthead banner={{ title: page.data.title, subtitle: page.data.description, image: page.data.hero ?? page.data.cover, textTone: page.data.textTone }} /></Fragment>
<section class="links-shell" aria-label="友链内容">
{sections.map((group) => <section class="links-category" aria-labelledby={`links-category-${group.name}`}>
<h2 id={`links-category-${group.name}`}>{group.name}</h2>
{group.links.length > 0 && <ul class="links-list">
{group.links.map((link) => <li class="link-entry">
<article class="link-card">
<img class="link-avatar" src={link.avatar} alt="" width="32" height="32" loading="lazy" />
<div class="link-copy">
<h3><a class="link-name" data-text-link href={link.url} target="_blank" rel="noreferrer">{link.name}</a></h3>
<p class="link-description">{link.description}</p>
</div>
</article>
</li>)}
</ul>}
{group.contentLines.some((line) => line.trim()) && <div class="links-content prose" set:html={renderMarkdown(group.contentLines.join('\n'))}></div>}
</section>)}
</section>
</BaseLayout>
<style>
.links-shell { width: min(calc(100% - 2.5rem), var(--article-content-width)); margin-inline: auto; padding-block: 2rem 5rem; }
.links-category + .links-category { margin-top: 2rem; }
.links-category h2 { margin: 0 0 .75rem; padding-bottom: .5rem; border-bottom: 1px solid var(--color-border); font-size: 1.2rem; font-weight: 600; }
.links-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .25rem; margin: 0; padding: 0; list-style: none; }
.link-entry { min-width: 0; margin: 0; }
.link-card { display: flex; align-items: flex-start; gap: .75rem; min-width: 0; padding: .75rem .65rem; border-radius: var(--radius-sm); transition: background-color var(--transition-fast); }
.link-entry:hover { background: var(--color-archive-hover); }
.link-avatar { width: 2rem; height: 2rem; flex: 0 0 2rem; border-radius: 50%; object-fit: cover; background: var(--color-surface); }
.link-copy { min-width: 0; line-height: 1.7; overflow-wrap: anywhere; }
.link-copy h3 { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.45; }
.link-name { display: inline; color: var(--color-accent); text-decoration: none; }
:global(:root[data-theme='sunset']) .link-name { color: color-mix(in srgb, var(--color-accent-strong) 82%, black); }
.link-description, .link-note { margin: .2rem 0 0; color: color-mix(in srgb, var(--color-text) 82%, var(--color-muted)); font-size: .88rem; }
.link-note { font-size: .86rem; }
.links-content { margin-top: 2rem; }
.links-content .links-nested-card { margin-block: 1rem; }
.links-content .link-card { margin: 0; }
.links-content .link-avatar { display: block; width: 2rem; height: 2rem; margin: 0; }
.links-content .link-copy h3 { margin: 0; font-size: 1rem; line-height: 1.45; }
.links-content .link-description { margin: .2rem 0 0; }
@media (max-width: 37.5rem) {
.links-shell { width: min(calc(100% - 1.75rem), var(--article-content-width)); padding-block: 2rem 3.5rem; }
.links-list { grid-template-columns: 1fr; }
}
</style>
+2 -2
View File
@@ -14,7 +14,7 @@ export async function getStaticPaths() {
} }
const { posts, page, total } = Astro.props; const { posts, page, total } = Astro.props;
--- ---
<BaseLayout title={`文章 - 第 ${page} 页`} navbarOverlay={Boolean(siteConfig.banner.image)}> <BaseLayout title={`文章 - 第 ${page} 页`} hasMasthead={siteConfig.banner.enabled} navbarOverlay={siteConfig.banner.enabled && Boolean(siteConfig.banner.image)}>
<Fragment slot="masthead"><Masthead /></Fragment> {siteConfig.banner.enabled && <Fragment slot="masthead"><Masthead /></Fragment>}
<PostList posts={posts} current={page} total={total} basePath="" /> <PostList posts={posts} current={page} total={total} basePath="" />
</BaseLayout> </BaseLayout>
+18 -20
View File
@@ -2,12 +2,12 @@ import type { SiteConfig } from './types/config';
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
site: { site: {
title: '路明笔记', title: 'Eidolon',
description: '一名高中生的技术与生活博客', description: '一款轻量、简洁且美观的 Astro 主题',
author: { author: {
name: 'RiseForever', name: 'Eidolon',
avatar: 'https://weavatar.com/avatar/302380667bdaf4e1390800e62494d4af?s=512&r=G', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=400&h=400&q=85',
bio: '不慌张,不绝望,不狂妄,不投降。', bio: '一款轻量、简洁且美观的 Astro 主题',
rotateAvatar: true rotateAvatar: true
}, },
locale: 'zh-CN', locale: 'zh-CN',
@@ -17,7 +17,8 @@ export const siteConfig: SiteConfig = {
navigation: [ navigation: [
{ label: '首页', href: '/', external: false }, { label: '首页', href: '/', external: false },
{ label: '归档', href: '/archives/', external: false }, { label: '归档', href: '/archives/', external: false },
{ label: '测试页面', href: '/test-page/', external: false } { label: '友人', href: '/links/', external: false },
{ label: '关于', href: '/about/', external: false }
], ],
pjax: { pjax: {
enabled: true enabled: true
@@ -28,9 +29,9 @@ export const siteConfig: SiteConfig = {
}, },
banner: { banner: {
enabled: true, enabled: true,
title: '路明笔记', title: 'Eidolon',
subtitle: '一名高中生的技术与生活博客', subtitle: '一款轻量、简洁且美观的 Astro 主题',
image: 'https://image.luming.cool/i/2026/07/26/6a6628967fe03.webp', image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2000&q=85',
position: 'center center', position: 'center center',
desktopHeightVh: 55, desktopHeightVh: 55,
mobileHeightVh: 40, mobileHeightVh: 40,
@@ -38,34 +39,31 @@ export const siteConfig: SiteConfig = {
textTone: 'auto' textTone: 'auto'
}, },
cards: { cards: {
defaultCovers: ['https://image.luming.cool/i/2026/08/03/6a6f79cc091ac.webp'] defaultCovers: ['https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80']
}, },
archives: { archives: {
tagLimit: 30 tagLimit: 30
}, },
footer: { footer: {
copyright: `© 2023-${new Date().getFullYear()} RiseForever`, copyright: `© 2023-${new Date().getFullYear()} 你的名字`, // 记得把"你的名字"改成你自己的名字。
// 外链(如果有)
links: [ links: [
{ label: 'BlogsClub', href: 'https://blogs.quest/luming', external: true }, { label: 'BlogsClub', href: 'https://blogs.club', external: true }
{ label: '笔墨迹', href: 'https://blogscn.fun/blogs/01k7zk4mhndsr6atqd1wm11f7b', external: true },
{ label: '十年之约', href: 'https://www.foreverblog.cn/blog/6506.html', external: true },
{ label: '博友圈', href: 'https://www.boyouquan.com/blogs/luming.cool', external: true },
{ label: '好站网', href: 'https://haozhan.wang/site_detail.php?id=176', external: true },
{ label: '集博栈', href: 'https://www.heyblog.net/site/019eff04-b025-76c9-b8f2-388deb7195cd', external: true },
{ label: '博客大联盟', href: 'https://bo.ke/luming.cool/', external: true }
] ]
}, },
// 评论系统采用 Twikoo,请自行部署云函数后,并在下方填写你的云函数 URL
comments: { comments: {
provider: 'twikoo', provider: 'twikoo',
envId: 'https://twokii.luming.cool/', envId: 'https://link.to.your.twikoo.service/',
region: 'cn', region: 'cn',
lang: 'zh-CN' lang: 'zh-CN'
}, },
// 搜索服务默认使用 PageFind,无需理会
search: { provider: 'pagefind', placeholder: '搜索文章' }, search: { provider: 'pagefind', placeholder: '搜索文章' },
// 这是顶栏右上角及侧栏底部的工具按钮,可自定义。下方示例已涵盖了站内功能、RSS 复制、外链跳转等场景。
toolbarItems: [ toolbarItems: [
{ type: 'search', icon: 'search', name: '搜索文章' }, { type: 'search', icon: 'search', name: '搜索文章' },
{ type: 'rss', icon: 'rss', name: 'RSS 订阅', href: '/rss.xml' }, { type: 'rss', icon: 'rss', name: 'RSS 订阅', href: '/rss.xml' },
{ type: 'link', icon: 'tram-front', name: '开往', href: 'https://www.travellings.cn/plain.html', external: true },
{ type: 'settings', icon: 'settings', name: '阅读设置' } { type: 'settings', icon: 'settings', name: '阅读设置' }
] ]
}; };
+134 -33
View File
@@ -131,54 +131,88 @@
font-family: var(--mirages-font-code); font-family: var(--mirages-font-code);
font-size: .875rem; font-size: .875rem;
line-height: 1.45; line-height: 1.45;
white-space: pre;
}
.prose .astro-code {
--code-line-digits: 1;
--code-gutter-background: color-mix(in srgb, var(--color-code) 65%, var(--color-border));
padding: 0;
overflow: hidden;
position: relative;
background: var(--color-code);
} }
.prose .astro-code code { .prose .astro-code code {
--code-line-digits: 1;
display: block; display: block;
position: relative;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: none;
width: 100%;
min-width: 0;
padding-block: 1rem; padding-block: 1rem;
counter-reset: code-line; counter-reset: code-line;
font-size: 0; font-size: .875rem;
line-height: 1.45;
scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent;
scrollbar-width: thin;
white-space: normal;
} }
.prose .astro-code code:has(.line:nth-child(10)) { --code-line-digits: 2; } .prose .astro-code code::-webkit-scrollbar { height: 8px; }
.prose .astro-code code:has(.line:nth-child(100)) { --code-line-digits: 3; } .prose .astro-code code::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); }
.prose .astro-code code:has(.line:nth-child(1000)) { --code-line-digits: 4; } .prose .astro-code code::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-muted) 78%, transparent); }
.prose .astro-code:has(.line:nth-child(10)) { --code-line-digits: 2; }
.prose .astro-code:has(.line:nth-child(100)) { --code-line-digits: 3; }
.prose .astro-code:has(.line:nth-child(1000)) { --code-line-digits: 4; }
.prose .astro-code .code-line-numbers {
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 20;
display: block;
width: calc(var(--code-line-digits) * 1ch + 2rem);
padding-block: 1rem;
font-family: var(--mirages-font-code);
font-size: .875rem;
line-height: 1.45;
background: var(--code-gutter-background);
border-right: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
box-sizing: border-box;
color: var(--color-muted);
font-variant-numeric: tabular-nums;
text-align: right;
user-select: none;
pointer-events: none;
}
.prose .astro-code .code-line-numbers > span {
display: block;
height: 1.45em;
padding-inline: 1rem;
box-sizing: border-box;
color: inherit;
}
.prose .astro-code .line { .prose .astro-code .line {
display: flex; display: block;
width: max-content;
min-width: 100%;
min-height: 1.45em; min-height: 1.45em;
padding-left: calc(var(--code-line-digits) * 1ch + 3rem);
box-sizing: border-box;
counter-increment: code-line; counter-increment: code-line;
font-size: .875rem; font-size: .875rem;
line-height: 1.45; line-height: 1.45;
white-space: pre;
} }
.prose .astro-code .line::before { .prose .astro-code .line::before {
display: inline-block; content: none;
width: calc(var(--code-line-digits) * 1ch + 2rem);
flex: 0 0 calc(var(--code-line-digits) * 1ch + 2rem);
margin-right: 1rem;
padding-inline: 1rem;
background: var(--color-code-gutter);
box-sizing: border-box;
color: var(--color-muted);
content: counter(code-line);
font-variant-numeric: tabular-nums;
opacity: 0.72;
text-align: right;
user-select: none;
}
.prose .astro-code .line:first-child::before {
box-shadow: 0 -1rem var(--color-code-gutter);
}
.prose .astro-code .line:last-child::before {
box-shadow: 0 1rem var(--color-code-gutter);
}
.prose .astro-code .line:only-child::before {
box-shadow: 0 -1rem var(--color-code-gutter), 0 1rem var(--color-code-gutter);
} }
.prose .astro-code span { .prose .astro-code span {
@@ -186,6 +220,8 @@
} }
:root[data-theme='dark'] .prose .astro-code { :root[data-theme='dark'] .prose .astro-code {
--code-gutter-background: var(--color-code-gutter);
--shortcode-code-gutter: var(--color-code-gutter);
background-color: var(--color-code); background-color: var(--color-code);
color: var(--shiki-dark); color: var(--shiki-dark);
} }
@@ -196,6 +232,8 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root[data-theme='auto'] .prose .astro-code { :root[data-theme='auto'] .prose .astro-code {
--code-gutter-background: var(--color-code-gutter);
--shortcode-code-gutter: var(--color-code-gutter);
background-color: var(--color-code); background-color: var(--color-code);
color: var(--shiki-dark); color: var(--shiki-dark);
} }
@@ -205,9 +243,47 @@
} }
} }
.prose .astro-code { .shortcode-code {
--shortcode-code-gutter: color-mix(in srgb, var(--color-code) 65%, var(--color-border));
padding: 0; padding: 0;
background: var(--color-code); overflow: hidden;
position: relative;
}
.shortcode-code code {
display: block;
position: relative;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: none;
width: 100%;
min-width: 0;
padding-block: 1rem;
font-size: .875rem;
line-height: 1.45;
scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent;
scrollbar-width: thin;
white-space: normal;
}
.shortcode-code code::-webkit-scrollbar { height: 8px; }
.shortcode-code code::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); }
.shortcode-code code::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-muted) 78%, transparent); }
.shortcode-code .line {
display: block;
width: max-content;
min-width: 100%;
min-height: 1.45em;
padding-left: 4rem;
box-sizing: border-box;
font-size: .875rem;
line-height: 1.45;
white-space: pre;
}
.shortcode-code .shortcode-line-number {
display: none;
} }
.prose .katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding-block: .35em; } .prose .katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding-block: .35em; }
@@ -215,9 +291,25 @@
.prose .mermaid { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 1rem 0; } .prose .mermaid { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 1rem 0; }
.prose .mermaid svg { max-width: none; height: auto; } .prose .mermaid svg { max-width: none; height: auto; }
.prose .table-scroll {
width: 100%;
max-width: 100%;
margin: 1.25em 0;
overflow-x: auto;
overscroll-behavior-inline: contain;
scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent;
scrollbar-width: thin;
}
.prose .table-scroll::-webkit-scrollbar { height: 8px; }
.prose .table-scroll::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); }
.prose table { .prose table {
width: 100%; width: 100%;
max-width: 100%;
margin: 0;
border-collapse: collapse; border-collapse: collapse;
table-layout: auto;
} }
.prose thead { .prose thead {
@@ -239,6 +331,15 @@
.prose :where(th, td) { .prose :where(th, td) {
padding: 0.55em 0.8em; padding: 0.55em 0.8em;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}
@media (max-width: 48rem) {
.prose .responsive-table {
width: max(100%, calc(var(--table-columns, 1) * 8rem));
}
} }
.prose :where(th, td)[align='left'] { text-align: left; } .prose :where(th, td)[align='left'] { text-align: left; }
+116 -31
View File
@@ -6,7 +6,7 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--color-accent); background: var(--color-accent);
color: var(--color-bg); color: var(--color-bg);
font-family: var(--mirages-font-ui); font-family: var(--reading-font-family);
font-size: 0.9em; font-size: 0.9em;
} }
.prose .shortcode-button:hover { .prose .shortcode-button:hover {
@@ -14,9 +14,54 @@
color: var(--color-bg); color: var(--color-bg);
} }
.prose .shortcode-file { .prose .shortcode-file {
display: inline-flex;
max-width: 100%;
min-height: 3.5rem;
align-items: center;
gap: 1rem;
margin: 0.25rem 0 0.25rem 0.5rem;
overflow: hidden;
padding: 0 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent; background: transparent;
color: var(--color-text);
font-family: var(--reading-font-family);
text-decoration: none;
white-space: nowrap;
transition: border-color 0.6s ease;
}
.prose .shortcode-file:first-child {
margin-left: 0;
}
.prose .content-file-icon {
width: 1.5rem;
height: 1.5rem;
flex: 0 0 1.5rem;
color: var(--color-text);
transition: color 0.3s ease;
}
.prose .content-file-filename {
min-width: 0;
overflow: hidden;
color: var(--color-text);
font-weight: 700;
text-overflow: ellipsis;
transition: color 0.3s ease;
}
.prose .shortcode-file:hover {
border-color: var(--color-accent);
color: var(--color-accent); color: var(--color-accent);
} }
.prose .shortcode-file:hover .content-file-icon,
.prose .shortcode-file:hover .content-file-filename {
color: var(--color-accent);
}
@media (prefers-reduced-motion: reduce) {
.prose .shortcode-file,
.prose .content-file-icon,
.prose .content-file-filename { transition: none; }
}
.shortcode-tag { .shortcode-tag {
display: inline-block; display: inline-block;
margin: 0.15em 0.3em 0.15em 0; margin: 0.15em 0.3em 0.15em 0;
@@ -24,7 +69,7 @@
border-radius: 999px; border-radius: 999px;
background: var(--color-accent); background: var(--color-accent);
color: var(--color-bg); color: var(--color-bg);
font: 0.82em var(--mirages-font-ui); font: 0.82em var(--reading-font-family);
} }
.shortcode-tag-outline { .shortcode-tag-outline {
background: transparent; background: transparent;
@@ -44,29 +89,60 @@
background: #35769b; background: #35769b;
} }
.shortcode-hint { .shortcode-hint {
--hint-color: var(--color-accent);
position: relative;
margin: 1.2em 0; margin: 1.2em 0;
padding: 0.7em 1em; padding: 1.5rem 1.25rem 1.5rem 3.5rem;
border-left: 4px solid var(--color-accent); border: 1px solid color-mix(in srgb, var(--hint-color) 28%, var(--color-border));
background: var(--color-surface); border-left: 0.25rem solid var(--hint-color);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--hint-color) 7%, var(--color-surface));
} }
.shortcode-hint strong { .content-hint-icon {
position: absolute;
top: 50%;
left: 1rem;
width: 1.5rem;
height: 1.5rem;
transform: translateY(-50%);
color: var(--hint-color);
}
.content-hint-heading {
display: grid;
min-height: 1.5rem;
grid-template-columns: 1.5rem minmax(0, 1fr);
align-items: center;
gap: 1rem;
margin: 0 0 0.5rem -2.5rem;
}
.content-hint-heading .content-hint-icon {
position: static;
transform: none;
}
.prose .content-hint-title {
display: block; display: block;
margin-bottom: 0.2em; margin: 0;
font-family: var(--mirages-font-ui); color: var(--hint-color);
font-size: 1.2em;
line-height: 1.5rem;
font-weight: 600;
} }
.shortcode-hint p { .shortcode-hint p:not(.content-hint-title) {
margin: 0.25em 0; margin: 0.25em 0;
} }
.shortcode-hint-warning, .shortcode-hint-warning,
.shortcode-hint-warn { .shortcode-hint-warn,
border-color: #a36b16; .hint-warning {
--hint-color: #d99b00;
} }
.shortcode-hint-error, .shortcode-hint-error,
.shortcode-hint-danger { .shortcode-hint-danger,
border-color: #ad3d48; .hint-danger {
--hint-color: #d34b50;
} }
.shortcode-hint-success { .shortcode-hint-success,
border-color: #287d5b; .hint-success {
--hint-color: #209b61;
} }
.shortcode-collapse { .shortcode-collapse {
margin: 1.2em 0; margin: 1.2em 0;
@@ -132,10 +208,6 @@
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.shortcode-body { transition: none; } .shortcode-body { transition: none; }
} }
.shortcode-body-inner table {
width: 100%;
border-collapse: collapse;
}
.shortcode-body .shortcode-collapse { .shortcode-body .shortcode-collapse {
margin: 1rem 0 0.25rem; margin: 1rem 0 0.25rem;
} }
@@ -156,6 +228,7 @@
} }
.shortcode-tabs { .shortcode-tabs {
margin: 1.2em 0; margin: 1.2em 0;
overflow: hidden;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--color-surface); background: var(--color-surface);
@@ -170,15 +243,19 @@
} }
.shortcode-tab-list button { .shortcode-tab-list button {
cursor: pointer; cursor: pointer;
padding: 0.4rem 0.7rem; padding: 0.4rem 0.75rem;
border: 0; border: 0;
border-radius: var(--radius-xs);
background: transparent; background: transparent;
color: var(--color-muted); color: var(--color-muted);
font-family: var(--reading-font-family);
transition: background-color 150ms ease, color 150ms ease;
} }
.shortcode-tab-list button[aria-selected="true"] { .shortcode-tab-list button[aria-selected="true"] {
background: var(--color-bg); background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
color: var(--color-accent); color: var(--color-accent);
font-weight: 600; font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
.shortcode-tabs [role="tabpanel"] { .shortcode-tabs [role="tabpanel"] {
padding: 0.8rem 1rem; padding: 0.8rem 1rem;
@@ -266,7 +343,7 @@
line-height: 1.25; line-height: 1.25;
} }
.shortcode-tool-description { .shortcode-tool-description {
color: var(--color-muted); color: color-mix(in srgb, var(--color-text) 82%, var(--color-muted));
font-size: 0.78rem; font-size: 0.78rem;
line-height: 1.3; line-height: 1.3;
} }
@@ -308,7 +385,7 @@
min-width: 0; min-width: 0;
margin: 0; margin: 0;
overflow-wrap: anywhere; overflow-wrap: anywhere;
font: 600 1.08rem/1.35 var(--mirages-font-ui); font: 600 1.08rem/1.35 var(--reading-font-family);
} }
.shortcode-github-icon { .shortcode-github-icon {
display: inline-block; display: inline-block;
@@ -336,7 +413,7 @@
justify-content: flex-end; justify-content: flex-end;
gap: 0.8rem; gap: 0.8rem;
color: var(--color-muted); color: var(--color-muted);
font: 0.75rem var(--mirages-font-ui); font: 0.75rem var(--reading-font-family);
text-align: right; text-align: right;
} }
.shortcode-github-stat { .shortcode-github-stat {
@@ -365,7 +442,7 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--color-accent); color: var(--color-accent);
font: 0.78rem var(--mirages-font-ui); font: 0.78rem var(--reading-font-family);
text-decoration: none; text-decoration: none;
} }
.shortcode-github-read-more:hover, .shortcode-github-read-more:hover,
@@ -383,7 +460,7 @@
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
color: var(--color-muted); color: var(--color-muted);
font: 0.75rem var(--mirages-font-ui); font: 0.75rem var(--reading-font-family);
} }
.shortcode-github-commit { .shortcode-github-commit {
min-width: 0; min-width: 0;
@@ -420,9 +497,17 @@
font-style: italic; font-style: italic;
} }
.shortcode-notice { .shortcode-notice {
display: inline-block; display: inline-flex;
padding: 0.1em 0.45em; width: 1em;
border-left: 2px solid var(--color-accent); height: 1em;
background: color-mix(in srgb, var(--color-accent) 8%, transparent); align-items: center;
color: var(--color-accent); justify-content: center;
color: inherit;
line-height: 1;
vertical-align: -0.125em;
}
.shortcode-notice svg {
display: block;
width: 1em;
height: 1em;
} }